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/src/cli/index.ts
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { pruneCache } from "../deps/dependency-cache.ts";
|
|
4
|
+
import { ALL_DETECTORS, DEFAULT_DETECTORS } from "../detectors/registry.ts";
|
|
5
|
+
import { analyzeWorkspace } from "../index.ts";
|
|
6
|
+
import type { Scope } from "../model/entities.ts";
|
|
7
|
+
import { filterFindings } from "../projection/finding-filters.ts";
|
|
8
|
+
import { groupFindings } from "../projection/finding-groups.ts";
|
|
9
|
+
import type { FindingSummary } from "../projection/finding-summary.ts";
|
|
10
|
+
import { projectFinding } from "../projection/finding-summary.ts";
|
|
11
|
+
import { applyBaseline, loadBaseline, saveBaseline } from "./baseline.ts";
|
|
12
|
+
import { runDiff } from "./diff.ts";
|
|
13
|
+
import { runEventsChains } from "./events-chains.ts";
|
|
14
|
+
import { runEventsFanout } from "./events-fanout.ts";
|
|
15
|
+
import { computeExitCode, parseFailOn } from "./exit-code.ts";
|
|
16
|
+
import { runFingerprint } from "./fingerprint.ts";
|
|
17
|
+
import { formatCompactJson } from "./format-compact-json.ts";
|
|
18
|
+
import { formatHtml } from "./format-html.ts";
|
|
19
|
+
import { formatFullModelJson } from "./format-json.ts";
|
|
20
|
+
import { formatSarif } from "./format-sarif.ts";
|
|
21
|
+
import { formatTerminal } from "./format-terminal.ts";
|
|
22
|
+
import { runPolicyCheck, runPolicyExplain } from "./policy.ts";
|
|
23
|
+
|
|
24
|
+
const SCOPE_VALUES = ["primary", "all"] as const;
|
|
25
|
+
|
|
26
|
+
const program = new Command();
|
|
27
|
+
|
|
28
|
+
program
|
|
29
|
+
.name("al-sem")
|
|
30
|
+
.description("Static semantic analysis engine for Microsoft Business Central AL code");
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command("analyze")
|
|
34
|
+
.argument("<workspace>", "path to the AL workspace root")
|
|
35
|
+
.option("--alpackages <dir>", "path to the .alpackages directory")
|
|
36
|
+
.option("--format <format>", "output format: auto | terminal | json | sarif | html", "auto")
|
|
37
|
+
.option("--deterministic", "pin timestamps for byte-stable output", false)
|
|
38
|
+
.option("--no-dep-summaries", "skip the behavioral dependency cold run (structural ABI only)")
|
|
39
|
+
.option("--no-roots-config", "ignore roots.config.json overlay even if present")
|
|
40
|
+
.option(
|
|
41
|
+
"--dep-cache-dir <dir>",
|
|
42
|
+
"override the dependency cache directory (default ~/.al-sem/cache/)",
|
|
43
|
+
)
|
|
44
|
+
.option("--dump-model", "emit the full SemanticModel (debug-only, can be >500 MB)")
|
|
45
|
+
.option(
|
|
46
|
+
"--min-severity <sev>",
|
|
47
|
+
"drop findings below this severity: critical|high|medium|low|info",
|
|
48
|
+
)
|
|
49
|
+
.option(
|
|
50
|
+
"--detector <ids>",
|
|
51
|
+
"comma-separated allow-list of detector ids (e.g. d1-db-op-in-loop,d3-missing-setloadfields)",
|
|
52
|
+
)
|
|
53
|
+
.option(
|
|
54
|
+
"--scope <scope>",
|
|
55
|
+
"primary (default) drops findings anchored in a dependency; all keeps them",
|
|
56
|
+
"primary",
|
|
57
|
+
)
|
|
58
|
+
.option("--limit <n>", "cap output at first N findings (after filtering)", (v: string) =>
|
|
59
|
+
Number.parseInt(v, 10),
|
|
60
|
+
)
|
|
61
|
+
.option("--group-by <by>", "group terminal output by: object | routine | table | detector | file")
|
|
62
|
+
.option("--baseline <file>", "baseline file (fingerprints to suppress on this run)")
|
|
63
|
+
.option("--update-baseline", "rewrite the baseline file from this run's findings", false)
|
|
64
|
+
.option(
|
|
65
|
+
"--fail-on <sev>",
|
|
66
|
+
"exit 1 if any finding at this severity or above (after baseline / filters)",
|
|
67
|
+
)
|
|
68
|
+
.action(
|
|
69
|
+
async (
|
|
70
|
+
workspace: string,
|
|
71
|
+
opts: {
|
|
72
|
+
alpackages?: string;
|
|
73
|
+
format: string;
|
|
74
|
+
deterministic: boolean;
|
|
75
|
+
depSummaries: boolean;
|
|
76
|
+
rootsConfig: boolean;
|
|
77
|
+
depCacheDir?: string;
|
|
78
|
+
dumpModel?: boolean;
|
|
79
|
+
minSeverity?: string;
|
|
80
|
+
detector?: string;
|
|
81
|
+
scope: string;
|
|
82
|
+
limit?: number;
|
|
83
|
+
groupBy?: string;
|
|
84
|
+
baseline?: string;
|
|
85
|
+
updateBaseline: boolean;
|
|
86
|
+
failOn?: string;
|
|
87
|
+
},
|
|
88
|
+
) => {
|
|
89
|
+
// --- enum flag validation ---
|
|
90
|
+
const SEVERITY_VALUES = ["critical", "high", "medium", "low", "info"] as const;
|
|
91
|
+
if (opts.minSeverity !== undefined && !SEVERITY_VALUES.includes(opts.minSeverity as never)) {
|
|
92
|
+
process.stderr.write(
|
|
93
|
+
`al-sem: invalid --min-severity '${opts.minSeverity}'. Expected one of: ${SEVERITY_VALUES.join(", ")}\n`,
|
|
94
|
+
);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!SCOPE_VALUES.includes(opts.scope as never)) {
|
|
100
|
+
process.stderr.write(
|
|
101
|
+
`al-sem: invalid --scope '${opts.scope}'. Expected one of: ${SCOPE_VALUES.join(", ")}\n`,
|
|
102
|
+
);
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const GROUP_BY_VALUES = ["object", "routine", "table", "detector", "file"] as const;
|
|
108
|
+
if (opts.groupBy !== undefined && !GROUP_BY_VALUES.includes(opts.groupBy as never)) {
|
|
109
|
+
process.stderr.write(
|
|
110
|
+
`al-sem: invalid --group-by '${opts.groupBy}'. Expected one of: ${GROUP_BY_VALUES.join(", ")}\n`,
|
|
111
|
+
);
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let failOn: FindingSummary["severity"] | undefined;
|
|
117
|
+
if (opts.failOn !== undefined) {
|
|
118
|
+
try {
|
|
119
|
+
failOn = parseFailOn(opts.failOn);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
process.stderr.write(`al-sem: ${(err as Error).message}\n`);
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (opts.updateBaseline === true && opts.baseline === undefined) {
|
|
128
|
+
process.stderr.write("al-sem: --update-baseline has no effect without --baseline\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Safe casts — values validated above
|
|
132
|
+
const minSeverity = opts.minSeverity as FindingSummary["severity"] | undefined;
|
|
133
|
+
const scope = opts.scope as "primary" | "all";
|
|
134
|
+
const groupBy = opts.groupBy as
|
|
135
|
+
| "object"
|
|
136
|
+
| "routine"
|
|
137
|
+
| "table"
|
|
138
|
+
| "detector"
|
|
139
|
+
| "file"
|
|
140
|
+
| undefined;
|
|
141
|
+
|
|
142
|
+
// If the user explicitly opted into a detector via `--detector` that isn't in
|
|
143
|
+
// DEFAULT_DETECTORS (e.g. d40-transitive-load-missing), run the union — otherwise
|
|
144
|
+
// the registry would silently never produce that detector's findings even though
|
|
145
|
+
// the user asked for them. Default registry remains in effect when --detector
|
|
146
|
+
// is absent or only names default-registry detectors.
|
|
147
|
+
const requestedDetectors = opts.detector
|
|
148
|
+
? opts.detector.split(",").map((s: string) => s.trim())
|
|
149
|
+
: undefined;
|
|
150
|
+
const defaultNames = new Set(DEFAULT_DETECTORS.map((d) => d.name));
|
|
151
|
+
const needsOptIn = requestedDetectors?.some((n) => !defaultNames.has(n)) ?? false;
|
|
152
|
+
const detectorList = needsOptIn ? ALL_DETECTORS : undefined;
|
|
153
|
+
|
|
154
|
+
const result = await analyzeWorkspace({
|
|
155
|
+
workspaceRoot: workspace,
|
|
156
|
+
alpackagesDir: opts.alpackages,
|
|
157
|
+
deterministic: opts.deterministic,
|
|
158
|
+
noDepSummaries: opts.depSummaries === false,
|
|
159
|
+
noRootsConfig: opts.rootsConfig === false,
|
|
160
|
+
dependencyCacheDir: opts.depCacheDir,
|
|
161
|
+
detectors: detectorList,
|
|
162
|
+
});
|
|
163
|
+
if (opts.dumpModel === true) {
|
|
164
|
+
process.stdout.write(`${formatFullModelJson(result)}\n`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- filter pipeline ---
|
|
169
|
+
// First pass: severity + detector (no limit yet)
|
|
170
|
+
const projected = result.findings.map((f) => projectFinding(f, result.model));
|
|
171
|
+
const filtered = filterFindings(projected, {
|
|
172
|
+
minSeverity,
|
|
173
|
+
detectors: opts.detector
|
|
174
|
+
? opts.detector.split(",").map((s: string) => s.trim())
|
|
175
|
+
: undefined,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// scope filter: primary (default) drops findings whose primaryLocation object is in a dep
|
|
179
|
+
const objectsById = new Map(result.model.objects.map((o) => [o.id, o]));
|
|
180
|
+
const scoped =
|
|
181
|
+
scope === "all"
|
|
182
|
+
? filtered
|
|
183
|
+
: filtered.filter((f) => {
|
|
184
|
+
const objId = f.primaryLocation.objectId;
|
|
185
|
+
if (!objId) return true; // unknown object — keep (don't silently drop)
|
|
186
|
+
return objectsById.get(objId)?.analysisRole !== "dependency";
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Second pass: limit only (applied after scope drop so users get expected count)
|
|
190
|
+
const limited = opts.limit !== undefined ? scoped.slice(0, opts.limit) : scoped;
|
|
191
|
+
|
|
192
|
+
// --- baseline suppression ---
|
|
193
|
+
const baseline = opts.baseline ? loadBaseline(opts.baseline) : new Set<string>();
|
|
194
|
+
const newFindings = applyBaseline(limited, baseline);
|
|
195
|
+
if (opts.updateBaseline && opts.baseline) {
|
|
196
|
+
// Save the current scoped+filtered set (not just new ones) — this becomes the new floor.
|
|
197
|
+
saveBaseline(opts.baseline, limited);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const filteredIds = new Set(newFindings.map((f) => f.id));
|
|
201
|
+
const filteredResult = {
|
|
202
|
+
...result,
|
|
203
|
+
findings: result.findings.filter((f) => filteredIds.has(f.id)),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const format =
|
|
207
|
+
opts.format === "auto" ? (process.stdout.isTTY ? "terminal" : "json") : opts.format;
|
|
208
|
+
|
|
209
|
+
// group-by terminal rendering
|
|
210
|
+
if (groupBy && format === "terminal") {
|
|
211
|
+
const groups = groupFindings(newFindings, groupBy);
|
|
212
|
+
const cov = result.model.coverage;
|
|
213
|
+
const lines: string[] = [];
|
|
214
|
+
lines.push(
|
|
215
|
+
`Analysed ${cov.routinesTotal} routines (${cov.routinesBodyAvailable} with bodies, ${cov.routinesParseIncomplete.length} parse-incomplete); ${cov.sourceUnitsParsed}/${cov.sourceUnitsTotal} source units parsed; ${cov.opaqueApps.length} opaque app(s).`,
|
|
216
|
+
);
|
|
217
|
+
lines.push("");
|
|
218
|
+
lines.push(`Grouped by ${groupBy} (top ${groups.length}):`);
|
|
219
|
+
for (const g of groups) {
|
|
220
|
+
lines.push(` ${g.key}: ${g.findings.length}`);
|
|
221
|
+
}
|
|
222
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
223
|
+
// still apply exit-code for group-by path
|
|
224
|
+
if (failOn) {
|
|
225
|
+
process.exitCode = computeExitCode(newFindings, failOn);
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (format === "json") {
|
|
231
|
+
process.stdout.write(`${formatCompactJson(filteredResult)}\n`);
|
|
232
|
+
} else if (format === "terminal") {
|
|
233
|
+
process.stdout.write(`${formatTerminal(filteredResult)}\n`);
|
|
234
|
+
} else if (format === "sarif") {
|
|
235
|
+
process.stdout.write(`${formatSarif(filteredResult)}\n`);
|
|
236
|
+
} else if (format === "html") {
|
|
237
|
+
process.stdout.write(`${formatHtml(filteredResult)}\n`);
|
|
238
|
+
} else {
|
|
239
|
+
process.stderr.write(
|
|
240
|
+
`al-sem: unknown format '${format}'. Expected: auto, terminal, json, sarif, html\n`,
|
|
241
|
+
);
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// --- exit-code gate ---
|
|
247
|
+
if (failOn) {
|
|
248
|
+
process.exitCode = computeExitCode(newFindings, failOn);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
program
|
|
254
|
+
.command("fingerprint")
|
|
255
|
+
.description("Show the per-root capability fingerprint for a workspace (Phase 1 §4.4 surface)")
|
|
256
|
+
.argument("<workspace>", "path to the AL workspace root")
|
|
257
|
+
.option("--format <fmt>", "output format: human | json | cbor | cbor.gz")
|
|
258
|
+
.option("--out <path>", "output file or shard directory")
|
|
259
|
+
.option("--shard <mode>", "primary-only | all")
|
|
260
|
+
.option("--deterministic", "pin generatedAt for byte-stable output", false)
|
|
261
|
+
.option("--alsem-version <v>", "version string in output", "0.0.0")
|
|
262
|
+
.option("--no-roots-config", "ignore roots.config.json overlay")
|
|
263
|
+
.option("--roots <kinds>", "comma-separated RootKind list (human only)")
|
|
264
|
+
.option(
|
|
265
|
+
"--routine <selector>",
|
|
266
|
+
"routine selector (display or StableRoutineId); repeatable",
|
|
267
|
+
collectRoutine,
|
|
268
|
+
[],
|
|
269
|
+
)
|
|
270
|
+
.option("--include-inherited", "include inherited facts (default)", true)
|
|
271
|
+
.option("--no-include-inherited", "direct facts only")
|
|
272
|
+
.option("--witness <mode>", "false | 0 | <1..256> | all", "3")
|
|
273
|
+
.option("--strict", "exit non-zero on any analyzer error-severity diagnostic", false)
|
|
274
|
+
.option("--debug", "print stack on internal error", false)
|
|
275
|
+
.option("--verbosity <v>", "compact | full (human only)", "compact")
|
|
276
|
+
.option("--color", "force color output", false)
|
|
277
|
+
.action(
|
|
278
|
+
async (
|
|
279
|
+
workspace: string,
|
|
280
|
+
cmdOpts: {
|
|
281
|
+
format?: string;
|
|
282
|
+
out?: string;
|
|
283
|
+
shard?: string;
|
|
284
|
+
deterministic?: boolean;
|
|
285
|
+
alsemVersion?: string;
|
|
286
|
+
rootsConfig: boolean;
|
|
287
|
+
roots?: string;
|
|
288
|
+
routine?: string[];
|
|
289
|
+
includeInherited?: boolean;
|
|
290
|
+
witness?: string;
|
|
291
|
+
strict?: boolean;
|
|
292
|
+
debug?: boolean;
|
|
293
|
+
verbosity?: string;
|
|
294
|
+
color?: boolean;
|
|
295
|
+
},
|
|
296
|
+
) => {
|
|
297
|
+
const specified = new Set<string>();
|
|
298
|
+
if (cmdOpts.roots !== undefined) specified.add("roots");
|
|
299
|
+
if (Array.isArray(cmdOpts.routine) && cmdOpts.routine.length > 0)
|
|
300
|
+
specified.add("routineSelectors");
|
|
301
|
+
if (cmdOpts.includeInherited === false) specified.add("includeInherited");
|
|
302
|
+
if (cmdOpts.witness !== undefined && cmdOpts.witness !== "3") specified.add("witness");
|
|
303
|
+
const witness =
|
|
304
|
+
cmdOpts.witness === undefined
|
|
305
|
+
? undefined
|
|
306
|
+
: cmdOpts.witness === "false"
|
|
307
|
+
? false
|
|
308
|
+
: cmdOpts.witness === "all"
|
|
309
|
+
? ("all" as const)
|
|
310
|
+
: Number.parseInt(cmdOpts.witness, 10);
|
|
311
|
+
const exitCode = await runFingerprint({
|
|
312
|
+
workspace,
|
|
313
|
+
format: cmdOpts.format as never,
|
|
314
|
+
out: cmdOpts.out,
|
|
315
|
+
shard: cmdOpts.shard as "primary-only" | "all" | undefined,
|
|
316
|
+
deterministic: cmdOpts.deterministic === true,
|
|
317
|
+
alsemVersion: cmdOpts.alsemVersion,
|
|
318
|
+
noRootsConfig: cmdOpts.rootsConfig === false,
|
|
319
|
+
roots:
|
|
320
|
+
cmdOpts.roots !== undefined
|
|
321
|
+
? String(cmdOpts.roots)
|
|
322
|
+
.split(",")
|
|
323
|
+
.map((s) => s.trim())
|
|
324
|
+
.filter((s) => s.length > 0)
|
|
325
|
+
: undefined,
|
|
326
|
+
routineSelectors:
|
|
327
|
+
Array.isArray(cmdOpts.routine) && cmdOpts.routine.length > 0
|
|
328
|
+
? cmdOpts.routine
|
|
329
|
+
: undefined,
|
|
330
|
+
includeInherited: cmdOpts.includeInherited !== false,
|
|
331
|
+
witness,
|
|
332
|
+
strict: cmdOpts.strict === true,
|
|
333
|
+
debug: cmdOpts.debug === true,
|
|
334
|
+
verbosity: cmdOpts.verbosity as "compact" | "full" | undefined,
|
|
335
|
+
color: cmdOpts.color === true,
|
|
336
|
+
_specifiedFlags: specified,
|
|
337
|
+
});
|
|
338
|
+
process.exit(exitCode);
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
program
|
|
343
|
+
.command("diff")
|
|
344
|
+
.argument("<old>", "snapshot file or workspace directory")
|
|
345
|
+
.argument("<new>", "snapshot file or workspace directory")
|
|
346
|
+
.option("--format <fmt>", "output format: human | json | sarif", "human")
|
|
347
|
+
.option("--out <path>", "write output to file instead of stdout")
|
|
348
|
+
.option("--coverage-policy <policy>", "loose | strict", "loose")
|
|
349
|
+
.option("--renames <path>", "rename overlay JSON file")
|
|
350
|
+
.option("--fail-on <sev>", "exit 1 if any finding ≥ severity")
|
|
351
|
+
.option("--strict", "exit 1 on analyzer error-severity diagnostic", false)
|
|
352
|
+
.option("--deterministic", "pin generatedAt for byte-stable output", false)
|
|
353
|
+
.option("--alsem-version <v>", "version string in output", "0.0.0")
|
|
354
|
+
.option("--debug", "print stack on internal error", false)
|
|
355
|
+
.action(
|
|
356
|
+
async (
|
|
357
|
+
oldArg: string,
|
|
358
|
+
newArg: string,
|
|
359
|
+
cmdOpts: {
|
|
360
|
+
format?: string;
|
|
361
|
+
out?: string;
|
|
362
|
+
coveragePolicy?: string;
|
|
363
|
+
renames?: string;
|
|
364
|
+
failOn?: string;
|
|
365
|
+
strict?: boolean;
|
|
366
|
+
deterministic?: boolean;
|
|
367
|
+
alsemVersion?: string;
|
|
368
|
+
debug?: boolean;
|
|
369
|
+
},
|
|
370
|
+
) => {
|
|
371
|
+
const exitCode = await runDiff({
|
|
372
|
+
oldArg,
|
|
373
|
+
newArg,
|
|
374
|
+
format: cmdOpts.format as never,
|
|
375
|
+
out: cmdOpts.out,
|
|
376
|
+
coveragePolicy: cmdOpts.coveragePolicy as never,
|
|
377
|
+
renamesPath: cmdOpts.renames,
|
|
378
|
+
failOn: cmdOpts.failOn as never,
|
|
379
|
+
strict: cmdOpts.strict === true,
|
|
380
|
+
deterministic: cmdOpts.deterministic === true,
|
|
381
|
+
alsemVersion: cmdOpts.alsemVersion,
|
|
382
|
+
debug: cmdOpts.debug === true,
|
|
383
|
+
});
|
|
384
|
+
process.exit(exitCode);
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const events = program.command("events").description("Event blast-radius reports");
|
|
389
|
+
|
|
390
|
+
events
|
|
391
|
+
.command("fanout")
|
|
392
|
+
.description("Per-publisher fanout report (direct subscribers + coverage)")
|
|
393
|
+
.argument("<workspace>", "path to the AL workspace root")
|
|
394
|
+
.option("--format <fmt>", "human | json", "human")
|
|
395
|
+
.option("--out <path>", "output file")
|
|
396
|
+
.option("--coverage-policy <p>", "warn | strict | ignore", "warn")
|
|
397
|
+
.option("--deterministic", "pin generated_at for byte-stable output", false)
|
|
398
|
+
.option("--alsem-version <v>", "version string in output", "0.0.0")
|
|
399
|
+
.option("--strict", "exit 1 on analyzer error-severity diagnostic", false)
|
|
400
|
+
.option(
|
|
401
|
+
"--scope <scope>",
|
|
402
|
+
"primary (default) drops dependency-only events; all keeps them",
|
|
403
|
+
"primary",
|
|
404
|
+
)
|
|
405
|
+
.action(async (workspace: string, cmdOpts: Record<string, unknown>) => {
|
|
406
|
+
if (!SCOPE_VALUES.includes(cmdOpts.scope as never)) {
|
|
407
|
+
process.stderr.write(
|
|
408
|
+
`al-sem: invalid --scope '${cmdOpts.scope}'. Expected one of: ${SCOPE_VALUES.join(", ")}\n`,
|
|
409
|
+
);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
const exitCode = await runEventsFanout({
|
|
413
|
+
workspace,
|
|
414
|
+
format: cmdOpts.format as never,
|
|
415
|
+
out: cmdOpts.out as string | undefined,
|
|
416
|
+
coveragePolicy: cmdOpts.coveragePolicy as never,
|
|
417
|
+
deterministic: cmdOpts.deterministic === true,
|
|
418
|
+
alsemVersion: cmdOpts.alsemVersion as string | undefined,
|
|
419
|
+
strict: cmdOpts.strict === true,
|
|
420
|
+
scope: cmdOpts.scope as Scope,
|
|
421
|
+
});
|
|
422
|
+
process.exit(exitCode);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
events
|
|
426
|
+
.command("chains")
|
|
427
|
+
.description("Per-publisher chain tree (event → subscriber → ...)")
|
|
428
|
+
.argument("<workspace>", "path to the AL workspace root")
|
|
429
|
+
.option("--format <fmt>", "human | json", "human")
|
|
430
|
+
.option("--out <path>", "output file")
|
|
431
|
+
.option("--coverage-policy <p>", "warn | strict | ignore", "warn")
|
|
432
|
+
.option("--max-depth <n>", "maximum tree depth (0..256)", (v: string) => Number.parseInt(v, 10))
|
|
433
|
+
.option("--deterministic", "pin generated_at for byte-stable output", false)
|
|
434
|
+
.option("--alsem-version <v>", "version string in output", "0.0.0")
|
|
435
|
+
.option("--strict", "exit 1 on analyzer error-severity diagnostic", false)
|
|
436
|
+
.option(
|
|
437
|
+
"--scope <scope>",
|
|
438
|
+
"primary (default) drops dependency-only events; all keeps them",
|
|
439
|
+
"primary",
|
|
440
|
+
)
|
|
441
|
+
.action(async (workspace: string, cmdOpts: Record<string, unknown>) => {
|
|
442
|
+
if (!SCOPE_VALUES.includes(cmdOpts.scope as never)) {
|
|
443
|
+
process.stderr.write(
|
|
444
|
+
`al-sem: invalid --scope '${cmdOpts.scope}'. Expected one of: ${SCOPE_VALUES.join(", ")}\n`,
|
|
445
|
+
);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
const exitCode = await runEventsChains({
|
|
449
|
+
workspace,
|
|
450
|
+
format: cmdOpts.format as never,
|
|
451
|
+
out: cmdOpts.out as string | undefined,
|
|
452
|
+
coveragePolicy: cmdOpts.coveragePolicy as never,
|
|
453
|
+
maxDepth: cmdOpts.maxDepth as number | undefined,
|
|
454
|
+
deterministic: cmdOpts.deterministic === true,
|
|
455
|
+
alsemVersion: cmdOpts.alsemVersion as string | undefined,
|
|
456
|
+
strict: cmdOpts.strict === true,
|
|
457
|
+
scope: cmdOpts.scope as Scope,
|
|
458
|
+
});
|
|
459
|
+
process.exit(exitCode);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const policy = program
|
|
463
|
+
.command("policy")
|
|
464
|
+
.description("Declarative policy rules over capability facts");
|
|
465
|
+
|
|
466
|
+
policy
|
|
467
|
+
.command("check")
|
|
468
|
+
.description("Check workspace against effective policy")
|
|
469
|
+
.argument("<workspace>", "path to the AL workspace root")
|
|
470
|
+
.option("--policy <path>", "explicit policy file")
|
|
471
|
+
.option("--no-policy", "disable all policy evaluation")
|
|
472
|
+
.option("--format <fmt>", "human | json | sarif", "human")
|
|
473
|
+
.option("--out <path>", "output file")
|
|
474
|
+
.option("--deterministic", "pin generated_at", false)
|
|
475
|
+
.option("--alsem-version <v>", "version string in output", "0.0.0")
|
|
476
|
+
.option("--strict", "exit 1 on analyzer error-severity diagnostic", false)
|
|
477
|
+
.option(
|
|
478
|
+
"--scope <scope>",
|
|
479
|
+
"primary (default) drops dependency-anchored findings; all keeps them",
|
|
480
|
+
"primary",
|
|
481
|
+
)
|
|
482
|
+
.action(async (workspace: string, cmdOpts: Record<string, unknown>) => {
|
|
483
|
+
if (!SCOPE_VALUES.includes(cmdOpts.scope as never)) {
|
|
484
|
+
process.stderr.write(
|
|
485
|
+
`al-sem: invalid --scope '${cmdOpts.scope}'. Expected one of: ${SCOPE_VALUES.join(", ")}\n`,
|
|
486
|
+
);
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
// commander's --no-X flips the value to false. Detect that as "disable".
|
|
490
|
+
const policyArg = cmdOpts.policy;
|
|
491
|
+
const noPolicy = policyArg === false;
|
|
492
|
+
const policyPath = typeof policyArg === "string" ? policyArg : undefined;
|
|
493
|
+
const exitCode = await runPolicyCheck({
|
|
494
|
+
workspace,
|
|
495
|
+
policyPath,
|
|
496
|
+
noPolicy,
|
|
497
|
+
format: cmdOpts.format as never,
|
|
498
|
+
out: cmdOpts.out as string | undefined,
|
|
499
|
+
deterministic: cmdOpts.deterministic === true,
|
|
500
|
+
alsemVersion: cmdOpts.alsemVersion as string | undefined,
|
|
501
|
+
strict: cmdOpts.strict === true,
|
|
502
|
+
scope: cmdOpts.scope as Scope,
|
|
503
|
+
});
|
|
504
|
+
process.exit(exitCode);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
policy
|
|
508
|
+
.command("explain")
|
|
509
|
+
.description("Explain a policy rule's semantics + matches")
|
|
510
|
+
.argument("<rule-id>", "rule id to explain")
|
|
511
|
+
.argument("<workspace>", "path to the AL workspace root")
|
|
512
|
+
.option("--policy <path>", "explicit policy file")
|
|
513
|
+
.option("--routine <selector>", "narrow trace to one routine")
|
|
514
|
+
.option("--finding <id>", "narrow trace to one finding id")
|
|
515
|
+
.option("--format <fmt>", "human | json", "human")
|
|
516
|
+
.action(async (ruleId: string, workspace: string, cmdOpts: Record<string, unknown>) => {
|
|
517
|
+
const exitCode = await runPolicyExplain({
|
|
518
|
+
workspace,
|
|
519
|
+
ruleId,
|
|
520
|
+
policyPath: typeof cmdOpts.policy === "string" ? cmdOpts.policy : undefined,
|
|
521
|
+
routine: cmdOpts.routine as string | undefined,
|
|
522
|
+
findingId: cmdOpts.finding as string | undefined,
|
|
523
|
+
format: cmdOpts.format as never,
|
|
524
|
+
});
|
|
525
|
+
process.exit(exitCode);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const cache = program.command("cache").description("Inspect and maintain the dependency cache");
|
|
529
|
+
|
|
530
|
+
cache
|
|
531
|
+
.command("prune")
|
|
532
|
+
.description(
|
|
533
|
+
"Remove dependency cache entries this build can no longer use (version mismatch, corrupt files, tampered content hash). Kept entries are not touched.",
|
|
534
|
+
)
|
|
535
|
+
.option(
|
|
536
|
+
"--dep-cache-dir <dir>",
|
|
537
|
+
"override the dependency cache directory (default ~/.al-sem/cache/)",
|
|
538
|
+
)
|
|
539
|
+
.option("--dry-run", "report what would be removed without deleting anything", false)
|
|
540
|
+
.action((opts: { depCacheDir?: string; dryRun: boolean }) => {
|
|
541
|
+
const result = pruneCache(opts.depCacheDir, { dryRun: opts.dryRun });
|
|
542
|
+
const kb = (n: number): string => `${(n / 1024).toFixed(1)} KB`;
|
|
543
|
+
const verb = opts.dryRun ? "would remove" : "removed";
|
|
544
|
+
process.stdout.write(`al-sem cache: ${result.cacheDir}\n`);
|
|
545
|
+
if (result.entries.length === 0) {
|
|
546
|
+
process.stdout.write(" (empty)\n");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const removed = result.entries.filter((e) => e.status !== "kept");
|
|
550
|
+
const kept = result.entries.length - removed.length;
|
|
551
|
+
process.stdout.write(
|
|
552
|
+
` ${verb} ${result.filesRemoved} file(s) totalling ${kb(result.bytesFreed)}; kept ${kept}.\n`,
|
|
553
|
+
);
|
|
554
|
+
for (const e of removed) {
|
|
555
|
+
process.stdout.write(` - ${e.file} (${kb(e.bytes)}) ${e.status}\n`);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
function collectRoutine(value: string, prev: string[]): string[] {
|
|
560
|
+
return [...prev, value];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
program.parseAsync(process.argv).catch((err: unknown) => {
|
|
564
|
+
process.stderr.write(`al-sem: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
565
|
+
process.exitCode = 1;
|
|
566
|
+
});
|