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,144 @@
|
|
|
1
|
+
import type { ObjectDecl, Routine, Table } from "../model/entities.ts";
|
|
2
|
+
import type { Diagnostic } from "../model/finding.ts";
|
|
3
|
+
import type { EventSymbol } from "../model/graph.ts";
|
|
4
|
+
import type { SemanticIndex } from "../model/model.ts";
|
|
5
|
+
import type { ManifestDependency } from "../symbols/app-manifest.ts";
|
|
6
|
+
|
|
7
|
+
/** Bump alongside `depCache` in cache-versions.ts when this shape changes. */
|
|
8
|
+
export const DEPENDENCY_ARTIFACT_SCHEMA_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
/** A discovered dependency .app on disk — NOT a SourceUnit. Produced by dependency-package-discovery. */
|
|
11
|
+
export interface DependencyPackageRef {
|
|
12
|
+
appPath: string;
|
|
13
|
+
appGuid: string;
|
|
14
|
+
publisher: string;
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
/** Raw-byte sha256 of the .app file — provenance only. */
|
|
18
|
+
packageHash: string;
|
|
19
|
+
/** Declared dependencies from NavxManifest.xml — drives DAG topo-sort + version resolution. */
|
|
20
|
+
manifestDependencies: ManifestDependency[];
|
|
21
|
+
/** ResourceExposurePolicy.IncludeSourceInSymbolFile. */
|
|
22
|
+
includesSource: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One direct dependency, identity + the artifact key it resolved to. Sorted by identity in keys. */
|
|
26
|
+
export interface DirectDependencyRef {
|
|
27
|
+
appGuid: string;
|
|
28
|
+
publisher: string;
|
|
29
|
+
name: string;
|
|
30
|
+
version: string;
|
|
31
|
+
artifactKey: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DependencyArtifactVersions {
|
|
35
|
+
analyzer: string;
|
|
36
|
+
grammar: string;
|
|
37
|
+
symbolReader: string;
|
|
38
|
+
summarySchema: string;
|
|
39
|
+
depCache: string;
|
|
40
|
+
resourcePolicy: string;
|
|
41
|
+
devFingerprint: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DependencyArtifactHeader {
|
|
45
|
+
schemaVersion: number;
|
|
46
|
+
versions: DependencyArtifactVersions;
|
|
47
|
+
/** Semantic input key — also the cache filename stem. */
|
|
48
|
+
artifactKey: string;
|
|
49
|
+
/** sha256 of the canonical artifact JSON minus this field — the real content address. */
|
|
50
|
+
artifactContentHash: string;
|
|
51
|
+
appIdentity: { appGuid: string; publisher: string; name: string; version: string };
|
|
52
|
+
/** Raw-byte sha256 of the .app — provenance only, NOT part of the key. */
|
|
53
|
+
packageHash: string;
|
|
54
|
+
/** sha256 of manifest identity+deps + SymbolReference.json hash + sorted embedded-.al hashes. */
|
|
55
|
+
packageSemanticHash: string;
|
|
56
|
+
directDependencies: DirectDependencyRef[];
|
|
57
|
+
summaryMode:
|
|
58
|
+
| "full"
|
|
59
|
+
| "structural-only-resource-guard"
|
|
60
|
+
| "structural-only-parser-unavailable"
|
|
61
|
+
| "structural-only-no-dep-summaries";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The structural + behavioral payload. `routines` are full model entities with **empty
|
|
66
|
+
* `features`**, `analysisRole: "dependency"`, and `summary` populated — the open-world
|
|
67
|
+
* residuals (`uncertainties`, `capabilityFactsDirect`, `capabilityFactsInherited`,
|
|
68
|
+
* `coverage`) live inside each `summary`. All ids use the artifact's fixed
|
|
69
|
+
* `dep:<artifactKey>` modelInstanceId, so the merge is verbatim.
|
|
70
|
+
*/
|
|
71
|
+
export interface DependencyArtifactAbi {
|
|
72
|
+
objects: ObjectDecl[];
|
|
73
|
+
tables: Table[];
|
|
74
|
+
routines: Routine[];
|
|
75
|
+
/** Exported event-publisher symbols — empty `features` on dep routines cannot feed buildEventGraph. */
|
|
76
|
+
eventPublishers: EventSymbol[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A cached, compact semantic artifact for one dependency .app. */
|
|
80
|
+
export interface DependencyArtifact {
|
|
81
|
+
header: DependencyArtifactHeader;
|
|
82
|
+
abi: DependencyArtifactAbi;
|
|
83
|
+
/** Analyzer-authored, stable, replayable degradation diagnostics. */
|
|
84
|
+
diagnostics: Diagnostic[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Structural guard used by validate-on-read. Checks shape, not semantic validity. */
|
|
88
|
+
export function isDependencyArtifact(x: unknown): x is DependencyArtifact {
|
|
89
|
+
if (x === null || typeof x !== "object") return false;
|
|
90
|
+
const a = x as Record<string, unknown>;
|
|
91
|
+
const h = a.header as Record<string, unknown> | undefined;
|
|
92
|
+
if (h === null || h === undefined || typeof h !== "object") return false;
|
|
93
|
+
if (h.schemaVersion !== DEPENDENCY_ARTIFACT_SCHEMA_VERSION) return false;
|
|
94
|
+
if (typeof h.artifactKey !== "string" || typeof h.artifactContentHash !== "string") return false;
|
|
95
|
+
if (typeof h.versions !== "object" || h.versions === null) return false;
|
|
96
|
+
if (!Array.isArray(h.directDependencies)) return false;
|
|
97
|
+
if (
|
|
98
|
+
h.summaryMode !== "full" &&
|
|
99
|
+
h.summaryMode !== "structural-only-resource-guard" &&
|
|
100
|
+
h.summaryMode !== "structural-only-parser-unavailable" &&
|
|
101
|
+
h.summaryMode !== "structural-only-no-dep-summaries"
|
|
102
|
+
)
|
|
103
|
+
return false;
|
|
104
|
+
const abi = a.abi as Record<string, unknown> | undefined;
|
|
105
|
+
if (
|
|
106
|
+
abi === null ||
|
|
107
|
+
abi === undefined ||
|
|
108
|
+
!Array.isArray(abi.objects) ||
|
|
109
|
+
!Array.isArray(abi.tables)
|
|
110
|
+
)
|
|
111
|
+
return false;
|
|
112
|
+
if (!Array.isArray(abi.routines) || !Array.isArray(abi.eventPublishers)) return false;
|
|
113
|
+
if (!Array.isArray(a.diagnostics)) return false;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Pure merge: fold dependency artifacts into a workspace SemanticIndex, returning a NEW
|
|
119
|
+
* index (the input is never mutated). Artifacts arrive in dependency topo order; entities
|
|
120
|
+
* within each artifact are already sorted, so the merged collections are deterministic.
|
|
121
|
+
* Dependency apps are appended with `analysisRole: "dependency"`.
|
|
122
|
+
*/
|
|
123
|
+
export function withDependencyArtifacts(
|
|
124
|
+
index: SemanticIndex,
|
|
125
|
+
artifacts: DependencyArtifact[],
|
|
126
|
+
): SemanticIndex {
|
|
127
|
+
const apps = [...index.apps];
|
|
128
|
+
const objects = [...index.objects];
|
|
129
|
+
const tables = [...index.tables];
|
|
130
|
+
const routines = [...index.routines];
|
|
131
|
+
for (const artifact of artifacts) {
|
|
132
|
+
apps.push({
|
|
133
|
+
appGuid: artifact.header.appIdentity.appGuid,
|
|
134
|
+
publisher: artifact.header.appIdentity.publisher,
|
|
135
|
+
name: artifact.header.appIdentity.name,
|
|
136
|
+
version: artifact.header.appIdentity.version,
|
|
137
|
+
analysisRole: "dependency",
|
|
138
|
+
});
|
|
139
|
+
objects.push(...artifact.abi.objects);
|
|
140
|
+
tables.push(...artifact.abi.tables);
|
|
141
|
+
routines.push(...artifact.abi.routines);
|
|
142
|
+
}
|
|
143
|
+
return { ...index, apps, objects, tables, routines };
|
|
144
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
statSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { CACHE_VERSIONS, devFingerprint } from "./cache-versions.ts";
|
|
15
|
+
import { canonicalStringify } from "./canonical-json.ts";
|
|
16
|
+
import type { DependencyArtifact, DirectDependencyRef } from "./dependency-artifact.ts";
|
|
17
|
+
import { isDependencyArtifact } from "./dependency-artifact.ts";
|
|
18
|
+
|
|
19
|
+
const sha256 = (s: string): string => createHash("sha256").update(s, "utf8").digest("hex");
|
|
20
|
+
|
|
21
|
+
/** Artifact variants the cache keeps namespaced. `full` carries behavioral summaries derived
|
|
22
|
+
* from embedded-source parsing; `structural-only` is the `--no-dep-summaries` mode (ABI only,
|
|
23
|
+
* no summaries on routines). The two artifacts for the same package live as separate cache
|
|
24
|
+
* entries so a flag-flipped run never reads the wrong shape. */
|
|
25
|
+
export type ArtifactMode = "full" | "structural-only";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The semantic input key for a dependency artifact — also the cache filename stem. Hashes
|
|
29
|
+
* domain-separated canonical key material: packageSemanticHash + sorted direct-dependency
|
|
30
|
+
* identity+key pairs + all cache-affecting version stamps + devFingerprint + artifact mode.
|
|
31
|
+
*/
|
|
32
|
+
export function computeArtifactKey(
|
|
33
|
+
packageSemanticHash: string,
|
|
34
|
+
directDependencies: DirectDependencyRef[],
|
|
35
|
+
mode: ArtifactMode = "full",
|
|
36
|
+
): string {
|
|
37
|
+
const keyMaterial = {
|
|
38
|
+
kind: "al-sem.depArtifactKey",
|
|
39
|
+
v: 2,
|
|
40
|
+
packageSemanticHash,
|
|
41
|
+
directDependencies: [...directDependencies].sort((a, b) =>
|
|
42
|
+
`${a.publisher}|${a.name}|${a.appGuid}` < `${b.publisher}|${b.name}|${b.appGuid}` ? -1 : 1,
|
|
43
|
+
),
|
|
44
|
+
versions: { ...CACHE_VERSIONS, devFingerprint: devFingerprint() },
|
|
45
|
+
mode,
|
|
46
|
+
};
|
|
47
|
+
return sha256(canonicalStringify(keyMaterial));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Resolve the cache directory: an explicit override, else `~/.al-sem/cache/`. */
|
|
51
|
+
export function resolveCacheDir(override?: string): string {
|
|
52
|
+
return override ?? join(homedir(), ".al-sem", "cache");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Canonical artifact JSON with `artifactContentHash` cleared — the input to the content hash. */
|
|
56
|
+
function canonicalWithoutContentHash(artifact: DependencyArtifact): string {
|
|
57
|
+
return canonicalStringify({
|
|
58
|
+
...artifact,
|
|
59
|
+
header: { ...artifact.header, artifactContentHash: "" },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Recompute the content hash from the on-disk text WITHOUT re-serializing the parsed artifact.
|
|
65
|
+
* `writeCachedArtifact` emits canonical bytes with the hash injected by string substitution, so
|
|
66
|
+
* the file is byte-identical to `canonicalStringify(finalArtifact)`; substituting the stored hash
|
|
67
|
+
* back to "" reproduces exactly `canonicalWithoutContentHash`. This avoids a full
|
|
68
|
+
* `canonicalStringify` walk of a multi-hundred-MB object tree on every cache hit. A file written
|
|
69
|
+
* by anything other than `writeCachedArtifact` (non-canonical, or tampered) yields a mismatch and
|
|
70
|
+
* is treated as a miss — strictly safe.
|
|
71
|
+
*/
|
|
72
|
+
function contentHashFromRawText(rawText: string, storedHash: string): string {
|
|
73
|
+
const emptyHashText = rawText.replace(
|
|
74
|
+
`"artifactContentHash":"${storedHash}"`,
|
|
75
|
+
'"artifactContentHash":""',
|
|
76
|
+
);
|
|
77
|
+
return sha256(emptyHashText);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Write an artifact to the cache atomically (temp file + rename). Computes artifactContentHash
|
|
82
|
+
* and returns the canonical in-memory form — `JSON.parse` of the exact bytes a future cache hit
|
|
83
|
+
* reads — so the cold-write path produces byte-identical downstream output to a warm cache hit
|
|
84
|
+
* WITHOUT a disk round-trip (the caller does not re-read + re-validate what it just wrote).
|
|
85
|
+
*
|
|
86
|
+
* Serializes the artifact ONCE (with an empty content hash) and injects the computed hash by
|
|
87
|
+
* string substitution. `canonicalStringify` is compact + recursively key-sorted, so the
|
|
88
|
+
* with-hash and without-hash serializations are identical except for the hash value; injecting
|
|
89
|
+
* avoids a second full serialization of a multi-hundred-MB artifact. The token
|
|
90
|
+
* `"artifactContentHash":""` is unique to the header (no data field carries that key, and `abi`
|
|
91
|
+
* sorts before `header`), so the first/only occurrence is the header field.
|
|
92
|
+
*/
|
|
93
|
+
export function writeCachedArtifact(
|
|
94
|
+
artifact: DependencyArtifact,
|
|
95
|
+
cacheDir: string,
|
|
96
|
+
): DependencyArtifact {
|
|
97
|
+
const jsonEmptyHash = canonicalWithoutContentHash(artifact);
|
|
98
|
+
const contentHash = sha256(jsonEmptyHash);
|
|
99
|
+
const json = jsonEmptyHash.replace(
|
|
100
|
+
'"artifactContentHash":""',
|
|
101
|
+
`"artifactContentHash":"${contentHash}"`,
|
|
102
|
+
);
|
|
103
|
+
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
|
|
104
|
+
const finalPath = join(cacheDir, `${artifact.header.artifactKey}.json`);
|
|
105
|
+
const tmpPath = `${finalPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
106
|
+
writeFileSync(tmpPath, json, "utf8");
|
|
107
|
+
try {
|
|
108
|
+
renameSync(tmpPath, finalPath);
|
|
109
|
+
} catch {
|
|
110
|
+
// rename-over-existing can fail on Windows/network FS; if a valid final exists, accept it.
|
|
111
|
+
if (!existsSync(finalPath)) throw new Error(`could not place cache file ${finalPath}`);
|
|
112
|
+
try {
|
|
113
|
+
unlinkSync(tmpPath);
|
|
114
|
+
} catch {
|
|
115
|
+
/* best-effort temp cleanup */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return JSON.parse(json) as DependencyArtifact;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read and validate a cached artifact. Returns null on any miss/failure (caller treats it as
|
|
123
|
+
* a cache miss). Validation: filename pattern, JSON parse, structural shape, key match,
|
|
124
|
+
* version-stamp match, and content-hash recompute.
|
|
125
|
+
*/
|
|
126
|
+
export function readCachedArtifact(
|
|
127
|
+
artifactKey: string,
|
|
128
|
+
cacheDir: string,
|
|
129
|
+
): DependencyArtifact | null {
|
|
130
|
+
if (!/^[a-f0-9]{64}$/.test(artifactKey)) return null;
|
|
131
|
+
const path = join(cacheDir, `${artifactKey}.json`);
|
|
132
|
+
if (!existsSync(path)) return null;
|
|
133
|
+
let rawText: string;
|
|
134
|
+
try {
|
|
135
|
+
rawText = readFileSync(path, "utf8");
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
let parsed: unknown;
|
|
140
|
+
try {
|
|
141
|
+
parsed = JSON.parse(rawText);
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (!isDependencyArtifact(parsed)) return null;
|
|
146
|
+
const artifact = parsed;
|
|
147
|
+
if (artifact.header.artifactKey !== artifactKey) return null;
|
|
148
|
+
const v = artifact.header.versions;
|
|
149
|
+
const expected = { ...CACHE_VERSIONS, devFingerprint: devFingerprint() };
|
|
150
|
+
for (const [k, val] of Object.entries(expected)) {
|
|
151
|
+
if ((v as unknown as Record<string, string>)[k] !== val) return null;
|
|
152
|
+
}
|
|
153
|
+
const recomputed = contentHashFromRawText(rawText, artifact.header.artifactContentHash);
|
|
154
|
+
if (recomputed !== artifact.header.artifactContentHash) return null;
|
|
155
|
+
return artifact;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** One entry classified by `pruneCache` — the file, its size, and why it was kept or dropped. */
|
|
159
|
+
export interface PruneCacheEntry {
|
|
160
|
+
file: string;
|
|
161
|
+
bytes: number;
|
|
162
|
+
status:
|
|
163
|
+
| "kept"
|
|
164
|
+
| "removed-bad-name"
|
|
165
|
+
| "removed-unreadable"
|
|
166
|
+
| "removed-version-mismatch"
|
|
167
|
+
| "removed-content-hash-mismatch";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface PruneCacheResult {
|
|
171
|
+
cacheDir: string;
|
|
172
|
+
entries: PruneCacheEntry[];
|
|
173
|
+
bytesFreed: number;
|
|
174
|
+
filesRemoved: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Reasons recognised by `pruneCache` as evidence the cached artifact is no longer usable
|
|
178
|
+
* by this build of al-sem. Read-but-validates-fine artifacts are kept. */
|
|
179
|
+
function classifyArtifactForPrune(path: string): PruneCacheEntry["status"] {
|
|
180
|
+
const fileName = path.split(/[\\/]/).pop() ?? path;
|
|
181
|
+
const match = fileName.match(/^([a-f0-9]{64})\.json$/);
|
|
182
|
+
if (match === null) return "removed-bad-name";
|
|
183
|
+
const key = match[1] as string;
|
|
184
|
+
let rawText: string;
|
|
185
|
+
try {
|
|
186
|
+
rawText = readFileSync(path, "utf8");
|
|
187
|
+
} catch {
|
|
188
|
+
return "removed-unreadable";
|
|
189
|
+
}
|
|
190
|
+
let parsed: unknown;
|
|
191
|
+
try {
|
|
192
|
+
parsed = JSON.parse(rawText);
|
|
193
|
+
} catch {
|
|
194
|
+
return "removed-unreadable";
|
|
195
|
+
}
|
|
196
|
+
if (!isDependencyArtifact(parsed)) return "removed-unreadable";
|
|
197
|
+
const artifact = parsed;
|
|
198
|
+
if (artifact.header.artifactKey !== key) return "removed-unreadable";
|
|
199
|
+
const v = artifact.header.versions;
|
|
200
|
+
const expected = { ...CACHE_VERSIONS, devFingerprint: devFingerprint() };
|
|
201
|
+
for (const [k, val] of Object.entries(expected)) {
|
|
202
|
+
if ((v as unknown as Record<string, string>)[k] !== val) return "removed-version-mismatch";
|
|
203
|
+
}
|
|
204
|
+
const recomputed = contentHashFromRawText(rawText, artifact.header.artifactContentHash);
|
|
205
|
+
if (recomputed !== artifact.header.artifactContentHash) return "removed-content-hash-mismatch";
|
|
206
|
+
return "kept";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Remove cached artifacts that this build of al-sem can no longer use — version-stamp
|
|
211
|
+
* mismatch (new analyzer / grammar / symbolReader / depCache / resourcePolicy /
|
|
212
|
+
* summarySchema / devFingerprint), corrupt files, mis-named files, or content-hash
|
|
213
|
+
* tampering. Artifacts that match the current versions stay; pruning is a no-op for them.
|
|
214
|
+
*
|
|
215
|
+
* Use `dryRun: true` to classify without deleting anything — the result still lists the
|
|
216
|
+
* entries that WOULD be removed, with `bytesFreed` totaling them.
|
|
217
|
+
*/
|
|
218
|
+
export function pruneCache(
|
|
219
|
+
cacheDirOverride: string | undefined,
|
|
220
|
+
options: { dryRun?: boolean } = {},
|
|
221
|
+
): PruneCacheResult {
|
|
222
|
+
const cacheDir = resolveCacheDir(cacheDirOverride);
|
|
223
|
+
const entries: PruneCacheEntry[] = [];
|
|
224
|
+
let bytesFreed = 0;
|
|
225
|
+
let filesRemoved = 0;
|
|
226
|
+
if (!existsSync(cacheDir)) {
|
|
227
|
+
return { cacheDir, entries, bytesFreed, filesRemoved };
|
|
228
|
+
}
|
|
229
|
+
let files: string[];
|
|
230
|
+
try {
|
|
231
|
+
files = readdirSync(cacheDir);
|
|
232
|
+
} catch {
|
|
233
|
+
return { cacheDir, entries, bytesFreed, filesRemoved };
|
|
234
|
+
}
|
|
235
|
+
for (const file of files.sort()) {
|
|
236
|
+
// Skip temp files left behind by interrupted writes — readCachedArtifact also ignores
|
|
237
|
+
// these. They are reaped by the next successful rename of the same target.
|
|
238
|
+
if (file.includes(".tmp.")) continue;
|
|
239
|
+
const path = join(cacheDir, file);
|
|
240
|
+
let bytes = 0;
|
|
241
|
+
try {
|
|
242
|
+
const st = statSync(path);
|
|
243
|
+
if (!st.isFile()) continue;
|
|
244
|
+
bytes = st.size;
|
|
245
|
+
} catch {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const status = classifyArtifactForPrune(path);
|
|
249
|
+
entries.push({ file, bytes, status });
|
|
250
|
+
if (status === "kept") continue;
|
|
251
|
+
if (options.dryRun !== true) {
|
|
252
|
+
try {
|
|
253
|
+
unlinkSync(path);
|
|
254
|
+
} catch {
|
|
255
|
+
continue; // leave it — better than half-deleting
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
bytesFreed += bytes;
|
|
259
|
+
filesRemoved++;
|
|
260
|
+
}
|
|
261
|
+
return { cacheDir, entries, bytesFreed, filesRemoved };
|
|
262
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import type { Diagnostic } from "../model/finding.ts";
|
|
3
|
+
import type { ManifestDependency } from "../symbols/app-manifest.ts";
|
|
4
|
+
import type { DependencyPackageRef } from "./dependency-artifact.ts";
|
|
5
|
+
|
|
6
|
+
/** Compare two BC `x.y.z.w` version strings numerically. Missing components count as 0. */
|
|
7
|
+
export function compareVersions(a: string, b: string): number {
|
|
8
|
+
const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
9
|
+
const pb = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
10
|
+
for (let i = 0; i < 4; i++) {
|
|
11
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
12
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
13
|
+
}
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ResolveDependencyDagResult {
|
|
18
|
+
/** Selected packages in dependency order (a dependency precedes its dependents). */
|
|
19
|
+
ordered: DependencyPackageRef[];
|
|
20
|
+
diagnostics: Diagnostic[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate the dependency DAG, resolve one package per appGuid against accumulated
|
|
25
|
+
* minVersion constraints, and topo-sort the reachable set. Never throws; failure modes
|
|
26
|
+
* surface as DEP0xx diagnostics. Deterministic: package grouping, constraint accumulation,
|
|
27
|
+
* version selection, and topo order are all sorted.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveDependencyDag(
|
|
30
|
+
packages: DependencyPackageRef[],
|
|
31
|
+
workspaceDependencies: ManifestDependency[],
|
|
32
|
+
): ResolveDependencyDagResult {
|
|
33
|
+
const diagnostics: Diagnostic[] = [];
|
|
34
|
+
|
|
35
|
+
// --- group by appGuid; flag exact-identity duplicates (DEP010) ---
|
|
36
|
+
const byGuid = new Map<string, DependencyPackageRef[]>();
|
|
37
|
+
for (const p of [...packages].sort((a, b) => (a.appPath < b.appPath ? -1 : 1))) {
|
|
38
|
+
const list = byGuid.get(p.appGuid) ?? [];
|
|
39
|
+
const dup = list.find((q) => q.version === p.version);
|
|
40
|
+
if (dup !== undefined) {
|
|
41
|
+
diagnostics.push({
|
|
42
|
+
severity: "warning",
|
|
43
|
+
stage: "discover",
|
|
44
|
+
message: `[DEP010] duplicate package identity ${p.name} ${p.version} (kept ${basename(dup.appPath)}, ignored ${basename(p.appPath)})`,
|
|
45
|
+
sourceRef: p.appPath,
|
|
46
|
+
});
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
list.push(p);
|
|
50
|
+
byGuid.set(p.appGuid, list);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- BFS the transitive closure, accumulating minVersion constraints per appGuid ---
|
|
54
|
+
const constraints = new Map<string, string>(); // appGuid -> highest required minVersion
|
|
55
|
+
const queue: ManifestDependency[] = [...workspaceDependencies];
|
|
56
|
+
const seenConstraintFrom = new Set<string>(); // appGuids whose own deps were already queued
|
|
57
|
+
const requireConstraint = (d: ManifestDependency): void => {
|
|
58
|
+
const prev = constraints.get(d.appGuid);
|
|
59
|
+
if (prev === undefined || compareVersions(d.minVersion, prev) > 0) {
|
|
60
|
+
constraints.set(d.appGuid, d.minVersion);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const selected = new Map<string, DependencyPackageRef>();
|
|
64
|
+
while (queue.length > 0) {
|
|
65
|
+
const d = queue.shift();
|
|
66
|
+
if (d === undefined) continue;
|
|
67
|
+
requireConstraint(d);
|
|
68
|
+
if (seenConstraintFrom.has(d.appGuid)) continue;
|
|
69
|
+
seenConstraintFrom.add(d.appGuid);
|
|
70
|
+
|
|
71
|
+
const available = byGuid.get(d.appGuid);
|
|
72
|
+
if (available === undefined || available.length === 0) {
|
|
73
|
+
diagnostics.push({
|
|
74
|
+
severity: "warning",
|
|
75
|
+
stage: "discover",
|
|
76
|
+
message: `[DEP012] declared dependency ${d.name} (${d.appGuid}) has no matching package in .alpackages`,
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// select highest version satisfying the (current) accumulated constraint
|
|
81
|
+
const min = constraints.get(d.appGuid) ?? "0.0.0.0";
|
|
82
|
+
const satisfying = available
|
|
83
|
+
.filter((p) => compareVersions(p.version, min) >= 0)
|
|
84
|
+
.sort((a, b) => compareVersions(a.version, b.version));
|
|
85
|
+
const chosen = satisfying[satisfying.length - 1];
|
|
86
|
+
if (chosen === undefined) {
|
|
87
|
+
const highest = [...available].sort((a, b) => compareVersions(a.version, b.version)).pop();
|
|
88
|
+
diagnostics.push({
|
|
89
|
+
severity: "warning",
|
|
90
|
+
stage: "discover",
|
|
91
|
+
message: `[DEP011] no version of ${d.name} satisfies minVersion ${min} (highest available ${highest?.version ?? "?"}) — dependency left opaque`,
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
selected.set(d.appGuid, chosen);
|
|
96
|
+
for (const td of chosen.manifestDependencies) queue.push(td);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- topo-sort the selected set; break cycles deterministically (DEP013) ---
|
|
100
|
+
const nodes = [...selected.values()].sort((a, b) =>
|
|
101
|
+
`${a.publisher}|${a.name}|${a.appGuid}` < `${b.publisher}|${b.name}|${b.appGuid}` ? -1 : 1,
|
|
102
|
+
);
|
|
103
|
+
const ordered: DependencyPackageRef[] = [];
|
|
104
|
+
const state = new Map<string, "visiting" | "done">();
|
|
105
|
+
const visit = (p: DependencyPackageRef): void => {
|
|
106
|
+
const s = state.get(p.appGuid);
|
|
107
|
+
if (s === "done") return;
|
|
108
|
+
if (s === "visiting") {
|
|
109
|
+
diagnostics.push({
|
|
110
|
+
severity: "error",
|
|
111
|
+
stage: "discover",
|
|
112
|
+
message: `[DEP013] dependency cycle involving ${p.name} — back-edge dropped`,
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
state.set(p.appGuid, "visiting");
|
|
117
|
+
const deps = [...p.manifestDependencies].sort((a, b) => (a.appGuid < b.appGuid ? -1 : 1));
|
|
118
|
+
for (const d of deps) {
|
|
119
|
+
const child = selected.get(d.appGuid);
|
|
120
|
+
if (child !== undefined) visit(child);
|
|
121
|
+
}
|
|
122
|
+
state.set(p.appGuid, "done");
|
|
123
|
+
ordered.push(p);
|
|
124
|
+
};
|
|
125
|
+
for (const p of nodes) visit(p);
|
|
126
|
+
|
|
127
|
+
return { ordered, diagnostics };
|
|
128
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { Diagnostic } from "../model/finding.ts";
|
|
4
|
+
import { readAppManifest } from "../symbols/app-manifest.ts";
|
|
5
|
+
import { packageHash } from "../symbols/package-hash.ts";
|
|
6
|
+
import type { DependencyPackageRef } from "./dependency-artifact.ts";
|
|
7
|
+
|
|
8
|
+
export interface DiscoverDependencyPackagesResult {
|
|
9
|
+
packages: DependencyPackageRef[];
|
|
10
|
+
diagnostics: Diagnostic[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Enumerate the .app packages in an .alpackages directory into DependencyPackageRefs. Reads
|
|
15
|
+
* only each package's NavxManifest.xml and raw bytes — never SymbolReference.json or source.
|
|
16
|
+
* Never throws: an unreadable directory or a corrupt .app becomes a `discover` diagnostic.
|
|
17
|
+
* Packages are returned sorted by file path for determinism.
|
|
18
|
+
*/
|
|
19
|
+
export function discoverDependencyPackages(
|
|
20
|
+
alpackagesDir: string,
|
|
21
|
+
): DiscoverDependencyPackagesResult {
|
|
22
|
+
const diagnostics: Diagnostic[] = [];
|
|
23
|
+
if (!existsSync(alpackagesDir)) {
|
|
24
|
+
diagnostics.push({
|
|
25
|
+
severity: "warning",
|
|
26
|
+
stage: "discover",
|
|
27
|
+
message: "[DEP001] .alpackages directory not found",
|
|
28
|
+
sourceRef: alpackagesDir,
|
|
29
|
+
});
|
|
30
|
+
return { packages: [], diagnostics };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let entries: string[];
|
|
34
|
+
try {
|
|
35
|
+
entries = readdirSync(alpackagesDir);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
severity: "warning",
|
|
39
|
+
stage: "discover",
|
|
40
|
+
message: `[DEP001] could not read .alpackages directory: ${(err as Error).message}`,
|
|
41
|
+
sourceRef: alpackagesDir,
|
|
42
|
+
});
|
|
43
|
+
return { packages: [], diagnostics };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const appFiles = entries.filter((e) => e.toLowerCase().endsWith(".app")).sort();
|
|
47
|
+
const packages: DependencyPackageRef[] = [];
|
|
48
|
+
for (const fileName of appFiles) {
|
|
49
|
+
const appPath = join(alpackagesDir, fileName);
|
|
50
|
+
const manifest = readAppManifest(appPath);
|
|
51
|
+
if (manifest.error !== undefined || manifest.identity.appGuid === "") {
|
|
52
|
+
diagnostics.push({
|
|
53
|
+
severity: "warning",
|
|
54
|
+
stage: "discover",
|
|
55
|
+
message: `[DEP002] skipped unreadable or corrupt package ${fileName}: ${manifest.error ?? "no app identity"}`,
|
|
56
|
+
sourceRef: appPath,
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
let hash: string;
|
|
61
|
+
try {
|
|
62
|
+
hash = packageHash(appPath);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
diagnostics.push({
|
|
65
|
+
severity: "warning",
|
|
66
|
+
stage: "discover",
|
|
67
|
+
message: `[DEP002] could not hash package ${fileName}: ${(err as Error).message}`,
|
|
68
|
+
sourceRef: appPath,
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
packages.push({
|
|
73
|
+
appPath,
|
|
74
|
+
appGuid: manifest.identity.appGuid,
|
|
75
|
+
publisher: manifest.identity.publisher,
|
|
76
|
+
name: manifest.identity.name,
|
|
77
|
+
version: manifest.identity.version,
|
|
78
|
+
packageHash: hash,
|
|
79
|
+
manifestDependencies: manifest.dependencies,
|
|
80
|
+
includesSource: manifest.includesSource,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { packages, diagnostics };
|
|
85
|
+
}
|