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,199 @@
|
|
|
1
|
+
import type { CapabilityFact } from "../model/capability.ts";
|
|
2
|
+
import type { CoverageRecord } from "../model/coverage.ts";
|
|
3
|
+
import type { PermissionFact } from "../model/permission.ts";
|
|
4
|
+
import type {
|
|
5
|
+
CapabilitySnapshot,
|
|
6
|
+
ContractFact,
|
|
7
|
+
EventDeclaration,
|
|
8
|
+
SchemaFact,
|
|
9
|
+
} from "../snapshot/types.ts";
|
|
10
|
+
import type { RenameDiagnostic, RenameTable } from "./diff-renames.ts";
|
|
11
|
+
import { validateOverlayAgainstSnapshots } from "./diff-renames.ts";
|
|
12
|
+
|
|
13
|
+
export interface SubjectOrigin {
|
|
14
|
+
normalizedStableId: string;
|
|
15
|
+
oldOriginalStableId?: string;
|
|
16
|
+
newStableId?: string;
|
|
17
|
+
renameApplied: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DiffIndexes {
|
|
21
|
+
// Per-snapshot display lookup keyed by normalized id.
|
|
22
|
+
oldDisplayByStableId: Map<string, string>;
|
|
23
|
+
newDisplayByStableId: Map<string, string>;
|
|
24
|
+
// Origin tracking — produces renderer-side "renamed from X" rendering.
|
|
25
|
+
originByNormalized: Map<string, SubjectOrigin>;
|
|
26
|
+
// Fact lookups, all OLD side rename-normalized.
|
|
27
|
+
oldContractsBySubject: Map<string, ContractFact>;
|
|
28
|
+
newContractsBySubject: Map<string, ContractFact>;
|
|
29
|
+
oldSchemaBySubject: Map<string, SchemaFact[]>;
|
|
30
|
+
newSchemaBySubject: Map<string, SchemaFact[]>;
|
|
31
|
+
oldPermissionsBySubject: Map<string, PermissionFact[]>;
|
|
32
|
+
newPermissionsBySubject: Map<string, PermissionFact[]>;
|
|
33
|
+
oldEventsBySubject: Map<string, EventDeclaration[]>;
|
|
34
|
+
newEventsBySubject: Map<string, EventDeclaration[]>;
|
|
35
|
+
oldCapabilityFactsBySubject: Map<string, CapabilityFact[]>;
|
|
36
|
+
newCapabilityFactsBySubject: Map<string, CapabilityFact[]>;
|
|
37
|
+
oldCoverageBySubject: Map<string, CoverageRecord>;
|
|
38
|
+
newCoverageBySubject: Map<string, CoverageRecord>;
|
|
39
|
+
// Stale-rename diagnostics surfaced here since membership is now known.
|
|
40
|
+
renameDiagnostics: readonly RenameDiagnostic[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalize(id: string, renameTable: RenameTable): string {
|
|
44
|
+
const entry = renameTable.get(id);
|
|
45
|
+
return entry?.newId ?? id;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pushByKey<V>(map: Map<string, V[]>, key: string, value: V): void {
|
|
49
|
+
const list = map.get(key) ?? [];
|
|
50
|
+
list.push(value);
|
|
51
|
+
map.set(key, list);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract the primary subject id from a PermissionFact.
|
|
56
|
+
*
|
|
57
|
+
* `DeclaredPermissionFact` groups by `permissionSet` (no routine subject).
|
|
58
|
+
* `RequiredPermissionFact` groups by `subject` (the routine the requirement
|
|
59
|
+
* is attached to). The `kind` discriminator selects the right field.
|
|
60
|
+
*/
|
|
61
|
+
function permissionFactSubject(fact: PermissionFact): string {
|
|
62
|
+
if (fact.kind === "required") {
|
|
63
|
+
return fact.subject;
|
|
64
|
+
}
|
|
65
|
+
// declared: group by permissionSet
|
|
66
|
+
return fact.permissionSet;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildDiffIndexes(
|
|
70
|
+
oldSnap: CapabilitySnapshot,
|
|
71
|
+
newSnap: CapabilitySnapshot,
|
|
72
|
+
renameTable: RenameTable,
|
|
73
|
+
): DiffIndexes {
|
|
74
|
+
const oldDisplayByStableId = new Map<string, string>();
|
|
75
|
+
const newDisplayByStableId = new Map<string, string>();
|
|
76
|
+
const originByNormalized = new Map<string, SubjectOrigin>();
|
|
77
|
+
|
|
78
|
+
// Old identities — normalize.
|
|
79
|
+
for (let i = 0; i < oldSnap.identities.stableIds.length; i++) {
|
|
80
|
+
const id = oldSnap.identities.stableIds[i] ?? "";
|
|
81
|
+
const display = oldSnap.identities.displayNames[i] ?? "";
|
|
82
|
+
if (id === "") continue;
|
|
83
|
+
const normalized = normalize(id, renameTable);
|
|
84
|
+
oldDisplayByStableId.set(normalized, display);
|
|
85
|
+
const renameApplied = normalized !== id;
|
|
86
|
+
originByNormalized.set(normalized, {
|
|
87
|
+
normalizedStableId: normalized,
|
|
88
|
+
oldOriginalStableId: renameApplied ? id : undefined,
|
|
89
|
+
renameApplied,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// New identities — never renamed; populate display + fold into origin.
|
|
94
|
+
for (let i = 0; i < newSnap.identities.stableIds.length; i++) {
|
|
95
|
+
const id = newSnap.identities.stableIds[i] ?? "";
|
|
96
|
+
const display = newSnap.identities.displayNames[i] ?? "";
|
|
97
|
+
if (id === "") continue;
|
|
98
|
+
newDisplayByStableId.set(id, display);
|
|
99
|
+
const origin = originByNormalized.get(id);
|
|
100
|
+
if (origin !== undefined) {
|
|
101
|
+
origin.newStableId = id;
|
|
102
|
+
} else {
|
|
103
|
+
originByNormalized.set(id, {
|
|
104
|
+
normalizedStableId: id,
|
|
105
|
+
newStableId: id,
|
|
106
|
+
renameApplied: false,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ContractFacts — keyed by stableId (the primary subject field on ContractFact).
|
|
112
|
+
const oldContractsBySubject = new Map<string, ContractFact>();
|
|
113
|
+
for (const fact of oldSnap.contractFacts) {
|
|
114
|
+
oldContractsBySubject.set(normalize(fact.stableId, renameTable), fact);
|
|
115
|
+
}
|
|
116
|
+
const newContractsBySubject = new Map<string, ContractFact>();
|
|
117
|
+
for (const fact of newSnap.contractFacts) {
|
|
118
|
+
newContractsBySubject.set(fact.stableId, fact);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// SchemaFacts — keyed by stableId (the primary subject field on SchemaFact).
|
|
122
|
+
const oldSchemaBySubject = new Map<string, SchemaFact[]>();
|
|
123
|
+
for (const fact of oldSnap.schemaFacts) {
|
|
124
|
+
pushByKey(oldSchemaBySubject, normalize(fact.stableId, renameTable), fact);
|
|
125
|
+
}
|
|
126
|
+
const newSchemaBySubject = new Map<string, SchemaFact[]>();
|
|
127
|
+
for (const fact of newSnap.schemaFacts) {
|
|
128
|
+
pushByKey(newSchemaBySubject, fact.stableId, fact);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// PermissionFacts — keyed by permissionFactSubject (declared→permissionSet,
|
|
132
|
+
// required→subject).
|
|
133
|
+
const oldPermissionsBySubject = new Map<string, PermissionFact[]>();
|
|
134
|
+
for (const fact of oldSnap.permissionFacts) {
|
|
135
|
+
pushByKey(oldPermissionsBySubject, normalize(permissionFactSubject(fact), renameTable), fact);
|
|
136
|
+
}
|
|
137
|
+
const newPermissionsBySubject = new Map<string, PermissionFact[]>();
|
|
138
|
+
for (const fact of newSnap.permissionFacts) {
|
|
139
|
+
pushByKey(newPermissionsBySubject, permissionFactSubject(fact), fact);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// EventDeclarations — keyed by routine (StableRoutineId).
|
|
143
|
+
const oldEventsBySubject = new Map<string, EventDeclaration[]>();
|
|
144
|
+
for (const decl of oldSnap.eventDeclarations) {
|
|
145
|
+
pushByKey(oldEventsBySubject, normalize(decl.routine, renameTable), decl);
|
|
146
|
+
}
|
|
147
|
+
const newEventsBySubject = new Map<string, EventDeclaration[]>();
|
|
148
|
+
for (const decl of newSnap.eventDeclarations) {
|
|
149
|
+
pushByKey(newEventsBySubject, decl.routine, decl);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// CapabilityFacts — keyed by subject (RoutineId).
|
|
153
|
+
const oldCapabilityFactsBySubject = new Map<string, CapabilityFact[]>();
|
|
154
|
+
for (const fact of oldSnap.capabilityFacts) {
|
|
155
|
+
pushByKey(oldCapabilityFactsBySubject, normalize(fact.subject, renameTable), fact);
|
|
156
|
+
}
|
|
157
|
+
const newCapabilityFactsBySubject = new Map<string, CapabilityFact[]>();
|
|
158
|
+
for (const fact of newSnap.capabilityFacts) {
|
|
159
|
+
pushByKey(newCapabilityFactsBySubject, fact.subject, fact);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// CoverageRecords — keyed by subject (RoutineId).
|
|
163
|
+
const oldCoverageBySubject = new Map<string, CoverageRecord>();
|
|
164
|
+
for (const rec of oldSnap.coverage) {
|
|
165
|
+
oldCoverageBySubject.set(normalize(rec.subject, renameTable), rec);
|
|
166
|
+
}
|
|
167
|
+
const newCoverageBySubject = new Map<string, CoverageRecord>();
|
|
168
|
+
for (const rec of newSnap.coverage) {
|
|
169
|
+
newCoverageBySubject.set(rec.subject, rec);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Stale-rename diagnostics now that snapshot membership is known.
|
|
173
|
+
const oldIds = new Set(oldSnap.identities.stableIds);
|
|
174
|
+
const newIds = new Set(newSnap.identities.stableIds);
|
|
175
|
+
const overlayLike: Record<string, string> = {};
|
|
176
|
+
for (const [oldId, entry] of renameTable) {
|
|
177
|
+
overlayLike[oldId] = entry.newId;
|
|
178
|
+
}
|
|
179
|
+
const renameDiagnostics = validateOverlayAgainstSnapshots(overlayLike, oldIds, newIds);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
oldDisplayByStableId,
|
|
183
|
+
newDisplayByStableId,
|
|
184
|
+
originByNormalized,
|
|
185
|
+
oldContractsBySubject,
|
|
186
|
+
newContractsBySubject,
|
|
187
|
+
oldSchemaBySubject,
|
|
188
|
+
newSchemaBySubject,
|
|
189
|
+
oldPermissionsBySubject,
|
|
190
|
+
newPermissionsBySubject,
|
|
191
|
+
oldEventsBySubject,
|
|
192
|
+
newEventsBySubject,
|
|
193
|
+
oldCapabilityFactsBySubject,
|
|
194
|
+
newCapabilityFactsBySubject,
|
|
195
|
+
oldCoverageBySubject,
|
|
196
|
+
newCoverageBySubject,
|
|
197
|
+
renameDiagnostics,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { RequiredPermissionFact } from "../model/permission.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 PermissionDetails {
|
|
7
|
+
kind:
|
|
8
|
+
| DiffKind.PermissionRightsExpanded
|
|
9
|
+
| DiffKind.PermissionRightsContracted
|
|
10
|
+
| DiffKind.PermissionTargetAdded
|
|
11
|
+
| DiffKind.PermissionTargetRemoved;
|
|
12
|
+
targetKind: string;
|
|
13
|
+
targetId: string;
|
|
14
|
+
oldRights?: string[];
|
|
15
|
+
newRights?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PermissionFinding {
|
|
19
|
+
id: string;
|
|
20
|
+
category: DiffCategory.Permissions;
|
|
21
|
+
kind: DiffKind;
|
|
22
|
+
severity: "critical" | "high" | "medium" | "low" | "info";
|
|
23
|
+
subject: {
|
|
24
|
+
normalizedStableId: string;
|
|
25
|
+
oldOriginalStableId?: string;
|
|
26
|
+
newStableId?: string;
|
|
27
|
+
displayName: string;
|
|
28
|
+
};
|
|
29
|
+
comparisonCone: readonly string[];
|
|
30
|
+
details: PermissionDetails;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SEVERITY: Partial<Record<DiffKind, PermissionFinding["severity"]>> = {
|
|
34
|
+
[DiffKind.PermissionRightsExpanded]: "high",
|
|
35
|
+
[DiffKind.PermissionTargetAdded]: "high",
|
|
36
|
+
[DiffKind.PermissionRightsContracted]: "low",
|
|
37
|
+
[DiffKind.PermissionTargetRemoved]: "low",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns true when newRights is a strict superset of oldRights.
|
|
42
|
+
* All elements of oldRights are present in newRights, and newRights has more.
|
|
43
|
+
*/
|
|
44
|
+
function isExpansion(oldRights: readonly string[], newRights: readonly string[]): boolean {
|
|
45
|
+
const oldSet = new Set(oldRights);
|
|
46
|
+
const newSet = new Set(newRights);
|
|
47
|
+
if (newSet.size <= oldSet.size) return false;
|
|
48
|
+
for (const r of oldSet) {
|
|
49
|
+
if (!newSet.has(r)) return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns true when newRights is a strict subset of oldRights.
|
|
56
|
+
* All elements of newRights are present in oldRights, and oldRights has more.
|
|
57
|
+
*/
|
|
58
|
+
function isContraction(oldRights: readonly string[], newRights: readonly string[]): boolean {
|
|
59
|
+
const oldSet = new Set(oldRights);
|
|
60
|
+
const newSet = new Set(newRights);
|
|
61
|
+
if (oldSet.size <= newSet.size) return false;
|
|
62
|
+
for (const r of newSet) {
|
|
63
|
+
if (!oldSet.has(r)) return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function makeFinding(
|
|
69
|
+
kind: PermissionDetails["kind"],
|
|
70
|
+
subjectId: string,
|
|
71
|
+
indexes: DiffIndexes,
|
|
72
|
+
details: PermissionDetails,
|
|
73
|
+
secondaryKey: string,
|
|
74
|
+
): PermissionFinding {
|
|
75
|
+
const origin = indexes.originByNormalized.get(subjectId);
|
|
76
|
+
const display =
|
|
77
|
+
indexes.newDisplayByStableId.get(subjectId) ??
|
|
78
|
+
indexes.oldDisplayByStableId.get(subjectId) ??
|
|
79
|
+
subjectId;
|
|
80
|
+
return {
|
|
81
|
+
id: computeDiffFingerprint({
|
|
82
|
+
category: DiffCategory.Permissions,
|
|
83
|
+
kind,
|
|
84
|
+
normalizedStableId: subjectId,
|
|
85
|
+
secondaryKey,
|
|
86
|
+
}),
|
|
87
|
+
category: DiffCategory.Permissions,
|
|
88
|
+
kind,
|
|
89
|
+
severity: SEVERITY[kind] ?? "medium",
|
|
90
|
+
subject: {
|
|
91
|
+
normalizedStableId: subjectId,
|
|
92
|
+
oldOriginalStableId: origin?.oldOriginalStableId,
|
|
93
|
+
newStableId: origin?.newStableId,
|
|
94
|
+
displayName: display,
|
|
95
|
+
},
|
|
96
|
+
comparisonCone: [subjectId],
|
|
97
|
+
details,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Diff required permission footprints between two snapshots.
|
|
103
|
+
*
|
|
104
|
+
* Only `RequiredPermissionFact` (kind === "required") are compared — they
|
|
105
|
+
* carry a `subject` routine id and a `target`/`targetKind`/`rights` tuple.
|
|
106
|
+
* `DeclaredPermissionFact` (kind === "declared") has a different shape
|
|
107
|
+
* (grouped by permissionSet, no routine subject) and is out of scope for
|
|
108
|
+
* this delta; a future task can add a declared-permission diff pass.
|
|
109
|
+
*
|
|
110
|
+
* Rights comparison uses set semantics over `PermissionRight[]`:
|
|
111
|
+
* - strict superset → PermissionRightsExpanded (high)
|
|
112
|
+
* - strict subset → PermissionRightsContracted (low)
|
|
113
|
+
* - mixed change → PermissionRightsExpanded (any new right is dangerous)
|
|
114
|
+
*/
|
|
115
|
+
export function diffPermissions(
|
|
116
|
+
_oldSnap: unknown,
|
|
117
|
+
_newSnap: unknown,
|
|
118
|
+
indexes: DiffIndexes,
|
|
119
|
+
_opts: DiffEngineOptions,
|
|
120
|
+
): PermissionFinding[] {
|
|
121
|
+
const out: PermissionFinding[] = [];
|
|
122
|
+
|
|
123
|
+
const allSubjects = new Set<string>();
|
|
124
|
+
for (const s of indexes.oldPermissionsBySubject.keys()) allSubjects.add(s);
|
|
125
|
+
for (const s of indexes.newPermissionsBySubject.keys()) allSubjects.add(s);
|
|
126
|
+
|
|
127
|
+
for (const subject of allSubjects) {
|
|
128
|
+
// Filter to RequiredPermissionFact only — DeclaredPermissionFact has no
|
|
129
|
+
// (subject, target) shape comparable here.
|
|
130
|
+
const oldFacts = (indexes.oldPermissionsBySubject.get(subject) ?? []).filter(
|
|
131
|
+
(f): f is RequiredPermissionFact => f.kind === "required",
|
|
132
|
+
);
|
|
133
|
+
const newFacts = (indexes.newPermissionsBySubject.get(subject) ?? []).filter(
|
|
134
|
+
(f): f is RequiredPermissionFact => f.kind === "required",
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const oldByTarget = new Map<string, { targetKind: string; rights: string[] }>();
|
|
138
|
+
const newByTarget = new Map<string, { targetKind: string; rights: string[] }>();
|
|
139
|
+
|
|
140
|
+
for (const f of oldFacts) {
|
|
141
|
+
const tk = f.targetKind;
|
|
142
|
+
const tid = String(f.target);
|
|
143
|
+
oldByTarget.set(`${tk}|${tid}`, { targetKind: tk, rights: f.rights as string[] });
|
|
144
|
+
}
|
|
145
|
+
for (const f of newFacts) {
|
|
146
|
+
const tk = f.targetKind;
|
|
147
|
+
const tid = String(f.target);
|
|
148
|
+
newByTarget.set(`${tk}|${tid}`, { targetKind: tk, rights: f.rights as string[] });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Walk old targets — detect removals and rights changes.
|
|
152
|
+
for (const [key, oldEntry] of oldByTarget) {
|
|
153
|
+
const newEntry = newByTarget.get(key);
|
|
154
|
+
const sepIdx = key.indexOf("|");
|
|
155
|
+
const tk = key.slice(0, sepIdx);
|
|
156
|
+
const tid = key.slice(sepIdx + 1);
|
|
157
|
+
|
|
158
|
+
if (newEntry === undefined) {
|
|
159
|
+
out.push(
|
|
160
|
+
makeFinding(
|
|
161
|
+
DiffKind.PermissionTargetRemoved,
|
|
162
|
+
subject,
|
|
163
|
+
indexes,
|
|
164
|
+
{
|
|
165
|
+
kind: DiffKind.PermissionTargetRemoved,
|
|
166
|
+
targetKind: tk,
|
|
167
|
+
targetId: tid,
|
|
168
|
+
oldRights: oldEntry.rights,
|
|
169
|
+
},
|
|
170
|
+
key,
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Compare rights as sets — skip when identical.
|
|
177
|
+
const oldSet = new Set(oldEntry.rights);
|
|
178
|
+
const newSet = new Set(newEntry.rights);
|
|
179
|
+
const sameSize = oldSet.size === newSet.size;
|
|
180
|
+
const identical = sameSize && [...oldSet].every((r) => newSet.has(r));
|
|
181
|
+
if (identical) continue;
|
|
182
|
+
|
|
183
|
+
if (isExpansion(oldEntry.rights, newEntry.rights)) {
|
|
184
|
+
out.push(
|
|
185
|
+
makeFinding(
|
|
186
|
+
DiffKind.PermissionRightsExpanded,
|
|
187
|
+
subject,
|
|
188
|
+
indexes,
|
|
189
|
+
{
|
|
190
|
+
kind: DiffKind.PermissionRightsExpanded,
|
|
191
|
+
targetKind: tk,
|
|
192
|
+
targetId: tid,
|
|
193
|
+
oldRights: oldEntry.rights,
|
|
194
|
+
newRights: newEntry.rights,
|
|
195
|
+
},
|
|
196
|
+
key,
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
} else if (isContraction(oldEntry.rights, newEntry.rights)) {
|
|
200
|
+
out.push(
|
|
201
|
+
makeFinding(
|
|
202
|
+
DiffKind.PermissionRightsContracted,
|
|
203
|
+
subject,
|
|
204
|
+
indexes,
|
|
205
|
+
{
|
|
206
|
+
kind: DiffKind.PermissionRightsContracted,
|
|
207
|
+
targetKind: tk,
|
|
208
|
+
targetId: tid,
|
|
209
|
+
oldRights: oldEntry.rights,
|
|
210
|
+
newRights: newEntry.rights,
|
|
211
|
+
},
|
|
212
|
+
key,
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
// Mixed change (some rights added, some removed): treat as expansion
|
|
217
|
+
// because any newly granted right is the dangerous direction.
|
|
218
|
+
out.push(
|
|
219
|
+
makeFinding(
|
|
220
|
+
DiffKind.PermissionRightsExpanded,
|
|
221
|
+
subject,
|
|
222
|
+
indexes,
|
|
223
|
+
{
|
|
224
|
+
kind: DiffKind.PermissionRightsExpanded,
|
|
225
|
+
targetKind: tk,
|
|
226
|
+
targetId: tid,
|
|
227
|
+
oldRights: oldEntry.rights,
|
|
228
|
+
newRights: newEntry.rights,
|
|
229
|
+
},
|
|
230
|
+
key,
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Walk new targets — detect additions (not present in old).
|
|
237
|
+
for (const [key, newEntry] of newByTarget) {
|
|
238
|
+
if (oldByTarget.has(key)) continue;
|
|
239
|
+
const sepIdx = key.indexOf("|");
|
|
240
|
+
const tk = key.slice(0, sepIdx);
|
|
241
|
+
const tid = key.slice(sepIdx + 1);
|
|
242
|
+
out.push(
|
|
243
|
+
makeFinding(
|
|
244
|
+
DiffKind.PermissionTargetAdded,
|
|
245
|
+
subject,
|
|
246
|
+
indexes,
|
|
247
|
+
{
|
|
248
|
+
kind: DiffKind.PermissionTargetAdded,
|
|
249
|
+
targetKind: tk,
|
|
250
|
+
targetId: tid,
|
|
251
|
+
newRights: newEntry.rights,
|
|
252
|
+
},
|
|
253
|
+
key,
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
260
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { CoverageStatus } from "../model/coverage.ts";
|
|
2
|
+
import { DiffKind } from "./diff-identity.ts";
|
|
3
|
+
import type { DiffIndexes } from "./diff-indexes.ts";
|
|
4
|
+
|
|
5
|
+
export interface PolicyDiagnostic {
|
|
6
|
+
kind: "coverage-incomplete";
|
|
7
|
+
subject: string;
|
|
8
|
+
cone: "old" | "new" | "both";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PolicyOptions {
|
|
12
|
+
coveragePolicy: "loose" | "strict";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A finding's shape post-categorisation; minimum the post-pass needs. */
|
|
16
|
+
export interface PolicyFinding {
|
|
17
|
+
id: string;
|
|
18
|
+
kind: DiffKind;
|
|
19
|
+
comparisonCone: readonly string[];
|
|
20
|
+
coverageState?: { old: CoverageStatus; new: CoverageStatus };
|
|
21
|
+
[k: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function coneStatus(
|
|
25
|
+
cone: readonly string[],
|
|
26
|
+
source: Map<string, { inheritedStatus: CoverageStatus }>,
|
|
27
|
+
): CoverageStatus {
|
|
28
|
+
let worst: CoverageStatus = "complete";
|
|
29
|
+
for (const id of cone) {
|
|
30
|
+
const rec = source.get(id);
|
|
31
|
+
const s: CoverageStatus = rec?.inheritedStatus ?? "unknown";
|
|
32
|
+
if (s === "unknown") return "unknown";
|
|
33
|
+
if (s === "partial") worst = "partial";
|
|
34
|
+
}
|
|
35
|
+
return worst;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const COVERAGE_SENSITIVE_KINDS: ReadonlySet<DiffKind> = new Set([
|
|
39
|
+
DiffKind.CapabilityLost,
|
|
40
|
+
DiffKind.CapabilityGainedWrite,
|
|
41
|
+
DiffKind.CapabilityGainedRead,
|
|
42
|
+
DiffKind.CapabilityGainedCommit,
|
|
43
|
+
DiffKind.CapabilityGainedHttp,
|
|
44
|
+
DiffKind.CapabilityGainedTelemetry,
|
|
45
|
+
DiffKind.CapabilityGainedIsolatedStorage,
|
|
46
|
+
DiffKind.CapabilityGainedFile,
|
|
47
|
+
DiffKind.CapabilityGainedDynamicDispatch,
|
|
48
|
+
DiffKind.CapabilityGainedEventPublish,
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
export function applyCoveragePolicy<T extends PolicyFinding>(
|
|
52
|
+
findings: readonly T[],
|
|
53
|
+
indexes: DiffIndexes,
|
|
54
|
+
opts: PolicyOptions,
|
|
55
|
+
): { findings: T[]; diagnostics: PolicyDiagnostic[] } {
|
|
56
|
+
const outFindings: T[] = [];
|
|
57
|
+
const diagnostics: PolicyDiagnostic[] = [];
|
|
58
|
+
|
|
59
|
+
for (const finding of findings) {
|
|
60
|
+
if (!COVERAGE_SENSITIVE_KINDS.has(finding.kind)) {
|
|
61
|
+
outFindings.push(finding);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const oldStatus = coneStatus(
|
|
65
|
+
finding.comparisonCone,
|
|
66
|
+
indexes.oldCoverageBySubject as unknown as Map<string, { inheritedStatus: CoverageStatus }>,
|
|
67
|
+
);
|
|
68
|
+
const newStatus = coneStatus(
|
|
69
|
+
finding.comparisonCone,
|
|
70
|
+
indexes.newCoverageBySubject as unknown as Map<string, { inheritedStatus: CoverageStatus }>,
|
|
71
|
+
);
|
|
72
|
+
const annotated: T = { ...finding, coverageState: { old: oldStatus, new: newStatus } };
|
|
73
|
+
|
|
74
|
+
const oldPartial = oldStatus !== "complete";
|
|
75
|
+
const newPartial = newStatus !== "complete";
|
|
76
|
+
|
|
77
|
+
if (opts.coveragePolicy === "strict") {
|
|
78
|
+
if (oldPartial || newPartial) {
|
|
79
|
+
const cone: "old" | "new" | "both" =
|
|
80
|
+
oldPartial && newPartial ? "both" : oldPartial ? "old" : "new";
|
|
81
|
+
diagnostics.push({
|
|
82
|
+
kind: "coverage-incomplete",
|
|
83
|
+
subject: finding.comparisonCone[0] ?? "",
|
|
84
|
+
cone,
|
|
85
|
+
});
|
|
86
|
+
continue; // drop finding under strict
|
|
87
|
+
}
|
|
88
|
+
outFindings.push(annotated);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Loose: downgrade CapabilityLost → CapabilityLostUnderCoverage when new is partial.
|
|
93
|
+
if (finding.kind === DiffKind.CapabilityLost && newPartial) {
|
|
94
|
+
outFindings.push({ ...annotated, kind: DiffKind.CapabilityLostUnderCoverage } as T);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
outFindings.push(annotated);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { findings: outFindings, diagnostics };
|
|
101
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { CapabilitySnapshot } from "../snapshot/types.ts";
|
|
2
|
+
|
|
3
|
+
export type PreflightDiagnostic =
|
|
4
|
+
| { kind: "schema-version-mismatch"; old: number; new: number }
|
|
5
|
+
| { kind: "alsem-version-mismatch"; old: string; new: string; severity: "warning" | "error" }
|
|
6
|
+
| { kind: "app-identity-mismatch"; oldAppId: string; newAppId: string };
|
|
7
|
+
|
|
8
|
+
export interface PreflightOptions {
|
|
9
|
+
coveragePolicy: "loose" | "strict";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PreflightResult {
|
|
13
|
+
diagnostics: readonly PreflightDiagnostic[];
|
|
14
|
+
/** True when the engine should short-circuit and emit diagnostics only. */
|
|
15
|
+
fatal: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function primaryAppId(snap: CapabilitySnapshot): string | undefined {
|
|
19
|
+
const app = snap.apps[0];
|
|
20
|
+
return app ? (app as { appGuid?: string }).appGuid : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function runPreflight(
|
|
24
|
+
oldSnap: CapabilitySnapshot,
|
|
25
|
+
newSnap: CapabilitySnapshot,
|
|
26
|
+
opts: PreflightOptions,
|
|
27
|
+
): PreflightResult {
|
|
28
|
+
const diagnostics: PreflightDiagnostic[] = [];
|
|
29
|
+
let fatal = false;
|
|
30
|
+
|
|
31
|
+
// Schema version mismatch — always fatal.
|
|
32
|
+
if (oldSnap.schemaVersion !== newSnap.schemaVersion) {
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
kind: "schema-version-mismatch",
|
|
35
|
+
old: oldSnap.schemaVersion as unknown as number,
|
|
36
|
+
new: newSnap.schemaVersion as unknown as number,
|
|
37
|
+
});
|
|
38
|
+
fatal = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// alsemVersion difference — warning loose, error strict.
|
|
42
|
+
if (oldSnap.alsemVersion !== newSnap.alsemVersion) {
|
|
43
|
+
const severity: "warning" | "error" = opts.coveragePolicy === "strict" ? "error" : "warning";
|
|
44
|
+
diagnostics.push({
|
|
45
|
+
kind: "alsem-version-mismatch",
|
|
46
|
+
old: oldSnap.alsemVersion,
|
|
47
|
+
new: newSnap.alsemVersion,
|
|
48
|
+
severity,
|
|
49
|
+
});
|
|
50
|
+
if (severity === "error") fatal = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// App identity mismatch — always fatal.
|
|
54
|
+
const oldAppId = primaryAppId(oldSnap);
|
|
55
|
+
const newAppId = primaryAppId(newSnap);
|
|
56
|
+
if (oldAppId !== undefined && newAppId !== undefined && oldAppId !== newAppId) {
|
|
57
|
+
diagnostics.push({
|
|
58
|
+
kind: "app-identity-mismatch",
|
|
59
|
+
oldAppId,
|
|
60
|
+
newAppId,
|
|
61
|
+
});
|
|
62
|
+
fatal = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { diagnostics, fatal };
|
|
66
|
+
}
|