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
//! `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(crate::commands::generated_artifact::check_against(
out,
&rendered,
"make gen-harness-migrations",
));
}
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
}
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;