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,176 @@
|
|
|
1
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
2
|
+
import { compareNatural, compareStrings } from "../engine/uncertainty-util.ts";
|
|
3
|
+
import { roleOf } from "../model/entities.ts";
|
|
4
|
+
import type { DetectorStats, Diagnostic, Finding } from "../model/finding.ts";
|
|
5
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
6
|
+
import { profilingActive, recordPhase } from "../perf/profiler.ts";
|
|
7
|
+
import { detectD1 } from "./d1-db-op-in-loop.ts";
|
|
8
|
+
import { detectD2 } from "./d2-event-fanout-in-loop.ts";
|
|
9
|
+
import { detectD3 } from "./d3-missing-setloadfields.ts";
|
|
10
|
+
import { detectD4 } from "./d4-repeated-lookup-in-loop.ts";
|
|
11
|
+
import { detectD5 } from "./d5-set-based-opportunity.ts";
|
|
12
|
+
import { detectD7 } from "./d7-recursive-event-expansion.ts";
|
|
13
|
+
import { detectD8 } from "./d8-commit-in-transaction.ts";
|
|
14
|
+
import { detectD9 } from "./d9-transaction-span-summary.ts";
|
|
15
|
+
import { detectD10 } from "./d10-self-modifying-loop.ts";
|
|
16
|
+
import { detectD11 } from "./d11-modify-without-get.ts";
|
|
17
|
+
import { detectD12 } from "./d12-dead-integration-event.ts";
|
|
18
|
+
import { detectD13 } from "./d13-cross-app-internal-call.ts";
|
|
19
|
+
import { detectD14 } from "./d14-dead-routine.ts";
|
|
20
|
+
import { detectD16 } from "./d16-obsolete-routine-call.ts";
|
|
21
|
+
import { detectD17 } from "./d17-min-version-drift.ts";
|
|
22
|
+
import { detectD18 } from "./d18-constant-filter-in-loop.ts";
|
|
23
|
+
import { detectD19 } from "./d19-unused-parameter.ts";
|
|
24
|
+
import { detectD20 } from "./d20-unreachable-after-exit.ts";
|
|
25
|
+
import { detectD21 } from "./d21-read-without-load.ts";
|
|
26
|
+
import { detectD22 } from "./d22-flowfield-without-calcfields.ts";
|
|
27
|
+
import { detectD29 } from "./d29-subscriber-modify-on-event-record.ts";
|
|
28
|
+
import { detectD32 } from "./d32-constant-boolean-parameter.ts";
|
|
29
|
+
import { detectD33 } from "./d33-unfiltered-bulk-write.ts";
|
|
30
|
+
import { detectD34 } from "./d34-commit-in-loop.ts";
|
|
31
|
+
import { detectD35 } from "./d35-commit-in-event-subscriber.ts";
|
|
32
|
+
import { detectD36 } from "./d36-late-setloadfields.ts";
|
|
33
|
+
import { detectD37 } from "./d37-validate-without-persist.ts";
|
|
34
|
+
import { detectD38 } from "./d38-subscriber-to-obsolete-event.ts";
|
|
35
|
+
import { detectD39 } from "./d39-record-left-dirty-across-chain.ts";
|
|
36
|
+
// D40 is intentionally NOT in DEFAULT_DETECTORS — opt-in only via
|
|
37
|
+
// `--detector d40-transitive-load-missing`. See the d40 module's JSDoc for the rationale
|
|
38
|
+
// (Phase 4 straight-line walker produces a high false-positive rate dominated by the
|
|
39
|
+
// loop-loaded record class; Phase 6's full walker closes it).
|
|
40
|
+
import { detectD40 } from "./d40-transitive-load-missing.ts";
|
|
41
|
+
import { detectD41 } from "./d41-transitive-filter-loss.ts";
|
|
42
|
+
import { detectD42 } from "./d42-cross-call-wrong-setloadfields.ts";
|
|
43
|
+
import { detectD43 } from "./d43-event-ishandled-skip.ts";
|
|
44
|
+
import { detectD44 } from "./d44-event-multi-subscriber-overlap.ts";
|
|
45
|
+
import { detectD45 } from "./d45-event-transitive-table-exposure.ts";
|
|
46
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
47
|
+
import { buildDetectorContext } from "./detector-context.ts";
|
|
48
|
+
|
|
49
|
+
/** A detector: a pure query over the summarised model + combined graph. */
|
|
50
|
+
export interface Detector {
|
|
51
|
+
name: string;
|
|
52
|
+
run(
|
|
53
|
+
model: SemanticModel,
|
|
54
|
+
graph: CombinedGraph,
|
|
55
|
+
ctx: DetectorContext,
|
|
56
|
+
): { findings: Finding[]; stats: DetectorStats };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The default detector registry. */
|
|
60
|
+
export const DEFAULT_DETECTORS: Detector[] = [
|
|
61
|
+
{ name: "d1-db-op-in-loop", run: detectD1 },
|
|
62
|
+
{ name: "d2-event-fanout-in-loop", run: detectD2 },
|
|
63
|
+
{ name: "d3-missing-setloadfields", run: detectD3 },
|
|
64
|
+
{ name: "d4-repeated-lookup-in-loop", run: detectD4 },
|
|
65
|
+
{ name: "d5-set-based-opportunity", run: detectD5 },
|
|
66
|
+
{ name: "d7-recursive-event-expansion", run: detectD7 },
|
|
67
|
+
{ name: "d8-commit-in-transaction", run: detectD8 },
|
|
68
|
+
{ name: "d9-transaction-span-summary", run: detectD9 },
|
|
69
|
+
{ name: "d10-self-modifying-loop", run: detectD10 },
|
|
70
|
+
{ name: "d11-modify-without-get", run: detectD11 },
|
|
71
|
+
{ name: "d12-dead-integration-event", run: detectD12 },
|
|
72
|
+
{ name: "d13-cross-app-internal-call", run: detectD13 },
|
|
73
|
+
{ name: "d14-dead-routine", run: detectD14 },
|
|
74
|
+
{ name: "d16-obsolete-routine-call", run: detectD16 },
|
|
75
|
+
{ name: "d17-min-version-drift", run: detectD17 },
|
|
76
|
+
{ name: "d18-constant-filter-in-loop", run: detectD18 },
|
|
77
|
+
{ name: "d19-unused-parameter", run: detectD19 },
|
|
78
|
+
{ name: "d20-unreachable-after-exit", run: detectD20 },
|
|
79
|
+
{ name: "d21-read-without-load", run: detectD21 },
|
|
80
|
+
{ name: "d22-flowfield-without-calcfields", run: detectD22 },
|
|
81
|
+
{ name: "d29-subscriber-modify-on-event-record", run: detectD29 },
|
|
82
|
+
{ name: "d32-constant-boolean-parameter", run: detectD32 },
|
|
83
|
+
{ name: "d33-unfiltered-bulk-write", run: detectD33 },
|
|
84
|
+
{ name: "d34-commit-in-loop", run: detectD34 },
|
|
85
|
+
{ name: "d35-commit-in-event-subscriber", run: detectD35 },
|
|
86
|
+
{ name: "d36-late-setloadfields", run: detectD36 },
|
|
87
|
+
{ name: "d37-validate-without-persist", run: detectD37 },
|
|
88
|
+
{ name: "d38-subscriber-to-obsolete-event", run: detectD38 },
|
|
89
|
+
{ name: "d39-record-left-dirty-across-chain", run: detectD39 },
|
|
90
|
+
{ name: "d41-transitive-filter-loss", run: detectD41 },
|
|
91
|
+
{ name: "d42-cross-call-wrong-setloadfields", run: detectD42 },
|
|
92
|
+
{ name: "d43-event-ishandled-skip", run: detectD43 },
|
|
93
|
+
{ name: "d44-event-multi-subscriber-overlap", run: detectD44 },
|
|
94
|
+
{ name: "d45-event-transitive-table-exposure", run: detectD45 },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Opt-in detectors — not in the default registry. Surfaced by `--detector <name>`
|
|
99
|
+
* (CLI) and `analyzeWorkspace({ detectors: [...DEFAULT_DETECTORS, ...OPT_IN_DETECTORS] })`
|
|
100
|
+
* (library) for callers that explicitly want them.
|
|
101
|
+
*
|
|
102
|
+
* Currently: D40 (transitive-load-missing) — Phase 6 graduates it back to the default
|
|
103
|
+
* registry once the full statement-tree walker eliminates the loop-loaded FP class.
|
|
104
|
+
*/
|
|
105
|
+
export const OPT_IN_DETECTORS: Detector[] = [
|
|
106
|
+
{ name: "d40-transitive-load-missing", run: detectD40 },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
/** All known detectors — default + opt-in. Useful for CLI surface enumeration. */
|
|
110
|
+
export const ALL_DETECTORS: Detector[] = [...DEFAULT_DETECTORS, ...OPT_IN_DETECTORS];
|
|
111
|
+
|
|
112
|
+
function primaryLocationKey(f: Finding): string {
|
|
113
|
+
const a = f.primaryLocation;
|
|
114
|
+
return `${a.sourceUnitId}:${a.range.startLine}:${a.range.startColumn}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Run every detector in isolation. A detector that throws does not kill the run — it becomes
|
|
119
|
+
* a `Diagnostic(stage: "detect")` and the rest still run. The combined Finding[] is sorted
|
|
120
|
+
* by (detector, primaryLocationKey, rootCauseKey) for deterministic output.
|
|
121
|
+
*/
|
|
122
|
+
export function runDetectors(
|
|
123
|
+
model: SemanticModel,
|
|
124
|
+
graph: CombinedGraph,
|
|
125
|
+
detectors: Detector[] = DEFAULT_DETECTORS,
|
|
126
|
+
): { findings: Finding[]; diagnostics: Diagnostic[]; detectorStats: DetectorStats[] } {
|
|
127
|
+
const findings: Finding[] = [];
|
|
128
|
+
const diagnostics: Diagnostic[] = [];
|
|
129
|
+
const detectorStats: DetectorStats[] = [];
|
|
130
|
+
const _tCtx = profilingActive() ? performance.now() : 0;
|
|
131
|
+
const ctx = buildDetectorContext(model, graph);
|
|
132
|
+
if (profilingActive()) recordPhase("detect:buildDetectorContext", performance.now() - _tCtx);
|
|
133
|
+
for (const detector of detectors) {
|
|
134
|
+
const _td = profilingActive() ? performance.now() : 0;
|
|
135
|
+
try {
|
|
136
|
+
const result = detector.run(model, graph, ctx);
|
|
137
|
+
findings.push(...result.findings);
|
|
138
|
+
detectorStats.push(result.stats);
|
|
139
|
+
if (profilingActive()) recordPhase(`detect:${detector.name}`, performance.now() - _td);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
diagnostics.push({
|
|
142
|
+
severity: "warning",
|
|
143
|
+
stage: "detect",
|
|
144
|
+
message: `Detector "${detector.name}" threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
diagnostics.push(...ctx.diagnostics);
|
|
149
|
+
const roleByRoutineId = new Map(model.routines.map((r) => [r.id, roleOf(r)]));
|
|
150
|
+
const scoped = findings.filter((f) => {
|
|
151
|
+
const primaryRole = roleByRoutineId.get(f.primaryLocation.enclosingRoutineId) ?? "primary";
|
|
152
|
+
if (primaryRole === "primary") return true;
|
|
153
|
+
// Detector flagged a primary-app actionable anchor (e.g. D1 whose terminal DB op
|
|
154
|
+
// is deep in a dependency, but the user's loop is in primary code).
|
|
155
|
+
if (f.actionableAnchor !== undefined) {
|
|
156
|
+
const anchorRole = roleByRoutineId.get(f.actionableAnchor.enclosingRoutineId) ?? "primary";
|
|
157
|
+
if (anchorRole === "primary") return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
});
|
|
161
|
+
scoped.sort((a, b) => {
|
|
162
|
+
if (a.detector !== b.detector) return compareNatural(a.detector, b.detector);
|
|
163
|
+
const la = primaryLocationKey(a);
|
|
164
|
+
const lb = primaryLocationKey(b);
|
|
165
|
+
if (la !== lb) return compareStrings(la, lb);
|
|
166
|
+
return compareStrings(a.rootCauseKey, b.rootCauseKey);
|
|
167
|
+
});
|
|
168
|
+
if (process.env.AL_SEM_DETECTOR_STATS === "1") {
|
|
169
|
+
for (const s of detectorStats) {
|
|
170
|
+
process.stderr.write(
|
|
171
|
+
`[detector-stats] ${s.detector}: candidates=${s.candidatesConsidered} emitted=${s.findingsEmitted} skipped=${JSON.stringify(s.skipped)}\n`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { findings: scoped, diagnostics, detectorStats };
|
|
176
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Routine, Table } from "../model/entities.ts";
|
|
2
|
+
import type { TableId } from "../model/ids.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Render a record-operation's target table for human consumption.
|
|
6
|
+
*
|
|
7
|
+
* Three tiers, in order of preference:
|
|
8
|
+
* 1. `op.tableId` resolves in `tableById` → return the table's NAME (e.g.
|
|
9
|
+
* `"Customer"`, `"CDC Template Field"`). This is what the user sees in
|
|
10
|
+
* their IDE.
|
|
11
|
+
* 2. `op.tableId` is undefined or unresolved AND the receiving record-variable
|
|
12
|
+
* has a declared `tableName` text → return that text, suffixed with
|
|
13
|
+
* `(type not loaded)`. This is the common case when the type lives in a
|
|
14
|
+
* dependency we couldn't load (e.g. opaque Continia deps, AL-language's
|
|
15
|
+
* `.dependencies/` cache that al-sem doesn't crawl). The user still gets
|
|
16
|
+
* the type they wrote in source.
|
|
17
|
+
* 3. Variable name itself, prefixed with `var `, as a last-resort identity
|
|
18
|
+
* (e.g. `var DocGroup`).
|
|
19
|
+
* 4. The string `"unknown table"`.
|
|
20
|
+
*
|
|
21
|
+
* Replaces the previous behavior of rendering the internal table id verbatim
|
|
22
|
+
* (e.g. `"table 6225286"` or
|
|
23
|
+
* `"f4b69b55-c90d-4937-8f53-2742898fa948/table/6175301"`) — useless to humans
|
|
24
|
+
* and the dominant UX paper-cut surfaced by the DC/Cloud precision study.
|
|
25
|
+
*/
|
|
26
|
+
export function describeTable(
|
|
27
|
+
op: { tableId?: TableId; recordVariableName: string },
|
|
28
|
+
routine: Routine | undefined,
|
|
29
|
+
tableById: Map<TableId, Table>,
|
|
30
|
+
): string {
|
|
31
|
+
if (op.tableId !== undefined) {
|
|
32
|
+
const table = tableById.get(op.tableId);
|
|
33
|
+
if (table) return table.name;
|
|
34
|
+
}
|
|
35
|
+
if (routine !== undefined) {
|
|
36
|
+
const lc = op.recordVariableName.toLowerCase();
|
|
37
|
+
const rv = routine.features.recordVariables.find((v) => v.name.toLowerCase() === lc);
|
|
38
|
+
if (rv?.tableName && rv.tableName !== "") return `${rv.tableName} (type not loaded)`;
|
|
39
|
+
}
|
|
40
|
+
if (op.recordVariableName !== "") return `var ${op.recordVariableName}`;
|
|
41
|
+
return "unknown table";
|
|
42
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { ContractFact } from "../snapshot/types.ts";
|
|
2
|
+
import { DiffCategory, DiffKind, computeDiffFingerprint } from "./diff-identity.ts";
|
|
3
|
+
import type { DiffIndexes } from "./diff-indexes.ts";
|
|
4
|
+
|
|
5
|
+
export interface DiffEngineOptions {
|
|
6
|
+
coveragePolicy: "loose" | "strict";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AbiDetails {
|
|
10
|
+
kind:
|
|
11
|
+
| DiffKind.ObjectRemoved
|
|
12
|
+
| DiffKind.ObjectAccessibilityNarrowed
|
|
13
|
+
| DiffKind.ProcedureRemoved
|
|
14
|
+
| DiffKind.ProcedureSignatureChanged
|
|
15
|
+
| DiffKind.ProcedureVarDirectionChanged
|
|
16
|
+
| DiffKind.ProcedureObsoletionRegressed
|
|
17
|
+
| DiffKind.ProcedureObsoletionProgressed
|
|
18
|
+
| DiffKind.ObjectAdded
|
|
19
|
+
| DiffKind.ProcedureAdded;
|
|
20
|
+
oldAccessibility?: string;
|
|
21
|
+
newAccessibility?: string;
|
|
22
|
+
oldObsoleteState?: string;
|
|
23
|
+
newObsoleteState?: string;
|
|
24
|
+
oldSignatureHash?: string;
|
|
25
|
+
newSignatureHash?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AbiFinding {
|
|
29
|
+
id: string;
|
|
30
|
+
category: DiffCategory.ABI;
|
|
31
|
+
kind: DiffKind;
|
|
32
|
+
severity: "critical" | "high" | "medium" | "low" | "info";
|
|
33
|
+
subject: {
|
|
34
|
+
normalizedStableId: string;
|
|
35
|
+
oldOriginalStableId?: string;
|
|
36
|
+
newStableId?: string;
|
|
37
|
+
displayName: string;
|
|
38
|
+
};
|
|
39
|
+
comparisonCone: readonly string[];
|
|
40
|
+
details: AbiDetails;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SEVERITY: Partial<Record<DiffKind, AbiFinding["severity"]>> = {
|
|
44
|
+
[DiffKind.ObjectRemoved]: "critical",
|
|
45
|
+
[DiffKind.ObjectAccessibilityNarrowed]: "critical",
|
|
46
|
+
[DiffKind.ProcedureRemoved]: "critical",
|
|
47
|
+
[DiffKind.ProcedureSignatureChanged]: "critical",
|
|
48
|
+
[DiffKind.ProcedureVarDirectionChanged]: "critical",
|
|
49
|
+
[DiffKind.ProcedureObsoletionRegressed]: "high",
|
|
50
|
+
[DiffKind.ProcedureObsoletionProgressed]: "info",
|
|
51
|
+
[DiffKind.ObjectAdded]: "info",
|
|
52
|
+
[DiffKind.ProcedureAdded]: "info",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Visibility lattice order: local < internal < protected < public.
|
|
57
|
+
* Narrowing = moving to a less visible level (higher index → lower index).
|
|
58
|
+
*/
|
|
59
|
+
const VISIBILITY_ORDER = ["local", "internal", "protected", "public"];
|
|
60
|
+
|
|
61
|
+
function isNarrowed(oldVisibility: string | undefined, newVisibility: string | undefined): boolean {
|
|
62
|
+
const o = VISIBILITY_ORDER.indexOf(oldVisibility ?? "");
|
|
63
|
+
const n = VISIBILITY_ORDER.indexOf(newVisibility ?? "");
|
|
64
|
+
return o > n && n >= 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns true when the contract fact's kind represents a procedure-like
|
|
69
|
+
* surface (routine, event-publisher) rather than a plain object or interface.
|
|
70
|
+
*/
|
|
71
|
+
function isProcedureLike(fact: ContractFact): boolean {
|
|
72
|
+
return fact.kind === "routine" || fact.kind === "event-publisher";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function makeFinding(
|
|
76
|
+
kind: AbiDetails["kind"],
|
|
77
|
+
subjectId: string,
|
|
78
|
+
indexes: DiffIndexes,
|
|
79
|
+
details: AbiDetails,
|
|
80
|
+
): AbiFinding {
|
|
81
|
+
const origin = indexes.originByNormalized.get(subjectId);
|
|
82
|
+
const display =
|
|
83
|
+
indexes.newDisplayByStableId.get(subjectId) ??
|
|
84
|
+
indexes.oldDisplayByStableId.get(subjectId) ??
|
|
85
|
+
subjectId;
|
|
86
|
+
return {
|
|
87
|
+
id: computeDiffFingerprint({
|
|
88
|
+
category: DiffCategory.ABI,
|
|
89
|
+
kind,
|
|
90
|
+
normalizedStableId: subjectId,
|
|
91
|
+
}),
|
|
92
|
+
category: DiffCategory.ABI,
|
|
93
|
+
kind,
|
|
94
|
+
severity: SEVERITY[kind] ?? "medium",
|
|
95
|
+
subject: {
|
|
96
|
+
normalizedStableId: subjectId,
|
|
97
|
+
oldOriginalStableId: origin?.oldOriginalStableId,
|
|
98
|
+
newStableId: origin?.newStableId,
|
|
99
|
+
displayName: display,
|
|
100
|
+
},
|
|
101
|
+
comparisonCone: [subjectId],
|
|
102
|
+
details,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function diffAbi(
|
|
107
|
+
_oldSnap: unknown,
|
|
108
|
+
_newSnap: unknown,
|
|
109
|
+
indexes: DiffIndexes,
|
|
110
|
+
_opts: DiffEngineOptions,
|
|
111
|
+
): AbiFinding[] {
|
|
112
|
+
const out: AbiFinding[] = [];
|
|
113
|
+
|
|
114
|
+
// Walk OLD facts — detect removals and in-place changes.
|
|
115
|
+
for (const [subject, oldFact] of indexes.oldContractsBySubject) {
|
|
116
|
+
const newFact = indexes.newContractsBySubject.get(subject);
|
|
117
|
+
if (newFact === undefined) {
|
|
118
|
+
// Removal — distinguish object vs procedure-like.
|
|
119
|
+
const removalKind = isProcedureLike(oldFact)
|
|
120
|
+
? DiffKind.ProcedureRemoved
|
|
121
|
+
: DiffKind.ObjectRemoved;
|
|
122
|
+
out.push(
|
|
123
|
+
makeFinding(removalKind as AbiDetails["kind"], subject, indexes, {
|
|
124
|
+
kind: removalKind as AbiDetails["kind"],
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Compare in place — visibility, obsoletion, signature.
|
|
131
|
+
const oldVisibility = oldFact.visibility;
|
|
132
|
+
const newVisibility = newFact.visibility;
|
|
133
|
+
if (isNarrowed(oldVisibility, newVisibility)) {
|
|
134
|
+
out.push(
|
|
135
|
+
makeFinding(DiffKind.ObjectAccessibilityNarrowed, subject, indexes, {
|
|
136
|
+
kind: DiffKind.ObjectAccessibilityNarrowed,
|
|
137
|
+
oldAccessibility: oldVisibility,
|
|
138
|
+
newAccessibility: newVisibility,
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Obsoletion transition: flat `obsoleteState` field ("Pending" | "Removed").
|
|
144
|
+
const oldObs = oldFact.obsoleteState;
|
|
145
|
+
const newObs = newFact.obsoleteState;
|
|
146
|
+
if (oldObs !== newObs) {
|
|
147
|
+
const order: Record<string, number> = { Pending: 1, Removed: 2 };
|
|
148
|
+
const oldRank = oldObs !== undefined ? (order[oldObs] ?? 0) : 0;
|
|
149
|
+
const newRank = newObs !== undefined ? (order[newObs] ?? 0) : 0;
|
|
150
|
+
if (newRank > oldRank) {
|
|
151
|
+
out.push(
|
|
152
|
+
makeFinding(DiffKind.ProcedureObsoletionProgressed, subject, indexes, {
|
|
153
|
+
kind: DiffKind.ProcedureObsoletionProgressed,
|
|
154
|
+
oldObsoleteState: oldObs,
|
|
155
|
+
newObsoleteState: newObs,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
} else if (newRank < oldRank) {
|
|
159
|
+
out.push(
|
|
160
|
+
makeFinding(DiffKind.ProcedureObsoletionRegressed, subject, indexes, {
|
|
161
|
+
kind: DiffKind.ProcedureObsoletionRegressed,
|
|
162
|
+
oldObsoleteState: oldObs,
|
|
163
|
+
newObsoleteState: newObs,
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Signature fingerprint change (routines and event-publishers carry this).
|
|
170
|
+
const oldSig = oldFact.signatureFingerprint;
|
|
171
|
+
const newSig = newFact.signatureFingerprint;
|
|
172
|
+
if (oldSig !== "" && newSig !== "" && oldSig !== newSig) {
|
|
173
|
+
out.push(
|
|
174
|
+
makeFinding(DiffKind.ProcedureSignatureChanged, subject, indexes, {
|
|
175
|
+
kind: DiffKind.ProcedureSignatureChanged,
|
|
176
|
+
oldSignatureHash: oldSig,
|
|
177
|
+
newSignatureHash: newSig,
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Walk NEW facts to find additions.
|
|
184
|
+
for (const [subject, newFact] of indexes.newContractsBySubject) {
|
|
185
|
+
if (indexes.oldContractsBySubject.has(subject)) continue;
|
|
186
|
+
const addKind = isProcedureLike(newFact) ? DiffKind.ProcedureAdded : DiffKind.ObjectAdded;
|
|
187
|
+
out.push(
|
|
188
|
+
makeFinding(addKind as AbiDetails["kind"], subject, indexes, {
|
|
189
|
+
kind: addKind as AbiDetails["kind"],
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
195
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { CapabilityFact, CapabilityResourceKind } from "../model/capability.ts";
|
|
2
|
+
import type { DiffEngineOptions } from "./diff-abi.ts";
|
|
3
|
+
import { DiffCategory, DiffKind, computeDiffFingerprint } from "./diff-identity.ts";
|
|
4
|
+
import type { DiffIndexes } from "./diff-indexes.ts";
|
|
5
|
+
|
|
6
|
+
export interface CapabilityDetails {
|
|
7
|
+
kind:
|
|
8
|
+
| DiffKind.CapabilityGainedWrite
|
|
9
|
+
| DiffKind.CapabilityGainedRead
|
|
10
|
+
| DiffKind.CapabilityGainedCommit
|
|
11
|
+
| DiffKind.CapabilityGainedHttp
|
|
12
|
+
| DiffKind.CapabilityGainedTelemetry
|
|
13
|
+
| DiffKind.CapabilityGainedIsolatedStorage
|
|
14
|
+
| DiffKind.CapabilityGainedFile
|
|
15
|
+
| DiffKind.CapabilityGainedDynamicDispatch
|
|
16
|
+
| DiffKind.CapabilityGainedEventPublish
|
|
17
|
+
| DiffKind.CapabilityLost
|
|
18
|
+
| DiffKind.CapabilityLostUnderCoverage;
|
|
19
|
+
resourceKind: string;
|
|
20
|
+
resourceId?: string;
|
|
21
|
+
op: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CapabilityFinding {
|
|
25
|
+
id: string;
|
|
26
|
+
category: DiffCategory.Capabilities;
|
|
27
|
+
kind: DiffKind;
|
|
28
|
+
severity: "critical" | "high" | "medium" | "low" | "info";
|
|
29
|
+
subject: {
|
|
30
|
+
normalizedStableId: string;
|
|
31
|
+
oldOriginalStableId?: string;
|
|
32
|
+
newStableId?: string;
|
|
33
|
+
displayName: string;
|
|
34
|
+
};
|
|
35
|
+
comparisonCone: readonly string[];
|
|
36
|
+
details: CapabilityDetails;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SEVERITY: Partial<Record<DiffKind, CapabilityFinding["severity"]>> = {
|
|
40
|
+
[DiffKind.CapabilityGainedCommit]: "high",
|
|
41
|
+
[DiffKind.CapabilityGainedDynamicDispatch]: "high",
|
|
42
|
+
[DiffKind.CapabilityGainedWrite]: "medium",
|
|
43
|
+
[DiffKind.CapabilityGainedRead]: "medium",
|
|
44
|
+
[DiffKind.CapabilityGainedHttp]: "medium",
|
|
45
|
+
[DiffKind.CapabilityGainedTelemetry]: "medium",
|
|
46
|
+
[DiffKind.CapabilityGainedIsolatedStorage]: "medium",
|
|
47
|
+
[DiffKind.CapabilityGainedFile]: "medium",
|
|
48
|
+
[DiffKind.CapabilityGainedEventPublish]: "medium",
|
|
49
|
+
[DiffKind.CapabilityLost]: "medium",
|
|
50
|
+
[DiffKind.CapabilityLostUnderCoverage]: "low",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function gainedKindFor(fact: CapabilityFact): DiffKind | undefined {
|
|
54
|
+
const kind = fact.resourceKind as CapabilityResourceKind;
|
|
55
|
+
const op = fact.op;
|
|
56
|
+
if (kind === "table") {
|
|
57
|
+
if (op === "read") return DiffKind.CapabilityGainedRead;
|
|
58
|
+
if (op === "insert" || op === "modify" || op === "delete")
|
|
59
|
+
return DiffKind.CapabilityGainedWrite;
|
|
60
|
+
}
|
|
61
|
+
if (kind === "transaction" && op === "commit") return DiffKind.CapabilityGainedCommit;
|
|
62
|
+
if (kind === "http") return DiffKind.CapabilityGainedHttp;
|
|
63
|
+
if (kind === "telemetry") return DiffKind.CapabilityGainedTelemetry;
|
|
64
|
+
if (kind === "isolated-storage") return DiffKind.CapabilityGainedIsolatedStorage;
|
|
65
|
+
if (kind === "file") return DiffKind.CapabilityGainedFile;
|
|
66
|
+
if (kind === "event" && op === "publish") return DiffKind.CapabilityGainedEventPublish;
|
|
67
|
+
if ((kind === "codeunit" || kind === "page" || kind === "report") && op === "execute") {
|
|
68
|
+
const extra = (fact as { extra?: { kind?: string } }).extra;
|
|
69
|
+
const dispatchUnresolved = extra?.kind === "dispatch" && fact.resourceId === undefined;
|
|
70
|
+
if (dispatchUnresolved) return DiffKind.CapabilityGainedDynamicDispatch;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Key for matching facts across snapshots. */
|
|
76
|
+
function factKey(fact: CapabilityFact): string {
|
|
77
|
+
return `${fact.op}|${fact.resourceKind}|${String(fact.resourceId ?? "")}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeFinding(
|
|
81
|
+
kind: CapabilityDetails["kind"],
|
|
82
|
+
subjectId: string,
|
|
83
|
+
indexes: DiffIndexes,
|
|
84
|
+
details: CapabilityDetails,
|
|
85
|
+
secondaryKey: string,
|
|
86
|
+
comparisonCone: readonly string[],
|
|
87
|
+
): CapabilityFinding {
|
|
88
|
+
const origin = indexes.originByNormalized.get(subjectId);
|
|
89
|
+
const display =
|
|
90
|
+
indexes.newDisplayByStableId.get(subjectId) ??
|
|
91
|
+
indexes.oldDisplayByStableId.get(subjectId) ??
|
|
92
|
+
subjectId;
|
|
93
|
+
return {
|
|
94
|
+
id: computeDiffFingerprint({
|
|
95
|
+
category: DiffCategory.Capabilities,
|
|
96
|
+
kind,
|
|
97
|
+
normalizedStableId: subjectId,
|
|
98
|
+
secondaryKey,
|
|
99
|
+
}),
|
|
100
|
+
category: DiffCategory.Capabilities,
|
|
101
|
+
kind,
|
|
102
|
+
severity: SEVERITY[kind] ?? "medium",
|
|
103
|
+
subject: {
|
|
104
|
+
normalizedStableId: subjectId,
|
|
105
|
+
oldOriginalStableId: origin?.oldOriginalStableId,
|
|
106
|
+
newStableId: origin?.newStableId,
|
|
107
|
+
displayName: display,
|
|
108
|
+
},
|
|
109
|
+
comparisonCone,
|
|
110
|
+
details,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function diffCapabilities(
|
|
115
|
+
_oldSnap: unknown,
|
|
116
|
+
_newSnap: unknown,
|
|
117
|
+
indexes: DiffIndexes,
|
|
118
|
+
_opts: DiffEngineOptions,
|
|
119
|
+
): CapabilityFinding[] {
|
|
120
|
+
const out: CapabilityFinding[] = [];
|
|
121
|
+
|
|
122
|
+
const allSubjects = new Set<string>();
|
|
123
|
+
for (const s of indexes.oldCapabilityFactsBySubject.keys()) allSubjects.add(s);
|
|
124
|
+
for (const s of indexes.newCapabilityFactsBySubject.keys()) allSubjects.add(s);
|
|
125
|
+
|
|
126
|
+
for (const subject of allSubjects) {
|
|
127
|
+
const oldFacts = indexes.oldCapabilityFactsBySubject.get(subject) ?? [];
|
|
128
|
+
const newFacts = indexes.newCapabilityFactsBySubject.get(subject) ?? [];
|
|
129
|
+
const oldByKey = new Map<string, CapabilityFact>();
|
|
130
|
+
const newByKey = new Map<string, CapabilityFact>();
|
|
131
|
+
for (const f of oldFacts) oldByKey.set(factKey(f), f);
|
|
132
|
+
for (const f of newFacts) newByKey.set(factKey(f), f);
|
|
133
|
+
|
|
134
|
+
// Gains: in new, not in old.
|
|
135
|
+
for (const [key, fact] of newByKey) {
|
|
136
|
+
if (oldByKey.has(key)) continue;
|
|
137
|
+
const gainKind = gainedKindFor(fact);
|
|
138
|
+
if (gainKind === undefined) continue;
|
|
139
|
+
out.push(
|
|
140
|
+
makeFinding(
|
|
141
|
+
gainKind as CapabilityDetails["kind"],
|
|
142
|
+
subject,
|
|
143
|
+
indexes,
|
|
144
|
+
{
|
|
145
|
+
kind: gainKind as CapabilityDetails["kind"],
|
|
146
|
+
resourceKind: fact.resourceKind,
|
|
147
|
+
resourceId: fact.resourceId !== undefined ? String(fact.resourceId) : undefined,
|
|
148
|
+
op: fact.op,
|
|
149
|
+
},
|
|
150
|
+
key,
|
|
151
|
+
[subject],
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Losses: in old, not in new. Emit provisional CapabilityLost; policy
|
|
157
|
+
// post-pass downgrades to CapabilityLostUnderCoverage when applicable.
|
|
158
|
+
for (const [key, fact] of oldByKey) {
|
|
159
|
+
if (newByKey.has(key)) continue;
|
|
160
|
+
out.push(
|
|
161
|
+
makeFinding(
|
|
162
|
+
DiffKind.CapabilityLost,
|
|
163
|
+
subject,
|
|
164
|
+
indexes,
|
|
165
|
+
{
|
|
166
|
+
kind: DiffKind.CapabilityLost,
|
|
167
|
+
resourceKind: fact.resourceKind,
|
|
168
|
+
resourceId: fact.resourceId !== undefined ? String(fact.resourceId) : undefined,
|
|
169
|
+
op: fact.op,
|
|
170
|
+
},
|
|
171
|
+
key,
|
|
172
|
+
[subject],
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
179
|
+
}
|