diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d5d322b3..38c5e7b60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Changed -- Updated the Coana CLI to v `15.10.54`. +- Updated the Coana CLI to v `15.10.55`. +- Generated Maven, Gradle and sbt `.socket.facts.json` files are now substantially smaller, making uploads for large JVM projects faster and more reliable. ## [1.1.178](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.178) - 2026-09-23 diff --git a/package.json b/package.json index 0e0370a23..3c7241e10 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "@babel/preset-typescript": "7.27.1", "@babel/runtime": "7.28.4", "@biomejs/biome": "2.2.4", - "@coana-tech/cli": "15.10.54", + "@coana-tech/cli": "15.10.55", "@cyclonedx/cdxgen": "12.1.2", "@dotenvx/dotenvx": "1.49.0", "@eslint/compat": "1.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9b4dee87..2a321a7cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,8 +138,8 @@ importers: specifier: 2.2.4 version: 2.2.4 '@coana-tech/cli': - specifier: 15.10.54 - version: 15.10.54 + specifier: 15.10.55 + version: 15.10.55 '@cyclonedx/cdxgen': specifier: 12.1.2 version: 12.1.2 @@ -827,8 +827,8 @@ packages: resolution: {integrity: sha512-hAs5PPKPCQ3/Nha+1fo4A4/gL85fIfxZwHPehsjCJ+BhQH2/yw6/xReuaPA/RfNQr6iz1PcD7BZcE3ctyyl3EA==} cpu: [x64] - '@coana-tech/cli@15.10.54': - resolution: {integrity: sha512-TYn3gtBUgooRvuJFvcrGWT4Okq35y5esKs2PfDrszp65DTqm+CKmeGICcEiTEFE+82rHBzAZuMRNuG0vSQrIfA==} + '@coana-tech/cli@15.10.55': + resolution: {integrity: sha512-zhWbiFA8dJUB/PelaUaMiSKfVXuF5M5KpLWuMgNamrXH5k9G+bk4BIlPPS6KvW5u/85UurzsvwAWLQOB3VeUEQ==} hasBin: true '@colors/colors@1.5.0': @@ -5702,7 +5702,7 @@ snapshots: '@cdxgen/cdxgen-plugins-bin@2.0.2': optional: true - '@coana-tech/cli@15.10.54': {} + '@coana-tech/cli@15.10.55': {} '@colors/colors@1.5.0': optional: true diff --git a/src/commands/manifest/enumerate-workspaces.test.mts b/src/commands/manifest/enumerate-workspaces.test.mts index 6d6735f8d..b8c0ec2c4 100644 --- a/src/commands/manifest/enumerate-workspaces.test.mts +++ b/src/commands/manifest/enumerate-workspaces.test.mts @@ -20,7 +20,6 @@ function okResult(): WorkspaceEnumerationResult { name: 'root', subprojectDir: '.', dependencies: [], - resolvedAs: [], }, ], stderr: '', diff --git a/src/commands/manifest/generate-recursive-manifests.test.mts b/src/commands/manifest/generate-recursive-manifests.test.mts index f2cf09d93..041a223aa 100644 --- a/src/commands/manifest/generate-recursive-manifests.test.mts +++ b/src/commands/manifest/generate-recursive-manifests.test.mts @@ -61,14 +61,12 @@ describe('generateRecursiveManifests', () => { name: 'moduleA', subprojectDir: 'moduleA', dependencies: [], - resolvedAs: [], }, { type: 'maven', name: 'moduleB', subprojectDir: 'moduleB', dependencies: [], - resolvedAs: [], }, ], } @@ -137,7 +135,6 @@ describe('generateRecursiveManifests', () => { name: 'shared-lib', subprojectDir: `../${sharedLibName}`, dependencies: [], - resolvedAs: [], }, ], } diff --git a/src/commands/manifest/run-manifest-facts.mts b/src/commands/manifest/run-manifest-facts.mts index f4e39a70a..a88db41e2 100644 --- a/src/commands/manifest/run-manifest-facts.mts +++ b/src/commands/manifest/run-manifest-facts.mts @@ -221,7 +221,11 @@ export async function runManifestFacts({ return } - await fs.writeFile(factsPath, JSON.stringify(facts, null, 2), 'utf8') + const socketCliVersion = constants.ENV.INLINED_SOCKET_CLI_VERSION + if (facts.metadata && socketCliVersion) { + facts.metadata.socketCliVersion = socketCliVersion + } + await fs.writeFile(factsPath, JSON.stringify(facts), 'utf8') if (withFiles && sidecarAcc) { // Key by the symlink-resolved path so the sidecar's keys are comparable diff --git a/src/commands/manifest/run-manifest-facts.test.mts b/src/commands/manifest/run-manifest-facts.test.mts index 7f185d277..21768a6ca 100644 --- a/src/commands/manifest/run-manifest-facts.test.mts +++ b/src/commands/manifest/run-manifest-facts.test.mts @@ -10,6 +10,7 @@ vi.mock('./scripts/run.mts', () => ({ import { runManifestFacts } from './run-manifest-facts.mts' import { runManifestScript } from './scripts/run.mts' +import constants from '../../constants.mts' import type { ManifestRunResult } from './scripts/run.mts' import type { SidecarAccumulator } from './scripts/sidecar.mts' @@ -29,6 +30,7 @@ function okResult(): ManifestRunResult { targetsByGav: new Map(), sourcesByCoord: new Map(), coords: new Set(), + classpathByProject: new Map(), }, stderr: '', stdout: '', @@ -122,7 +124,6 @@ describe('runManifestFacts - sidecar', () => { version: '1.0', subprojectDir: '.', dependencies: [], - resolvedAs: [], }, ] vi.mocked(runManifestScript).mockResolvedValue(result) @@ -137,4 +138,24 @@ describe('runManifestFacts - sidecar', () => { const bucket = sidecarAcc.get(expectedFactsFile) expect(bucket?.projects.find(m => m.name === 'app')).toBeDefined() }) + it('stamps the inlined socket-cli version into the written facts metadata', async () => { + const result = okResult() + result.facts.metadata = { + format: 'socket-facts-sbom', + tool: 'maven', + toolVersion: '3.9.9', + } + vi.mocked(runManifestScript).mockResolvedValue(result) + + await runManifestFacts({ ...baseArgs, cwd }) + + const written = JSON.parse( + await fs.readFile(path.join(cwd, '.socket.facts.json'), 'utf8'), + ) + // Unit tests run unbuilt, where the version isn't inlined; the field is + // then omitted rather than written empty. + expect(written.metadata.socketCliVersion).toBe( + constants.ENV.INLINED_SOCKET_CLI_VERSION || undefined, + ) + }) }) diff --git a/src/commands/manifest/scripts/assemble.mts b/src/commands/manifest/scripts/assemble.mts index bce869906..f4501a310 100644 --- a/src/commands/manifest/scripts/assemble.mts +++ b/src/commands/manifest/scripts/assemble.mts @@ -1,4 +1,3 @@ -import { createHash } from 'node:crypto' import { existsSync } from 'node:fs' import { @@ -8,6 +7,7 @@ import { type SocketFactsSbomMetadata, type SocketFactsSbomProject, mavenCoordinateKey, + projectClasspathKey, } from './facts.mts' import type { ParsedRecords, RawCoord, RawProject } from './records.mts' @@ -51,14 +51,14 @@ export function assembleFacts( ): AssembleResult { const fileExists = opts.fileExists ?? existsSync const perRoot = buildPerRoot(parsed) - const { directByRoot, finalNodes } = mergePathSensitive(perRoot) + const { directByRoot, finalNodes } = mergeByCoordinate(perRoot) const tool = (parsed.tool || 'gradle') as SocketFactsSbomMetadata['tool'] const components = buildComponents(finalNodes) const projects = opts.emitProjects === false ? [] - : buildProjects(parsed, finalNodes, directByRoot, perRoot) + : buildProjects(parsed, directByRoot, perRoot) const metadata: SocketFactsSbomMetadata = { format: 'socket-facts-sbom', @@ -77,6 +77,7 @@ export function assembleFacts( artifactPaths: buildArtifactPaths( finalNodes, [...parsed.projects.values()], + perRoot, fileExists, ), } @@ -86,10 +87,6 @@ function gav(group: string, name: string, version: string): string { return `${group}:${name}:${version}` } -function shortHash(s: string): string { - return createHash('sha256').update(s, 'utf8').digest('hex').slice(0, 12) -} - function buildPerRoot(parsed: ParsedRecords): Map { const out = new Map() for (const [rootId, r] of parsed.roots) { @@ -117,7 +114,7 @@ function buildPerRoot(parsed: ParsedRecords): Map { for (const [coordId, n] of r.nodes) { nodes.set(coordId, { coord: n.coord, - children: [...(childrenByParent.get(coordId) ?? [])].sort(), + children: [...(childrenByParent.get(coordId) ?? [])], direct: n.direct, targets: n.targets, }) @@ -127,83 +124,18 @@ function buildPerRoot(parsed: ParsedRecords): Map { return out } -// A coordinate with identical subtrees everywhere collapses to one node (id = -// coordId); divergent subtrees each get a content-addressed id -// (`#`) so per-subproject overrides stay distinct. -function mergePathSensitive(perRoot: Map): { +// Components are merged by coordinate across every resolution root; which +// coordinates belong to which subproject is kept separately (classpathByProject) +// for reachability, which needs each subproject's exact classpath. +function mergeByCoordinate(perRoot: Map): { finalNodes: Map directByRoot: Map> } { - const memo = new Map() - const nodesOf = (rootId: string) => perRoot.get(rootId)?.nodes - - function computeSig( - rootId: string, - coordId: string, - onPath: Set, - ): string { - const memoKey = rootId + ' ' + coordId - const cached = memo.get(memoKey) - if (cached !== undefined) { - return cached - } - if (onPath.has(coordId)) { - // Cycle: back-edge as leaf. - return coordId - } - const node = nodesOf(rootId)?.get(coordId) - if (!node) { - return coordId - } - onPath.add(coordId) - const childSigs = node.children.map(c => computeSig(rootId, c, onPath)) - onPath.delete(coordId) - // Digest, not the raw string: caching expanded subtree strings OOMs on - // reconverging DAGs; a fixed-size digest keeps the pass O(V+E). - const sig = coordId + '{' + childSigs.join(',') + '}' - const digest = createHash('sha256') - .update(sig, 'utf8') - .digest('hex') - .slice(0, 16) - memo.set(memoKey, digest) - return digest - } - - // Sorted iteration keeps cyclic-graph signatures stable run-to-run. - const sigsByCoord = new Map>() - for (const rootId of [...perRoot.keys()].sort()) { - const nodes = perRoot.get(rootId)!.nodes - for (const coordId of [...nodes.keys()].sort()) { - const sig = computeSig(rootId, coordId, new Set()) - let set = sigsByCoord.get(coordId) - if (!set) { - set = new Set() - sigsByCoord.set(coordId, set) - } - set.add(sig) - } - } - const divergent = (coordId: string): boolean => - (sigsByCoord.get(coordId)?.size ?? 0) > 1 - const emittedIdMemo = new Map() - const emittedIdFor = (rootId: string, coordId: string): string => { - const k = rootId + ' ' + coordId - let v = emittedIdMemo.get(k) - if (v === undefined) { - v = divergent(coordId) - ? coordId + '#' + shortHash(computeSig(rootId, coordId, new Set())) - : coordId - emittedIdMemo.set(k, v) - } - return v - } - const finalNodes = new Map() const directByRoot = new Map>() for (const [rootId, { nodes, prod }] of perRoot) { for (const [coordId, node] of nodes) { - const eid = emittedIdFor(rootId, coordId) - let fn = finalNodes.get(eid) + let fn = finalNodes.get(coordId) if (!fn) { fn = { coord: node.coord, @@ -212,7 +144,7 @@ function mergePathSensitive(perRoot: Map): { direct: false, targets: new Set(), } - finalNodes.set(eid, fn) + finalNodes.set(coordId, fn) } if (prod) { fn.prod = true @@ -221,7 +153,7 @@ function mergePathSensitive(perRoot: Map): { fn.direct = true } for (const c of node.children) { - fn.children.add(emittedIdFor(rootId, c)) + fn.children.add(c) } for (const t of node.targets) { fn.targets.add(t) @@ -232,7 +164,7 @@ function mergePathSensitive(perRoot: Map): { d = new Set() directByRoot.set(rootId, d) } - d.add(eid) + d.add(coordId) } } } @@ -277,20 +209,9 @@ function buildComponents( function buildProjects( parsed: ParsedRecords, - finalNodes: Map, directByRoot: Map>, perRoot: Map, ): SocketFactsSbomProject[] { - const idsByGav = new Map>() - for (const [id, fn] of finalNodes) { - const key = gav(fn.coord.group, fn.coord.name, fn.coord.version ?? '') - let set = idsByGav.get(key) - if (!set) { - set = new Set() - idsByGav.set(key, set) - } - set.add(id) - } const directByProject = new Map>() for (const [rootId, ids] of directByRoot) { const pk = perRoot.get(rootId)?.projectKey ?? '' @@ -312,9 +233,6 @@ function buildProjects( ...(p.version ? { version: p.version } : {}), subprojectDir: p.dir, dependencies: [...(directByProject.get(p.projectKey) ?? [])].sort(), - resolvedAs: [ - ...(idsByGav.get(gav(p.group, p.name, p.version)) ?? []), - ].sort(), } return entry }) @@ -346,9 +264,46 @@ function unionInto( } } +function buildClasspathByProject( + projects: RawProject[], + perRoot: Map, +): Map { + const idsByProjectKey = new Map>() + for (const { nodes, projectKey } of perRoot.values()) { + let set = idsByProjectKey.get(projectKey) + if (!set) { + set = new Set() + idsByProjectKey.set(projectKey, set) + } + for (const coordId of nodes.keys()) { + set.add(coordId) + } + } + const classpathByProject = new Map>() + for (const p of projects) { + const key = projectClasspathKey({ + name: p.name, + namespace: p.group, + subprojectDir: p.dir, + }) + let set = classpathByProject.get(key) + if (!set) { + set = new Set() + classpathByProject.set(key, set) + } + for (const id of idsByProjectKey.get(p.projectKey) ?? []) { + set.add(id) + } + } + return new Map( + [...classpathByProject].map(({ 0: key, 1: ids }) => [key, [...ids].sort()]), + ) +} + function buildArtifactPaths( finalNodes: Map, projects: RawProject[], + perRoot: Map, fileExists: (path: string) => boolean, ): ResolvedArtifactPaths { const projectsByGav = new Map< @@ -430,7 +385,13 @@ function buildArtifactPaths( unionInto(targetsByCoord, coordKey, targets) unionInto(targetsByGav, coordKey, targets) } - return { targetsByCoord, targetsByGav, sourcesByCoord, coords } + return { + targetsByCoord, + targetsByGav, + sourcesByCoord, + coords, + classpathByProject: buildClasspathByProject(projects, perRoot), + } } function buildReport(parsed: ParsedRecords): ResolutionReport { diff --git a/src/commands/manifest/scripts/assemble.test.mts b/src/commands/manifest/scripts/assemble.test.mts index 0d896f4a2..af78da6d9 100644 --- a/src/commands/manifest/scripts/assemble.test.mts +++ b/src/commands/manifest/scripts/assemble.test.mts @@ -52,9 +52,9 @@ describe('records → assemble → sidecar', () => { version: '1.0', subprojectDir: '/abs/app', dependencies: ['com.example:bom:2.0', 'com.example:lib:jar:1.0'], - resolvedAs: [], targets: ['/abs/app/build/classes'], sources: ['/abs/app/src/main/java'], + classpath: ['com.example:bom:2.0', 'com.example:lib:jar:1.0'], }, ]) @@ -69,4 +69,51 @@ describe('records → assemble → sidecar', () => { expect(bom?.targets).toEqual([]) expect(bom?.sources).toEqual([]) }) + it('merges a coordinate with divergent subtrees into one component and scopes classpaths per project', () => { + // :a and :b both depend on `lib`, which pulls a different `dep` version in + // each subproject. + const records = [ + 'meta\tgradle\t8.0\t17', + 'project\t:a\tcom.example\ta\t1.0\ta', + 'project\t:b\tcom.example\tb\t1.0\tb', + 'root\tr1\t:a\truntimeClasspath\t1', + 'node\tr1\tg:lib:jar:1\tg\tlib\t1\tjar\t\t1', + 'node\tr1\tg:dep:jar:1\tg\tdep\t1\tjar\t\t0', + 'edge\tr1\tg:lib:jar:1\tg:dep:jar:1', + 'root\tr2\t:b\truntimeClasspath\t1', + 'node\tr2\tg:lib:jar:1\tg\tlib\t1\tjar\t\t1', + 'node\tr2\tg:dep:jar:2\tg\tdep\t2\tjar\t\t0', + 'edge\tr2\tg:lib:jar:1\tg:dep:jar:2', + 'root\tr3\t:b\ttestRuntimeClasspath\t0', + 'node\tr3\tg:junit:jar:4\tg\tjunit\t4\tjar\t\t1', + ].join('\n') + const { artifactPaths, facts } = assembleFacts(parseRecords(records), { + fileExists: () => true, + }) + + expect(facts.components.map(c => c.id)).toEqual([ + 'g:dep:jar:1', + 'g:dep:jar:2', + 'g:junit:jar:4', + 'g:lib:jar:1', + ]) + expect( + facts.components.find(c => c.id === 'g:lib:jar:1')?.dependencies, + ).toEqual(['g:dep:jar:1', 'g:dep:jar:2']) + + const acc: SidecarAccumulator = new Map() + accumulateSidecar(acc, facts, artifactPaths, '/abs/.socket.facts.json') + const byName = new Map( + serializeSidecar(acc)['/abs/.socket.facts.json']!.projects.map(p => [ + p.name, + p.classpath, + ]), + ) + expect(byName.get('a')).toEqual(['g:dep:jar:1', 'g:lib:jar:1']) + expect(byName.get('b')).toEqual([ + 'g:dep:jar:2', + 'g:junit:jar:4', + 'g:lib:jar:1', + ]) + }) }) diff --git a/src/commands/manifest/scripts/facts.mts b/src/commands/manifest/scripts/facts.mts index 66859d183..1ff25965f 100644 --- a/src/commands/manifest/scripts/facts.mts +++ b/src/commands/manifest/scripts/facts.mts @@ -19,6 +19,8 @@ export type SocketFactsSbomMetadata = { tool: 'gradle' | 'maven' | 'sbt' toolVersion: string javaVersion?: string | undefined + // Lets a facts file be traced back to the generator release that produced it. + socketCliVersion?: string | undefined } export type SocketFactsSbomComponent = AnyPURL & { @@ -31,7 +33,6 @@ export type SocketFactsSbomComponent = AnyPURL & { export type SocketFactsSbomProject = AnyPURL & { subprojectDir: string dependencies: string[] - resolvedAs: string[] } // Resolved on-disk paths for a --with-files run, keyed by coordinate. `targets` @@ -44,6 +45,15 @@ export type ResolvedArtifactPaths = { targetsByGav: Map sourcesByCoord: Map coords: Set + // Component ids on each project's resolved classpath (union over its + // configurations), keyed by projectClasspathKey. + classpathByProject: Map +} + +export function projectClasspathKey( + project: Pick, +): string { + return `${project.subprojectDir} ${project.namespace ?? ''}:${project.name}` } // Coordinate-based (not `id`-based) so it also matches foreign SBOMs like diff --git a/src/commands/manifest/scripts/sidecar.mts b/src/commands/manifest/scripts/sidecar.mts index f5363fde1..067f7ea3b 100644 --- a/src/commands/manifest/scripts/sidecar.mts +++ b/src/commands/manifest/scripts/sidecar.mts @@ -1,4 +1,4 @@ -import { mavenCoordinateKey } from './facts.mts' +import { mavenCoordinateKey, projectClasspathKey } from './facts.mts' import type { AnyPURL, @@ -22,6 +22,9 @@ export type SidecarComponentEntry = SocketFactsSbomComponent & { export type SidecarProjectEntry = SocketFactsSbomProject & { targets?: string[] | undefined sources?: string[] | undefined + // Ids of this facts file's components[] forming the project's full + // transitive classpath across all its configurations. + classpath: string[] } // Frozen contract with `coana run --compute-artifacts-sidecar`; change only @@ -105,9 +108,13 @@ export function accumulateSidecar( ): void { acc.set(factsFile, { components: facts.components.map(comp => attachPaths(comp, artifactPaths)), - projects: (facts.projects ?? []).map(proj => - attachPaths(proj, artifactPaths), - ), + projects: (facts.projects ?? []).map(proj => ({ + ...attachPaths(proj, artifactPaths), + classpath: [ + ...(artifactPaths.classpathByProject.get(projectClasspathKey(proj)) ?? + []), + ], + })), }) } diff --git a/src/commands/manifest/scripts/sidecar.test.mts b/src/commands/manifest/scripts/sidecar.test.mts index 2e894a4f2..f632be662 100644 --- a/src/commands/manifest/scripts/sidecar.test.mts +++ b/src/commands/manifest/scripts/sidecar.test.mts @@ -17,6 +17,7 @@ function emptyArtifactPaths(): ResolvedArtifactPaths { targetsByGav: new Map(), sourcesByCoord: new Map(), coords: new Set(), + classpathByProject: new Map(), } } @@ -177,7 +178,6 @@ describe('compute-artifacts sidecar', () => { version: '1.0', subprojectDir: 'app', dependencies: [], - resolvedAs: [], }, ], } @@ -202,13 +202,41 @@ describe('compute-artifacts sidecar', () => { version: '1.0', subprojectDir: 'app', dependencies: [], - resolvedAs: [], targets: ['/abs/app/build/classes'], sources: ['/abs/app/src/main/java'], + classpath: [], }, ]) }) + it('attaches each project its own classpath ids, keyed by subprojectDir and name', () => { + const project = { + type: 'maven', + namespace: 'com.example', + version: '1.0', + dependencies: [], + } + const facts: SocketFactsSbom = { + components: [], + projects: [ + { ...project, name: 'a', subprojectDir: 'a' }, + { ...project, name: 'b', subprojectDir: 'b' }, + ], + } + const artifactPaths = emptyArtifactPaths() + artifactPaths.classpathByProject.set('a com.example:a', ['g:x:jar:1']) + artifactPaths.classpathByProject.set('b com.example:b', ['g:x:jar:2']) + + const acc: SidecarAccumulator = new Map() + accumulateSidecar(acc, facts, artifactPaths, '/root/.socket.facts.json') + const projects = serializeSidecar(acc)['/root/.socket.facts.json']!.projects + + expect(projects.map(p => p.classpath)).toEqual([ + ['g:x:jar:1'], + ['g:x:jar:2'], + ]) + }) + it('does NOT reunion the same external coordinate across build roots - duplication across reactors is intentional', () => { const acc: SidecarAccumulator = new Map() const a = mkComponentFixture('/root-a/a.jar') @@ -236,7 +264,6 @@ describe('compute-artifacts sidecar', () => { version: '1.0', subprojectDir: '.', dependencies: [], - resolvedAs: [], }, ], } diff --git a/src/commands/manifest/scripts/socket-facts.init.gradle b/src/commands/manifest/scripts/socket-facts.init.gradle index d4f061327..96bfb2d7c 100644 --- a/src/commands/manifest/scripts/socket-facts.init.gradle +++ b/src/commands/manifest/scripts/socket-facts.init.gradle @@ -5,7 +5,7 @@ import java.util.Collections // Emits a flat line-protocol RECORDS file (NOT the final .socket.facts.json, and NOT to stdout). The // TS assembler (utils/src/manifest-scripts/assemble.ts) reads the records and owns all SBOM -// construction — graph merge, content-addressed ids, contentHash. This script only RESOLVES and emits +// construction — graph merge and per-subproject classpaths. This script only RESOLVES and emits // raw facts. See records.ts for the record grammar. // `Project.findProperty` only exists since Gradle 2.13; fall back to hasProperty/property for older Gradle. @@ -447,8 +447,8 @@ allprojects { project -> // Record it instead of swallowing; the build still exits cleanly and the TS layer surfaces it. state.unscannable << [config: cfg.name, detail: rawDetail(e)] } - // Stash this resolution root's tree; the key only has to be unique per root (aggregator - // groups by subtree hash). Skip empty configs — they'd contribute nothing. + // Stash this resolution root's tree; the key only has to be unique per root. Skip empty + // configs — they'd contribute nothing. if (!nodes.isEmpty()) { synchronized (state.perSub) { state.perSub["${project.path}::${cfg.name}".toString()] = @@ -497,8 +497,8 @@ rootProject { rp -> } } - // One resolution root per (subproject, configuration); the TS assembler merges them - // path-sensitively and content-addresses divergent subtrees. + // One resolution root per (subproject, configuration); the TS assembler merges them by + // coordinate and derives each subproject's classpath from its roots. def perSub synchronized (state.perSub) { perSub = new LinkedHashMap(state.perSub) } int rootIdx = 0 diff --git a/src/commands/manifest/scripts/socket-facts.plugin.scala b/src/commands/manifest/scripts/socket-facts.plugin.scala index 491919bc8..63aec33fd 100644 --- a/src/commands/manifest/scripts/socket-facts.plugin.scala +++ b/src/commands/manifest/scripts/socket-facts.plugin.scala @@ -10,8 +10,8 @@ import scala.reflect.ClassTag * Emits a flat line-protocol RECORDS file at the build root (NOT the final * `.socket.facts.json`, and NOT to stdout — sbt prints resolution noise to stdout * with no way to silence it). The TS assembler (utils/src/manifest-scripts/assemble.ts) - * reads the records and owns all SBOM construction — graph merge, content-addressed - * ids, contentHash. This plugin only RESOLVES and emits raw facts. See records.ts for + * reads the records and owns all SBOM construction — graph merge and per-subproject + * classpaths. This plugin only RESOLVES and emits raw facts. See records.ts for * the record grammar. * * Must compile on Scala 2.10/sbt 0.13 and Scala 2.12/sbt 1.x (compiled by the sbt @@ -100,8 +100,8 @@ object SocketFactsPlugin extends AutoPlugin { } } - // One resolution root per (subproject, configuration); the TS assembler content-addresses - // divergent subtrees. + // One resolution root per (subproject, configuration); the TS assembler merges them by + // coordinate and derives each subproject's classpath from its roots. var rootIdx = 0 perSub.foreach { case (_, tree) => diff --git a/src/commands/manifest/setup-recursive-manifest-config.test.mts b/src/commands/manifest/setup-recursive-manifest-config.test.mts index 0036481f3..237e664ea 100644 --- a/src/commands/manifest/setup-recursive-manifest-config.test.mts +++ b/src/commands/manifest/setup-recursive-manifest-config.test.mts @@ -240,14 +240,12 @@ describe('markWorkspaceCoverage', () => { name: 'moduleA', subprojectDir: 'moduleA', dependencies: [], - resolvedAs: [], }, { type: 'maven', name: 'moduleB', subprojectDir: 'moduleB', dependencies: [], - resolvedAs: [], }, ], }) @@ -274,14 +272,12 @@ describe('markWorkspaceCoverage', () => { name: 'moduleA', subprojectDir: 'moduleA', dependencies: [], - resolvedAs: [], }, { type: 'maven', name: 'shared-lib', subprojectDir: '../shared-lib', dependencies: [], - resolvedAs: [], }, ], }) @@ -1171,14 +1167,12 @@ describe('setupRecursiveManifestConfig', () => { name: 'module-a', subprojectDir: 'module-a', dependencies: [], - resolvedAs: [], }, { type: 'maven', name: 'module-b', subprojectDir: 'module-b', dependencies: [], - resolvedAs: [], }, ], } diff --git a/src/commands/scan/handle-create-new-scan.test.mts b/src/commands/scan/handle-create-new-scan.test.mts index 1f63639a9..13652875b 100644 --- a/src/commands/scan/handle-create-new-scan.test.mts +++ b/src/commands/scan/handle-create-new-scan.test.mts @@ -334,7 +334,6 @@ describe('handleCreateNewScan excludePaths', () => { version: '1.0', subprojectDir: '.', dependencies: [], - resolvedAs: [], targets: ['/repo/service-a/build/classes'], sources: ['/repo/service-a/src/main/java'], },