@clossys/launcher 0.3.1 → 0.4.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.
- package/README.md +1324 -60
- package/contracts/conversation-contract.md +2 -2
- package/contracts/product-ci-workflow.yml +74 -0
- package/contracts/repository-inventory.json +53 -0
- package/dist/admission-fixture.d.ts +168 -0
- package/dist/admission-fixture.d.ts.map +1 -0
- package/dist/admission-fixture.js +453 -0
- package/dist/admission-fixture.js.map +1 -0
- package/dist/admission.d.ts +124 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/admission.js +799 -0
- package/dist/admission.js.map +1 -0
- package/dist/agents-guide.d.ts +9 -0
- package/dist/agents-guide.d.ts.map +1 -0
- package/dist/agents-guide.js +26 -0
- package/dist/agents-guide.js.map +1 -0
- package/dist/apply-command-options.check.d.ts +12 -0
- package/dist/apply-command-options.check.d.ts.map +1 -0
- package/dist/apply-command-options.check.js +20 -0
- package/dist/apply-command-options.check.js.map +1 -0
- package/dist/apply-plan-cli.d.ts +39 -1
- package/dist/apply-plan-cli.d.ts.map +1 -1
- package/dist/apply-plan-cli.js +432 -15
- package/dist/apply-plan-cli.js.map +1 -1
- package/dist/apply-plan.d.ts +46 -59
- package/dist/apply-plan.d.ts.map +1 -1
- package/dist/apply-plan.js +112 -97
- package/dist/apply-plan.js.map +1 -1
- package/dist/apply-step-fixture.d.ts +87 -0
- package/dist/apply-step-fixture.d.ts.map +1 -0
- package/dist/apply-step-fixture.js +199 -0
- package/dist/apply-step-fixture.js.map +1 -0
- package/dist/apply-store.d.ts +93 -0
- package/dist/apply-store.d.ts.map +1 -0
- package/dist/apply-store.js +625 -0
- package/dist/apply-store.js.map +1 -0
- package/dist/approval-sheet.d.ts +21 -0
- package/dist/approval-sheet.d.ts.map +1 -0
- package/dist/approval-sheet.js +157 -0
- package/dist/approval-sheet.js.map +1 -0
- package/dist/body-command.d.ts +42 -0
- package/dist/body-command.d.ts.map +1 -0
- package/dist/body-command.js +143 -0
- package/dist/body-command.js.map +1 -0
- package/dist/change-set-contract.d.ts +381 -0
- package/dist/change-set-contract.d.ts.map +1 -0
- package/dist/change-set-contract.js +738 -0
- package/dist/change-set-contract.js.map +1 -0
- package/dist/change-set-digest.d.ts +28 -0
- package/dist/change-set-digest.d.ts.map +1 -0
- package/dist/change-set-digest.js +65 -0
- package/dist/change-set-digest.js.map +1 -0
- package/dist/check-cli.d.ts.map +1 -1
- package/dist/check-cli.js +14 -3
- package/dist/check-cli.js.map +1 -1
- package/dist/cli.d.ts +17 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +84 -23
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +79 -22
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +843 -268
- package/dist/core.js.map +1 -1
- package/dist/dry-materialize.d.ts +63 -0
- package/dist/dry-materialize.d.ts.map +1 -0
- package/dist/dry-materialize.js +330 -0
- package/dist/dry-materialize.js.map +1 -0
- package/dist/generated/contract-schema.generated.d.ts +97 -0
- package/dist/generated/contract-schema.generated.d.ts.map +1 -0
- package/dist/generated/contract-schema.generated.js +496 -0
- package/dist/generated/contract-schema.generated.js.map +1 -0
- package/dist/generated/package-scope.generated.d.ts +6 -0
- package/dist/generated/package-scope.generated.d.ts.map +1 -0
- package/dist/generated/package-scope.generated.js +10 -0
- package/dist/generated/package-scope.generated.js.map +1 -0
- package/dist/generated/plan-contracts.generated.d.ts +3 -0
- package/dist/generated/plan-contracts.generated.d.ts.map +1 -0
- package/dist/generated/plan-contracts.generated.js +2840 -0
- package/dist/generated/plan-contracts.generated.js.map +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +11 -0
- package/dist/host.js.map +1 -1
- package/dist/identity.d.ts +15 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +48 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/inventory-adoption.d.ts +24 -5
- package/dist/inventory-adoption.d.ts.map +1 -1
- package/dist/inventory-adoption.js +70 -25
- package/dist/inventory-adoption.js.map +1 -1
- package/dist/inventory-choice.d.ts +40 -0
- package/dist/inventory-choice.d.ts.map +1 -0
- package/dist/inventory-choice.js +156 -0
- package/dist/inventory-choice.js.map +1 -0
- package/dist/inventory-contract.d.ts +89 -0
- package/dist/inventory-contract.d.ts.map +1 -0
- package/dist/inventory-contract.js +121 -0
- package/dist/inventory-contract.js.map +1 -0
- package/dist/key-editor.d.ts +30 -0
- package/dist/key-editor.d.ts.map +1 -0
- package/dist/key-editor.js +445 -0
- package/dist/key-editor.js.map +1 -0
- package/dist/ledger-contract.d.ts +187 -0
- package/dist/ledger-contract.d.ts.map +1 -0
- package/dist/ledger-contract.js +532 -0
- package/dist/ledger-contract.js.map +1 -0
- package/dist/ledger-trust.d.ts +90 -0
- package/dist/ledger-trust.d.ts.map +1 -0
- package/dist/ledger-trust.js +198 -0
- package/dist/ledger-trust.js.map +1 -0
- package/dist/lockfile-invariants.d.ts +48 -0
- package/dist/lockfile-invariants.d.ts.map +1 -0
- package/dist/lockfile-invariants.js +375 -0
- package/dist/lockfile-invariants.js.map +1 -0
- package/dist/lockfile-readers.d.ts +72 -0
- package/dist/lockfile-readers.d.ts.map +1 -0
- package/dist/lockfile-readers.js +713 -0
- package/dist/lockfile-readers.js.map +1 -0
- package/dist/lockfile-regen.d.ts +106 -0
- package/dist/lockfile-regen.d.ts.map +1 -0
- package/dist/lockfile-regen.js +760 -0
- package/dist/lockfile-regen.js.map +1 -0
- package/dist/lockfile-tool-env.d.ts +29 -0
- package/dist/lockfile-tool-env.d.ts.map +1 -0
- package/dist/lockfile-tool-env.js +111 -0
- package/dist/lockfile-tool-env.js.map +1 -0
- package/dist/materialize.d.ts +113 -0
- package/dist/materialize.d.ts.map +1 -0
- package/dist/materialize.js +840 -0
- package/dist/materialize.js.map +1 -0
- package/dist/observe-repository.d.ts +90 -0
- package/dist/observe-repository.d.ts.map +1 -0
- package/dist/observe-repository.js +1367 -0
- package/dist/observe-repository.js.map +1 -0
- package/dist/plan-bundle-setup-fixture.d.ts +68 -0
- package/dist/plan-bundle-setup-fixture.d.ts.map +1 -0
- package/dist/plan-bundle-setup-fixture.js +167 -0
- package/dist/plan-bundle-setup-fixture.js.map +1 -0
- package/dist/plan-bundle.d.ts +250 -0
- package/dist/plan-bundle.d.ts.map +1 -0
- package/dist/plan-bundle.js +827 -0
- package/dist/plan-bundle.js.map +1 -0
- package/dist/plan-command.d.ts +29 -0
- package/dist/plan-command.d.ts.map +1 -0
- package/dist/plan-command.js +493 -0
- package/dist/plan-command.js.map +1 -0
- package/dist/plan-contract.d.ts +153 -0
- package/dist/plan-contract.d.ts.map +1 -0
- package/dist/plan-contract.js +61 -0
- package/dist/plan-contract.js.map +1 -0
- package/dist/plan-digest.d.ts +25 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plan-digest.js +106 -0
- package/dist/plan-digest.js.map +1 -0
- package/dist/plan-rules.d.ts +23 -0
- package/dist/plan-rules.d.ts.map +1 -0
- package/dist/plan-rules.js +177 -0
- package/dist/plan-rules.js.map +1 -0
- package/dist/planned-bundle.d.ts +20 -0
- package/dist/planned-bundle.d.ts.map +1 -0
- package/dist/planned-bundle.js +191 -0
- package/dist/planned-bundle.js.map +1 -0
- package/dist/product-repository.d.ts +4 -0
- package/dist/product-repository.d.ts.map +1 -1
- package/dist/product-repository.js +9 -1
- package/dist/product-repository.js.map +1 -1
- package/dist/provenance-gate.d.ts +48 -0
- package/dist/provenance-gate.d.ts.map +1 -0
- package/dist/provenance-gate.js +324 -0
- package/dist/provenance-gate.js.map +1 -0
- package/dist/pull-request-body.d.ts +45 -0
- package/dist/pull-request-body.d.ts.map +1 -0
- package/dist/pull-request-body.js +232 -0
- package/dist/pull-request-body.js.map +1 -0
- package/dist/registry-snapshot.d.ts +141 -0
- package/dist/registry-snapshot.d.ts.map +1 -0
- package/dist/registry-snapshot.js +483 -0
- package/dist/registry-snapshot.js.map +1 -0
- package/dist/release-age-edit.d.ts +52 -0
- package/dist/release-age-edit.d.ts.map +1 -0
- package/dist/release-age-edit.js +413 -0
- package/dist/release-age-edit.js.map +1 -0
- package/dist/root-entries.d.ts +36 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +80 -0
- package/dist/root-entries.js.map +1 -0
- package/dist/setup-template-scripts.d.ts +34 -0
- package/dist/setup-template-scripts.d.ts.map +1 -0
- package/dist/setup-template-scripts.js +557 -0
- package/dist/setup-template-scripts.js.map +1 -0
- package/dist/setup-templates.d.ts +54 -0
- package/dist/setup-templates.d.ts.map +1 -0
- package/dist/setup-templates.js +427 -0
- package/dist/setup-templates.js.map +1 -0
- package/dist/skills.d.ts +34 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +129 -17
- package/dist/skills.js.map +1 -1
- package/dist/status.d.ts +63 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +539 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +151 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/skeleton/README.md +14 -9
- package/skeleton/package.json +2 -1
- package/skill/SKILL.md +23 -7
- package/skill-catalogue/advisor/SKILL.md +59 -6
- package/skill-catalogue/architect/SKILL.md +2 -2
- package/skill-catalogue/bouncer/SKILL.md +2 -2
- package/skill-catalogue/builder/SKILL.md +2 -2
- package/skill-catalogue/butler/SKILL.md +2 -2
- package/skill-catalogue/controller/SKILL.md +2 -2
- package/skill-catalogue/customer/SKILL.md +2 -2
- package/skill-catalogue/designer/SKILL.md +4 -2
- package/skill-catalogue/giver/SKILL.md +2 -2
- package/skill-catalogue/influencer/SKILL.md +2 -2
- package/skill-catalogue/inspector/SKILL.md +2 -2
- package/skill-catalogue/integrator/SKILL.md +2 -2
- package/skill-catalogue/keeper/SKILL.md +2 -2
- package/skill-catalogue/launcher/SKILL.md +23 -7
- package/skill-catalogue/locksmith/SKILL.md +2 -2
- package/skill-catalogue/messenger/SKILL.md +2 -2
- package/skill-catalogue/observer/SKILL.md +2 -2
- package/skill-catalogue/publisher/SKILL.md +2 -2
- package/skill-catalogue/starter/SKILL.md +3 -2
- package/skill-catalogue/strategist/SKILL.md +12 -4
- package/skill-catalogue/writer/SKILL.md +2 -2
- package/src/admission-fixture.ts +572 -0
- package/src/admission.ts +816 -0
- package/src/agents-guide.ts +29 -0
- package/src/apply-command-options.check.ts +27 -0
- package/src/apply-plan-cli.ts +454 -14
- package/src/apply-plan.ts +112 -124
- package/src/apply-step-fixture.ts +236 -0
- package/src/apply-store.ts +584 -0
- package/src/approval-sheet.ts +164 -0
- package/src/body-command.ts +162 -0
- package/src/change-set-contract.ts +937 -0
- package/src/change-set-digest.ts +70 -0
- package/src/check-cli.ts +14 -3
- package/src/cli.ts +90 -22
- package/src/core.ts +973 -275
- package/src/dry-materialize.ts +353 -0
- package/src/generated/contract-schema.generated.ts +520 -0
- package/src/generated/package-scope.generated.ts +10 -0
- package/src/generated/plan-contracts.generated.ts +2840 -0
- package/src/host.ts +10 -0
- package/src/identity.ts +51 -0
- package/src/index.ts +72 -3
- package/src/inventory-adoption.ts +107 -29
- package/src/inventory-choice.ts +172 -0
- package/src/inventory-contract.ts +166 -0
- package/src/key-editor.ts +446 -0
- package/src/ledger-contract.ts +637 -0
- package/src/ledger-trust.ts +267 -0
- package/src/lockfile-invariants.ts +421 -0
- package/src/lockfile-readers.ts +749 -0
- package/src/lockfile-regen.ts +851 -0
- package/src/lockfile-tool-env.ts +131 -0
- package/src/materialize.ts +886 -0
- package/src/observe-repository.ts +1365 -0
- package/src/plan-bundle-setup-fixture.ts +200 -0
- package/src/plan-bundle.ts +964 -0
- package/src/plan-command.ts +509 -0
- package/src/plan-contract.ts +179 -0
- package/src/plan-digest.ts +102 -0
- package/src/plan-rules.ts +188 -0
- package/src/planned-bundle.ts +211 -0
- package/src/product-repository.ts +10 -1
- package/src/provenance-gate.ts +352 -0
- package/src/pull-request-body.ts +261 -0
- package/src/registry-snapshot.ts +534 -0
- package/src/release-age-edit.ts +430 -0
- package/src/root-entries.ts +81 -0
- package/src/setup-template-scripts.ts +571 -0
- package/src/setup-templates.ts +471 -0
- package/src/skills.ts +161 -18
- package/src/status.ts +557 -0
- package/src/types.ts +148 -13
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import type { DependencyPlacement } from "./change-set-contract.js";
|
|
2
|
+
import { compareCodeUnits } from "./change-set-contract.js";
|
|
3
|
+
import { PACKAGE_SCOPE } from "./generated/package-scope.generated.js";
|
|
4
|
+
import type { LockfileEntry, LockfileFormat, LockfileUnreadableReason, LockfileView, RootDependency } from "./lockfile-readers.js";
|
|
5
|
+
import { isUnreadable, readLockfile } from "./lockfile-readers.js";
|
|
6
|
+
|
|
7
|
+
export type LockfileInvariantId = "I1" | "I2" | "I3" | "I4" | "I5";
|
|
8
|
+
|
|
9
|
+
/** One approved package the regenerated lockfile must resolve exactly. */
|
|
10
|
+
export interface LockfileInvariantPackage {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly version: string;
|
|
13
|
+
readonly integrity: string;
|
|
14
|
+
readonly placement: DependencyPlacement;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LockfileInvariantInput {
|
|
18
|
+
readonly format: LockfileFormat;
|
|
19
|
+
readonly base: string;
|
|
20
|
+
readonly regenerated: string;
|
|
21
|
+
readonly packages: readonly LockfileInvariantPackage[];
|
|
22
|
+
/** Defaults to PACKAGE_SCOPE. */
|
|
23
|
+
readonly publishing?: { readonly scope: string; readonly registry: string };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LockfileInvariantViolation {
|
|
27
|
+
readonly invariant: LockfileInvariantId;
|
|
28
|
+
/** The package name this violation is about; "" for I4, whose violation is about the lockfile format as a whole, not one package. */
|
|
29
|
+
readonly name: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TransitiveCounts {
|
|
33
|
+
/** `name@version` present after and not before. */
|
|
34
|
+
readonly added: number;
|
|
35
|
+
/** `name@version` present before and not after. */
|
|
36
|
+
readonly removed: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type LockfileInvariantResult =
|
|
40
|
+
| { readonly verdict: "satisfied"; readonly violations: readonly []; readonly transitive: TransitiveCounts }
|
|
41
|
+
| { readonly verdict: "violated"; readonly violations: readonly LockfileInvariantViolation[]; readonly transitive: TransitiveCounts }
|
|
42
|
+
| { readonly verdict: "indeterminate"; readonly reason: LockfileUnreadableReason; readonly side: "base" | "regenerated" };
|
|
43
|
+
|
|
44
|
+
const LAST_RESORT: LockfileInvariantResult = { verdict: "indeterminate", reason: "lockfile-unreadable", side: "regenerated" };
|
|
45
|
+
|
|
46
|
+
function entryKey(name: string, version: string): string {
|
|
47
|
+
return `${name}\u0000${version}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Every distinct `name@version` in `entries`, as `entryKey` strings. A versionless npm link entry (fix 7)
|
|
52
|
+
* is never a `name@version` and is excluded here; I5 inspects link entries separately, by key.
|
|
53
|
+
*/
|
|
54
|
+
function entryKeySet(entries: readonly LockfileEntry[]): Set<string> {
|
|
55
|
+
const keys = new Set<string>();
|
|
56
|
+
for (const entry of entries) if (entry.version !== null) keys.add(entryKey(entry.name, entry.version));
|
|
57
|
+
return keys;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setsEqual<T>(left: ReadonlySet<T>, right: ReadonlySet<T>): boolean {
|
|
61
|
+
if (left.size !== right.size) return false;
|
|
62
|
+
for (const value of left) if (!right.has(value)) return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `added`/`removed` distinct `name@version` between the two entry lists. Counted, never refused. */
|
|
67
|
+
function computeTransitiveCounts(base: LockfileView, regenerated: LockfileView): TransitiveCounts {
|
|
68
|
+
const baseKeys = entryKeySet(base.entries);
|
|
69
|
+
const regeneratedKeys = entryKeySet(regenerated.entries);
|
|
70
|
+
let added = 0;
|
|
71
|
+
for (const key of regeneratedKeys) if (!baseKeys.has(key)) added += 1;
|
|
72
|
+
let removed = 0;
|
|
73
|
+
for (const key of baseKeys) if (!regeneratedKeys.has(key)) removed += 1;
|
|
74
|
+
return { added, removed };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* I1: every input package resolves in the regenerated root exactly, under its own placement and no other.
|
|
79
|
+
* A pnpm `(patch_hash=...)` group on the raw version is also a mismatch (fix 8): stripping it (as `version`
|
|
80
|
+
* already does, same as any peer-group suffix) would otherwise let a patched install through even though a
|
|
81
|
+
* patch changes the installed content.
|
|
82
|
+
*/
|
|
83
|
+
function checkI1(input: LockfileInvariantInput, regenerated: LockfileView): LockfileInvariantViolation[] {
|
|
84
|
+
const violations: LockfileInvariantViolation[] = [];
|
|
85
|
+
for (const pkg of input.packages) {
|
|
86
|
+
const sameName = regenerated.root.filter((dep) => dep.name === pkg.name);
|
|
87
|
+
const exact = sameName.find((dep) => dep.placement === pkg.placement);
|
|
88
|
+
const patched = exact?.rawVersion?.includes("patch_hash=") ?? false;
|
|
89
|
+
const exactMismatch =
|
|
90
|
+
!exact || exact.specifier !== pkg.version || exact.version !== pkg.version || exact.integrity !== pkg.integrity || exact.link || patched;
|
|
91
|
+
const otherPlacement = sameName.some((dep) => dep.placement !== pkg.placement);
|
|
92
|
+
if (exactMismatch || otherPlacement) violations.push({ invariant: "I1", name: pkg.name });
|
|
93
|
+
}
|
|
94
|
+
return violations;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Lockfile entry key for one root dependency's resolved package row. npm: always `node_modules/<name>` (versionless link entries included). pnpm: `name@version`, or null when version is absent. */
|
|
98
|
+
function rootPackageEntryKey(format: LockfileFormat, dep: RootDependency): string | null {
|
|
99
|
+
if (format === "npm") return `node_modules/${dep.name}`;
|
|
100
|
+
if (dep.version === null) return null;
|
|
101
|
+
return `${dep.name}@${dep.version}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** I2: every root dep not among the input package names is unchanged, keyed by (placement, name). */
|
|
105
|
+
function checkI2(input: LockfileInvariantInput, base: LockfileView, regenerated: LockfileView): LockfileInvariantViolation[] {
|
|
106
|
+
const inputNames = new Set(input.packages.map((pkg) => pkg.name));
|
|
107
|
+
const rootKey = (dep: RootDependency): string => `${dep.placement}\u0000${dep.name}`;
|
|
108
|
+
const baseRoot = new Map(base.root.filter((dep) => !inputNames.has(dep.name)).map((dep) => [rootKey(dep), dep] as const));
|
|
109
|
+
const regeneratedRoot = new Map(
|
|
110
|
+
regenerated.root.filter((dep) => !inputNames.has(dep.name)).map((dep) => [rootKey(dep), dep] as const),
|
|
111
|
+
);
|
|
112
|
+
const baseEntriesByKey = new Map(base.entries.map((entry) => [entry.key, entry] as const));
|
|
113
|
+
const regeneratedEntriesByKey = new Map(regenerated.entries.map((entry) => [entry.key, entry] as const));
|
|
114
|
+
const violations: LockfileInvariantViolation[] = [];
|
|
115
|
+
const allKeys = new Set([...baseRoot.keys(), ...regeneratedRoot.keys()]);
|
|
116
|
+
for (const key of allKeys) {
|
|
117
|
+
const beforeDep = baseRoot.get(key);
|
|
118
|
+
const afterDep = regeneratedRoot.get(key);
|
|
119
|
+
const name = (beforeDep ?? afterDep)!.name;
|
|
120
|
+
if (!beforeDep || !afterDep) {
|
|
121
|
+
violations.push({ invariant: "I2", name });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
beforeDep.specifier !== afterDep.specifier ||
|
|
126
|
+
beforeDep.version !== afterDep.version ||
|
|
127
|
+
beforeDep.integrity !== afterDep.integrity ||
|
|
128
|
+
beforeDep.link !== afterDep.link
|
|
129
|
+
) {
|
|
130
|
+
violations.push({ invariant: "I2", name });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const beforeEntryKey = rootPackageEntryKey(input.format, beforeDep);
|
|
134
|
+
const afterEntryKey = rootPackageEntryKey(input.format, afterDep);
|
|
135
|
+
if (beforeEntryKey === null || afterEntryKey === null || beforeEntryKey !== afterEntryKey) {
|
|
136
|
+
violations.push({ invariant: "I2", name });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const beforeEntry = baseEntriesByKey.get(beforeEntryKey);
|
|
140
|
+
const afterEntry = regeneratedEntriesByKey.get(afterEntryKey);
|
|
141
|
+
if (beforeEntry === undefined || afterEntry === undefined || !entryMatchesAtKey(beforeEntry, afterEntry)) {
|
|
142
|
+
violations.push({ invariant: "I2", name });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return violations;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* I3: no `name@version` present in both entry lists changes its set of integrity strings. A versionless
|
|
150
|
+
* npm link entry (fix 7) never participates: it is not a `name@version`.
|
|
151
|
+
*/
|
|
152
|
+
function checkI3(base: LockfileView, regenerated: LockfileView): LockfileInvariantViolation[] {
|
|
153
|
+
const violations: LockfileInvariantViolation[] = [];
|
|
154
|
+
const baseByName = new Map<string, Map<string, Set<string | null>>>();
|
|
155
|
+
for (const entry of base.entries) {
|
|
156
|
+
if (entry.version === null) continue;
|
|
157
|
+
let versions = baseByName.get(entry.name);
|
|
158
|
+
if (!versions) {
|
|
159
|
+
versions = new Map();
|
|
160
|
+
baseByName.set(entry.name, versions);
|
|
161
|
+
}
|
|
162
|
+
let integrities = versions.get(entry.version);
|
|
163
|
+
if (!integrities) {
|
|
164
|
+
integrities = new Set();
|
|
165
|
+
versions.set(entry.version, integrities);
|
|
166
|
+
}
|
|
167
|
+
integrities.add(entry.integrity);
|
|
168
|
+
}
|
|
169
|
+
const regeneratedByName = new Map<string, Map<string, Set<string | null>>>();
|
|
170
|
+
for (const entry of regenerated.entries) {
|
|
171
|
+
if (entry.version === null) continue;
|
|
172
|
+
let versions = regeneratedByName.get(entry.name);
|
|
173
|
+
if (!versions) {
|
|
174
|
+
versions = new Map();
|
|
175
|
+
regeneratedByName.set(entry.name, versions);
|
|
176
|
+
}
|
|
177
|
+
let integrities = versions.get(entry.version);
|
|
178
|
+
if (!integrities) {
|
|
179
|
+
integrities = new Set();
|
|
180
|
+
versions.set(entry.version, integrities);
|
|
181
|
+
}
|
|
182
|
+
integrities.add(entry.integrity);
|
|
183
|
+
}
|
|
184
|
+
for (const [name, regeneratedVersions] of regeneratedByName) {
|
|
185
|
+
const baseVersions = baseByName.get(name);
|
|
186
|
+
if (!baseVersions) continue;
|
|
187
|
+
for (const [version, regeneratedIntegrities] of regeneratedVersions) {
|
|
188
|
+
const baseIntegrities = baseVersions.get(version);
|
|
189
|
+
if (!baseIntegrities) continue;
|
|
190
|
+
if (!setsEqual(baseIntegrities, regeneratedIntegrities)) violations.push({ invariant: "I3", name });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return violations;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Whether one newly-appearing scoped entry resolves to the publishing registry, per format. */
|
|
197
|
+
function resolvesToRegistry(entry: LockfileEntry, format: LockfileFormat, registryOrigin: string): boolean {
|
|
198
|
+
if (entry.integrity === null || !entry.integrity.startsWith("sha512-")) return false;
|
|
199
|
+
if (format === "npm") {
|
|
200
|
+
if (entry.link || entry.tarball === null) return false;
|
|
201
|
+
try {
|
|
202
|
+
const url = new URL(entry.tarball);
|
|
203
|
+
return url.origin === registryOrigin && url.pathname.startsWith(`/${entry.name}/-/`);
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (entry.otherResolutionKeys.length > 0) return false;
|
|
209
|
+
if (entry.tarball === null) return true;
|
|
210
|
+
try {
|
|
211
|
+
return new URL(entry.tarball).origin === registryOrigin;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Strips a trailing `(...)` peer-group (or patch-hash) suffix from a pnpm dependency-map value, the same way the reader strips a root dependency's `version`. */
|
|
218
|
+
function stripParenSuffix(value: string): string {
|
|
219
|
+
const parenIndex = value.indexOf("(");
|
|
220
|
+
return parenIndex === -1 ? value : value.slice(0, parenIndex);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Whether a pnpm dependency-map value (after stripping any peer-group/patch-hash suffix) is an alias, a
|
|
225
|
+
* link, or a file reference (fix 6) rather than a plain resolved version. A plain version never contains
|
|
226
|
+
* `@`; an alias is exactly `<name>@<version>`, and even a scoped alias name's own leading `@` still leaves
|
|
227
|
+
* a second `@` as the name/version separator, so a bare `includes("@")` check is enough.
|
|
228
|
+
*/
|
|
229
|
+
function isSuspiciousPnpmDependencyValue(core: string): boolean {
|
|
230
|
+
return core.startsWith("link:") || core.startsWith("file:") || core.includes("@");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resolutionKeysEqual(left: readonly string[], right: readonly string[]): boolean {
|
|
234
|
+
if (left.length !== right.length) return false;
|
|
235
|
+
for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false;
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Whether one base entry at the same key matches every resolution field on `entry`. */
|
|
240
|
+
function entryMatchesAtKey(baseEntry: LockfileEntry, entry: LockfileEntry): boolean {
|
|
241
|
+
return (
|
|
242
|
+
baseEntry.name === entry.name &&
|
|
243
|
+
baseEntry.installedName === entry.installedName &&
|
|
244
|
+
baseEntry.version === entry.version &&
|
|
245
|
+
baseEntry.integrity === entry.integrity &&
|
|
246
|
+
baseEntry.tarball === entry.tarball &&
|
|
247
|
+
baseEntry.link === entry.link &&
|
|
248
|
+
resolutionKeysEqual(baseEntry.otherResolutionKeys, entry.otherResolutionKeys)
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Tarball from a base entry with this declared `name@version` that has no link and no extra pnpm resolution
|
|
254
|
+
* keys (fix round 4). Undefined when the base has no such entry.
|
|
255
|
+
*/
|
|
256
|
+
function canonicalBaseTarballForNameVersion(
|
|
257
|
+
name: string,
|
|
258
|
+
version: string,
|
|
259
|
+
baseEntries: readonly LockfileEntry[],
|
|
260
|
+
format: LockfileFormat,
|
|
261
|
+
registryOrigin: string | undefined,
|
|
262
|
+
): string | null | undefined {
|
|
263
|
+
for (const baseEntry of baseEntries) {
|
|
264
|
+
if (baseEntry.version === null || baseEntry.installedName !== baseEntry.name) continue;
|
|
265
|
+
if (baseEntry.name !== name || baseEntry.version !== version) continue;
|
|
266
|
+
if (baseEntry.link || baseEntry.otherResolutionKeys.length > 0) continue;
|
|
267
|
+
if (registryOrigin === undefined || !resolvesToRegistry(baseEntry, format, registryOrigin)) continue;
|
|
268
|
+
return baseEntry.tarball;
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Whether a versioned entry is already represented in the base. npm alias entries need key-level equality
|
|
275
|
+
* (fix round 3). The `name@version` shortcut (fix round 4) applies only for a plain registry tarball match.
|
|
276
|
+
*/
|
|
277
|
+
function isUnchangedVersionedEntry(
|
|
278
|
+
entry: LockfileEntry,
|
|
279
|
+
baseVersionedKeys: ReadonlySet<string>,
|
|
280
|
+
baseEntriesByKey: ReadonlyMap<string, LockfileEntry>,
|
|
281
|
+
baseEntries: readonly LockfileEntry[],
|
|
282
|
+
format: LockfileFormat,
|
|
283
|
+
registryOrigin: string | undefined,
|
|
284
|
+
): boolean {
|
|
285
|
+
if (entry.version === null) return false;
|
|
286
|
+
if (entry.installedName !== entry.name) {
|
|
287
|
+
const baseEntry = baseEntriesByKey.get(entry.key);
|
|
288
|
+
if (baseEntry === undefined) return false;
|
|
289
|
+
return entryMatchesAtKey(baseEntry, entry);
|
|
290
|
+
}
|
|
291
|
+
const baseAtKey = baseEntriesByKey.get(entry.key);
|
|
292
|
+
if (baseAtKey !== undefined && entryMatchesAtKey(baseAtKey, entry)) return true;
|
|
293
|
+
if (
|
|
294
|
+
!entry.link &&
|
|
295
|
+
entry.otherResolutionKeys.length === 0 &&
|
|
296
|
+
baseVersionedKeys.has(entryKey(entry.name, entry.version))
|
|
297
|
+
) {
|
|
298
|
+
const canonicalTarball = canonicalBaseTarballForNameVersion(entry.name, entry.version, baseEntries, format, registryOrigin);
|
|
299
|
+
if (canonicalTarball !== undefined && entry.tarball === canonicalTarball) return true;
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* I5: every regenerated entry newly resolving a scoped package name comes from the publishing registry.
|
|
306
|
+
*
|
|
307
|
+
* Checks both the installed (alias) name and the declared name (fix 6): an entry that is new and scoped
|
|
308
|
+
* under either name must have the two agree, since an alias into or out of the scope is itself a
|
|
309
|
+
* violation, in addition to resolving to the publishing registry as before. A link entry under the scope
|
|
310
|
+
* (fix 7) is its own violation unless the base already holds an identical link at the same key. For pnpm,
|
|
311
|
+
* every importer's and snapshot's dependency-map value naming a scoped package is also checked directly
|
|
312
|
+
* (fix 6): a value that is an alias, a `link:` or a `file:` reference is a violation unless the base holds
|
|
313
|
+
* the identical name and value at the same place -- this catches an alias hidden inside a snapshot's
|
|
314
|
+
* dependency map, which never appears as its own `packages` entry under the scope.
|
|
315
|
+
*
|
|
316
|
+
* Fails closed when the publishing registry does not parse as a URL (fix 10): every new scoped entry is a
|
|
317
|
+
* violation, or, when there is none at all, a single violation named `""`.
|
|
318
|
+
*/
|
|
319
|
+
function checkI5(
|
|
320
|
+
input: LockfileInvariantInput,
|
|
321
|
+
base: LockfileView,
|
|
322
|
+
regenerated: LockfileView,
|
|
323
|
+
publishing: { readonly scope: string; readonly registry: string },
|
|
324
|
+
): LockfileInvariantViolation[] {
|
|
325
|
+
const violations: LockfileInvariantViolation[] = [];
|
|
326
|
+
const scopePrefix = `${publishing.scope}/`;
|
|
327
|
+
const baseVersionedKeys = entryKeySet(base.entries);
|
|
328
|
+
const baseEntriesByKey = new Map(base.entries.map((entry) => [entry.key, entry] as const));
|
|
329
|
+
const baseLinksByKey = new Map(base.entries.filter((entry) => entry.version === null).map((entry) => [entry.key, entry] as const));
|
|
330
|
+
|
|
331
|
+
let registryOrigin: string | undefined;
|
|
332
|
+
try {
|
|
333
|
+
registryOrigin = new URL(publishing.registry).origin;
|
|
334
|
+
} catch {
|
|
335
|
+
registryOrigin = undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let anyNewScoped = false;
|
|
339
|
+
for (const entry of regenerated.entries) {
|
|
340
|
+
const scoped = entry.name.startsWith(scopePrefix) || entry.installedName.startsWith(scopePrefix);
|
|
341
|
+
if (!scoped) continue;
|
|
342
|
+
if (entry.version === null) {
|
|
343
|
+
const baseLink = baseLinksByKey.get(entry.key);
|
|
344
|
+
if (baseLink !== undefined && baseLink.tarball === entry.tarball) continue; // the identical link already in base
|
|
345
|
+
anyNewScoped = true;
|
|
346
|
+
violations.push({ invariant: "I5", name: entry.name });
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (isUnchangedVersionedEntry(entry, baseVersionedKeys, baseEntriesByKey, base.entries, input.format, registryOrigin))
|
|
350
|
+
continue; // not new
|
|
351
|
+
anyNewScoped = true;
|
|
352
|
+
if (registryOrigin === undefined) {
|
|
353
|
+
violations.push({ invariant: "I5", name: entry.name });
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (entry.installedName !== entry.name) {
|
|
357
|
+
violations.push({ invariant: "I5", name: entry.name });
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (!resolvesToRegistry(entry, input.format, registryOrigin)) violations.push({ invariant: "I5", name: entry.name });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (input.format === "pnpm") {
|
|
364
|
+
const baseRefs = new Map(base.dependencyRefs.map((ref) => [`${ref.place}\u0000${ref.name}`, ref.value] as const));
|
|
365
|
+
for (const ref of regenerated.dependencyRefs) {
|
|
366
|
+
if (!ref.name.startsWith(scopePrefix)) continue;
|
|
367
|
+
if (!isSuspiciousPnpmDependencyValue(stripParenSuffix(ref.value))) continue;
|
|
368
|
+
if (baseRefs.get(`${ref.place}\u0000${ref.name}`) === ref.value) continue; // identical name and value at the same place as the base
|
|
369
|
+
anyNewScoped = true;
|
|
370
|
+
violations.push({ invariant: "I5", name: ref.name });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (registryOrigin === undefined && !anyNewScoped) violations.push({ invariant: "I5", name: "" });
|
|
375
|
+
return violations;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function finalizeViolations(violations: readonly LockfileInvariantViolation[]): LockfileInvariantViolation[] {
|
|
379
|
+
const deduped = new Map<string, LockfileInvariantViolation>();
|
|
380
|
+
for (const violation of violations) deduped.set(`${violation.invariant}\u0000${violation.name}`, violation);
|
|
381
|
+
return [...deduped.values()].sort(
|
|
382
|
+
(left, right) => compareCodeUnits(left.invariant, right.invariant) || compareCodeUnits(left.name, right.name),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function checkAgainstReadable(input: LockfileInvariantInput, base: LockfileView, regenerated: LockfileView): LockfileInvariantResult {
|
|
387
|
+
const publishing = input.publishing ?? PACKAGE_SCOPE;
|
|
388
|
+
const violations: LockfileInvariantViolation[] = [];
|
|
389
|
+
if (base.lockfileVersion !== regenerated.lockfileVersion) violations.push({ invariant: "I4", name: "" });
|
|
390
|
+
violations.push(...checkI1(input, regenerated));
|
|
391
|
+
violations.push(...checkI2(input, base, regenerated));
|
|
392
|
+
violations.push(...checkI3(base, regenerated));
|
|
393
|
+
violations.push(...checkI5(input, base, regenerated, publishing));
|
|
394
|
+
const transitive = computeTransitiveCounts(base, regenerated);
|
|
395
|
+
const deduped = finalizeViolations(violations);
|
|
396
|
+
return deduped.length === 0
|
|
397
|
+
? { verdict: "satisfied", violations: [], transitive }
|
|
398
|
+
: { verdict: "violated", violations: deduped, transitive };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function checkLockfileInvariantsUnsafe(input: LockfileInvariantInput): LockfileInvariantResult {
|
|
402
|
+
const base = readLockfile(input.format, input.base);
|
|
403
|
+
if (isUnreadable(base)) return { verdict: "indeterminate", reason: base.reason, side: "base" };
|
|
404
|
+
const regenerated = readLockfile(input.format, input.regenerated);
|
|
405
|
+
if (isUnreadable(regenerated)) {
|
|
406
|
+
if (regenerated.reason === "lockfile-format-unsupported") {
|
|
407
|
+
return { verdict: "violated", violations: [{ invariant: "I4", name: "" }], transitive: { added: 0, removed: 0 } };
|
|
408
|
+
}
|
|
409
|
+
return { verdict: "indeterminate", reason: regenerated.reason, side: "regenerated" };
|
|
410
|
+
}
|
|
411
|
+
return checkAgainstReadable(input, base, regenerated);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Checks I1 to I5 between a base and a regenerated lockfile. Pure; never throws. */
|
|
415
|
+
export function checkLockfileInvariants(input: LockfileInvariantInput): LockfileInvariantResult {
|
|
416
|
+
try {
|
|
417
|
+
return checkLockfileInvariantsUnsafe(input);
|
|
418
|
+
} catch {
|
|
419
|
+
return LAST_RESORT;
|
|
420
|
+
}
|
|
421
|
+
}
|