1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
//! Root `[dev-dependencies]` publish-safety tripwire — Phase 128 FORK 1 exit (b).
//!
//! Root `Cargo.toml`'s `pmcp-code-mode` and `pmcp-code-mode-derive` dev-deps must
//! be the table form carrying a `path` and **no** version requirement.
//!
//! # The mechanism, stated once
//!
//! Cargo strips a `[dev-dependencies]` entry from the published manifest ONLY when
//! it carries no version requirement. An entry carrying one is RETAINED, and
//! `cargo publish -p pmcp` must resolve it against crates.io while preparing the
//! manifest. `CLAUDE.md` § Release & Publish Workflow item 9b records the measured
//! failure that produces for exactly this shape: exit 101, "failed to select a
//! version for the requirement". The `exclude` list does not save it — the failure
//! is at manifest-prep time, so excluding `examples/` would remove the consumers,
//! not the manifest entry.
//!
//! # Why that matters HERE, and why it did not before Phase 128
//!
//! That retention is what USED to force `pmcp-code-mode` to publish BEFORE `pmcp`
//! in `.github/workflows/release.yml`, and Phase 128 made that order impossible:
//! `crates/pmcp-code-mode/Cargo.toml` now requires a `pmcp` carrying the
//! `schema-validation` feature (D-09 / Q2 — the one copy of the placeholder floor
//! lives in core and `pmcp-code-mode` re-exports it). A code-mode publish running
//! FIRST would resolve the highest ALREADY-published `pmcp`, which has no such
//! feature, and die at the first step — and every publish step in `release.yml`
//! tolerates only an "already exists" failure, so the whole release job would exit
//! having published nothing.
//!
//! Dropping the `version` key from BOTH entries frees `cargo publish -p pmcp` from
//! needing either crate on crates.io, which is what lets `pmcp` publish ahead of
//! them. Both must be path-only: leaving `pmcp-code-mode-derive`'s key would keep
//! the publish blocked on an unpublished `pmcp-code-mode-derive`.
//!
//! # Why the publish VERIFY build does not then fail on a stripped dep
//!
//! Exactly one example consumes these dev-deps — `examples/s41_code_mode_graphql.rs`
//! — and its `[[example]]` stanza carries `required-features = ["full"]`, which the
//! root `default = ["logging", "v1-compat"]` does NOT satisfy. So the verify build
//! SKIPS it rather than failing to compile it. That is the same condition root
//! `Cargo.toml`'s `pmcp-agent` / `s53` comment already records for its own path-only
//! entry, and this test asserts it rather than trusting it (see
//! [`s41_example_requires_a_feature_default_does_not_satisfy`]).
//!
//! # Re-adding a version requirement is the one change this file exists to prevent
//!
//! An unexplained non-pin reads as an omission, which is precisely how the two
//! ordering bugs `CLAUDE.md` items 2 and 12 record were introduced. If you tripped
//! this test while "restoring a pin", the pin is the bug.
//!
//! # What this tripwire does NOT cover
//!
//! Only root's two code-mode dev-dep entries and the `s41` feature condition. It is
//! deliberately narrow:
//!
//! - **The publish ORDER itself** is machine-checked by
//! `scripts/check-release-coverage.sh`'s `BEGIN PHASE-128 ORDER ASSERTION`
//! region, which runs inside `make quality-gate` and the CI quality-gate job.
//! This file cannot see `release.yml` and that region cannot see a manifest.
//! Both halves are needed: reverting either one alone strands the release.
//! - **Root's `pmcp-macros` dev-dep DELIBERATELY keeps its `version` key**, and
//! must not be "made consistent" with these two: `pmcp-macros` publishes AHEAD of
//! `pmcp` in `release.yml` (CLAUDE.md item 1b), so its requirement resolves at
//! publish time. The rule is about publish ORDER, not about tidiness.
//! - **The standing `mcp-tester` risk** CLAUDE.md documents is the same class: six
//! in-repo crates carry an `mcp-tester` dev-dep with BOTH `path` and `version`
//! and four publish before `mcp-tester`, green only while the pinned version is
//! already published. Nothing here covers those.
/// The workspace-root manifest, embedded at compile time.
const ROOT_CARGO_TOML: &str = include_str!("../Cargo.toml");
/// The two entries that must be path-only, with the path each must point at.
const PATH_ONLY_DEV_DEPS: [(&str, &str); 2] = [
("pmcp-code-mode", "crates/pmcp-code-mode"),
("pmcp-code-mode-derive", "crates/pmcp-code-mode-derive"),
];
/// FORK 1 exit (b): neither code-mode dev-dep may carry a `version` key.
///
/// The table form is required because the string shorthand
/// (`pmcp-code-mode = "0.6"`) IS a bare version requirement by definition — there
/// is no shorthand that expresses a path.
#[test]
fn root_code_mode_dev_deps_are_path_only() {
let manifest: toml::Value =
toml::from_str(ROOT_CARGO_TOML).expect("parse the workspace-root Cargo.toml");
let dev_deps = manifest
.get("dev-dependencies")
.expect("the workspace-root Cargo.toml has a [dev-dependencies] table");
for (name, expected_path) in PATH_ONLY_DEV_DEPS {
let dep = dev_deps
.get(name)
.unwrap_or_else(|| panic!("root Cargo.toml has no [dev-dependencies].{name}"));
let table = dep.as_table().unwrap_or_else(|| {
panic!(
"[dev-dependencies].{name} must be the table form \
`{{ path = \"{expected_path}\" }}`, found {dep:?}. The string shorthand IS a \
bare version requirement, which is exactly the shape FORK 1 forbids."
)
});
let path = table
.get("path")
.and_then(toml::Value::as_str)
.unwrap_or_else(|| panic!("[dev-dependencies].{name} must carry a `path` key"));
assert_eq!(
path, expected_path,
"[dev-dependencies].{name} must point at the in-repo sibling crate"
);
assert!(
table.get("version").is_none(),
"[dev-dependencies].{name} must NOT carry a `version` key (Phase 128 FORK 1 exit b). \
Cargo strips a dev-dep from the published manifest only when it has no version \
requirement; one that has a requirement is RETAINED, and `cargo publish -p pmcp` \
must then resolve it against crates.io while preparing the manifest — measured \
failure: exit 101, \"failed to select a version for the requirement\" (CLAUDE.md \
item 9b). release.yml now publishes `pmcp` BEFORE {name}, precisely because \
pmcp-code-mode requires a `pmcp` carrying `schema-validation`, so this lookup CANNOT \
succeed and the publish step's fallback (which tolerates only \"already exists\") \
would fail the whole release job. If you added this key to \"restore a pin\", the pin \
is the bug — see the module docs above and scripts/check-release-coverage.sh's \
PHASE-128 order region."
);
}
}
/// Root's `pmcp-macros` dev-dep must KEEP its version key.
///
/// The negative control for the test above. Without it, "make every dev-dep
/// path-only" would satisfy that test while silently dropping a requirement that is
/// safe and load-bearing: `pmcp-macros` publishes AHEAD of `pmcp` (CLAUDE.md item
/// 1b), so its requirement resolves at publish time and states a real compatibility
/// claim for the published `pmcp`.
#[test]
fn root_pmcp_macros_dev_dep_still_carries_its_version_requirement() {
let manifest: toml::Value =
toml::from_str(ROOT_CARGO_TOML).expect("parse the workspace-root Cargo.toml");
let dep = manifest
.get("dev-dependencies")
.and_then(|table| table.get("pmcp-macros"))
.expect("root Cargo.toml has no [dev-dependencies].pmcp-macros");
assert!(
dep.get("version").and_then(toml::Value::as_str).is_some(),
"[dev-dependencies].pmcp-macros must KEEP its `version` key. It publishes BEFORE `pmcp` \
in release.yml (CLAUDE.md item 1b), so the requirement resolves — the FORK-1 rule is \
about publish ORDER, not about making every dev-dep look alike. Found: {dep:?}"
);
}
/// The one measured way FORK 1's path-only change could break `cargo publish -p pmcp`.
///
/// Cargo strips a path-only dev-dep at publish time, so any example that consumes
/// `pmcp-code-mode` and is REACHABLE from the publish verify build would fail to
/// compile. `s41_code_mode_graphql` is the only such example, and it is safe only
/// while its `required-features` names something `default` does not provide.
///
/// Asserted structurally rather than trusted: the stanza's declared features must
/// not all be satisfiable from `default` plus what `default` transitively enables
/// at the top level of the feature table.
#[test]
fn s41_example_requires_a_feature_default_does_not_satisfy() {
let manifest: toml::Value =
toml::from_str(ROOT_CARGO_TOML).expect("parse the workspace-root Cargo.toml");
let examples = manifest
.get("example")
.and_then(toml::Value::as_array)
.expect("root Cargo.toml declares [[example]] stanzas");
let s41 = examples
.iter()
.find(|e| e.get("name").and_then(toml::Value::as_str) == Some("s41_code_mode_graphql"))
.expect("root Cargo.toml declares the s41_code_mode_graphql example");
let required: Vec<&str> = s41
.get("required-features")
.and_then(toml::Value::as_array)
.expect(
"s41_code_mode_graphql MUST declare `required-features`. Without it the publish \
VERIFY build compiles the example against the STRIPPED path-only pmcp-code-mode \
dev-dep and `cargo publish -p pmcp` fails — which is the one measured way FORK 1's \
exit (b) can break the release.",
)
.iter()
.filter_map(toml::Value::as_str)
.collect();
assert!(
!required.is_empty(),
"s41_code_mode_graphql's `required-features` must be non-empty"
);
// The default set, expanded one level: `default = ["logging", "v1-compat"]`
// plus whatever those two enable. One level is enough for the assertion that
// matters — `full` is a SIBLING of `default`, never reachable from it.
let features = manifest
.get("features")
.and_then(toml::Value::as_table)
.expect("root Cargo.toml has a [features] table");
let mut default_set: Vec<String> = Vec::new();
let mut frontier: Vec<String> = features
.get("default")
.and_then(toml::Value::as_array)
.expect("[features].default exists")
.iter()
.filter_map(toml::Value::as_str)
.map(ToString::to_string)
.collect();
while let Some(name) = frontier.pop() {
if default_set.contains(&name) {
continue;
}
if let Some(children) = features.get(&name).and_then(toml::Value::as_array) {
for child in children.iter().filter_map(toml::Value::as_str) {
if !child.contains('/') && !child.starts_with("dep:") {
frontier.push(child.to_string());
}
}
}
default_set.push(name);
}
// POSITIVE CONTROL: the expansion must actually have found `logging`, or the
// "not in the default set" assertion below would pass on an empty set and
// prove nothing.
assert!(
default_set.iter().any(|f| f == "logging"),
"positive control: the default-feature expansion must contain `logging`, got {default_set:?}"
);
let unsatisfied: Vec<&str> = required
.iter()
.copied()
.filter(|f| !default_set.iter().any(|d| d == f))
.collect();
assert!(
!unsatisfied.is_empty(),
"s41_code_mode_graphql's required-features {required:?} are ALL satisfied by the default \
feature set {default_set:?} (unsatisfied: {unsatisfied:?}), so `cargo publish -p pmcp`'s \
verify build would compile the example against the STRIPPED path-only pmcp-code-mode \
dev-dep and fail. Either give the example a non-default required feature, or give root's \
code-mode dev-deps the treatment root Cargo.toml's pmcp-agent / s53 comment describes."
);
}