Advanced

Architecture

How SCAL-P's components fit together — CLI routing, policy engine, trust scoring, hashing, lockfile management, and reporting.

SCAL-P is not a daemon, not a proxy, not a plugin. It's a CLI tool that wraps your package manager. Every invocation is stateless: load policy, resolve, evaluate, act, log, exit.

Package structure

cmd/scalp/main.go              # entrypoint → cli.Run()
internal/
├── cli/                       # command routing (install, audit, ci, verify, checksum, stage, policy)
├── policy/                    # policy loading, evaluation, enforcement
├── lockfile/                  # .scalp/lockfile.json management + hash verification
├── hash/                      # SHA-512 hashing (directory, single file, raw bytes)
├── trust/                     # trust score engine, cache, npm API client
├── reporter/                  # JSON, Markdown, and SARIF 2.1.0 reports
├── audit/                     # NDJSON audit logger
├── ctxutil/                   # context helpers
├── pkgmanager/                # PackageManager interface + registry
├── npm/                       # npm adapter
├── pnpm/                      # pnpm adapter
├── yarn/                      # yarn (Berry) adapter
├── bun/                       # bun adapter
└── version/                   # build-time version injection

Data flow (guarded install)

passed CLIscalp install --guarded 1. Load policy 2. Resolvenpm install --package-lock-only "3. Parse lockfileadapter.ParseLockfile(ctx)→ PackageNode[ "4. Evaluate policyallowlist / denylist / audit-only→ Violation[ "5. Evaluate trust score4 factors: hash, maturity, downloads, CVEs→ Violation[ 6. Enforceblock → exit 1warn → log + continue 7. Installnpm install / pnpm install / etc. 8. Get dependency treeadapter.GetTree(ctx)→ DependencyTree 9. Hash synchash.Dir() for each package→ .scalp/lockfile.json 10. Audit log→ .scalp/audit.log (NDJSON)

Data flow (stage verify)

CLIscalp stage verify --stage-id <pkg> 1. Load policy 2. Stream tarball from stdinio.TeeReader + sha512 streamingSimultaneously hash + decompress 3. Extract package identityarchive/tarFind package/package.json 4. Verify checksumcompare h.Sum() vs --checksum 5. Verify stage IDcompare extracted name vs --stage-id 6. Denylist checkinternal/policy/Check against deny rules 7. SARIF outputinternal/reporter/ 8. Enforceinternal/policy/

Component diagram

if passed CLIcli.goinstall, audit, ci, verify... pkgmanagerinterface npm / pnpm / yarn / bunadapter pattern policy.json policy.Evaluatetrust.Evaluateallow/deny/transitive4 scoring factors enforcementblock / warn / log hashSHA-512.scalp/lockfile.json audit + reporterNDJSON + JSON + Markdown + SARIF

Package manager adapter

Every package manager implements the same interface:

PackageManager
├── Name() string
├── Resolve(ctx, args...) error
├── ParseLockfile(ctx) ([]PackageNode, error)
├── Install(ctx, args...) error
├── GetTree(ctx) (DependencyTree, error)
└── LocalPath(name) string

Currently implemented: npm, pnpm, yarn (Berry v2+), bun. The registry in internal/pkgmanager/registry.go maps names to constructors.

To add a new package manager: implement the interface, register it in init(), done.

Trust score pipeline

yes no scorer.Evaluate(ctx, policy, nodes, lockfile) Load cache.scalp/cache/trust.json Fetch CVEsnpm audit --json For each PackageNode Hash verified?30 pts / 0 pts Version >= 1.0.0?15 pts / 0 pts Downloads0–20 pts CVEs15 pts / 0 pts Score < min_score? violation Next PackageNode Update cache Save cache.scalp/cache/trust.json

CI command flow

yes no scalp ci 1. Load policy 2. Resolve (lockfile-only) 3. Parse lockfile 4. Evaluate policy + trust 5. Violations? Annotate PRWrite reportOptional SARIF→ exit 1 6. Install--ignore-scripts in fork context 7. Hash sync→ .scalp/lockfile.json 8. Verify against tree(detect tampering) 9. Write report + optional SARIF 10. Exit 0

File layout

.scalp/
├── policy.json             ← your policy (checked in)
├── policy.schema.json      ← JSON Schema (checked in)
├── lockfile.json           ← auto-generated hashes (recommended to commit)
├── ci-report.json          ← last CI run (never commit)
├── cache/
│   └── trust.json          ← download counts, CVEs (TTL 7 days)
└── audit.log               ← append-only event log (never commit)

Design constraints

  • Zero external Go dependencies — stdlib only. No framework, no SDK, no ORM.
  • No daemon — every invocation is stateless. Load, evaluate, act, exit.
  • Offline-first — network is a cache amplifier, not a requirement.
  • Deterministic — same inputs (policy, lockfile, cache) produce the same outputs.

On this page