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,22 @@
|
|
|
1
|
+
import type { CapabilitySnapshot } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pretty JSON serializer with recursively sorted object keys, 2-space indent,
|
|
5
|
+
* newline-terminated. Byte-stable for a given input.
|
|
6
|
+
*
|
|
7
|
+
* Arrays passed through as-is — derivers sort their own outputs per the
|
|
8
|
+
* canonical-sort contract. Sorting twice would mask deriver bugs.
|
|
9
|
+
*/
|
|
10
|
+
export function serializeJson(snapshot: CapabilitySnapshot): string {
|
|
11
|
+
return `${JSON.stringify(snapshot, sortedReplacer(), 2)}\n`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function sortedReplacer(): (key: string, value: unknown) => unknown {
|
|
15
|
+
return (_key, value) => {
|
|
16
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
17
|
+
const obj = value as Record<string, unknown>;
|
|
18
|
+
const sorted: Record<string, unknown> = {};
|
|
19
|
+
for (const k of Object.keys(obj).sort()) sorted[k] = obj[k];
|
|
20
|
+
return sorted;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { serializeCborGz } from "./serialize-cbor-gz.ts";
|
|
2
|
+
import { serializeCbor } from "./serialize-cbor.ts";
|
|
3
|
+
import { serializeJson } from "./serialize-json.ts";
|
|
4
|
+
import type {
|
|
5
|
+
AppIdentity,
|
|
6
|
+
CapabilitySnapshot,
|
|
7
|
+
ShardManifest,
|
|
8
|
+
SnapshotFormat,
|
|
9
|
+
SnapshotIdentityTable,
|
|
10
|
+
} from "./types.ts";
|
|
11
|
+
import { SNAPSHOT_SCHEMA_VERSION } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
export interface ShardingOptions {
|
|
14
|
+
format: SnapshotFormat;
|
|
15
|
+
/** Drop dependency shards; keep only the primary app. Default false. */
|
|
16
|
+
primaryOnly?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Split a monolithic snapshot into per-app shards + a manifest.
|
|
21
|
+
*
|
|
22
|
+
* Returns a Map<filename, bytes>. Caller writes each entry to disk under
|
|
23
|
+
* the user's --out directory.
|
|
24
|
+
*
|
|
25
|
+
* Slicing rule: a fact belongs to the shard whose appGuid is the prefix of
|
|
26
|
+
* its stableId (Stable*Ids start with `${appGuid}:`). The primary shard is
|
|
27
|
+
* the FIRST app in snapshot.apps (apps array is sorted; the primary is
|
|
28
|
+
* established by analyzeWorkspace and stable across runs).
|
|
29
|
+
*
|
|
30
|
+
* Each shard is a fully valid CapabilitySnapshot — schemaVersion,
|
|
31
|
+
* alsemVersion, workspaceFingerprint, generatedAt are copied; apps,
|
|
32
|
+
* identities, inputs are narrowed to the shard's app. typedEdges that cross
|
|
33
|
+
* apps are duplicated into BOTH shards.
|
|
34
|
+
*/
|
|
35
|
+
export function serializeSharded(
|
|
36
|
+
snapshot: CapabilitySnapshot,
|
|
37
|
+
opts: ShardingOptions,
|
|
38
|
+
): Map<string, Uint8Array> {
|
|
39
|
+
const apps = snapshot.apps;
|
|
40
|
+
const out = new Map<string, Uint8Array>();
|
|
41
|
+
if (apps.length === 0) {
|
|
42
|
+
out.set("manifest.json", encodeManifest(snapshot, []));
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const primaryGuid = apps[0]?.appGuid;
|
|
47
|
+
if (primaryGuid === undefined) {
|
|
48
|
+
out.set("manifest.json", encodeManifest(snapshot, []));
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const shardEntries: ShardManifest["shards"] = [];
|
|
53
|
+
|
|
54
|
+
for (const app of apps) {
|
|
55
|
+
const role = app.appGuid === primaryGuid ? "primary" : "dependency";
|
|
56
|
+
if (opts.primaryOnly && role !== "primary") continue;
|
|
57
|
+
|
|
58
|
+
const shard = sliceForApp(snapshot, app);
|
|
59
|
+
const fileBase = role === "primary" ? "primary" : `dep-${app.appGuid}`;
|
|
60
|
+
const ext = opts.format === "json" ? "json" : opts.format === "cbor" ? "cbor" : "cbor.gz";
|
|
61
|
+
const fileName = `${fileBase}.${ext}`;
|
|
62
|
+
out.set(fileName, serializeOne(shard, opts.format));
|
|
63
|
+
shardEntries.push({ appGuid: app.appGuid, role, file: fileName });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
out.set("manifest.json", encodeManifest(snapshot, shardEntries));
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sliceForApp(s: CapabilitySnapshot, app: AppIdentity): CapabilitySnapshot {
|
|
71
|
+
const inApp = (stableId: string): boolean => stableId.startsWith(`${app.appGuid}:`);
|
|
72
|
+
const shard: CapabilitySnapshot = {
|
|
73
|
+
schemaVersion: s.schemaVersion,
|
|
74
|
+
alsemVersion: s.alsemVersion,
|
|
75
|
+
workspaceFingerprint: s.workspaceFingerprint,
|
|
76
|
+
generatedAt: s.generatedAt,
|
|
77
|
+
apps: [app],
|
|
78
|
+
identities: sliceIdentities(s.identities, app.appGuid),
|
|
79
|
+
contractFacts: s.contractFacts.filter((c) => inApp(c.stableId)),
|
|
80
|
+
schemaFacts: s.schemaFacts.filter((c) => inApp(c.stableId)),
|
|
81
|
+
permissionFacts: s.permissionFacts.filter((p) =>
|
|
82
|
+
inApp(p.kind === "declared" ? String(p.permissionSet) : String(p.subject)),
|
|
83
|
+
),
|
|
84
|
+
rootClassifications: s.rootClassifications.filter((r) => inApp(String(r.routineId))),
|
|
85
|
+
capabilityFacts: s.capabilityFacts.filter((f) => inApp(String(f.subject))),
|
|
86
|
+
typedEdges: s.typedEdges.filter(
|
|
87
|
+
(e) =>
|
|
88
|
+
("from" in e && inApp(String(e.from))) ||
|
|
89
|
+
("to" in e && inApp(String((e as { to?: unknown }).to ?? ""))),
|
|
90
|
+
),
|
|
91
|
+
operationIndex: s.operationIndex.filter((o) => inApp(String(o.routine))),
|
|
92
|
+
callsiteIndex: s.callsiteIndex.filter((c) => inApp(String(c.routine))),
|
|
93
|
+
coverage: s.coverage.filter((c) => inApp(String(c.subject))),
|
|
94
|
+
inputs: s.inputs,
|
|
95
|
+
eventDeclarations: s.eventDeclarations.filter(
|
|
96
|
+
(d) => inApp(String(d.routine)) || (d.binding ? inApp(d.binding.publisherObject) : false),
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
// Side-band metadata is workspace-scoped, not app-scoped — mirror onto
|
|
100
|
+
// every shard so each is a self-contained snapshot.
|
|
101
|
+
if (s.inputsMetadata !== undefined) {
|
|
102
|
+
shard.inputsMetadata = s.inputsMetadata;
|
|
103
|
+
}
|
|
104
|
+
return shard;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sliceIdentities(table: SnapshotIdentityTable, appGuid: string): SnapshotIdentityTable {
|
|
108
|
+
const stableIds: string[] = [];
|
|
109
|
+
const displayNames: string[] = [];
|
|
110
|
+
for (let i = 0; i < table.stableIds.length; i++) {
|
|
111
|
+
const id = table.stableIds[i];
|
|
112
|
+
if (id?.startsWith(`${appGuid}:`)) {
|
|
113
|
+
stableIds.push(id);
|
|
114
|
+
displayNames.push(table.displayNames[i] ?? "");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { stableIds, displayNames };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function serializeOne(s: CapabilitySnapshot, fmt: SnapshotFormat): Uint8Array {
|
|
121
|
+
if (fmt === "json") return new TextEncoder().encode(serializeJson(s));
|
|
122
|
+
if (fmt === "cbor") return serializeCbor(s);
|
|
123
|
+
return serializeCborGz(s);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function encodeManifest(s: CapabilitySnapshot, shards: ShardManifest["shards"]): Uint8Array {
|
|
127
|
+
const m: ShardManifest = {
|
|
128
|
+
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
129
|
+
alsemVersion: s.alsemVersion,
|
|
130
|
+
workspaceFingerprint: s.workspaceFingerprint,
|
|
131
|
+
shards: [...shards].sort((a, b) => a.appGuid.localeCompare(b.appGuid)),
|
|
132
|
+
};
|
|
133
|
+
return new TextEncoder().encode(`${JSON.stringify(m, null, 2)}\n`);
|
|
134
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { CapabilityFact } from "../model/capability.ts";
|
|
2
|
+
import type { CoverageRecord } from "../model/coverage.ts";
|
|
3
|
+
import type { GraphEdge } from "../model/graph-edge.ts";
|
|
4
|
+
import type { SourceAnchor } from "../model/identity.ts";
|
|
5
|
+
import type { CallsiteId, OperationId } from "../model/ids.ts";
|
|
6
|
+
import type { PermissionFact } from "../model/permission.ts";
|
|
7
|
+
import type {
|
|
8
|
+
StableAppId,
|
|
9
|
+
StableEventId,
|
|
10
|
+
StableObjectId,
|
|
11
|
+
StableRoutineId,
|
|
12
|
+
} from "../model/stable-identity.ts";
|
|
13
|
+
|
|
14
|
+
/** Snapshot schema version. Bump per CLAUDE.md "schema bumps are cheap"
|
|
15
|
+
* recipe; deserializer asserts and may run a migration shim. */
|
|
16
|
+
export const SNAPSHOT_SCHEMA_VERSION = 1 as const;
|
|
17
|
+
export type SnapshotSchemaVersion = typeof SNAPSHOT_SCHEMA_VERSION;
|
|
18
|
+
|
|
19
|
+
export type SnapshotFormat = "json" | "cbor" | "cbor.gz";
|
|
20
|
+
export type SnapshotShardingMode = "monolithic" | "sharded";
|
|
21
|
+
|
|
22
|
+
/** Stable app metadata. Sorted by `appGuid` in the snapshot. */
|
|
23
|
+
export interface AppIdentity {
|
|
24
|
+
appGuid: StableAppId;
|
|
25
|
+
publisher: string;
|
|
26
|
+
name: string;
|
|
27
|
+
version: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Interning table — stable IDs and parallel display names. Facts reference
|
|
31
|
+
* ids; the table provides the human-readable name once per id. Arrays are
|
|
32
|
+
* parallel: stableIds[i] ↔ displayNames[i]. Sorted by stableIds. */
|
|
33
|
+
export interface SnapshotIdentityTable {
|
|
34
|
+
stableIds: string[];
|
|
35
|
+
displayNames: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Canonical fingerprint of one parsed attribute — name + SHA-256 of
|
|
39
|
+
* normalized argument list. */
|
|
40
|
+
export interface AttributeFingerprint {
|
|
41
|
+
name: string;
|
|
42
|
+
argsHash: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Contract surface — drives Phase 2 ABI / contract diff. */
|
|
46
|
+
export interface ContractFact {
|
|
47
|
+
kind: "object" | "routine" | "event-publisher" | "interface";
|
|
48
|
+
stableId: string;
|
|
49
|
+
visibility: "public" | "internal" | "local" | "protected";
|
|
50
|
+
obsoleteState?: "Pending" | "Removed";
|
|
51
|
+
obsoleteReason?: string;
|
|
52
|
+
/** Routines + events: parameter shape hash + return-type hash. */
|
|
53
|
+
signatureFingerprint: string;
|
|
54
|
+
attributes: AttributeFingerprint[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Schema surface — drives Phase 2 schema diff. */
|
|
58
|
+
export interface SchemaFact {
|
|
59
|
+
kind: "table" | "field" | "enum" | "enum-value" | "key";
|
|
60
|
+
stableId: string;
|
|
61
|
+
/** Type/length/option/relation/dataclassification, canonicalised + SHA-256. */
|
|
62
|
+
shapeFingerprint: string;
|
|
63
|
+
dataClassification?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Anchor metadata for replaying op witnesses without re-reading AL source. */
|
|
67
|
+
export interface OperationEvidence {
|
|
68
|
+
operationId: OperationId;
|
|
69
|
+
routine: StableRoutineId;
|
|
70
|
+
sourceFile: string;
|
|
71
|
+
startLine: number;
|
|
72
|
+
startColumn: number;
|
|
73
|
+
endLine: number;
|
|
74
|
+
endColumn: number;
|
|
75
|
+
displayText: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Anchor metadata for call witnesses. */
|
|
79
|
+
export interface CallsiteEvidence {
|
|
80
|
+
callsiteId: CallsiteId;
|
|
81
|
+
routine: StableRoutineId;
|
|
82
|
+
sourceFile: string;
|
|
83
|
+
startLine: number;
|
|
84
|
+
startColumn: number;
|
|
85
|
+
endLine: number;
|
|
86
|
+
endColumn: number;
|
|
87
|
+
calleeDisplay: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Reproducibility input — every file whose content contributes to the
|
|
91
|
+
* snapshot identity. */
|
|
92
|
+
export interface SnapshotInput {
|
|
93
|
+
kind: "app-json" | "roots-config" | "policy" | "dep-package" | "app-package";
|
|
94
|
+
path: string;
|
|
95
|
+
contentHash: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Side-band metadata about the inputs list. Tracks operator overrides
|
|
100
|
+
* that affect what does or doesn't appear in `inputs` (e.g.
|
|
101
|
+
* roots.config.json skipped via --no-roots-config). Distinct from
|
|
102
|
+
* `inputs` itself so diff tools can tell "config doesn't exist" (no
|
|
103
|
+
* inputsMetadata) from "config existed but was ignored"
|
|
104
|
+
* (`rootsConfigIgnored: true`).
|
|
105
|
+
*/
|
|
106
|
+
export interface SnapshotInputsMetadata {
|
|
107
|
+
/** True if `roots.config.json` was present on disk but skipped via --no-roots-config. */
|
|
108
|
+
rootsConfigIgnored?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Subscriber binding — points to publisher object + event name. */
|
|
112
|
+
export interface SubscriberBinding {
|
|
113
|
+
publisherObject: StableObjectId;
|
|
114
|
+
eventName: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Bipartite publisher / subscriber declaration. */
|
|
118
|
+
export interface EventDeclaration {
|
|
119
|
+
kind: "publisher" | "subscriber";
|
|
120
|
+
routine: StableRoutineId;
|
|
121
|
+
eventId: StableEventId;
|
|
122
|
+
binding?: SubscriberBinding;
|
|
123
|
+
sourceAnchor: SourceAnchor;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Phase 0c reserves this slot; Phase 1's root-classifier populates it. */
|
|
127
|
+
export interface RootClassificationSlot {
|
|
128
|
+
routineId: StableRoutineId;
|
|
129
|
+
kinds: string[];
|
|
130
|
+
externallyReachable: boolean;
|
|
131
|
+
source: "ast" | "config" | "ast+config";
|
|
132
|
+
confidence: "static" | "user-asserted";
|
|
133
|
+
sourceAnchor?: SourceAnchor;
|
|
134
|
+
configEntryId?: string;
|
|
135
|
+
resolutionStatus?: "resolved" | "ambiguous" | "unresolved";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Sharded layout manifest. References per-app shard files by stable app id. */
|
|
139
|
+
export interface ShardManifest {
|
|
140
|
+
schemaVersion: SnapshotSchemaVersion;
|
|
141
|
+
alsemVersion: string;
|
|
142
|
+
workspaceFingerprint: string;
|
|
143
|
+
shards: Array<{
|
|
144
|
+
appGuid: StableAppId;
|
|
145
|
+
role: "primary" | "dependency";
|
|
146
|
+
file: string;
|
|
147
|
+
}>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The semantic snapshot — single source of truth for Phase 1+ consumers. */
|
|
151
|
+
export interface CapabilitySnapshot {
|
|
152
|
+
schemaVersion: SnapshotSchemaVersion;
|
|
153
|
+
alsemVersion: string;
|
|
154
|
+
workspaceFingerprint: string;
|
|
155
|
+
generatedAt: string;
|
|
156
|
+
apps: AppIdentity[];
|
|
157
|
+
identities: SnapshotIdentityTable;
|
|
158
|
+
contractFacts: ContractFact[];
|
|
159
|
+
schemaFacts: SchemaFact[];
|
|
160
|
+
permissionFacts: PermissionFact[];
|
|
161
|
+
rootClassifications: RootClassificationSlot[];
|
|
162
|
+
capabilityFacts: CapabilityFact[];
|
|
163
|
+
typedEdges: GraphEdge[];
|
|
164
|
+
operationIndex: OperationEvidence[];
|
|
165
|
+
callsiteIndex: CallsiteEvidence[];
|
|
166
|
+
coverage: CoverageRecord[];
|
|
167
|
+
inputs: SnapshotInput[];
|
|
168
|
+
/**
|
|
169
|
+
* Side-band metadata about the inputs list. Tracks operator overrides
|
|
170
|
+
* that affect what does or doesn't appear in `inputs` (e.g.
|
|
171
|
+
* roots.config.json skipped via --no-roots-config). Distinct from
|
|
172
|
+
* `inputs` itself so diff tools can tell "config doesn't exist" (no
|
|
173
|
+
* inputsMetadata) from "config existed but was ignored"
|
|
174
|
+
* (`rootsConfigIgnored: true`).
|
|
175
|
+
*
|
|
176
|
+
* Omitted entirely when no overrides apply — avoids noisy
|
|
177
|
+
* `inputsMetadata: {}` in serialized output.
|
|
178
|
+
*/
|
|
179
|
+
inputsMetadata?: SnapshotInputsMetadata;
|
|
180
|
+
eventDeclarations: EventDeclaration[];
|
|
181
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { filteredUnzip } from "./app-package-zip.ts";
|
|
3
|
+
|
|
4
|
+
export interface ManifestAppIdentity {
|
|
5
|
+
appGuid: string;
|
|
6
|
+
name: string;
|
|
7
|
+
publisher: string;
|
|
8
|
+
version: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ManifestDependency {
|
|
12
|
+
appGuid: string;
|
|
13
|
+
name: string;
|
|
14
|
+
publisher: string;
|
|
15
|
+
minVersion: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AppManifest {
|
|
19
|
+
identity: ManifestAppIdentity;
|
|
20
|
+
dependencies: ManifestDependency[];
|
|
21
|
+
/** ResourceExposurePolicy.IncludeSourceInSymbolFile — whether embedded .al source is present. */
|
|
22
|
+
includesSource: boolean;
|
|
23
|
+
/** Set when the manifest could not be parsed; identity is then empty. */
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Build an empty manifest carrying an error — used for every failure path (no silent clean). */
|
|
28
|
+
function failManifest(error: string): AppManifest {
|
|
29
|
+
return {
|
|
30
|
+
identity: { appGuid: "", name: "", publisher: "", version: "" },
|
|
31
|
+
dependencies: [],
|
|
32
|
+
includesSource: false,
|
|
33
|
+
error,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Read an attribute value out of an opening tag, anchored on a word boundary so a
|
|
38
|
+
* longer attribute name (e.g. `CompatibilityId`) cannot be matched as a shorter one (`Id`). */
|
|
39
|
+
function readTagAttr(tag: string, attr: string): string {
|
|
40
|
+
return tag.match(new RegExp(`\\b${attr}\\s*=\\s*"([^"]*)"`, "i"))?.[1] ?? "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Read one attribute from the first occurrence of `<tag ...>`, tolerant of attribute order. */
|
|
44
|
+
function readAttr(xml: string, tag: string, attr: string): string {
|
|
45
|
+
const tagMatch = xml.match(new RegExp(`<${tag}\\b[^>]*>`, "i"));
|
|
46
|
+
if (!tagMatch) return "";
|
|
47
|
+
return readTagAttr(tagMatch[0], attr);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parse the text of a NavxManifest.xml. Never throws — failure is reported via `error`. */
|
|
51
|
+
export function parseAppManifestXml(xml: string): AppManifest {
|
|
52
|
+
if (!/<App\b/i.test(xml)) {
|
|
53
|
+
return failManifest("no <App> element found in manifest");
|
|
54
|
+
}
|
|
55
|
+
const identity: ManifestAppIdentity = {
|
|
56
|
+
appGuid: readAttr(xml, "App", "Id"),
|
|
57
|
+
name: readAttr(xml, "App", "Name"),
|
|
58
|
+
publisher: readAttr(xml, "App", "Publisher"),
|
|
59
|
+
version: readAttr(xml, "App", "Version"),
|
|
60
|
+
};
|
|
61
|
+
if (identity.appGuid === "") {
|
|
62
|
+
return failManifest("<App> element missing Id attribute");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const dependencies: ManifestDependency[] = [];
|
|
66
|
+
for (const m of xml.matchAll(/<Dependency\b[^>]*>/gi)) {
|
|
67
|
+
const tag = m[0];
|
|
68
|
+
dependencies.push({
|
|
69
|
+
appGuid: readTagAttr(tag, "Id"),
|
|
70
|
+
name: readTagAttr(tag, "Name"),
|
|
71
|
+
publisher: readTagAttr(tag, "Publisher"),
|
|
72
|
+
minVersion: readTagAttr(tag, "MinVersion"),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const includesSource = /IncludeSourceInSymbolFile\s*=\s*"true"/i.test(xml);
|
|
77
|
+
|
|
78
|
+
return { identity, dependencies, includesSource };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Read and parse NavxManifest.xml from a .app on disk. Never throws. */
|
|
82
|
+
export function readAppManifest(appPath: string): AppManifest {
|
|
83
|
+
let xml: string;
|
|
84
|
+
try {
|
|
85
|
+
const bytes = new Uint8Array(readFileSync(appPath));
|
|
86
|
+
const entries = filteredUnzip(bytes, (name) => name.toLowerCase().endsWith("navxmanifest.xml"));
|
|
87
|
+
const key = Object.keys(entries)[0];
|
|
88
|
+
if (key === undefined) {
|
|
89
|
+
return failManifest("NavxManifest.xml not found in package");
|
|
90
|
+
}
|
|
91
|
+
xml = new TextDecoder("utf-8").decode(entries[key]);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return failManifest(`could not read package: ${(err as Error).message}`);
|
|
94
|
+
}
|
|
95
|
+
return parseAppManifestXml(xml);
|
|
96
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Unzipped, unzipSync } from "fflate";
|
|
2
|
+
|
|
3
|
+
/** BC .app files may carry a binary header before the ZIP. The ZIP starts at PK\x03\x04. */
|
|
4
|
+
export function stripAppHeader(bytes: Uint8Array): Uint8Array {
|
|
5
|
+
const limit = Math.min(bytes.length - 4, 4096);
|
|
6
|
+
for (let i = 0; i < limit; i++) {
|
|
7
|
+
if (
|
|
8
|
+
bytes[i] === 0x50 &&
|
|
9
|
+
bytes[i + 1] === 0x4b &&
|
|
10
|
+
bytes[i + 2] === 0x03 &&
|
|
11
|
+
bytes[i + 3] === 0x04
|
|
12
|
+
) {
|
|
13
|
+
return i === 0 ? bytes : bytes.subarray(i);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return bytes; // assume it is already a plain ZIP
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Normalise a ZIP entry key: backslashes -> forward slashes. */
|
|
20
|
+
export function normalizeZipEntryName(key: string): string {
|
|
21
|
+
return key.replace(/\\/g, "/");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Unzip only the entries `accept` returns true for. The `.app` header is stripped first.
|
|
26
|
+
* Keys in the returned map are normalised to forward slashes.
|
|
27
|
+
*/
|
|
28
|
+
export function filteredUnzip(appBytes: Uint8Array, accept: (name: string) => boolean): Unzipped {
|
|
29
|
+
const zip = stripAppHeader(appBytes);
|
|
30
|
+
const raw = unzipSync(zip, { filter: (f) => accept(normalizeZipEntryName(f.name)) });
|
|
31
|
+
const out: Unzipped = {};
|
|
32
|
+
for (const [k, v] of Object.entries(raw)) out[normalizeZipEntryName(k)] = v;
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Enumerate every ZIP entry name without decompressing anything — the `filter` callback
|
|
38
|
+
* sees every entry's metadata and we always return false.
|
|
39
|
+
*/
|
|
40
|
+
export function listZipEntryNames(appBytes: Uint8Array): string[] {
|
|
41
|
+
const zip = stripAppHeader(appBytes);
|
|
42
|
+
const names: string[] = [];
|
|
43
|
+
unzipSync(zip, {
|
|
44
|
+
filter: (f) => {
|
|
45
|
+
names.push(normalizeZipEntryName(f.name));
|
|
46
|
+
return false;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return names;
|
|
50
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { filteredUnzip } from "./app-package-zip.ts";
|
|
3
|
+
|
|
4
|
+
export interface EmbeddedSourceFile {
|
|
5
|
+
/** Forward-slash relative path inside the package, e.g. "src/Foo.Codeunit.al". URL-escaped form kept as-is. */
|
|
6
|
+
relativePath: string;
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Async-iterate the embedded .al files of a .app, sorted by relative path.
|
|
12
|
+
*
|
|
13
|
+
* Decompresses every .al entry in ONE pass over the ZIP central directory, then yields
|
|
14
|
+
* them one at a time. The previous implementation called `filteredUnzip` per file, which
|
|
15
|
+
* re-parsed the ZIP central directory once per entry — on Microsoft Base Application
|
|
16
|
+
* (7,634 .al files in a ~41 MB compressed package) that was the dominant cost of the
|
|
17
|
+
* cold-build loop (~40 s of pure ZIP overhead, not accounted for in any per-phase timer).
|
|
18
|
+
*
|
|
19
|
+
* Peak retained memory still tracks "one file in transit at a time": each entry is
|
|
20
|
+
* removed from the entries map as it is yielded, so the GC can reclaim its bytes as
|
|
21
|
+
* the consumer advances.
|
|
22
|
+
*/
|
|
23
|
+
export async function* iterateEmbeddedSourceBytes(
|
|
24
|
+
appBytes: Uint8Array,
|
|
25
|
+
): AsyncIterable<EmbeddedSourceFile> {
|
|
26
|
+
const entries = filteredUnzip(appBytes, (n) => n.toLowerCase().endsWith(".al"));
|
|
27
|
+
const names = Object.keys(entries).sort();
|
|
28
|
+
const decoder = new TextDecoder("utf-8");
|
|
29
|
+
for (const name of names) {
|
|
30
|
+
const bytes = entries[name];
|
|
31
|
+
if (bytes === undefined) continue; // entry vanished — defensive, should not happen
|
|
32
|
+
delete entries[name]; // free the buffer once we've handed it to the consumer
|
|
33
|
+
yield { relativePath: name, content: decoder.decode(bytes) };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Disk-path convenience: read the .app once, then async-iterate its embedded .al files. */
|
|
38
|
+
export async function* iterateEmbeddedSource(appPath: string): AsyncIterable<EmbeddedSourceFile> {
|
|
39
|
+
const bytes = new Uint8Array(readFileSync(appPath));
|
|
40
|
+
yield* iterateEmbeddedSourceBytes(bytes);
|
|
41
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { sha256OfStrings } from "../hash.ts";
|
|
4
|
+
import { parseAppManifestXml } from "./app-manifest.ts";
|
|
5
|
+
import { filteredUnzip } from "./app-package-zip.ts";
|
|
6
|
+
|
|
7
|
+
/** Raw-byte sha256 of a .app's bytes — provenance only. */
|
|
8
|
+
export function packageHashOfBytes(bytes: Uint8Array): string {
|
|
9
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Disk-path convenience. */
|
|
13
|
+
export function packageHash(appPath: string): string {
|
|
14
|
+
return packageHashOfBytes(new Uint8Array(readFileSync(appPath)));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const sha256 = (b: Uint8Array): string => createHash("sha256").update(b).digest("hex");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Semantic content hash of a .app — the cache-key input. Hashes only what affects analysis:
|
|
21
|
+
* normalized manifest identity + declared dependency constraints, the SymbolReference.json
|
|
22
|
+
* content, and every embedded .al's content hash (sorted by relative path). Deliberately
|
|
23
|
+
* ignores ZIP metadata, entry order, timestamps, signing, and the .app binary header — so a
|
|
24
|
+
* re-download of the same release reuses the cache.
|
|
25
|
+
*
|
|
26
|
+
* Single-pass ZIP extraction: every relevant entry (manifest + symbol reference + every
|
|
27
|
+
* .al) is decompressed in ONE `filteredUnzip` call. The previous implementation made
|
|
28
|
+
* one `filteredUnzip` call PER .al entry plus separate calls for manifest and symbol
|
|
29
|
+
* reference — on Base Application (7,634 .al files) that re-parsed the ZIP central
|
|
30
|
+
* directory 7,636 times per cache-key computation.
|
|
31
|
+
*/
|
|
32
|
+
export function packageSemanticHashOfBytes(bytes: Uint8Array): string {
|
|
33
|
+
const entries = filteredUnzip(bytes, (name) => {
|
|
34
|
+
const lower = name.toLowerCase();
|
|
35
|
+
return (
|
|
36
|
+
lower.endsWith(".al") ||
|
|
37
|
+
lower.endsWith("navxmanifest.xml") ||
|
|
38
|
+
lower.endsWith("symbolreference.json")
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 1. manifest identity + declared dependency constraints
|
|
43
|
+
const manifestKey = Object.keys(entries).find((k) =>
|
|
44
|
+
k.toLowerCase().endsWith("navxmanifest.xml"),
|
|
45
|
+
);
|
|
46
|
+
const manifestXml =
|
|
47
|
+
manifestKey === undefined ? "" : new TextDecoder("utf-8").decode(entries[manifestKey]);
|
|
48
|
+
const manifest = parseAppManifestXml(manifestXml);
|
|
49
|
+
const manifestPart = sha256OfStrings([
|
|
50
|
+
manifest.identity.appGuid,
|
|
51
|
+
manifest.identity.name,
|
|
52
|
+
manifest.identity.publisher,
|
|
53
|
+
manifest.identity.version,
|
|
54
|
+
...manifest.dependencies
|
|
55
|
+
.map((d) => `${d.appGuid}|${d.publisher}|${d.name}|${d.minVersion}`)
|
|
56
|
+
.sort(),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// 2. SymbolReference.json content
|
|
60
|
+
const symKey = Object.keys(entries).find((k) => k.toLowerCase().endsWith("symbolreference.json"));
|
|
61
|
+
const symPart = symKey === undefined ? "" : sha256(entries[symKey] ?? new Uint8Array());
|
|
62
|
+
|
|
63
|
+
// 3. embedded .al content hashes, sorted by relative path
|
|
64
|
+
const alNames = Object.keys(entries)
|
|
65
|
+
.filter((n) => n.toLowerCase().endsWith(".al"))
|
|
66
|
+
.sort();
|
|
67
|
+
const alParts: string[] = [];
|
|
68
|
+
for (const name of alNames) {
|
|
69
|
+
const b = entries[name];
|
|
70
|
+
if (b !== undefined) {
|
|
71
|
+
alParts.push(`${name}:${sha256(b)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return sha256OfStrings(["pkgSemantic/v1", manifestPart, symPart, ...alParts]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Disk-path convenience. */
|
|
79
|
+
export function packageSemanticHash(appPath: string): string {
|
|
80
|
+
return packageSemanticHashOfBytes(new Uint8Array(readFileSync(appPath)));
|
|
81
|
+
}
|