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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// scripts/precision-sample.ts
|
|
2
|
+
// Stratified sampler for precision-study rounds. Reads compact JSON output
|
|
3
|
+
// from `analyze --format json --deterministic`, filters to a detector, and
|
|
4
|
+
// prints a manageable sample with the anchor + evidence path so a reviewer
|
|
5
|
+
// can open each one and judge TP/FP.
|
|
6
|
+
//
|
|
7
|
+
// Stratification: deterministic sample — first N per (severity, rootCause
|
|
8
|
+
// shape) bucket to ensure breadth across the finding distribution.
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
|
|
12
|
+
interface Location {
|
|
13
|
+
file: string;
|
|
14
|
+
line: number;
|
|
15
|
+
column: number;
|
|
16
|
+
objectName?: string;
|
|
17
|
+
routineName?: string;
|
|
18
|
+
}
|
|
19
|
+
interface Finding {
|
|
20
|
+
id: string;
|
|
21
|
+
detector: string;
|
|
22
|
+
severity: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
rootCause: string;
|
|
25
|
+
primaryLocation: Location;
|
|
26
|
+
terminalLocation?: Location;
|
|
27
|
+
pathCount?: number;
|
|
28
|
+
affectedTables?: string[];
|
|
29
|
+
}
|
|
30
|
+
interface RunJson {
|
|
31
|
+
findings: Finding[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function shortLoc(a: Location | undefined): string {
|
|
35
|
+
if (!a) return "<no-anchor>";
|
|
36
|
+
const file = a.file.replace(/^ws:/, "");
|
|
37
|
+
const where = a.objectName && a.routineName ? ` (${a.objectName} :: ${a.routineName})` : "";
|
|
38
|
+
return `${file}:${a.line}:${a.column}${where}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rootCauseShape(rc: string): string {
|
|
42
|
+
// Strip noun-phrase variables — keep the rule's verbal skeleton.
|
|
43
|
+
// Goal: cluster findings that hit the same antipattern with different operands.
|
|
44
|
+
return rc
|
|
45
|
+
.replace(/[A-Z][A-Za-z0-9_]*(\s+[A-Z][A-Za-z0-9_]*)*/g, "<Name>")
|
|
46
|
+
.replace(/'[^']*'/g, "<str>")
|
|
47
|
+
.replace(/"[^"]*"/g, "<str>")
|
|
48
|
+
.replace(/\s+/g, " ")
|
|
49
|
+
.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function main(): void {
|
|
53
|
+
const [path, detectorFilter, perBucketStr] = process.argv.slice(2);
|
|
54
|
+
if (!path || !detectorFilter) {
|
|
55
|
+
console.error("usage: precision-sample.ts <run.json> <detector-id> [perBucket=3]");
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
const perBucket = Number(perBucketStr ?? 3);
|
|
59
|
+
const run = JSON.parse(readFileSync(path, "utf8")) as RunJson;
|
|
60
|
+
const matching = run.findings.filter((f) => f.detector === detectorFilter);
|
|
61
|
+
|
|
62
|
+
// Bucket by (severity, rootCauseShape) for stratified sampling.
|
|
63
|
+
const buckets = new Map<string, Finding[]>();
|
|
64
|
+
for (const f of matching) {
|
|
65
|
+
const key = `${f.severity}|${rootCauseShape(f.rootCause)}`;
|
|
66
|
+
const arr = buckets.get(key) ?? [];
|
|
67
|
+
arr.push(f);
|
|
68
|
+
buckets.set(key, arr);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`# Sample for ${detectorFilter} — ${matching.length} total, ${buckets.size} buckets`);
|
|
72
|
+
console.log("");
|
|
73
|
+
const sortedKeys = [...buckets.keys()].sort();
|
|
74
|
+
let sampleCount = 0;
|
|
75
|
+
for (const key of sortedKeys) {
|
|
76
|
+
const all = buckets.get(key) ?? [];
|
|
77
|
+
const take = all.slice(0, perBucket);
|
|
78
|
+
const [sev, shape] = key.split("|", 2);
|
|
79
|
+
console.log(`## bucket: severity=${sev}, total=${all.length}`);
|
|
80
|
+
console.log(`shape: ${shape}`);
|
|
81
|
+
console.log("");
|
|
82
|
+
for (const f of take) {
|
|
83
|
+
sampleCount++;
|
|
84
|
+
console.log(`### finding ${sampleCount}`);
|
|
85
|
+
console.log(`anchor: ${shortLoc(f.primaryLocation)}`);
|
|
86
|
+
if (f.terminalLocation) console.log(`terminal: ${shortLoc(f.terminalLocation)}`);
|
|
87
|
+
console.log(`rootCause: ${f.rootCause}`);
|
|
88
|
+
if (f.pathCount && f.pathCount > 1) console.log(`pathCount: ${f.pathCount}`);
|
|
89
|
+
console.log("");
|
|
90
|
+
}
|
|
91
|
+
if (all.length > perBucket) {
|
|
92
|
+
console.log(`(... + ${all.length - perBucket} more in this bucket)`);
|
|
93
|
+
console.log("");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
console.log(`# Sampled ${sampleCount} of ${matching.length} findings.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// scripts/precision-study.ts
|
|
2
|
+
//
|
|
3
|
+
// Sample N findings per detector from a workspace and emit a CSV the developer can
|
|
4
|
+
// fill in with manual classifications. Used to drive evidence-based detector tuning.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// bun run scripts/precision-study.ts <workspace> [N]
|
|
8
|
+
//
|
|
9
|
+
// N defaults to 30. Output is CSV to stdout.
|
|
10
|
+
|
|
11
|
+
import { analyzeWorkspace } from "../src/index.ts";
|
|
12
|
+
import { projectFinding } from "../src/projection/finding-summary.ts";
|
|
13
|
+
|
|
14
|
+
const workspaceRoot = process.argv[2];
|
|
15
|
+
if (!workspaceRoot) {
|
|
16
|
+
process.stderr.write("usage: bun run scripts/precision-study.ts <workspace> [N]\n");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const N = Number.parseInt(process.argv[3] ?? "30", 10);
|
|
20
|
+
|
|
21
|
+
const result = await analyzeWorkspace({ workspaceRoot, deterministic: true });
|
|
22
|
+
|
|
23
|
+
const byDetector = new Map<string, typeof result.findings>();
|
|
24
|
+
for (const f of result.findings) {
|
|
25
|
+
const list = byDetector.get(f.detector);
|
|
26
|
+
if (list) list.push(f);
|
|
27
|
+
else byDetector.set(f.detector, [f]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.stdout.write("detector,severity,file,line,object,routine,table,classification,notes\n");
|
|
31
|
+
for (const [detector, list] of byDetector) {
|
|
32
|
+
for (const raw of list.slice(0, N)) {
|
|
33
|
+
const f = projectFinding(raw, result.model);
|
|
34
|
+
const file = f.primaryLocation.file.replaceAll(",", ";"); // CSV-safe
|
|
35
|
+
const object = (f.primaryLocation.objectName ?? "").replaceAll(",", ";");
|
|
36
|
+
const routine = (f.primaryLocation.routineName ?? "").replaceAll(",", ";");
|
|
37
|
+
const table = (f.affectedTables[0] ?? "").replaceAll(",", ";");
|
|
38
|
+
process.stdout.write(
|
|
39
|
+
`${detector},${f.severity},${file},${f.primaryLocation.line},${object},${routine},${table},,\n`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// scripts/precision-tabulate.ts
|
|
2
|
+
// Per-detector + per-severity tabulation for precision-study rounds.
|
|
3
|
+
// Reads compact JSON output from `analyze --format json --deterministic`.
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
interface FindingLike {
|
|
8
|
+
detector: string;
|
|
9
|
+
severity: string;
|
|
10
|
+
}
|
|
11
|
+
interface RunJson {
|
|
12
|
+
findings: FindingLike[];
|
|
13
|
+
[k: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function load(path: string): RunJson {
|
|
17
|
+
const raw = readFileSync(path, "utf8");
|
|
18
|
+
return JSON.parse(raw) as RunJson;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function tabulate(label: string, run: RunJson): void {
|
|
22
|
+
const total = run.findings.length;
|
|
23
|
+
const byDet = new Map<string, Map<string, number>>();
|
|
24
|
+
for (const f of run.findings) {
|
|
25
|
+
const sevMap = byDet.get(f.detector) ?? new Map<string, number>();
|
|
26
|
+
sevMap.set(f.severity, (sevMap.get(f.severity) ?? 0) + 1);
|
|
27
|
+
byDet.set(f.detector, sevMap);
|
|
28
|
+
}
|
|
29
|
+
const SEV_ORDER = ["critical", "high", "medium", "low", "info"];
|
|
30
|
+
const dets = [...byDet.keys()].sort();
|
|
31
|
+
console.log(`\n=== ${label} — total ${total} ===`);
|
|
32
|
+
console.log(
|
|
33
|
+
`${"detector".padEnd(40)} ${"total".padStart(6)} ${SEV_ORDER.map((s) => s.padStart(8)).join(" ")}`,
|
|
34
|
+
);
|
|
35
|
+
console.log("-".repeat(40 + 7 + SEV_ORDER.length * 9));
|
|
36
|
+
for (const det of dets) {
|
|
37
|
+
const sevMap = byDet.get(det) ?? new Map();
|
|
38
|
+
const detTotal = [...sevMap.values()].reduce((a, b) => a + b, 0);
|
|
39
|
+
const cells = SEV_ORDER.map((s) => String(sevMap.get(s) ?? 0).padStart(8));
|
|
40
|
+
console.log(`${det.padEnd(40)} ${String(detTotal).padStart(6)} ${cells.join(" ")}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const args = process.argv.slice(2);
|
|
45
|
+
for (const arg of args) {
|
|
46
|
+
const [label, path] = arg.split("=");
|
|
47
|
+
if (label === undefined || path === undefined) {
|
|
48
|
+
console.error(`bad arg: ${arg} — expected label=path`);
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
tabulate(label, load(path));
|
|
52
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import type { FindingSummary } from "../projection/finding-summary.ts";
|
|
3
|
+
|
|
4
|
+
export interface BaselineFile {
|
|
5
|
+
schemaVersion: "1";
|
|
6
|
+
generatedAt: string;
|
|
7
|
+
fingerprints: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Load a baseline file. Missing or empty file → empty set, no error. */
|
|
11
|
+
export function loadBaseline(path: string): Set<string> {
|
|
12
|
+
if (!existsSync(path)) return new Set();
|
|
13
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as BaselineFile;
|
|
14
|
+
return new Set(parsed.fingerprints);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Write a baseline file with sorted, deduped fingerprints from `findings`. */
|
|
18
|
+
export function saveBaseline(path: string, findings: FindingSummary[]): void {
|
|
19
|
+
const fingerprints = [...new Set(findings.map((f) => f.fingerprint))].sort();
|
|
20
|
+
const file: BaselineFile = {
|
|
21
|
+
schemaVersion: "1",
|
|
22
|
+
generatedAt: new Date(0).toISOString(), // pinned for determinism
|
|
23
|
+
fingerprints,
|
|
24
|
+
};
|
|
25
|
+
writeFileSync(path, `${JSON.stringify(file, null, 2)}\n`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Return only findings whose fingerprint is not in the baseline. */
|
|
29
|
+
export function applyBaseline(findings: FindingSummary[], baseline: Set<string>): FindingSummary[] {
|
|
30
|
+
return findings.filter((f) => !baseline.has(f.fingerprint));
|
|
31
|
+
}
|
package/src/cli/diff.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { existsSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { extname } from "node:path";
|
|
4
|
+
import { type DiffEngineResult, runDiffEngine } from "../diff/diff-engine.ts";
|
|
5
|
+
import { loadRenameOverlay } from "../diff/diff-renames.ts";
|
|
6
|
+
import { formatDiff } from "../diff/format-diff.ts";
|
|
7
|
+
import { analyzeWorkspace } from "../index.ts";
|
|
8
|
+
import { loadSnapshotFromApp } from "../snapshot/app-snapshot.ts";
|
|
9
|
+
import { composeSnapshot } from "../snapshot/compose.ts";
|
|
10
|
+
import { deserializeSnapshot } from "../snapshot/deserialize.ts";
|
|
11
|
+
import type { CapabilitySnapshot, SnapshotFormat } from "../snapshot/types.ts";
|
|
12
|
+
|
|
13
|
+
export type DiffFormat = "human" | "json" | "sarif";
|
|
14
|
+
|
|
15
|
+
export interface DiffCliOptions {
|
|
16
|
+
oldArg: string;
|
|
17
|
+
newArg: string;
|
|
18
|
+
format?: DiffFormat;
|
|
19
|
+
out?: string;
|
|
20
|
+
coveragePolicy?: "loose" | "strict";
|
|
21
|
+
renamesPath?: string;
|
|
22
|
+
failOn?: "critical" | "high" | "medium" | "low" | "info";
|
|
23
|
+
strict?: boolean;
|
|
24
|
+
deterministic?: boolean;
|
|
25
|
+
alsemVersion?: string;
|
|
26
|
+
debug?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class DiffCliError extends Error {
|
|
30
|
+
constructor(
|
|
31
|
+
public exitCode: 1 | 2,
|
|
32
|
+
message: string,
|
|
33
|
+
) {
|
|
34
|
+
super(message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const VALID_FORMATS: readonly DiffFormat[] = ["human", "json", "sarif"];
|
|
39
|
+
const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 } as const;
|
|
40
|
+
|
|
41
|
+
function detectInputKind(arg: string): "snapshot" | "workspace" | "app" {
|
|
42
|
+
if (!existsSync(arg)) {
|
|
43
|
+
throw new DiffCliError(1, `input not found: ${arg}`);
|
|
44
|
+
}
|
|
45
|
+
const stat = statSync(arg);
|
|
46
|
+
if (stat.isDirectory()) return "workspace";
|
|
47
|
+
if (arg.toLowerCase().endsWith(".app")) return "app";
|
|
48
|
+
return "snapshot";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function loadSnapshotFromPath(path: string): Promise<CapabilitySnapshot> {
|
|
52
|
+
const ext = extname(path).toLowerCase();
|
|
53
|
+
const formatHint: SnapshotFormat | undefined =
|
|
54
|
+
ext === ".json"
|
|
55
|
+
? "json"
|
|
56
|
+
: ext === ".cbor"
|
|
57
|
+
? "cbor"
|
|
58
|
+
: ext === ".gz" || path.endsWith(".cbor.gz")
|
|
59
|
+
? "cbor.gz"
|
|
60
|
+
: undefined;
|
|
61
|
+
const bytes = new Uint8Array(await readFile(path));
|
|
62
|
+
return deserializeSnapshot(bytes, formatHint);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function loadSnapshotFromWorkspace(
|
|
66
|
+
dir: string,
|
|
67
|
+
alsemVersion: string,
|
|
68
|
+
deterministic: boolean,
|
|
69
|
+
): Promise<{ snapshot: CapabilitySnapshot; diagnostics: { severity: string; message: string }[] }> {
|
|
70
|
+
const { model, diagnostics } = await analyzeWorkspace({ workspaceRoot: dir });
|
|
71
|
+
const snapshot = composeSnapshot(model, {
|
|
72
|
+
workspaceDir: dir,
|
|
73
|
+
alsemVersion,
|
|
74
|
+
deterministic,
|
|
75
|
+
});
|
|
76
|
+
return { snapshot, diagnostics };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function runDiff(opts: DiffCliOptions): Promise<number> {
|
|
80
|
+
// Validate flags up-front.
|
|
81
|
+
const format: DiffFormat = opts.format ?? "human";
|
|
82
|
+
if (!VALID_FORMATS.includes(format)) {
|
|
83
|
+
process.stderr.write(`unknown --format '${format}'; valid: ${VALID_FORMATS.join(", ")}\n`);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
const coveragePolicy = opts.coveragePolicy ?? "loose";
|
|
87
|
+
if (coveragePolicy !== "loose" && coveragePolicy !== "strict") {
|
|
88
|
+
process.stderr.write(`--coverage-policy must be loose|strict (got '${coveragePolicy}')\n`);
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
if (opts.failOn !== undefined && !(opts.failOn in SEVERITY_RANK)) {
|
|
92
|
+
process.stderr.write("--fail-on must be one of: critical|high|medium|low|info\n");
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
const deterministic = opts.deterministic ?? false;
|
|
96
|
+
const alsemVersion = opts.alsemVersion ?? "0.0.0";
|
|
97
|
+
|
|
98
|
+
let oldSnap: CapabilitySnapshot;
|
|
99
|
+
let newSnap: CapabilitySnapshot;
|
|
100
|
+
const analyzerDiagnostics: { severity: string; message: string }[] = [];
|
|
101
|
+
let workspaceMode = false;
|
|
102
|
+
try {
|
|
103
|
+
const oldKind = detectInputKind(opts.oldArg);
|
|
104
|
+
const newKind = detectInputKind(opts.newArg);
|
|
105
|
+
if (
|
|
106
|
+
oldKind === "workspace" ||
|
|
107
|
+
newKind === "workspace" ||
|
|
108
|
+
oldKind === "app" ||
|
|
109
|
+
newKind === "app"
|
|
110
|
+
) {
|
|
111
|
+
workspaceMode = true;
|
|
112
|
+
}
|
|
113
|
+
if (oldKind === "workspace") {
|
|
114
|
+
const r = await loadSnapshotFromWorkspace(opts.oldArg, alsemVersion, deterministic);
|
|
115
|
+
oldSnap = r.snapshot;
|
|
116
|
+
analyzerDiagnostics.push(...r.diagnostics);
|
|
117
|
+
} else if (oldKind === "app") {
|
|
118
|
+
const r = await loadSnapshotFromApp(opts.oldArg, alsemVersion, deterministic);
|
|
119
|
+
oldSnap = r.snapshot;
|
|
120
|
+
analyzerDiagnostics.push(...r.diagnostics);
|
|
121
|
+
} else {
|
|
122
|
+
oldSnap = await loadSnapshotFromPath(opts.oldArg);
|
|
123
|
+
}
|
|
124
|
+
if (newKind === "workspace") {
|
|
125
|
+
const r = await loadSnapshotFromWorkspace(opts.newArg, alsemVersion, deterministic);
|
|
126
|
+
newSnap = r.snapshot;
|
|
127
|
+
analyzerDiagnostics.push(...r.diagnostics);
|
|
128
|
+
} else if (newKind === "app") {
|
|
129
|
+
const r = await loadSnapshotFromApp(opts.newArg, alsemVersion, deterministic);
|
|
130
|
+
newSnap = r.snapshot;
|
|
131
|
+
analyzerDiagnostics.push(...r.diagnostics);
|
|
132
|
+
} else {
|
|
133
|
+
newSnap = await loadSnapshotFromPath(opts.newArg);
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err instanceof DiffCliError) {
|
|
137
|
+
process.stderr.write(`${err.message}\n`);
|
|
138
|
+
return err.exitCode;
|
|
139
|
+
}
|
|
140
|
+
process.stderr.write(`failed to load snapshot: ${(err as Error).message}\n`);
|
|
141
|
+
if (opts.debug) process.stderr.write(`${(err as Error).stack ?? ""}\n`);
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Optional rename overlay.
|
|
146
|
+
let renameOverlay: Record<string, string> | undefined;
|
|
147
|
+
try {
|
|
148
|
+
const r = await loadRenameOverlay(opts.renamesPath);
|
|
149
|
+
renameOverlay = r.overlay;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
process.stderr.write(`failed to load rename overlay: ${(err as Error).message}\n`);
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (opts.strict === true) {
|
|
156
|
+
const fatal = analyzerDiagnostics.find((d) => d.severity === "error");
|
|
157
|
+
if (fatal !== undefined) {
|
|
158
|
+
for (const d of analyzerDiagnostics) {
|
|
159
|
+
process.stderr.write(`${d.severity}: ${d.message}\n`);
|
|
160
|
+
}
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result: DiffEngineResult = runDiffEngine(oldSnap, newSnap, {
|
|
166
|
+
coveragePolicy,
|
|
167
|
+
deterministic,
|
|
168
|
+
renameOverlay,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const text = formatDiff(result, { format, deterministic });
|
|
172
|
+
try {
|
|
173
|
+
if (opts.out !== undefined) writeFileSync(opts.out, text);
|
|
174
|
+
else process.stdout.write(text);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
process.stderr.write(`failed to write: ${(err as Error).message}\n`);
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const d of analyzerDiagnostics) {
|
|
181
|
+
process.stderr.write(`${d.severity}: ${d.message}\n`);
|
|
182
|
+
}
|
|
183
|
+
if (workspaceMode) {
|
|
184
|
+
process.stderr.write(
|
|
185
|
+
"note: workspace-mode reanalyzes both sides; for CI, persist snapshots with 'al-sem fingerprint --format cbor.gz' and diff those instead.\n",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Exit code: --fail-on severity OR strict-coverage diagnostic.
|
|
190
|
+
const strictCoverageFailed = result.diagnostics.some((d) => d.kind === "coverage-incomplete");
|
|
191
|
+
if (coveragePolicy === "strict" && strictCoverageFailed) return 1;
|
|
192
|
+
if (opts.failOn !== undefined) {
|
|
193
|
+
const threshold = SEVERITY_RANK[opts.failOn];
|
|
194
|
+
for (const f of result.findings) {
|
|
195
|
+
if (SEVERITY_RANK[f.severity] <= threshold) return 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { buildEventFlowIndexes, computeChainReport } from "../engine/event-flow.ts";
|
|
3
|
+
import { analyzeWorkspace } from "../index.ts";
|
|
4
|
+
import type { EventsFanoutOptions } from "./events-fanout.ts";
|
|
5
|
+
import { formatChains } from "./format-events.ts";
|
|
6
|
+
|
|
7
|
+
export interface EventsChainsOptions extends EventsFanoutOptions {
|
|
8
|
+
maxDepth?: number;
|
|
9
|
+
maxNodes?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const VALID_FORMATS = new Set(["human", "json"]);
|
|
13
|
+
const VALID_COVERAGE = new Set(["warn", "strict", "ignore"]);
|
|
14
|
+
|
|
15
|
+
export async function runEventsChains(opts: EventsChainsOptions): Promise<number> {
|
|
16
|
+
const format = opts.format ?? "human";
|
|
17
|
+
if (!VALID_FORMATS.has(format)) {
|
|
18
|
+
process.stderr.write(`al-sem events chains: invalid --format '${format}'\n`);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
const coverage = opts.coveragePolicy ?? "warn";
|
|
22
|
+
if (!VALID_COVERAGE.has(coverage)) {
|
|
23
|
+
process.stderr.write(`al-sem events chains: invalid --coverage-policy '${coverage}'\n`);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
if (opts.maxDepth !== undefined && (opts.maxDepth < 0 || opts.maxDepth > 256)) {
|
|
27
|
+
process.stderr.write("al-sem events chains: --max-depth must be in 0..256\n");
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
const { model, diagnostics } = await analyzeWorkspace({ workspaceRoot: opts.workspace });
|
|
31
|
+
if (opts.strict === true && diagnostics.some((d) => d.severity === "error")) {
|
|
32
|
+
for (const d of diagnostics) process.stderr.write(`${d.severity}: ${d.message}\n`);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
const ix = buildEventFlowIndexes(model);
|
|
36
|
+
const report = computeChainReport(ix, {
|
|
37
|
+
maxDepth: opts.maxDepth,
|
|
38
|
+
maxNodes: opts.maxNodes,
|
|
39
|
+
scope: opts.scope ?? "primary",
|
|
40
|
+
});
|
|
41
|
+
const text = formatChains(report, {
|
|
42
|
+
format,
|
|
43
|
+
coveragePolicy: coverage,
|
|
44
|
+
deterministic: opts.deterministic,
|
|
45
|
+
alsemVersion: opts.alsemVersion,
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
if (opts.out !== undefined) writeFileSync(opts.out, text);
|
|
49
|
+
else process.stdout.write(text);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
process.stderr.write(`failed to write: ${(err as Error).message}\n`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
for (const d of diagnostics) process.stderr.write(`${d.severity}: ${d.message}\n`);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { buildEventFlowIndexes, computeFanoutReport } from "../engine/event-flow.ts";
|
|
3
|
+
import { analyzeWorkspace } from "../index.ts";
|
|
4
|
+
import type { Scope } from "../model/entities.ts";
|
|
5
|
+
import { formatFanout } from "./format-events.ts";
|
|
6
|
+
|
|
7
|
+
export interface EventsFanoutOptions {
|
|
8
|
+
workspace: string;
|
|
9
|
+
format?: "human" | "json";
|
|
10
|
+
out?: string;
|
|
11
|
+
coveragePolicy?: "warn" | "strict" | "ignore";
|
|
12
|
+
deterministic?: boolean;
|
|
13
|
+
alsemVersion?: string;
|
|
14
|
+
strict?: boolean;
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
scope?: Scope;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const VALID_FORMATS = new Set(["human", "json"]);
|
|
20
|
+
const VALID_COVERAGE = new Set(["warn", "strict", "ignore"]);
|
|
21
|
+
|
|
22
|
+
export async function runEventsFanout(opts: EventsFanoutOptions): Promise<number> {
|
|
23
|
+
const format = opts.format ?? "human";
|
|
24
|
+
if (!VALID_FORMATS.has(format)) {
|
|
25
|
+
process.stderr.write(`al-sem events fanout: invalid --format '${format}'\n`);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const coverage = opts.coveragePolicy ?? "warn";
|
|
29
|
+
if (!VALID_COVERAGE.has(coverage)) {
|
|
30
|
+
process.stderr.write(`al-sem events fanout: invalid --coverage-policy '${coverage}'\n`);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const { model, diagnostics } = await analyzeWorkspace({ workspaceRoot: opts.workspace });
|
|
34
|
+
if (opts.strict === true && diagnostics.some((d) => d.severity === "error")) {
|
|
35
|
+
for (const d of diagnostics) process.stderr.write(`${d.severity}: ${d.message}\n`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
const ix = buildEventFlowIndexes(model);
|
|
39
|
+
let report = computeFanoutReport(model, ix, { scope: opts.scope ?? "primary" });
|
|
40
|
+
|
|
41
|
+
let coverageExitElevated = false;
|
|
42
|
+
if (coverage === "strict") {
|
|
43
|
+
const partial = report.entries.filter(
|
|
44
|
+
(e) =>
|
|
45
|
+
e.coverage.dispatchEdges === "partial" || e.coverage.capabilityComposition === "partial",
|
|
46
|
+
);
|
|
47
|
+
if (partial.length > 0) {
|
|
48
|
+
for (const e of partial) {
|
|
49
|
+
process.stderr.write(
|
|
50
|
+
`coverage-incomplete: event ${e.eventId} dispatchEdges=${e.coverage.dispatchEdges} capability=${e.coverage.capabilityComposition}\n`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const partialSet = new Set(partial);
|
|
54
|
+
report = { ...report, entries: report.entries.filter((e) => !partialSet.has(e)) };
|
|
55
|
+
coverageExitElevated = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (coverage === "ignore") {
|
|
59
|
+
report = {
|
|
60
|
+
...report,
|
|
61
|
+
entries: report.entries.map((e) => ({
|
|
62
|
+
...e,
|
|
63
|
+
coverage: {
|
|
64
|
+
dispatchEdges: "complete" as const,
|
|
65
|
+
subscriberDiscovery: "complete" as const,
|
|
66
|
+
capabilityComposition: "complete" as const,
|
|
67
|
+
},
|
|
68
|
+
})),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const text = formatFanout(report, {
|
|
73
|
+
format,
|
|
74
|
+
coveragePolicy: coverage,
|
|
75
|
+
deterministic: opts.deterministic,
|
|
76
|
+
alsemVersion: opts.alsemVersion,
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
if (opts.out !== undefined) writeFileSync(opts.out, text);
|
|
80
|
+
else process.stdout.write(text);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
process.stderr.write(`failed to write: ${(err as Error).message}\n`);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
for (const d of diagnostics) process.stderr.write(`${d.severity}: ${d.message}\n`);
|
|
86
|
+
return coverageExitElevated ? 1 : 0;
|
|
87
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FindingSummary } from "../projection/finding-summary.ts";
|
|
2
|
+
|
|
3
|
+
const SEV_RANK = { critical: 5, high: 4, medium: 3, low: 2, info: 1 } as const;
|
|
4
|
+
|
|
5
|
+
const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low", "info"] as const);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Map filtered findings + a `--fail-on` threshold to a process exit code.
|
|
9
|
+
* - No `failOn` set → exit 0 always.
|
|
10
|
+
* - Any finding at the threshold severity or higher → exit 1.
|
|
11
|
+
* - Otherwise → exit 0.
|
|
12
|
+
*/
|
|
13
|
+
export function computeExitCode(
|
|
14
|
+
findings: FindingSummary[],
|
|
15
|
+
failOn?: FindingSummary["severity"],
|
|
16
|
+
): number {
|
|
17
|
+
if (failOn === undefined) return 0;
|
|
18
|
+
const min = SEV_RANK[failOn];
|
|
19
|
+
return findings.some((f) => SEV_RANK[f.severity] >= min) ? 1 : 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Validate a --fail-on string. Returns the typed severity or throws (caller catches and emits usage error). */
|
|
23
|
+
export function parseFailOn(input: string): FindingSummary["severity"] {
|
|
24
|
+
if (!VALID_SEVERITIES.has(input as never)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`invalid --fail-on '${input}'. Expected: critical | high | medium | low | info`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return input as FindingSummary["severity"];
|
|
30
|
+
}
|