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
//! `harn dump-harness-migrations` — project the runtime's migration record
//! into a table `harn-parser` can read.
//!
//! `harn_vm::stdlib::harness_migration_for_builtin` knows which capability
//! method replaced a removed global. `harn-parser` sits *below* `harn-vm`, so
//! the type checker cannot call it and grew a hand-written `match` instead —
//! unguarded, and backstopped by a Levenshtein search that answered `uuid_v5`
//! for `uuid_v7` (harn#6151).
//!
//! Generating the projection is how the repo already resolves this shape; see
//! `connector_schema_codegen` and `dump-protocol-artifacts`. `--check`
//! re-renders and compares without writing, so the table cannot drift from the
//! registry it mirrors.
//!
//! **The projection is a fallback, not a replacement.** The issue assumed the
//! hand-written table was a strict subset of the registry. Measured against 417
//! generated rows, it is not: 60 hand-written entries agree, 84 have no registry
//! row at all (the whole `harness.net.*` family among them), and **4 disagree**.
//! Three of those disagree dangerously, because the registry is keyed by builtin
//! name and `read_file` / `write_file` / `delete_file` each collide with an
//! unrelated `harness.tools.*` agent tool. So `harn-parser` consults its own
//! tables first and this one last; `HARNESS_MIGRATION_DISAGREEMENTS` pins the
//! four, and the tests here assert the collisions are still real.
use std::collections::BTreeMap;
use std::path::Path;
use crate::cli::DumpHarnessMigrationsArgs;
/// The vendored generated file, relative to the repo root.
const DEFAULT_OUTPUT: &str = "crates/harn-parser/src/diagnostic/harness_migrations_generated.rs";
pub(crate) fn run(args: &DumpHarnessMigrationsArgs) -> i32 {
match run_inner(args) {
Ok(code) => code,
Err(message) => {
eprintln!("harn dump-harness-migrations: {message}");
1
}
}
}
fn run_inner(args: &DumpHarnessMigrationsArgs) -> Result<i32, String> {
let rendered = render(&migrations());
let out = Path::new(args.out.as_deref().unwrap_or(DEFAULT_OUTPUT));
if args.check {
return Ok(check_against(out, &rendered));
}
harn_vm::atomic_io::atomic_write(out, rendered.as_bytes())
.map_err(|error| format!("failed to write {}: {error}", out.display()))?;
eprintln!("wrote {}", out.display());
Ok(0)
}
/// Every removed global the runtime can route, as
/// `legacy name -> harness.<capability>.<method>`.
///
/// Sorted and deduplicated by construction: the output is a `BTreeMap`, so the
/// generated table is stable across runs and bisectable in review.
pub(crate) fn migrations() -> BTreeMap<String, String> {
let mut rows = BTreeMap::new();
for name in harn_vm::stdlib::stdlib_builtin_names() {
// `__`-prefixed names are compiler plumbing and are never spelled in
// source, so a "did you mean" for one would be noise.
if name.starts_with("__") {
continue;
}
let Some(migration) = harn_vm::stdlib::harness_migration_for_builtin(&name) else {
continue;
};
rows.insert(
name,
format!(
"harness.{}.{}",
migration.capability.field_name(),
migration.method
),
);
}
rows
}
/// Render the table.
///
/// `#[rustfmt::skip]` is load-bearing, not cosmetic. Without it rustfmt wraps
/// the longer rows across three lines, the committed file stops matching what
/// the generator emits, and `--check` reports drift on every run with no way to
/// converge — the generator would have to reproduce rustfmt's wrapping rule
/// exactly. Letting the generator own its own layout is the only stable
/// arrangement.
pub(crate) fn render(rows: &BTreeMap<String, String>) -> String {
let mut out = String::from(HEADER);
out.push_str("#[rustfmt::skip]\n");
out.push_str("pub(super) const HARNESS_MIGRATIONS: &[(&str, &str)] = &[\n");
for (name, replacement) in rows {
out.push_str(&format!(" ({name:?}, {replacement:?}),\n"));
}
out.push_str("];\n");
out
}
fn check_against(out: &Path, rendered: &str) -> i32 {
match std::fs::read_to_string(out) {
Ok(existing) if normalize(&existing) == normalize(rendered) => 0,
Ok(_) => {
eprintln!(
"{} is out of date; regenerate with `make gen-harness-migrations`",
out.display()
);
1
}
Err(_) => {
eprintln!(
"{} is missing; generate it with `make gen-harness-migrations`",
out.display()
);
1
}
}
}
/// Compare ignoring line-ending differences so the check passes on Windows
/// checkouts (mirrors `dump-protocol-artifacts`).
fn normalize(text: &str) -> String {
text.replace("\r\n", "\n")
}
const HEADER: &str = "\
// DO NOT EDIT — generated by `harn dump-harness-migrations`.
//
// Source of truth: harn_vm::stdlib::harness_migration_for_builtin
// Regenerate with: make gen-harness-migrations
// Verify (CI): make check-harness-migrations
//
// `harn-parser` cannot call the runtime registry — it compiles below harn-vm —
// so this table is the projection it reads instead of a second hand-written
// one. Every entry is `legacy global -> the typed harness path that replaced
// it`; the argument shapes stay in the runtime recipe, because a mapping that
// happens to compile is not necessarily the one the author meant.
";
#[cfg(test)]
mod tests;