Repository navigation
Activity transcripts: migrate the 21 remaining unkeyed LogMessage sites, and mirror the 41 activity.* keys #3281
Description
Follow-up to #3236, which landed the seam (LogMessage.MessageKey / MessageArgs,
LocalizationCatalog.GetNamed, message.Localize(locale) / host.Localize(message)) plus 37 of 58
migrated write sites and the UnkeyedActivityLogMessageRatchetGuard inventory.
What is left, and why each is where it is
test/UnkeyedActivityLogMessages.allow carries the 21 remaining unkeyed new LogMessage( sites
under src/. They split three ways, and only the first group is actual work.
1. MIGRATABLE — needs a key threaded through a public signature (9 sites)
| File | sites | what it needs |
|---|---|---|
MeshWeaver.GitSync/ActivityRunner.cs |
3 | The activity TITLE, each step's message and the terminal finalMessage arrive as string parameters of RunActivity / Log / Finish. The eight callers in GitHubActivityExtensions compose real sentences ("Commit {space} to GitHub", "Re-import {space} at {commitish}", "Open pull request", …), so these ARE translatable — they need an optional key parameter (or a LogMessage overload) on the runner. |
MeshWeaver.Compiler.Pipeline/NodeTypeCompilationActivity.cs |
2 | AppendLog(…, string message, …) and Update(…, string? error, …). The batched AppendLogs(…, LogMessage[] …) already accepts a keyed message, so the migration is at the ~40 CALLERS of AppendLog, one sentence at a time — no signature change here. |
MeshWeaver.Compiler.Pipeline/MeshNodeCompilationService.cs |
3 | AppendInfo / AppendWarning / AppendError(log, string message). Same shape: migrate the callers, or give the helpers a key parameter. |
MeshWeaver.Graph/StaticRepoImporter.cs |
1 of 4 | The content-sync ledger's string message parameter. |
The two composed import SUMMARIES in StaticRepoImporter (the lock stamp and the terminal line) are
a judgement call rather than a mechanical migration: each folds 6+ conditional notes carrying #604 /
#3101 incident wording. Keying them well means keying each note and composing the translated pieces —
worth doing, but it is its own change with its own review.
2. GENERIC PLUMBING — nothing to key at the site
An ILogger → activity adapter (ActivityLogLogger, Activity.Log<TState>) or ActivityLog.Fail:
the text arrives already formatted. A caller that wants a key builds the LogMessage itself.
3. VERBATIM UPSTREAM TEXT — no catalog can carry it
ex.Message, a Roslyn diagnostic, settle.Diagnosis, a descendant hub's own refusal, the
stranded-instances report. These stay English by the same rule as the Describe* family
(Doc/Architecture/Localization → "viewer message vs. owner diagnostic"), and where such text is
spliced into a sentence the platform owns, the lead is already keyed and the detail rides as a
{detail} argument.
Also outstanding
- The
MeshWeaver.Pluginsclient mirror. ActivityLog messages are persisted English strings — a rendered surface that cannot be localized (53 sites, 0 localizable) #3236 added 41activity.*keys to
src/MeshWeaver.Messaging.Hub/Localization/strings.{en,de}.json. Their second home is
MeshWeaver.Plugins/clients/react/src/i18n/strings.{en,de}.json, whose drift guard compares
VALUES; core merged first, so that repo'sRN app + web clients (typecheck + test)job is red
until the mirror lands. - The AI engine's activity views. They ship in
MeshWeaver.Pluginsand still render
m.Message. Switching them tohost.Localize(m)/m.Localize(locale)is safe on every row
(an unkeyed entry renders its stored English unchanged) and is what makes the migrated keys
actually visible to a German viewer in the surface most people read.
The census trap, recorded because it cost the original issue three sites
grep 'new LogMessage(' does not see a target-typed Messages = [new("…", level)]. Three such sites
in LayoutClientExtensions were missing from #3236's 53. They are now spelled out and
UnkeyedActivityLogMessageRatchetGuard.NoTargetTypedLogMessageConstructionHidesFromTheRatchet refuses
the shape outright, so the ratchet's inventory is complete rather than merely consistent.
Activity
rbuergi commentedon Sep 4, 2026
🚨 Correction to this issue's "Also outstanding → Plugins client mirror" bullet, measured
2026-09-04 in the plugins checkout.
Adding keys in core does not red the plugins repo.
clients/react/src/i18n/localize.test.tscompares the client catalog against a PINNED core commit recorded in
clients/react/src/i18n/catalog-source.json, not against core'smain— pinned deliberately inMeshWeaver.Plugins#893, because core merges faster than a Plugins CI cycle and an unpinned guard
could not converge (it reddened every unrelated PR on a subsystem its diff could not reach).
Seventy keys behind, every gate green in both repos. So the real failure mode is the opposite of
"the mirror goes red": it goes silently stale, and nothing anywhere will remind anyone. The sync
is a deliberate step against the merged core sha:
which rewrites both
strings.{en,de}.json, therefand the recorded key counts together.MeshWeaver#3282 corrects this in all three guidance homes (AGENTS.md, the
/i18nskill,Doc/Architecture/Localization).rbuergi commentedon Sep 4, 2026
The mirror half is done — and the issue's premise about it was wrong
It was not red. Measured today before touching anything:
clients/react/src/i18n/catalog-source.json)e7f1d699e(2026-09-03T08:05Z)mainf9d2a335aThe drift guard compares the mirror against its own pin, never against core's default branch — so
a key added in core reddens nothing and the mirror goes silently stale instead. 179 strings were
rendering English for a German viewer of the React and RN clients, with both repos green for ~30 hours.
Landed as Systemorph/MeshWeaver.Plugins#1332 (
e7f1d699e -> f9d2a335a, en/de 1283 each). Of the 179added keys only 7 equal their English value, all deliberate:
Embeddings,Registry,Plugins,Provider,Thread(glossary terms kept English by policy),Plan: {0}(identical in German) and'{path}': {warning}(pure interpolation).Worth adding to this issue's scope
The guard cannot see this class of drift by construction — it asks "does the mirror match its
pin", never "is the pin current". That is the same shape as a gate that tests its own inputs: it
passes having checked something real, while the thing you care about is unobserved. A syncing PR fixes
today's drift and restores the same blind spot for tomorrow.
A check that fails when the pin falls behind core's default branch by more than N keys or M days would
close it. Until then the sync happens whenever someone happens to look, which is what produced 179.
Still open on this issue
The 9 migratable
LogMessagesites (group 1) and the AI engine's activity views in Plugins areuntouched by the above.
rbuergi commentedon Sep 4, 2026
Group 1 is migrated — MeshWeaver#3305
The ratchet's inventory drops 21 → 18, and every line left in it is there because it cannot
carry a key rather than because nobody got to it. 56 keys added to
strings.{en,de}.json.Migrated, per the table in this issue:
MeshWeaver.GitSync/ActivityRunner.csMeshWeaver.Compiler.Pipeline/MeshNodeCompilationService.csMeshWeaver.Graph/StaticRepoImporter.csMeshWeaver.Compiler.Pipeline/NodeTypeCompilationActivity.csAppendLogsThe issue's prediction about
AppendLogheld exactly: no signature change was needed there.Three notes worth keeping, because they are the shape any future migration takes:
Append{Info,Warning,Error}now require a key. Anoptional parameter leaves the helper looking migrated while the next caller silently re-opens the
hole, and the ratchet cannot see past a helper to the callers behind it. The one caller that
genuinely cannot supply a key (Roslyn's own diagnostics) calls a separately named
AppendVerbatimWarning— so the un-keyed case is a decision someone wrote down.{suffix}argument.", repository created"spliced into a translated sentence is untranslated English inside German word order.
.WithKeyonto the constructor. The ratchet caught me building aLogMessageon oneline and chaining on the next — correctly, because English and key are then free to drift.
The two "Also outstanding" items — both measured, both blocked
Neither is deferred by choice, so #3305 deliberately does not carry a
Closes.1. The client mirror is blocked on #3305's merge sha, since the sync takes it as an argument
(
npm run sync:i18n -- --ref <sha>). Nothing red will remind anyone — the drift guard comparesagainst a pinned core commit — which is why it is written into #3305's body as an explicit hand-over.
2. The activity views are blocked on a core PIN BUMP, not on effort. Three Plugins surfaces still
render the raw English:
MeshWeaver.Blazor/Components/CompileProgressIndicator.razor.csmsgs[^1].MessageMeshWeaver.ContentCollections.Indexing.Graph/ContentIndexSettingsTab.csEsc(m.Message)MeshWeaver.Blazor.EntityViews/EditFormView.razor@logMessage— the record'sToString(), so this one is a display bug today, independent of languageThe fix is
m.Localize(viewerLocale)and it cannot compile there yet. Measured againstMeshWeaver.Plugins'
MW_PLATFORM_REF=b88be8a(2026-09-03):src/MeshWeaver.Data/LogMessageLocalizationExtensions.cs— theLocalizeseam #3236 introduced —does not exist at that commit. So this half follows the pin.
Core's own GitSync renderer is already wired (
GitHubSyncSettingsTab.ActivityHtmlcallsm.Localize(host.ViewerLocale())), so the GitSync transcript is live in German the moment #3305merges — it is the compile-progress and content-index surfaces that wait.
One thing this turned up that is not in the issue
Adding the
RunActivity(…, LogMessage title, …)overload makes an unqualified<see cref="ActivityRunner.RunActivity"/>CS0419 in any dependent — an error under-warnaserror. That is break shape 3 inCrossRepoPairGate.md: no gate sees it, and becauseMeshWeaver.Plugins pins core it would not have gone red on the merge, but on whichever later PR
moved the pin. Landed ahead of core as Systemorph/MeshWeaver.Plugins#1338.
🤖 Filed from Claude Code.
rbuergi commentedon Sep 4, 2026
Merged — MeshWeaver#3305 is
59352715f7f4f338bd76b1293f1d03de5760b024(14:21Z)Group 1 is done: ratchet 21 → 18, 56 keys in both catalogs, and the GitSync transcript renders in
the viewer's language from now on (core's own
GitHubSyncSettingsTab.ActivityHtmlalready resolveshost.ViewerLocale()).This issue stays open deliberately, with two items whose blockers are now measured rather than
assumed. Both are in MeshWeaver.Plugins.
1. The client mirror — handed over, ref available now
The merge sha above is the argument the sync needs:
Systemorph/MeshWeaver.Plugins#1337 is already open syncing the mirror, but it was cut before this
merge — it says "1301/1301, zero drift" while core is now at 1357. Noted on that PR. Nothing will
go red about it: the guard compares against the pinned commit in
catalog-source.json, not core main.2. The activity renderers — blocked on the pin, and here is the check
Three surfaces still render the raw English:
MeshWeaver.Blazor/Components/CompileProgressIndicator.razor.csmsgs[^1].MessageMeshWeaver.ContentCollections.Indexing.Graph/ContentIndexSettingsTab.csEsc(m.Message)MeshWeaver.Blazor.EntityViews/EditFormView.razor@logMessage— the record'sToString(), so this one printsLogMessage { Message = …, LogLevel = … }today, in any languageThe fix is
m.Localize(viewerLocale)and it cannot compile there yet. Measured, not inferred:b88be8ais MeshWeaver.Plugins'MW_PLATFORM_REF(2026-09-03T17:57Z); theLocalizeseam #3236introduced does not exist at it. So this half follows a pin bump, and #1333 / #1331 are the ones
moving it.
One thing this turned up that was not in the issue
Adding
RunActivity(…, LogMessage title, …)makes an unqualified<see cref="ActivityRunner.RunActivity"/>CS0419 in any dependent — an error under
-warnaserror. Break shape 3 inCrossRepoPairGate.md:no gate sees it, and since MeshWeaver.Plugins pins core it would not have reddened on this merge but
on whichever later PR moved the pin. Systemorph/MeshWeaver.Plugins#1338 fixes the three affected
crefs; a signature-qualified cref resolves against one overload as well as two, so it is correct both
before and after. It must land before any pin bump that carries core past
5935271— #1333 /#1331 both target earlier core commits, so they are unaffected as they stand.
🤖 Filed from Claude Code.
rbuergi commentedon Sep 4, 2026
Correction: the renderer half is UNBLOCKED as of Plugins#1333
My earlier comment said the three Plugins renderers could not compile because the
Localizeseam wasabsent at that repo's
MW_PLATFORM_REF. That was true when measured and is no longer true. Theblocker was removed by the pin move, not by anything on this issue.
Authoritative re-measurement, via
git ls-tree(which prints the path or nothing, and so cannotconfuse "absent" with "bad sha"):
b88be8a05wasMW_PLATFORM_REFwhen I measured, so the verdict was correct then.MW_PLATFORM_REFto7d644de95at 21:48Z, where the seamis present.
So
m.Localize(viewerLocale)compiles against the current pin today, and this half needs no pinbump and no dependency on #1338.
Two things worth recording, because both nearly cost more than the fix
1. A "blocked on the pin" verdict has a shelf life of one pin bump. Nothing on this issue changed
when the pin moved; the blocker simply expired silently. Any such verdict should name the sha it was
measured against — and be re-measured before it is repeated. Mine did name the sha, which is the only
reason the expiry was noticeable at all.
2.
git cat-file -e "$sha:$path" && echo PRESENT || echo ABSENTis not a safe check. It exitsnon-zero for an absent path, a bad sha, a missing object and bad quoting alike — one word covering
several states. On re-checking, that idiom handed me a false PRESENT for
b88be8a05and brieflyconvinced me my original, correct measurement had been wrong.
git ls-treeanswers with the path orwith silence and cannot be confused. (This is the shape written up in
Controls That Cannot Fail; it is mildly embarrassing to
have hit it inside the same day, which is rather the point of that page.)
Plugins carries four core-ish refs and they are not interchangeable:
uses:@sha(workflowdefinition),
MW_PLATFORM_REF(the core commitsrc/compiles against),MW_PLATFORM_SET/MW_IMAGE_DIGEST(the platform image), andclients/react/src/i18n/catalog-source.jsonref(thei18n mirror's baseline — not a build pin at all). They held the same value earlier on 2026-09-04
and diverged at 21:48Z, so "which pin?" is a real question even when the answer looks obvious.
Where that leaves this issue
59352715f.CompileProgressIndicator(msgs[^1].Message),ContentIndexSettingsTab(Esc(m.Message)),EditFormView.razor(@logMessage, which renders therecord's
ToString()and is a display bug in any language).npm run sync:i18n -- --ref 59352715f7f4f338bd76b1293f1d03de5760b024;note
catalog-source.jsonstill recordsb88be8a05, so the mirror is now behind on two counts.🤖 Filed from Claude Code.
rbuergi commentedon Sep 5, 2026
Complete — closing
All three parts are merged and verified on
mainin both repos, not merely merged:59352715f)Localizepresent in all three filesref=e02801615, en 1365 / de 1365A German viewer now reads the GitSync transcript, the compile pipeline's progress and failure lines,
the import's progress and content-sync ledger, and the compile-progress tail under a compiling
NodeType — in German. Every English fallback is character-for-character what it was, so rows written
before #3236 and any un-migrated writer render exactly as they did.
EditFormView.razorturned out to carry a plain display bug independent of language: it rendered theLogMessagerecord, so a validation error printedLogMessage { Message = …, LogLevel = …, Timestamp = … }to every viewer, presumably for as long as that block existed.Three things worth keeping, all of which cost time here
1. A "blocked at the pin" verdict has a shelf life of one pin bump. The renderer half was
correctly measured as unbuildable (
LogMessageLocalizationExtensionsabsent atb88be8a05), and thatverdict expired silently hours later when Plugins#1333 moved
MW_PLATFORM_REFto7d644de95, wherethe seam exists. Nothing announced it. Name the sha a blocker is measured against, and re-measure
before repeating it.
2.
git cat-file -e "$sha:$path" && echo PRESENT || echo ABSENTis not a safe check — it exitsnon-zero for an absent path, a bad sha, a missing object and bad quoting alike, and it produced a
false
PRESENThere.git ls-tree --name-only <sha> -- <path>prints the path or prints nothing.3. The mirror goes stale silently, and its own sync can be stale on arrival. #1337 synced earlier
the same day but was cut before #3305 merged, recording
1301/1301while core was at 1365 — andnothing goes red, because the guard compares against the pinned commit, not core
main. #1350'ssync was verified against core by an independent reader (0 missing, 0 extra, 0 value-diffs, both
languages) rather than by the script's own success line.
All three are written up in
Doc/Architecture/ControlsThatCannotFailandDoc/Architecture/Localization.🤖 Filed from Claude Code.