Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/external-pin-floors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@codelitdev/platform-cli": patch
"@codelitdev/mcp-server-kit": patch
---

Raise the `next` pin to 16.4.1 (critical RCE fixes) and the `@modelcontextprotocol/sdk` pin to 1.32.1. `doctor` now treats external pins as caret floors, so products can take later patch and minor releases (including security fixes) without failing conformance.
86 changes: 44 additions & 42 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ Products depend on individual packages; CI verifies that resolved versions match
}
```

`recommended` is the exact version emitted into a new product. `supported` is the range covered by conformance for an existing adopter. `minimumSecure` is monotonic within a supported major line and makes `doctor` and conformance fail when the resolved version is older. The preset validates the product manifest and lockfile; it does not mutate dependencies. Dependabot performs upgrades through reviewable product pull requests.
`recommended` is the exact version emitted into a new product. `supported` is the range covered by conformance for an existing adopter. `minimumSecure` is monotonic within a supported major line and makes `doctor` and conformance fail when the resolved version is older. An `external` pin is the exact version `create` emits and, for `doctor`, a caret floor: later releases on the same major line pass, older ones fail (ADR 0016). The preset validates the product manifest and lockfile; it does not mutate dependencies. Dependabot performs upgrades through reviewable product pull requests.

### 6.8 @codelitdev/platform-conformance

Expand Down
33 changes: 33 additions & 0 deletions docs/decisions/0016-external-pins-are-caret-floors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ADR 0016: External preset pins are caret floors

Status: accepted; amends ADR 0002 and ADR 0006
Date: 2026-11-09

## Context

`doctor` required each external dependency in the preset (`next`, the MCP
SDK, and others) to resolve to exactly the pinned version. When Next.js
16.3.6 picked up critical RCE advisories fixed in 16.4.1, product
dependency-audit pull requests that took the fix failed the platform
conformance check (issue #14). A product could stay conformant or take the
security fix, not both, until the platform shipped a new CLI release.

## Decision

- An `external` pin is still the exact version `create` writes into a new
product and the version the template declares (`sync-preset.mjs` is
unchanged).
- `doctor` treats it as a caret floor: a resolved version passes if it is
at or above the pin on the same major line (same minor while the major
is 0). Older versions and the next breaking line still fail as
`external_mismatch`.
- When an advisory affects a pinned external, raise the template's pin to
the patched version so `doctor` rejects the vulnerable release.

## Consequences

- Products take patch and minor security fixes through Dependabot without
waiting for a platform release.
- Conformance no longer proves a product runs the exact external versions
the template was tested with. Minor releases of mature dependencies are
trusted to be compatible; a major upgrade still requires a preset change.
2 changes: 1 addition & 1 deletion examples/reference-product/apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"@codelitdev/design-system": "workspace:*",
"@codelitdev/observability": "workspace:*",
"next": "^16.3.4",
"next": "^16.4.1",
"posthog-js": "^1.427.2",
"react": "^19.2.8",
"react-dom": "^19.2.8"
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"tag": "latest"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"@modelcontextprotocol/sdk": "^1.32.1",
"zod": "^3.25.76"
},
"peerDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/platform-cli/src/preset.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@
"pino": "9.6.0",
"@types/bun": "1.4.1",
"@electric-sql/pglite": "0.5.8",
"@modelcontextprotocol/sdk": "1.30.0",
"@modelcontextprotocol/sdk": "1.32.1",
"dodopayments": "2.48.0",
"next": "16.3.6",
"next": "16.4.1",
"react": "19.2.8",
"react-dom": "19.2.8"
}
Expand Down
10 changes: 9 additions & 1 deletion packages/platform-cli/src/preset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe("platform preset", () => {
expect(ok).toEqual([]);
});

it("checks exact external compatibility pins", () => {
it("treats external pins as caret floors", () => {
const manifest = loadPresetManifest();
expect(
validateResolvedVersions(
Expand All @@ -37,5 +37,13 @@ describe("platform preset", () => {
expect(
validateResolvedVersions({ typescript: manifest.external.typescript }, manifest),
).toEqual([]);
const pinned = { ...manifest, external: { next: "16.4.1", pino: "0.5.2" } };
const reasons = (resolved: Record<string, string>) =>
validateResolvedVersions(resolved, pinned).map((issue) => issue.packageName);
expect(reasons({ next: "16.4.1", pino: "0.5.2" })).toEqual([]);
expect(reasons({ next: "16.4.5", pino: "0.5.9" })).toEqual([]);
expect(reasons({ next: "16.5.0" })).toEqual([]);
expect(reasons({ next: "16.3.6", pino: "0.5.1" })).toEqual(["next", "pino"]);
expect(reasons({ next: "17.0.0", pino: "0.6.0" })).toEqual(["next", "pino"]);
});
});
22 changes: 19 additions & 3 deletions packages/platform-cli/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export type PackagePin = {

/**
* Compatibility rules for products: recommended, supported, and minimum-secure
* versions of each @codelitdev package, plus exact pins for the external
* packages the template uses. `scripts/sync-preset.mjs` writes `recommended`
* versions of each @codelitdev package, plus pins for the external packages
* the template uses. An external pin is the exact version `create` writes and
* the floor `doctor` accepts: any later release on the same caret line passes
* (ADR 0016). `scripts/sync-preset.mjs` writes `recommended`
* and `external`; `supported` and `minimumSecure` are hand-maintained.
*/
export type PresetManifest = {
Expand Down Expand Up @@ -83,6 +85,20 @@ function satisfies(version: string, range: string): boolean {
});
}

/**
* `^pin`: at or above the pin without crossing the next breaking line
* (the major, or the minor while the major is 0). Lets products take patch
* and minor security fixes without a preset release.
*/
function satisfiesCaret(version: string, pin: string): boolean {
const v = parseVersion(version);
const p = parseVersion(pin);
if (!v || !p) return cmp(version, pin) === 0;
if (v.major !== p.major) return false;
if (p.major === 0 && v.minor !== p.minor) return false;
return cmp(version, pin) >= 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude prereleases from external caret floors

When a product resolves an external dependency to a prerelease above its floor—for example, next@16.5.0-canary.1 with a 16.4.1 pin—this comparison accepts it, so doctor reports conformance even though ^16.4.1 excludes prereleases and the policy only permits later patch and minor releases. A prerelease may be unstable or incompatible; exclude prereleases or apply actual caret-range semantics.

Useful? React with 👍 / 👎.

}

export type PresetIssue = {
packageName: string;
reason:
Expand All @@ -101,7 +117,7 @@ export function validateResolvedVersions(
for (const [name, version] of Object.entries(resolved)) {
const pin = manifest.packages[name];
if (!pin && name in manifest.external) {
if (cmp(version, manifest.external[name]!) !== 0) {
if (!satisfiesCaret(version, manifest.external[name]!)) {
issues.push({
packageName: name,
reason: "external_mismatch",
Expand Down
2 changes: 1 addition & 1 deletion templates/saas-product/apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"@codelitdev/design-system": "workspace:*",
"@codelitdev/observability": "workspace:*",
"next": "16.3.6",
"next": "16.4.1",
"posthog-js": "^1.427.2",
"react": "19.2.8",
"react-dom": "19.2.8"
Expand Down
Loading