border-customs 0.3.2 → 0.4.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.
@@ -28,6 +28,7 @@ import { EngineRunError, type EngineOptions } from "../engines/support.ts";
28
28
  import { LIFECYCLE_SCRIPT_KEYS, parseNpmManifest, unexpectedEntries } from "./manifestDiff.ts";
29
29
  import { residueFindings, scanNpmLifecycleHook, sweepResidueNpmArtifact } from "./residue.ts";
30
30
  import { RESIDUE_T1_RULE } from "../rules/residueMatchers.ts";
31
+ import { npmPackCoherenceFindings } from "../rules/releaseCoherence.ts";
31
32
  import {
32
33
  NPM_LIFECYCLE_RULE,
33
34
  NPM_PUBLINT_RULE,
@@ -197,6 +198,19 @@ export async function runNpmArtifactStage(o: NpmStageOptions): Promise<NpmStageR
197
198
  // not the working tree — npm may rewrite package.json inside the tarball.
198
199
  const packedManifest = parseNpmManifest(readFileSync(join(extractDir, packedManifestRel), "utf8"));
199
200
 
201
+ // W4.1 release coherence: packed package.json ↔ a packed package-lock.json (root +
202
+ // packages[""]). ABSENT-SOURCE BOUNDARY (load-bearing): locks are usually NOT in the
203
+ // tarball (files whitelist) — an unpacked lock is not drift and never a finding;
204
+ // false-positive gates just train users to pass --force. See rules/releaseCoherence.ts.
205
+ const lockRel = `${root}/package-lock.json`;
206
+ findings.push(...npmPackCoherenceFindings({
207
+ packedVersion: packedManifest.version,
208
+ lockText: relPaths.includes(lockRel) ? readFileSync(join(extractDir, lockRel), "utf8") : null,
209
+ lockRelPath: lockRel,
210
+ identity,
211
+ ...(o.sanitizer !== undefined ? { sanitizer: o.sanitizer } : {}),
212
+ }));
213
+
200
214
  // RESIDUE-CONTRACT §1.6/§3 (R2): a hook is downgraded to MEDIUM ONLY when the
201
215
  // classifier matches closed signature #1 in full; every other outcome — unknown
202
216
  // grammar, unreadable target, out-of-signature literal — keeps the 0.2.0
@@ -40,6 +40,7 @@ import { basename, dirname, join, relative, resolve } from "node:path";
40
40
  import { extractArchive, removeSandbox } from "./extract.ts";
41
41
  import { residueFindings } from "./residue.ts";
42
42
  import { residuePypiHits } from "./residuePy.ts";
43
+ import { pypiArtifactCoherenceFindings } from "../rules/releaseCoherence.ts";
43
44
  import type { Finding } from "../findings.ts";
44
45
  import { globToRegExp } from "../rules/artifactMatchers.ts";
45
46
  import { scanTree } from "../engines/gitleaks.ts";
@@ -316,6 +317,25 @@ export async function scanPyPiArtifacts(o: PypiInput): Promise<PypiScanResult> {
316
317
  identity: basename(artifact.path),
317
318
  ...(o.sanitizer !== undefined ? { sanitizer: o.sanitizer } : {}),
318
319
  })) absorb(f, f.path ?? "");
320
+ // W4.1 release coherence: every version source inside THIS artifact — wheel:
321
+ // filename ↔ dist-info dir ↔ METADATA Version ↔ __version__ literals (the aihr
322
+ // replay is caught from the wheel bytes alone); sdist: pyproject/setup.* ↔
323
+ // PKG-INFO ↔ __version__. Regex-text probes only, nothing executed; a source
324
+ // absent from the tree is NOT drift (releaseCoherence.ts header boundary).
325
+ for (const f of pypiArtifactCoherenceFindings({
326
+ kind: artifact.kind,
327
+ archiveName: basename(artifact.path),
328
+ files: rest,
329
+ read: (rel: string) => {
330
+ try {
331
+ return readFileSync(join(scanRoot, rel), "utf8");
332
+ } catch {
333
+ return null;
334
+ }
335
+ },
336
+ identity: basename(artifact.path),
337
+ ...(o.sanitizer !== undefined ? { sanitizer: o.sanitizer } : {}),
338
+ })) absorb(f, f.path ?? "");
319
339
  if (artifact.kind === "sdist") for (const f of manifestFindings(o, rest, target)) absorb(f, f.path ?? "");
320
340
  } finally {
321
341
  removeSandbox(sandbox);
@@ -32,6 +32,7 @@ import { BORDER_STATE_DIR, ensureStateDir } from "../check/lock.ts";
32
32
  import { extractArchive, removeSandbox } from "./extract.ts";
33
33
  import { type ResidueHit, residueFindings } from "./residue.ts";
34
34
  import { residueGemHits } from "./residueGem.ts";
35
+ import { gemCoherenceFindings } from "../rules/releaseCoherence.ts";
35
36
  import { scanTree } from "../engines/gitleaks.ts";
36
37
  import { scanPaths, type SecretlintMode } from "../engines/secretlint.ts";
37
38
  import { EngineMissingError, EngineRunError, type EngineOptions } from "../engines/support.ts";
@@ -265,6 +266,17 @@ export async function runRubygemsArtifactStage(o: GemStageOptions): Promise<GemS
265
266
  })) {
266
267
  findings.push(f);
267
268
  }
269
+ // W4.1 release coherence: metadata.gz is the shipped bytes' own Gem::Specification —
270
+ // reconcile it against the built filename. NOT vacuous: gemspec.ts's GEMSPEC_VERSION
271
+ // regex takes the FIRST literal s.version= while Ruby assignment order makes the LAST
272
+ // win, so a twice-assigned literal drifts border's filename from the real spec.
273
+ findings.push(...gemCoherenceFindings({
274
+ specName: identity.name,
275
+ specVersion: identity.version,
276
+ metadataText: metadata,
277
+ identity: gemName,
278
+ ...(o.sanitizer !== undefined ? { sanitizer: o.sanitizer } : {}),
279
+ }));
268
280
  }
269
281
  } finally {
270
282
  removeSandbox(extractDir);