proof-of-commitment 1.31.1 → 1.33.0

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.
Files changed (2) hide show
  1. package/index.js +120 -9
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * proof-of-commitment CLI v1.31.0
3
+ * proof-of-commitment CLI v1.33.0
4
4
  * Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
5
5
  * Usage: npx proof-of-commitment [packages...] [options]
6
6
  */
@@ -313,6 +313,40 @@ function hasCritical(flags) {
313
313
  return flags && flags.some(f => typeof f === 'string' && f.startsWith('CRITICAL'));
314
314
  }
315
315
 
316
+ /**
317
+ * Footer-calibration helper — diagnoses when the CRITICAL count is so dense
318
+ * that the label has lost actionable meaning, and produces a reframe.
319
+ *
320
+ * Why this exists. Lockfile audits on real Node projects routinely report
321
+ * 40–60% of scanned packages as CRITICAL — the threshold (sole publisher +
322
+ * >10M weekly downloads) is structurally widespread in npm. When the footer
323
+ * says "157 CRITICAL packages found" without context, a new user reads it as
324
+ * either alarmist noise (close the tab) or as an unbounded ask (157 things
325
+ * to investigate — uninstall the tool). Both kill activation.
326
+ *
327
+ * The dogfood signal: 2026-06-16 commit-landing-v2 lockfile, authenticated,
328
+ * 326 scanned, 157 CRITICAL (48%). Reflection: "label loses meaning when
329
+ * half the tree is red." Calibration reframes 157-of-326 as "typical npm
330
+ * baseline; act on the top 3-5 by impact" — preserves the data and gives
331
+ * the user a concrete next step. Threshold 20% chosen because that's the
332
+ * point at which per-package action stops being feasible.
333
+ *
334
+ * Returns null when calibration shouldn't fire (low ratio, no scan total,
335
+ * trivially small scan). Returns a `{ ratio, percent, baseline }` object
336
+ * otherwise — caller decides how to render it.
337
+ */
338
+ function summarizeCriticalConcentration(criticalCount, totalScanned) {
339
+ if (!totalScanned || totalScanned < 10) return null;
340
+ if (!criticalCount || criticalCount <= 0) return null;
341
+ const ratio = criticalCount / totalScanned;
342
+ if (ratio < 0.20) return null;
343
+ return {
344
+ ratio,
345
+ percent: Math.round(ratio * 100),
346
+ baseline: true,
347
+ };
348
+ }
349
+
316
350
  function riskColor(flags, score) {
317
351
  if (hasCritical(flags)) return c.red + c.bold;
318
352
  if (score < 40) return c.yellow + c.bold;
@@ -592,9 +626,22 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
592
626
 
593
627
  const effectiveCritical = totalCritical !== undefined ? totalCritical : criticalInDisplay;
594
628
  if (effectiveCritical > 0) {
595
- const suffix = totalScanned ? ` (in ${totalScanned} packages scanned)` : '';
596
- console.log('\n' + clr(c.red + c.bold, `⚠ ${effectiveCritical} CRITICAL package${effectiveCritical > 1 ? 's' : ''} found${suffix}.`));
597
- console.log(clr(c.dim, ' CRITICAL = sole npm publisher + >10M weekly downloads (publish-access concentration risk)'));
629
+ const conc = summarizeCriticalConcentration(effectiveCritical, totalScanned);
630
+ if (conc) {
631
+ // High-density reframe: at ≥20% CRITICAL the per-package alarm framing
632
+ // loses meaning (the user can't audit a third of their tree). Recast as
633
+ // ecosystem baseline + actionable shortlist. Pinned by
634
+ // test/cli-critical-concentration.test.ts.
635
+ console.log('\n' + clr(c.red + c.bold, `⚠ ${effectiveCritical} CRITICAL packages (${conc.percent}% of ${totalScanned} scanned).`));
636
+ console.log(clr(c.dim, ' CRITICAL = sole npm publisher + >10M weekly downloads.'));
637
+ console.log(clr(c.dim, ' At this density you\'re seeing npm\'s baseline — most popular packages have'));
638
+ console.log(clr(c.dim, ' one publisher. Watch the top 3-5 above (highest impact); pin your lockfile'));
639
+ console.log(clr(c.dim, ' and audit deltas for the rest — concentration you can\'t avoid per-dep.'));
640
+ } else {
641
+ const suffix = totalScanned ? ` (in ${totalScanned} packages scanned)` : '';
642
+ console.log('\n' + clr(c.red + c.bold, `⚠ ${effectiveCritical} CRITICAL package${effectiveCritical > 1 ? 's' : ''} found${suffix}.`));
643
+ console.log(clr(c.dim, ' CRITICAL = sole npm publisher + >10M weekly downloads (publish-access concentration risk)'));
644
+ }
598
645
  if (provenanceCount > 0 && provenanceCount < results.length) {
599
646
  console.log(clr(c.cyan, ` 🔐 ${provenanceCount}/${results.length} use Trusted Publishing (OIDC provenance) — partial mitigation`));
600
647
  }
@@ -609,7 +656,22 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
609
656
  if (process.env.GITHUB_ACTIONS === 'true') {
610
657
  if (effectiveCritical > 0) {
611
658
  const critNames = results.filter(r => hasCritical(r.riskFlags)).slice(0, 5).map(r => r.name).join(', ');
612
- console.error(`::warning title=Commit: ${effectiveCritical} CRITICAL package${effectiveCritical > 1 ? 's' : ''}::Sole npm publisher + >10M downloads/week: ${critNames}. Details: getcommit.dev/audit?packages=${encodeURIComponent(critNames)}&utm_source=cli&utm_medium=ci-annotation`);
659
+ // PR-check annotation is the single user-visible artifact outside the raw
660
+ // CI log. It must point at the conversion destination (/get-started with
661
+ // watchlist auto-seed), not the viewer (/audit). Pre-fix the annotation
662
+ // linked to /audit?packages= — same data the CI viewer already saw
663
+ // scrolled up in the log + an inline signup form one extra scroll away.
664
+ // Post-fix the link routes directly to /get-started?watch=…&eco=… which
665
+ // seeds the user's watchlist from this scan and arrives at an email
666
+ // capture as the primary form. Mirrors the same `?watch=…` URL contract
667
+ // (test/get-started-watch-param.test.ts) used by /audit's bottom CTA
668
+ // and the AUR-1579 / Snyk-comparison / Miasma / IronWorm blog posts.
669
+ // ref=cli-watch (not cli-ci-critical) because the latter is not in the
670
+ // backend's VALID_SOURCES and would get coerced to "web" — destroying
671
+ // attribution. cli-watch is already an accepted source.
672
+ const watchUrl = buildWatchUrl(results, 'cli-watch');
673
+ const ctaUrl = watchUrl || `https://getcommit.dev/audit?packages=${encodeURIComponent(critNames)}&utm_source=cli&utm_medium=ci-annotation`;
674
+ console.error(`::warning title=Commit: ${effectiveCritical} CRITICAL package${effectiveCritical > 1 ? 's' : ''}::Sole npm publisher + >10M downloads/week: ${critNames}. Watch + alert if scores change: ${ctaUrl}`);
613
675
  }
614
676
  if (compromisedCount > 0) {
615
677
  const compNames = results.filter(r => r.compromised).slice(0, 5).map(r => r.name).join(', ');
@@ -650,10 +712,21 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
650
712
  console.log(clr(c.dim, `\n 📊 Monitor ${effectiveCritical === 1 ? 'this package' : 'these packages'}: `) +
651
713
  clr(c.cyan, `poc watch ${results.find(r => hasCritical(r.riskFlags))?.name || results[0]?.name}`));
652
714
  } else if (!process.stdin.isTTY || !process.stdout.isTTY) {
653
- // Non-TTY (CI, piped): show one-step watch command since interactive prompt won't work
715
+ // Non-TTY (CI, piped): show both a clickable signup URL AND the one-step
716
+ // watch command. URL is for CI viewers reading log output in the browser
717
+ // (PR check tabs auto-link URLs); CLI command is for local-pipe / script
718
+ // contexts where copying a URL to a browser is friction. Two paths to
719
+ // the same outcome — see buildWatchUrl docstring for the rationale.
720
+ // Pre-fix this branch only emitted the CLI command, which requires the
721
+ // user to know the package name, run a second `npx`, and remember an
722
+ // email — a 3-decision ask in a CI log line that scrolls past.
654
723
  const watchPkg = results.find(r => hasCritical(r.riskFlags))?.name || results[0]?.name;
724
+ const watchUrl = buildWatchUrl(results, 'cli-watch');
655
725
  console.log(clr(c.dim, `\n 📊 Monitor ${effectiveCritical === 1 ? 'this' : 'these ' + effectiveCritical} CRITICAL ${effectiveCritical === 1 ? 'package' : 'packages'} — get alerted when scores change.`));
656
- console.log(clr(c.dim, ' One step: ') + clr(c.cyan, `poc watch ${watchPkg} --email you@company.com`));
726
+ if (watchUrl) {
727
+ console.log(clr(c.dim, ' Web: ') + clr(c.cyan, watchUrl));
728
+ }
729
+ console.log(clr(c.dim, ' CLI: ') + clr(c.cyan, `poc watch ${watchPkg} --email you@company.com`));
657
730
  console.log(clr(c.dim, ' Free: 3 packages, weekly digest. Developer $15/mo: 15 packages, daily scans.'));
658
731
  }
659
732
  // else: TTY mode — inlineSignup() will prompt interactively after printTable
@@ -664,8 +737,17 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
664
737
  // text only in CI/piped output where interactive prompts can't fire.
665
738
  // ref=audit-baseline distinguishes this funnel from audit-cli-429
666
739
  // (rate-limit rescue) and from the static utm_source=cli help-line.
740
+ // Healthy-results non-TTY (CI, piped, no saved key) CTA. Mirror the
741
+ // CRITICAL branch's dual-path (Web + CLI). ref=audit-baseline keeps this
742
+ // funnel distinguishable from CRITICAL-driven cli-watch in api_keys.source
743
+ // breakdowns — important for measuring whether degradation-alert framing
744
+ // converts at all on clean results, separate from CRITICAL conversions.
745
+ const watchUrl = buildWatchUrl(results, 'audit-baseline');
667
746
  console.log(clr(c.dim, '\n 📊 Get alerted if any package degrades:'));
668
- console.log(clr(c.dim, ' ') + clr(c.cyan, `poc watch ${results[0]?.name || '<package>'} --email you@company.com`) + clr(c.dim, ' (free: 3 packages, weekly digest)'));
747
+ if (watchUrl) {
748
+ console.log(clr(c.dim, ' Web: ') + clr(c.cyan, watchUrl));
749
+ }
750
+ console.log(clr(c.dim, ' CLI: ') + clr(c.cyan, `poc watch ${results[0]?.name || '<package>'} --email you@company.com`) + clr(c.dim, ' (free: 3 packages, weekly digest)'));
669
751
  }
670
752
  console.log();
671
753
  }
@@ -703,6 +785,35 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
703
785
  * Closes the second proposition-gap layer (CLI-side mirror of the
704
786
  * 2026-06-11 audit-page watchlist auto-seed at abe53f1/df8a8be).
705
787
  */
788
+ /**
789
+ * Build a clickable /get-started URL that pre-seeds the watchlist from the
790
+ * scanned packages. Mirrors the `?watch=pkg1,pkg2,pkg3&eco=npm` URL contract
791
+ * pinned by test/get-started-watch-param.test.ts. The free-tier cap (3) is
792
+ * enforced via buildCliWatchSeeds(); /get-started.astro re-validates and the
793
+ * backend re-caps at PACKAGE_LIMITS.free (defense in depth across drift).
794
+ *
795
+ * Why this exists: the proven Snyk-comparison/AUR-1579 blog conversion pattern
796
+ * (mid-essay dual CTA + bottom dual CTA + `?watch=…` pre-seed) was sitting
797
+ * one repo over from the CLI for weeks. The CLI emits the highest-intent
798
+ * touchpoint after npm install — a CRITICAL finding in a real user's tree —
799
+ * but the only conversion paths it offered non-TTY users were a multi-token
800
+ * `poc watch X --email Y` CLI command (high friction) and a `getcommit.dev/
801
+ * audit?packages=…` link that drops users on the viewer page (not a converter).
802
+ * This helper produces the clickable conversion URL with packages pre-seeded.
803
+ *
804
+ * Returns null when no valid seeds exist (e.g. empty results, or all names
805
+ * fail the npm-name regex via buildCliWatchSeeds). Callers degrade gracefully
806
+ * to text-only CTAs without a URL.
807
+ */
808
+ function buildWatchUrl(results, ref) {
809
+ const seeds = buildCliWatchSeeds(results);
810
+ if (seeds.length === 0) return null;
811
+ // All seeds from one scan share the same ecosystem; pick from the first.
812
+ const eco = seeds[0].ecosystem || 'npm';
813
+ const names = seeds.map(s => s.name).join(',');
814
+ return `https://getcommit.dev/get-started?watch=${encodeURIComponent(names)}&eco=${eco}&ref=${ref}&utm_source=cli`;
815
+ }
816
+
706
817
  function buildCliWatchSeeds(results) {
707
818
  if (!Array.isArray(results) || results.length === 0) return [];
708
819
  const VALID_ECOS = new Set(['npm', 'pypi', 'cargo', 'golang']);
@@ -878,7 +989,7 @@ async function inlineSignup(results, opts = {}) {
878
989
 
879
990
  function printHelp() {
880
991
  console.log(`
881
- ${clr(c.bold, 'proof-of-commitment')} v1.31.0 — supply chain risk scorer
992
+ ${clr(c.bold, 'proof-of-commitment')} v1.33.0 — supply chain risk scorer
882
993
 
883
994
  ${clr(c.bold, 'Usage:')}
884
995
  npx proof-of-commitment Auto-detect manifest in current dir
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proof-of-commitment",
3
- "version": "1.31.1",
3
+ "version": "1.33.0",
4
4
  "mcpName": "io.github.piiiico/proof-of-commitment",
5
5
  "description": "Supply chain security risk scorer for npm, PyPI, Cargo, and Go packages — behavioral signals that can't be faked",
6
6
  "type": "module",