al-sem 0.0.1
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/LICENSE +21 -0
- package/README.md +361 -0
- package/package.json +64 -0
- package/scripts/d40-diff.ts +44 -0
- package/scripts/fetch-native-parser.ts +179 -0
- package/scripts/precision-sample.ts +99 -0
- package/scripts/precision-study.ts +42 -0
- package/scripts/precision-tabulate.ts +52 -0
- package/src/cli/baseline.ts +31 -0
- package/src/cli/diff.ts +199 -0
- package/src/cli/events-chains.ts +56 -0
- package/src/cli/events-fanout.ts +87 -0
- package/src/cli/exit-code.ts +30 -0
- package/src/cli/fingerprint-indexes.ts +130 -0
- package/src/cli/fingerprint-query.ts +543 -0
- package/src/cli/fingerprint-witness.ts +493 -0
- package/src/cli/fingerprint.ts +292 -0
- package/src/cli/format-compact-json.ts +45 -0
- package/src/cli/format-events.ts +77 -0
- package/src/cli/format-fingerprint.ts +295 -0
- package/src/cli/format-html.ts +503 -0
- package/src/cli/format-json.ts +13 -0
- package/src/cli/format-policy.ts +95 -0
- package/src/cli/format-sarif.ts +186 -0
- package/src/cli/format-terminal.ts +153 -0
- package/src/cli/index.ts +566 -0
- package/src/cli/policy.ts +204 -0
- package/src/config/roots-config.ts +302 -0
- package/src/deps/cache-versions.ts +74 -0
- package/src/deps/canonical-json.ts +27 -0
- package/src/deps/dependency-artifact.ts +144 -0
- package/src/deps/dependency-cache.ts +262 -0
- package/src/deps/dependency-dag.ts +128 -0
- package/src/deps/dependency-package-discovery.ts +85 -0
- package/src/deps/dependency-pipeline.ts +483 -0
- package/src/deps/dependency-projection.ts +211 -0
- package/src/deps/dependency-resolver.ts +154 -0
- package/src/deps/workspace-dependencies.ts +114 -0
- package/src/detectors/capability-query.ts +145 -0
- package/src/detectors/confidence.ts +52 -0
- package/src/detectors/d1-db-op-in-loop.ts +457 -0
- package/src/detectors/d10-self-modifying-loop.ts +114 -0
- package/src/detectors/d11-modify-without-get.ts +129 -0
- package/src/detectors/d12-dead-integration-event.ts +81 -0
- package/src/detectors/d13-cross-app-internal-call.ts +105 -0
- package/src/detectors/d14-dead-routine.ts +151 -0
- package/src/detectors/d16-obsolete-routine-call.ts +94 -0
- package/src/detectors/d17-min-version-drift.ts +157 -0
- package/src/detectors/d18-constant-filter-in-loop.ts +151 -0
- package/src/detectors/d19-unused-parameter.ts +116 -0
- package/src/detectors/d2-event-fanout-in-loop.ts +240 -0
- package/src/detectors/d20-unreachable-after-exit.ts +92 -0
- package/src/detectors/d21-read-without-load.ts +128 -0
- package/src/detectors/d22-flowfield-without-calcfields.ts +168 -0
- package/src/detectors/d29-subscriber-modify-on-event-record.ts +163 -0
- package/src/detectors/d3-load-state.ts +72 -0
- package/src/detectors/d3-missing-setloadfields.ts +234 -0
- package/src/detectors/d32-constant-boolean-parameter.ts +185 -0
- package/src/detectors/d33-unfiltered-bulk-write.ts +173 -0
- package/src/detectors/d34-commit-in-loop.ts +206 -0
- package/src/detectors/d35-commit-in-event-subscriber.ts +138 -0
- package/src/detectors/d36-late-setloadfields.ts +162 -0
- package/src/detectors/d37-validate-without-persist.ts +271 -0
- package/src/detectors/d38-subscriber-to-obsolete-event.ts +140 -0
- package/src/detectors/d39-record-left-dirty-across-chain.ts +165 -0
- package/src/detectors/d4-repeated-lookup-in-loop.ts +128 -0
- package/src/detectors/d40-transitive-load-missing.ts +217 -0
- package/src/detectors/d41-transitive-filter-loss.ts +200 -0
- package/src/detectors/d42-cross-call-wrong-setloadfields.ts +243 -0
- package/src/detectors/d43-event-ishandled-skip.ts +257 -0
- package/src/detectors/d44-event-multi-subscriber-overlap.ts +223 -0
- package/src/detectors/d45-event-transitive-table-exposure.ts +159 -0
- package/src/detectors/d5-set-based-opportunity.ts +162 -0
- package/src/detectors/d7-recursive-event-expansion.ts +151 -0
- package/src/detectors/d8-commit-in-transaction.ts +132 -0
- package/src/detectors/d9-transaction-span-summary.ts +107 -0
- package/src/detectors/detector-context.ts +121 -0
- package/src/detectors/finding-grouping.ts +61 -0
- package/src/detectors/path-merge.ts +174 -0
- package/src/detectors/registry.ts +176 -0
- package/src/detectors/table-display.ts +42 -0
- package/src/diff/diff-abi.ts +195 -0
- package/src/diff/diff-capabilities.ts +179 -0
- package/src/diff/diff-engine.ts +146 -0
- package/src/diff/diff-events.ts +323 -0
- package/src/diff/diff-identity.ts +73 -0
- package/src/diff/diff-indexes.ts +199 -0
- package/src/diff/diff-permissions.ts +260 -0
- package/src/diff/diff-policy.ts +101 -0
- package/src/diff/diff-preflight.ts +66 -0
- package/src/diff/diff-renames.ts +104 -0
- package/src/diff/diff-schema.ts +232 -0
- package/src/diff/format-diff.ts +148 -0
- package/src/engine/attribute-parser.ts +50 -0
- package/src/engine/capability-cone.ts +531 -0
- package/src/engine/combined-graph.ts +357 -0
- package/src/engine/control-flow-walker.ts +1317 -0
- package/src/engine/dispatch-sites.ts +199 -0
- package/src/engine/effect-lattice.ts +81 -0
- package/src/engine/entry-points.ts +57 -0
- package/src/engine/event-flow.ts +524 -0
- package/src/engine/event-relay.ts +92 -0
- package/src/engine/op-classification.ts +92 -0
- package/src/engine/path-walker.ts +189 -0
- package/src/engine/reverse-call-graph.ts +23 -0
- package/src/engine/root-classifier-overlay.ts +194 -0
- package/src/engine/root-classifier.ts +135 -0
- package/src/engine/scc.ts +110 -0
- package/src/engine/source-anchor.ts +25 -0
- package/src/engine/summary-context.ts +104 -0
- package/src/engine/summary-engine.ts +296 -0
- package/src/engine/summary-runner.ts +560 -0
- package/src/engine/transaction-spans.ts +112 -0
- package/src/engine/uncertainty-util.ts +54 -0
- package/src/hash.ts +31 -0
- package/src/index/attribute-from-node.ts +141 -0
- package/src/index/callee-from-node.ts +181 -0
- package/src/index/capability/background.ts +90 -0
- package/src/index/capability/commit.ts +44 -0
- package/src/index/capability/dispatch.ts +164 -0
- package/src/index/capability/events.ts +65 -0
- package/src/index/capability/extractor.ts +124 -0
- package/src/index/capability/file-blob.ts +137 -0
- package/src/index/capability/http.ts +159 -0
- package/src/index/capability/hyperlink.ts +60 -0
- package/src/index/capability/isolated-storage.ts +179 -0
- package/src/index/capability/table.ts +113 -0
- package/src/index/capability/telemetry.ts +84 -0
- package/src/index/capability/ui.ts +55 -0
- package/src/index/capability/value-source.ts +202 -0
- package/src/index/expression-from-node.ts +117 -0
- package/src/index/indexer.ts +102 -0
- package/src/index/intraprocedural-body.ts +1467 -0
- package/src/index/intraprocedural-ops.ts +253 -0
- package/src/index/intraprocedural-refs.ts +188 -0
- package/src/index/object-indexer.ts +279 -0
- package/src/index/routine-indexer.ts +282 -0
- package/src/index/routine-signature.ts +46 -0
- package/src/index/variable-indexer.ts +134 -0
- package/src/index/variable-initializer-extractor.ts +155 -0
- package/src/index/variable-type-normalizer.ts +83 -0
- package/src/index.ts +267 -0
- package/src/mcp/server.ts +72 -0
- package/src/mcp/session.ts +49 -0
- package/src/mcp/tools/explain-path.ts +75 -0
- package/src/mcp/tools/get-analysis-health.ts +62 -0
- package/src/mcp/tools/get-finding.ts +47 -0
- package/src/mcp/tools/get-routine-summary.ts +126 -0
- package/src/mcp/tools/list-findings.ts +85 -0
- package/src/mcp/tools/list-hotspots.ts +78 -0
- package/src/mcp/tools/list-rollups.ts +103 -0
- package/src/mcp/tools/validators.ts +25 -0
- package/src/model/attributes.ts +120 -0
- package/src/model/callee.ts +45 -0
- package/src/model/capability.ts +187 -0
- package/src/model/coverage.ts +85 -0
- package/src/model/entities.ts +628 -0
- package/src/model/expression.ts +98 -0
- package/src/model/finding.ts +110 -0
- package/src/model/graph-edge.ts +93 -0
- package/src/model/graph.ts +62 -0
- package/src/model/identity.ts +81 -0
- package/src/model/ids.ts +90 -0
- package/src/model/index.ts +13 -0
- package/src/model/model.ts +51 -0
- package/src/model/permission.ts +76 -0
- package/src/model/root-classification.ts +116 -0
- package/src/model/stable-identity.ts +102 -0
- package/src/model/summary.ts +96 -0
- package/src/parser/ast.ts +82 -0
- package/src/parser/native/ffi.ts +145 -0
- package/src/parser/native/parse-index-pool.ts +148 -0
- package/src/parser/native/parse-index-worker.ts +94 -0
- package/src/parser/native/wrapper.ts +353 -0
- package/src/parser/parser-init.ts +43 -0
- package/src/perf/profiler.ts +66 -0
- package/src/policy/policy-default.yaml +83 -0
- package/src/policy/policy-engine.ts +339 -0
- package/src/policy/policy-loader.ts +257 -0
- package/src/policy/policy-schema.json +379 -0
- package/src/policy/policy-types.ts +81 -0
- package/src/policy/predicate-compiler.ts +151 -0
- package/src/policy/predicate-evaluator.ts +267 -0
- package/src/policy/predicate-fields.ts +439 -0
- package/src/projection/actionable-anchor.ts +48 -0
- package/src/projection/finding-filters.ts +44 -0
- package/src/projection/finding-fingerprint.ts +54 -0
- package/src/projection/finding-groups.ts +41 -0
- package/src/projection/finding-summary.ts +110 -0
- package/src/projection/rollup-findings.ts +105 -0
- package/src/providers/discover.ts +88 -0
- package/src/providers/external.ts +46 -0
- package/src/providers/types.ts +36 -0
- package/src/providers/workspace.ts +117 -0
- package/src/resolve/call-resolver.ts +117 -0
- package/src/resolve/coverage.ts +61 -0
- package/src/resolve/event-graph.ts +166 -0
- package/src/resolve/implicit-edges.ts +53 -0
- package/src/resolve/record-types.ts +36 -0
- package/src/resolve/resolver.ts +23 -0
- package/src/resolve/semantic-graph.ts +29 -0
- package/src/resolve/symbol-table.ts +69 -0
- package/src/snapshot/app-snapshot.ts +74 -0
- package/src/snapshot/compose.ts +100 -0
- package/src/snapshot/derive/callsite-evidence.ts +76 -0
- package/src/snapshot/derive/capability-facts.ts +70 -0
- package/src/snapshot/derive/contracts.ts +131 -0
- package/src/snapshot/derive/coverage.ts +35 -0
- package/src/snapshot/derive/event-declarations.ts +140 -0
- package/src/snapshot/derive/identity-table.ts +58 -0
- package/src/snapshot/derive/inputs.ts +91 -0
- package/src/snapshot/derive/operation-evidence.ts +70 -0
- package/src/snapshot/derive/permissions.ts +186 -0
- package/src/snapshot/derive/root-classifications.ts +56 -0
- package/src/snapshot/derive/schema.ts +130 -0
- package/src/snapshot/derive/typed-edges.ts +60 -0
- package/src/snapshot/derive/workspace-fingerprint.ts +19 -0
- package/src/snapshot/deserialize.ts +40 -0
- package/src/snapshot/serialize-cbor-gz.ts +12 -0
- package/src/snapshot/serialize-cbor.ts +19 -0
- package/src/snapshot/serialize-json.ts +22 -0
- package/src/snapshot/shard.ts +134 -0
- package/src/snapshot/types.ts +181 -0
- package/src/symbols/app-manifest.ts +96 -0
- package/src/symbols/app-package-zip.ts +50 -0
- package/src/symbols/embedded-source-reader.ts +41 -0
- package/src/symbols/package-hash.ts +81 -0
- package/src/symbols/symbol-reader.ts +101 -0
- package/src/symbols/symbol-reference-parser.ts +378 -0
- package/src/symbols/symbol-reference-reader.ts +27 -0
- package/tsconfig.json +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Torben Leth
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# al-sem — Static semantic analyzer for AL (Microsoft Business Central)
|
|
2
|
+
|
|
3
|
+
[](https://typescriptlang.org)
|
|
4
|
+
[](https://bun.sh)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Find cross-file performance, correctness, and compatibility bugs in an AL workspace —
|
|
8
|
+
the kind per-file linters can't catch because they require the whole call graph:
|
|
9
|
+
DB-ops-inside-loops walked across procedures, Commit inside a posting transaction span,
|
|
10
|
+
event-subscriber cycles, integration events with no subscribers anywhere, MinVersion
|
|
11
|
+
drift against actual call sites, and ten more. Pure static analysis (no profile, no
|
|
12
|
+
runtime), but accurate enough to be actionable in CI and in an editor.
|
|
13
|
+
|
|
14
|
+
34 default detectors plus 1 opt-in (D40), 35 total. Tuned on a real Continia BC extension — see
|
|
15
|
+
[precision study](docs/superpowers/precision-study-do-cloud.md).
|
|
16
|
+
|
|
17
|
+
Beyond `analyze`, al-sem also ships four standalone surfaces over the same `SemanticModel`:
|
|
18
|
+
declarative **`policy`** rules over capability facts, event blast-radius reports (**`events
|
|
19
|
+
fanout`** / **`events chains`**), a cross-version snapshot **`diff`**, and **`fingerprint`** (emit a
|
|
20
|
+
`CapabilitySnapshot`). `diff` and `fingerprint` accept either a workspace directory or a raw `.app`
|
|
21
|
+
symbol package, so two `.app` versions can be compared with no source checkout.
|
|
22
|
+
|
|
23
|
+
**Status (2026-06-02):** code-complete through Phase 4 (record-flow framework) plus the L6 policy
|
|
24
|
+
layer, event blast-radius, snapshot diff/fingerprint, and HTML report surfaces. 1828 tests pass;
|
|
25
|
+
`tsc` and Biome clean. See [STATUS](docs/superpowers/STATUS.md).
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun add al-sem
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
al-sem ships a native tree-sitter parser per platform that downloads via a postinstall
|
|
34
|
+
hook. Bun requires you to opt in:
|
|
35
|
+
|
|
36
|
+
```jsonc
|
|
37
|
+
// package.json
|
|
38
|
+
{ "trustedDependencies": ["al-sem"] }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Supported platforms:** `win32-x64`, `linux-x64`, `darwin-arm64` shipped today;
|
|
42
|
+
`darwin-x64` (Intel macOS) is pending the next `tree-sitter-al` release. Other
|
|
43
|
+
platforms fail at first parse with a clear `NativeParserUnavailableError`.
|
|
44
|
+
|
|
45
|
+
**Install-time environment overrides (for air-gapped / mirrored environments):**
|
|
46
|
+
|
|
47
|
+
- `AL_SEM_NATIVE_PARSER_PATH=/abs/path/to/lib` — use a preseeded artifact, skip download.
|
|
48
|
+
- `AL_SEM_NATIVE_PARSER_OFFLINE=1` — require the canonical artifact to already exist; never download.
|
|
49
|
+
- `AL_SEM_NATIVE_PARSER_BASE_URL=https://internal-mirror/...` — fetch from an internal mirror instead of GitHub.
|
|
50
|
+
|
|
51
|
+
If `trustedDependencies` is not configured, preseed the artifact via `AL_SEM_NATIVE_PARSER_PATH`.
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bunx al-sem analyze . --min-severity high --format terminal
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or run the bundled demo against a small intentionally-buggy workspace:
|
|
60
|
+
`bash demo/run-demos.sh all` — walks the cross-file detectors the standard
|
|
61
|
+
AL cops can't replicate and writes a sample HTML report to `demo/report.html`.
|
|
62
|
+
|
|
63
|
+
Sample output:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
Analysed 1234 routines (1230 with bodies, 4 parse-incomplete); 251/251 source units parsed; 0 opaque app(s).
|
|
67
|
+
|
|
68
|
+
HIGH (12):
|
|
69
|
+
[d1-db-op-in-loop] Database operation inside a loop — A loop in PostSalesDoc reaches FindSet on Sales Line.
|
|
70
|
+
ws:src/Codeunit/SalesPostHelper.Codeunit.al:204:13 in Sales-Post Helper :: PostSalesDoc
|
|
71
|
+
confidence: likely
|
|
72
|
+
fix (medium): Move the database operation outside the loop, or batch it into a set-based operation.
|
|
73
|
+
[d3-missing-setloadfields] Missing SetLoadFields ...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
By default `--format auto` emits terminal on a TTY and compact JSON on a pipe.
|
|
77
|
+
`--format json` always emits the compact summary; `--format sarif` emits SARIF
|
|
78
|
+
2.1.0 for GitHub code-scanning; `--format html` emits a self-contained visual
|
|
79
|
+
report (per-finding interprocedural evidence-path flows + a publisher→subscriber
|
|
80
|
+
event graph, no external assets) for sharing or blog embedding; `--dump-model`
|
|
81
|
+
opts into the legacy full-model dump (debug-only, can exceed 500 MB).
|
|
82
|
+
|
|
83
|
+
## CI integration
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
- name: al-sem
|
|
87
|
+
run: |
|
|
88
|
+
bunx al-sem analyze . \
|
|
89
|
+
--baseline .al-sem-baseline.json \
|
|
90
|
+
--fail-on high \
|
|
91
|
+
--format sarif > al-sem.sarif
|
|
92
|
+
|
|
93
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
94
|
+
with: { sarif_file: al-sem.sarif }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The first run is noisy by design — generate a baseline once and commit it:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
bunx al-sem analyze . --baseline .al-sem-baseline.json --update-baseline
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Subsequent runs report only NEW findings; the baseline survives nearby edits
|
|
104
|
+
because fingerprints exclude line numbers. `--update-baseline` without
|
|
105
|
+
`--baseline` is a no-op and writes a warning to stderr.
|
|
106
|
+
|
|
107
|
+
## CLI options
|
|
108
|
+
|
|
109
|
+
| Flag | Default | Description |
|
|
110
|
+
|------|---------|-------------|
|
|
111
|
+
| `--alpackages <dir>` | `<ws>/.alpackages` if present | Explicit path to the dependency `.alpackages` directory. |
|
|
112
|
+
| `--format <fmt>` | `auto` | `auto` \| `terminal` \| `json` \| `sarif` \| `html`. `html` emits a self-contained visual report (evidence-path flows + event graph). |
|
|
113
|
+
| `--deterministic` | off | Pin timestamps for byte-stable output. |
|
|
114
|
+
| `--no-dep-summaries` | off | Skip behavioral dependency cold run (structural ABI only). Cached separately from the full-mode cache, so the second run with this flag is warm. |
|
|
115
|
+
| `--dep-cache-dir <dir>` | `~/.al-sem/cache/` | Override the dependency cache directory. |
|
|
116
|
+
| `--dump-model` | off | Emit the full SemanticModel (debug-only, can be >500 MB). |
|
|
117
|
+
| `--min-severity <sev>` | none | Drop findings below `critical \| high \| medium \| low \| info`. |
|
|
118
|
+
| `--detector <ids>` | all | Comma-separated allow-list of detector ids. |
|
|
119
|
+
| `--scope <scope>` | `primary` | `primary` drops findings whose actionable anchor is in a dependency. |
|
|
120
|
+
| `--limit <n>` | unlimited | Cap output at the first N findings (after filtering and scope). |
|
|
121
|
+
| `--group-by <by>` | off | Terminal-only grouped output: `object \| routine \| table \| detector \| file`. |
|
|
122
|
+
| `--baseline <file>` | none | Suppress fingerprints present in the baseline file. |
|
|
123
|
+
| `--update-baseline` | off | Rewrite the baseline file from this run's findings. |
|
|
124
|
+
| `--fail-on <sev>` | none | Exit 1 if any finding at this severity or above (after baseline / filters). |
|
|
125
|
+
|
|
126
|
+
## Cache maintenance
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
bunx al-sem cache prune # remove stale dep-cache entries
|
|
130
|
+
bunx al-sem cache prune --dry-run # classify without deleting
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Stale = version-stamp mismatch with this build, corrupt file, mis-named file, or
|
|
134
|
+
tampered content hash. Valid current-version artifacts are kept untouched.
|
|
135
|
+
`--dep-cache-dir <dir>` overrides the cache location for both `analyze` and
|
|
136
|
+
`cache prune`.
|
|
137
|
+
|
|
138
|
+
## Other commands
|
|
139
|
+
|
|
140
|
+
All of these run the same pipeline as `analyze` and reuse the dependency cache.
|
|
141
|
+
|
|
142
|
+
### `policy` — declarative rules over capability facts
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
bunx al-sem policy check . # workspace's own app(s), bundled default rules
|
|
146
|
+
bunx al-sem policy check . --scope all # include dependency-anchored findings
|
|
147
|
+
bunx al-sem policy explain no-commit-in-event-subscribers
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A policy is a YAML file of rules whose `when`/`except` predicates match capability facts
|
|
151
|
+
(op, resource, root kind, confidence, …) under Kleene tri-state semantics. al-sem auto-detects
|
|
152
|
+
`al-sem.policy.yaml` in the workspace, else applies the 8 bundled defaults (no Commit in event
|
|
153
|
+
subscribers / triggers, no interactive UI or ledger writes from API roots, etc.). `--format
|
|
154
|
+
human | json | sarif`. Like `analyze`, `policy check` defaults to `--scope primary` (the
|
|
155
|
+
workspace's own app); `--scope all` reports model-wide.
|
|
156
|
+
|
|
157
|
+
### `events` — event blast-radius reports
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
bunx al-sem events fanout . # per-event publisher → subscriber counts + coverage
|
|
161
|
+
bunx al-sem events chains . # publisher → subscriber relay trees (cycle/depth-bounded)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Both default to `--scope primary` ("primary participates": the publisher or any subscriber is in
|
|
165
|
+
the workspace's own app); `--scope all` enumerates the entire merged event graph. `--format
|
|
166
|
+
human | json`.
|
|
167
|
+
|
|
168
|
+
### `diff` — compare two snapshots / workspaces / `.app` files
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
bunx al-sem diff old.app new.app # cross-version .app diff (no checkout needed)
|
|
172
|
+
bunx al-sem diff ./baseline.cbor.gz . # persisted snapshot vs live workspace
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Each side may be a workspace directory, a persisted snapshot artifact, or a raw `.app`. Reports
|
|
176
|
+
deltas across five axes — ABI/contract, schema, events, capabilities, permissions — with
|
|
177
|
+
`--format human | json | sarif` and `--fail-on <sev>`.
|
|
178
|
+
|
|
179
|
+
### `fingerprint` — emit a CapabilitySnapshot
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
bunx al-sem fingerprint . --format cbor.gz --out snapshot.cbor.gz
|
|
183
|
+
bunx al-sem fingerprint some.app --format json # snapshot a raw .app directly
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Persist a snapshot for later `diff` (the CI-friendly path: snapshot each release, diff the
|
|
187
|
+
artifacts) or inspect per-root capability fingerprints (`--format human`). Accepts a workspace
|
|
188
|
+
directory or a `.app` file.
|
|
189
|
+
|
|
190
|
+
## Library usage
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import {
|
|
194
|
+
analyzeWorkspace,
|
|
195
|
+
projectFinding,
|
|
196
|
+
filterFindings,
|
|
197
|
+
applyBaseline,
|
|
198
|
+
loadBaseline,
|
|
199
|
+
computeExitCode,
|
|
200
|
+
} from "al-sem";
|
|
201
|
+
|
|
202
|
+
const result = await analyzeWorkspace({ workspaceRoot: "./", deterministic: true });
|
|
203
|
+
const compact = result.findings.map((f) => projectFinding(f, result.model));
|
|
204
|
+
const high = filterFindings(compact, { minSeverity: "high" });
|
|
205
|
+
|
|
206
|
+
// CI gate: load a baseline, drop known findings, fail on remaining "high" or worse.
|
|
207
|
+
const baseline = loadBaseline(".al-sem-baseline.json");
|
|
208
|
+
const newOnly = applyBaseline(high, baseline);
|
|
209
|
+
process.exitCode = computeExitCode(newOnly, "high");
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Re-exports from the package root, by area:
|
|
213
|
+
|
|
214
|
+
| Area | Exports |
|
|
215
|
+
|------|---------|
|
|
216
|
+
| Pipeline | `analyzeWorkspace`, `indexWorkspace`, `AnalyzeWorkspaceOptions`, `AnalyzeWorkspaceResult`, `IndexWorkspaceResult` |
|
|
217
|
+
| Model types | `Finding`, `FindingSummary`, `FindingLocation`, `Diagnostic`, `DetectorStats`, `SemanticModel`, `Routine`, `ObjectDecl`, `Table`, `SourceAnchor`, … (everything from `./model/index.ts`) |
|
|
218
|
+
| Projection | `projectFinding`, `filterFindings`, `FilterOptions`, `groupFindings`, `FindingGroup`, `GroupBy`, `fingerprintOf` |
|
|
219
|
+
| Output | `buildCompactReport`, `CompactReport`, `formatCompactJson`, `formatSarif` |
|
|
220
|
+
| Baseline / CI | `loadBaseline`, `saveBaseline`, `applyBaseline`, `BaselineFile`, `computeExitCode`, `parseFailOn` |
|
|
221
|
+
| Sources | `SourceUnit`, `SourceProvider`, `ExternalSourceProvider` |
|
|
222
|
+
|
|
223
|
+
`indexWorkspace(options)` stops after L2 (discovery + indexing only), for callers
|
|
224
|
+
that drive `resolveModel` themselves. `analyzeWorkspace` runs the full pipeline.
|
|
225
|
+
|
|
226
|
+
## MCP server
|
|
227
|
+
|
|
228
|
+
al-sem also ships an MCP server (`bunx al-sem-mcp` or `bun run src/mcp/server.ts`)
|
|
229
|
+
exposing seven progressive-disclosure tools — `list_findings`, `list_rollups`
|
|
230
|
+
(multi-detector view), `get_finding`, `list_hotspots`, `get_routine_summary`,
|
|
231
|
+
`explain_path`, `get_analysis_health`. See [docs/MCP.md](docs/MCP.md) for wiring
|
|
232
|
+
instructions.
|
|
233
|
+
|
|
234
|
+
## Detectors
|
|
235
|
+
|
|
236
|
+
| Detector | Category | Flags |
|
|
237
|
+
|----------|----------|-------|
|
|
238
|
+
| `d1-db-op-in-loop` | Performance | Database operation reachable inside a loop, interprocedurally; severity by op class. |
|
|
239
|
+
| `d2-event-fanout-in-loop` | Performance | Event raised inside a loop whose subscribers touch the database. |
|
|
240
|
+
| `d3-missing-setloadfields` | Performance | Record retrieval whose loaded field set doesn't cover the fields accessed (same routine + directly-resolved callees). |
|
|
241
|
+
| `d4-repeated-lookup-in-loop` | Performance | Identical Get/FindFirst/FindLast called repeatedly in a loop with a literal key. |
|
|
242
|
+
| `d5-set-based-opportunity` | Performance | Loop body is a single Modify on the iterating record — ModifyAll candidate. |
|
|
243
|
+
| `d7-recursive-event-expansion` | Correctness | Event subscriber chain forms a cycle (runtime infinite recursion). |
|
|
244
|
+
| `d8-commit-in-transaction` | Correctness | Commit inside a posting transaction span — breaks atomicity. |
|
|
245
|
+
| `d9-transaction-span-summary` | Info | Transaction span describes its routine / table / event reach. |
|
|
246
|
+
| `d10-self-modifying-loop` | Correctness | Modify/Validate/Delete on the loop-iterating record. |
|
|
247
|
+
| `d11-modify-without-get` | Correctness | Modify/Validate on a record that was never loaded (no Get/Find/Init/Insert) in this routine. |
|
|
248
|
+
| `d12-dead-integration-event` | Hygiene | Published IntegrationEvent has no subscribers anywhere. |
|
|
249
|
+
| `d13-cross-app-internal-call` | Hygiene | Calls a routine marked Access=Internal in another app. |
|
|
250
|
+
| `d14-dead-routine` | Hygiene | `local procedure` unreachable from any entry-point or non-local procedure. |
|
|
251
|
+
| `d16-obsolete-routine-call` | Compatibility | Calls a routine marked [Obsolete(...)] (info Pending, high Removed). |
|
|
252
|
+
| `d17-min-version-drift` | Compatibility | Calls into a dependency whose installed version exceeds the declared MinVersion (app-level precision; per-routine pending upstream metadata). |
|
|
253
|
+
| `d18-constant-filter-in-loop` | Performance | `SetRange`/`SetFilter` with literal-only arguments inside a loop — the same filter is applied every iteration; hoist it out. |
|
|
254
|
+
| `d19-unused-parameter` | Hygiene | Procedure parameter declared but never referenced in the body. Skips triggers and event-subscribers (signatures dictated by the publisher). |
|
|
255
|
+
| `d20-unreachable-after-exit` | Correctness | Statement that follows `Exit;`, `Error(...)`, or `CurrReport.Quit` at the same nesting level — control leaves the routine before it can run. |
|
|
256
|
+
| `d21-read-without-load` | Correctness | `TestField` / `CalcFields` / `CalcSums` on a record never loaded earlier in the routine — read returns the AL default. D11's read-side sibling. |
|
|
257
|
+
| `d22-flowfield-without-calcfields` | Correctness | Reads a FlowField with no prior `CalcFields(<that field>)` on the same record-var — silent zero/empty result. |
|
|
258
|
+
| `d29-subscriber-modify-on-event-record` | Correctness | Subscriber to an `OnAfter*Modify` / `OnBefore*Delete` event mutates the inbound record parameter — re-fires the same event, recursive-trigger risk. |
|
|
259
|
+
| `d32-constant-boolean-parameter` | Hygiene | `local procedure` Boolean parameter where every resolved primary-app caller passes the same literal — dead parameter, candidate for flattening. |
|
|
260
|
+
| `d33-unfiltered-bulk-write` | Correctness | `DeleteAll` (critical) or `ModifyAll` (high) on a local non-temp record with no prior SetRange/SetFilter since the last Reset — whole-table impact. |
|
|
261
|
+
| `d34-commit-in-loop` | Correctness | `Commit` inside a loop, direct or transitive via callee summary. Per-iteration commits break atomicity; nested-loop case escalates to critical. |
|
|
262
|
+
| `d35-commit-in-event-subscriber` | Correctness | `Commit` reachable from an `[EventSubscriber]` routine. Publisher cannot roll back what the subscriber committed. |
|
|
263
|
+
| `d36-late-setloadfields` | Performance | `SetLoadFields` / `AddLoadFields` placed AFTER a Get/Find, with no later load — the partial-record optimisation cannot apply. |
|
|
264
|
+
| `d37-validate-without-persist` | Correctness | `Validate` on a record with no subsequent Modify/Insert before the record is reloaded or the routine exits — the field write is silently discarded. |
|
|
265
|
+
| `d38-subscriber-to-obsolete-event` | Upgrade | `[EventSubscriber]` bound to a publisher routine carrying `[Obsolete(...)]`. Pending → info (plan migration); Removed → high (subscriber will stop firing). |
|
|
266
|
+
| `d39-record-left-dirty-across-chain` | Correctness | Caller forwards a record to a helper that exits dirty (path-proven `Validate` with no subsequent `Modify`/`Insert` on at least one exit path), and the caller never persists after the call — the field write is silently discarded across the chain. Strictly interprocedural; only fires on path-proven `dirtyAtExit === "yes"` from the P6.T2 walker. |
|
|
267
|
+
| `d40-transitive-load-missing` *(opt-in)* | Correctness | Caller forwards a record to a helper that reads or mutates without loading. Strictly interprocedural — closes D11/D21's by-var-parameter precision gap. Currently opt-in (Phase 4 straight-line walker; Phase 6's full walker re-enables by default after the loop-loaded false-positive class is closed). Enable via `--detector d40-transitive-load-missing`. |
|
|
268
|
+
| `d41-transitive-filter-loss` | Correctness | Caller sets filters on a record, forwards it by-var to a helper that calls Reset, and then performs a filter-sensitive op (FindFirst/FindLast/FindSet/Find/Next/CalcSums/DeleteAll/ModifyAll/Count/IsEmpty) on the record without re-filtering — the filters are silently lost and the subsequent op runs on the unfiltered set. Strictly interprocedural; the post-call-use requirement prevents flagging intentional reset helpers. |
|
|
269
|
+
| `d42-cross-call-wrong-setloadfields` | Performance | Caller narrowed a record's load via SetLoadFields/AddLoadFields then forwards it to a helper that reads a field outside the narrow — the runtime issues an extra SQL round-trip to fetch the missing field, defeating the partial-load optimisation. Strictly interprocedural; only fires when both sides are concrete (caller narrow and callee `requiredLoadedFieldsAtEntry` from the Phase 6 walker). |
|
|
270
|
+
| `d43-event-ishandled-skip` | Correctness | Invoker raises an `IsHandled`-guarded integration event whose subscriber set may set the guard, skipping the invoker's own guarded table writes — the writes are silently bypassed. Dispatch-site (invoker-centric) analysis. |
|
|
271
|
+
| `d44-event-multi-subscriber-overlap` | Correctness | Multiple subscribers to one event write the same table (execution-order-dependent outcome), plus a read-after-write hazard class across subscribers. |
|
|
272
|
+
| `d45-event-transitive-table-exposure` | Correctness | A primary publisher's event reaches, via an N-hop subscriber→publisher relay chain, a subscriber that writes a sensitive table — transitive table exposure the publisher doesn't see locally. |
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Architecture (advanced)
|
|
277
|
+
|
|
278
|
+
A layered pipeline, each layer a pure transform over the previous:
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
L0 parser / symbols parse AL + read .app symbol packages
|
|
282
|
+
L1 providers discover workspace + external sources
|
|
283
|
+
L1.5 deps cached dependency artifacts merged into the index
|
|
284
|
+
L2 index → SemanticIndex (objects, routines, tables, features)
|
|
285
|
+
L3 resolve → SemanticModel (call graph, event graph, coverage)
|
|
286
|
+
L4 engine combined graph → Tarjan SCC → fixed-point RoutineSummary
|
|
287
|
+
L5 detectors walk the model + summaries → Finding[] (scoped to primary)
|
|
288
|
+
L6 projection compact FindingSummary + filter + group + fingerprint
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`analyzeWorkspace` runs the whole pipeline:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
discoverSources → buildSemanticIndex → resolveModel
|
|
295
|
+
→ buildCombinedGraph → computeSummaries → runDetectors
|
|
296
|
+
→ { model, findings, diagnostics, detectorStats }
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Key design principles
|
|
300
|
+
|
|
301
|
+
- **The engine never throws.** Failures — unparseable files, missing symbols, resolution
|
|
302
|
+
gaps — surface as `Diagnostic[]`, never exceptions. There is no "silent clean".
|
|
303
|
+
- **Determinism is a contract.** With `deterministic: true`, output is byte-stable:
|
|
304
|
+
timestamps are pinned, every derived collection has a canonical sort, Map/Set iteration
|
|
305
|
+
never leaks into output unsorted. `test/e2e.test.ts` guards this.
|
|
306
|
+
- **Detectors are pure queries** over the `SemanticModel` + summaries. They prune via
|
|
307
|
+
`RoutineSummary`, then use the shared path-walker with a detector-specific policy to
|
|
308
|
+
build evidence-backed `Finding`s. Each detector dedupes findings by `id` before sorting.
|
|
309
|
+
- **L4 summaries** compose per-routine effects bottom-up over the call graph's SCC
|
|
310
|
+
condensation, using a finite monotone fixed-point so recursive cycles converge.
|
|
311
|
+
- **Dependency direction is one-way**: al-sem knows nothing of al-perf. al-perf
|
|
312
|
+
consumes al-sem as a library.
|
|
313
|
+
|
|
314
|
+
### Source layout
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
src/
|
|
318
|
+
parser/ AL parsing (native bun:ffi tree-sitter) + AST helpers
|
|
319
|
+
symbols/ .app symbol-package reader
|
|
320
|
+
providers/ workspace + external source discovery
|
|
321
|
+
deps/ L1.5 — dependency artifact types, cache, pipeline orchestration
|
|
322
|
+
index/ SemanticIndex construction (objects, routines, intraprocedural features)
|
|
323
|
+
resolve/ call resolution, event graph, record types, coverage → SemanticModel
|
|
324
|
+
engine/ L4 — combined graph, SCC, effect lattice, summary engine, path-walker,
|
|
325
|
+
reverse call graph, entry points, transaction spans, attribute parser
|
|
326
|
+
detectors/ L5 — 34 default detectors plus 1 opt-in (D40), 35 total + shared
|
|
327
|
+
DetectorContext, confidence mapping, registry
|
|
328
|
+
policy/ L6 — declarative capability-fact rules (Kleene tri-state evaluator)
|
|
329
|
+
snapshot/ CapabilitySnapshot compose + serialize (json/cbor/cbor.gz)
|
|
330
|
+
diff/ cross-snapshot delta engine (ABI, schema, events, capabilities, permissions)
|
|
331
|
+
model/ shared types — entities, graph, summary, finding, identity, ids, analysisRole
|
|
332
|
+
cli/ commander CLI + terminal / JSON / SARIF formatters
|
|
333
|
+
mcp/ MCP server (seven tools, progressive-disclosure)
|
|
334
|
+
index.ts public library entry point
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Development
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
bun install
|
|
341
|
+
bun test # run all tests
|
|
342
|
+
bun run typecheck # bunx tsc --noEmit
|
|
343
|
+
bun run lint # bunx biome check src test
|
|
344
|
+
bun run format # bunx biome format --write src test
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Tech stack:** Bun · TypeScript · `bun:ffi` + native `tree-sitter-al` shared library ·
|
|
348
|
+
`commander` · `fflate` (`.app` package extraction) · `bun:test` · Biome.
|
|
349
|
+
|
|
350
|
+
Design specs and implementation plans live under `docs/superpowers/` —
|
|
351
|
+
`specs/` for designs, `plans/` for the phased TDD implementation plans.
|
|
352
|
+
|
|
353
|
+
## Status
|
|
354
|
+
|
|
355
|
+
See [docs/superpowers/STATUS.md](docs/superpowers/STATUS.md) for the current phase
|
|
356
|
+
status and roadmap.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
**Author**: Torben Leth
|
|
361
|
+
**License**: MIT (see [LICENSE](LICENSE))
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "al-sem",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Static semantic analysis engine for Microsoft Business Central AL code",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Torben Leth",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"business-central",
|
|
9
|
+
"al",
|
|
10
|
+
"static-analysis",
|
|
11
|
+
"semantic-analysis",
|
|
12
|
+
"call-graph",
|
|
13
|
+
"performance",
|
|
14
|
+
"linter",
|
|
15
|
+
"dynamics-365",
|
|
16
|
+
"bun"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"!src/parser/native/lib-*",
|
|
21
|
+
"!src/parser/native/lib.*",
|
|
22
|
+
"scripts",
|
|
23
|
+
"tsconfig.json"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/SShadowS/al-sem.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/SShadowS/al-sem#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/SShadowS/al-sem/issues"
|
|
32
|
+
},
|
|
33
|
+
"type": "module",
|
|
34
|
+
"module": "src/index.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": "./src/index.ts"
|
|
37
|
+
},
|
|
38
|
+
"bin": {
|
|
39
|
+
"al-sem": "src/cli/index.ts",
|
|
40
|
+
"al-sem-mcp": "src/mcp/server.ts"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"bun": ">=1.0.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "bun test",
|
|
47
|
+
"typecheck": "bunx tsc --noEmit",
|
|
48
|
+
"lint": "bunx biome check src test",
|
|
49
|
+
"format": "bunx biome format --write src test",
|
|
50
|
+
"postinstall": "bun run scripts/fetch-native-parser.ts"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
54
|
+
"cbor-x": "^1.6.4",
|
|
55
|
+
"commander": "^14.0.3",
|
|
56
|
+
"fflate": "^0.8.2",
|
|
57
|
+
"yaml": "^2.9.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@biomejs/biome": "^1.9.0",
|
|
61
|
+
"@types/bun": "latest",
|
|
62
|
+
"typescript": "^5.6.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// scripts/d40-diff.ts
|
|
2
|
+
// One-shot diff between two precision runs (Round N vs Round N+1).
|
|
3
|
+
// Prints the count of added/removed findings + a sample of removed rootCauses
|
|
4
|
+
// so the reviewer can confirm a fix targeted the intended FP class.
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
interface FindingLike {
|
|
9
|
+
id: string;
|
|
10
|
+
severity: string;
|
|
11
|
+
rootCause: string;
|
|
12
|
+
}
|
|
13
|
+
interface RunJson {
|
|
14
|
+
findings: FindingLike[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const [beforePath, afterPath] = process.argv.slice(2);
|
|
18
|
+
if (!beforePath || !afterPath) {
|
|
19
|
+
console.error("usage: d40-diff.ts <before.json> <after.json>");
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const before = JSON.parse(readFileSync(beforePath, "utf8")) as RunJson;
|
|
24
|
+
const after = JSON.parse(readFileSync(afterPath, "utf8")) as RunJson;
|
|
25
|
+
|
|
26
|
+
const idsBefore = new Set(before.findings.map((f) => f.id));
|
|
27
|
+
const idsAfter = new Set(after.findings.map((f) => f.id));
|
|
28
|
+
const removed = before.findings.filter((f) => !idsAfter.has(f.id));
|
|
29
|
+
const added = after.findings.filter((f) => !idsBefore.has(f.id));
|
|
30
|
+
|
|
31
|
+
console.log(`before: ${before.findings.length}, after: ${after.findings.length}`);
|
|
32
|
+
console.log(`removed: ${removed.length}, added: ${added.length}`);
|
|
33
|
+
console.log("");
|
|
34
|
+
console.log("=== Sample of removed findings (first 10) ===");
|
|
35
|
+
for (const f of removed.slice(0, 10)) {
|
|
36
|
+
console.log(`- [${f.severity}] ${f.rootCause}`);
|
|
37
|
+
}
|
|
38
|
+
if (added.length > 0) {
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log("=== Sample of added findings (first 10) ===");
|
|
41
|
+
for (const f of added.slice(0, 10)) {
|
|
42
|
+
console.log(`- [${f.severity}] ${f.rootCause}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// scripts/fetch-native-parser.ts
|
|
2
|
+
// Postinstall: idempotent provisioning of the fat shim for the current platform.
|
|
3
|
+
// Resolution order:
|
|
4
|
+
// 1. AL_SEM_NATIVE_PARSER_PATH — copy from absolute path, write meta.
|
|
5
|
+
// 2. AL_SEM_NATIVE_PARSER_OFFLINE=1 — require canonical lib; normalize meta if absent.
|
|
6
|
+
// 3. AL_SEM_NATIVE_PARSER_BASE_URL — override download base URL.
|
|
7
|
+
// 4. Default: download from GH releases.
|
|
8
|
+
// Failures other than offline-missing-lib soft-fail (warn + exit 0). Runtime
|
|
9
|
+
// surfaces the missing parser as a single NativeParserUnavailableError.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { platform as nodePlatform, arch as nodeArch } from "node:process";
|
|
18
|
+
|
|
19
|
+
export const TREE_SITTER_AL_RELEASE_TAG = "v2.5.2-shim";
|
|
20
|
+
|
|
21
|
+
const SUPPORTED: Record<string, string> = {
|
|
22
|
+
"win32-x64": "tree-sitter-al-win32-x64.dll",
|
|
23
|
+
"linux-x64": "tree-sitter-al-linux-x64.so",
|
|
24
|
+
"darwin-x64": "tree-sitter-al-darwin-x64.dylib",
|
|
25
|
+
"darwin-arm64": "tree-sitter-al-darwin-arm64.dylib",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function resolveAssetName(platform: string, arch: string): string {
|
|
29
|
+
const key = `${platform}-${arch}`;
|
|
30
|
+
const name = SUPPORTED[key];
|
|
31
|
+
if (!name) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`al-sem: native parser unavailable for ${key}. Supported: ${Object.keys(SUPPORTED).join(", ")}. ` +
|
|
34
|
+
`File an issue at https://github.com/SShadowS/tree-sitter-al/issues to add this platform.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return name;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LibMeta {
|
|
41
|
+
tag: string;
|
|
42
|
+
platform: string;
|
|
43
|
+
arch: string;
|
|
44
|
+
asset: string;
|
|
45
|
+
sha256: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sha256Hex(buf: Buffer): string {
|
|
49
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function normalizeMeta(
|
|
53
|
+
libPath: string,
|
|
54
|
+
ref: { tag: string; platform: string; arch: string },
|
|
55
|
+
): LibMeta {
|
|
56
|
+
const bytes = readFileSync(libPath);
|
|
57
|
+
return {
|
|
58
|
+
tag: ref.tag,
|
|
59
|
+
platform: ref.platform,
|
|
60
|
+
arch: ref.arch,
|
|
61
|
+
asset: resolveAssetName(ref.platform, ref.arch),
|
|
62
|
+
sha256: sha256Hex(bytes),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function writeMeta(nativeDir: string, meta: LibMeta): void {
|
|
67
|
+
writeFileSync(join(nativeDir, "lib.meta.json"), JSON.stringify(meta, null, 2));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function readMeta(nativeDir: string): LibMeta | null {
|
|
71
|
+
const p = join(nativeDir, "lib.meta.json");
|
|
72
|
+
if (!existsSync(p)) return null;
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(readFileSync(p, "utf8")) as LibMeta;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function libFilename(tag: string, platform: string, arch: string): string {
|
|
81
|
+
const asset = SUPPORTED[`${platform}-${arch}`] ?? "";
|
|
82
|
+
const ext = asset.split(".").pop() ?? "so";
|
|
83
|
+
return `lib-${tag}-${platform}-${arch}.${ext}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function downloadTo(url: string, destPath: string): Promise<void> {
|
|
87
|
+
const res = await fetch(url);
|
|
88
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
89
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
90
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
91
|
+
writeFileSync(destPath, buf);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function main(): Promise<void> {
|
|
95
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
96
|
+
const nativeDir = join(here, "..", "src", "parser", "native");
|
|
97
|
+
mkdirSync(nativeDir, { recursive: true });
|
|
98
|
+
const platform = nodePlatform;
|
|
99
|
+
const arch = nodeArch;
|
|
100
|
+
const tag = TREE_SITTER_AL_RELEASE_TAG;
|
|
101
|
+
|
|
102
|
+
let asset: string;
|
|
103
|
+
try {
|
|
104
|
+
asset = resolveAssetName(platform, arch);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
process.stderr.write(`${(err as Error).message}\n`);
|
|
107
|
+
process.exit(0); // soft-fail; runtime surfaces it
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const targetFile = libFilename(tag, platform, arch);
|
|
111
|
+
const targetPath = join(nativeDir, targetFile);
|
|
112
|
+
|
|
113
|
+
// 1. AL_SEM_NATIVE_PARSER_PATH override
|
|
114
|
+
const explicit = process.env.AL_SEM_NATIVE_PARSER_PATH;
|
|
115
|
+
if (explicit) {
|
|
116
|
+
if (!existsSync(explicit)) {
|
|
117
|
+
process.stderr.write(`al-sem: AL_SEM_NATIVE_PARSER_PATH=${explicit} does not exist\n`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
copyFileSync(explicit, targetPath);
|
|
121
|
+
writeMeta(nativeDir, normalizeMeta(targetPath, { tag, platform, arch }));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. Offline mode
|
|
126
|
+
if (process.env.AL_SEM_NATIVE_PARSER_OFFLINE === "1") {
|
|
127
|
+
if (!existsSync(targetPath)) {
|
|
128
|
+
process.stderr.write(
|
|
129
|
+
`al-sem: AL_SEM_NATIVE_PARSER_OFFLINE=1 but ${targetPath} is missing. ` +
|
|
130
|
+
`Preseed the artifact or unset the env var.\n`,
|
|
131
|
+
);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
if (!readMeta(nativeDir)) {
|
|
135
|
+
writeMeta(nativeDir, normalizeMeta(targetPath, { tag, platform, arch }));
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 3. Idempotent check
|
|
141
|
+
const meta = readMeta(nativeDir);
|
|
142
|
+
if (
|
|
143
|
+
meta &&
|
|
144
|
+
meta.tag === tag &&
|
|
145
|
+
meta.platform === platform &&
|
|
146
|
+
meta.arch === arch &&
|
|
147
|
+
existsSync(targetPath)
|
|
148
|
+
) {
|
|
149
|
+
const actual = sha256Hex(readFileSync(targetPath));
|
|
150
|
+
if (actual === meta.sha256) return;
|
|
151
|
+
process.stderr.write(`al-sem: cache mismatch — re-downloading ${targetFile}\n`);
|
|
152
|
+
try { unlinkSync(targetPath); } catch {}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 4. Download
|
|
156
|
+
const baseUrl =
|
|
157
|
+
process.env.AL_SEM_NATIVE_PARSER_BASE_URL ??
|
|
158
|
+
"https://github.com/SShadowS/tree-sitter-al/releases/download/";
|
|
159
|
+
const url = `${baseUrl}${tag}/${asset}`;
|
|
160
|
+
try {
|
|
161
|
+
await downloadTo(url, targetPath);
|
|
162
|
+
writeMeta(nativeDir, normalizeMeta(targetPath, { tag, platform, arch }));
|
|
163
|
+
} catch (err) {
|
|
164
|
+
process.stderr.write(
|
|
165
|
+
`al-sem: could not fetch ${url}: ${(err as Error).message}. ` +
|
|
166
|
+
`Re-run bun install with network, or set AL_SEM_NATIVE_PARSER_BASE_URL ` +
|
|
167
|
+
`(mirror) or AL_SEM_NATIVE_PARSER_PATH (preseeded artifact).\n`,
|
|
168
|
+
);
|
|
169
|
+
process.exit(0); // soft-fail; defer to runtime
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Allow `bun run scripts/fetch-native-parser.ts` AND `import` (for tests).
|
|
174
|
+
if (import.meta.main) {
|
|
175
|
+
main().catch((err) => {
|
|
176
|
+
process.stderr.write(`al-sem postinstall: ${err}\n`);
|
|
177
|
+
process.exit(0);
|
|
178
|
+
});
|
|
179
|
+
}
|