Skip to content

fix: a content verdict is final for the NODE that earned it, never for the partition (#4459, #4456) - #4467

Merged
meshweaver-cloud[bot] merged 5 commits into
mainfrom
fix/4459-content-verdict-is-per-node
Sep 16, 2026
Merged

meshweaver-cloud[bot] merged 5 commits into
mainfrom
fix/4459-content-verdict-is-per-node

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4459
Fixes #4456

Pairs-with: none — this diff removes no public type and no public member from src/. It ADDS one public record (FailedImport) and one init property (StaticRepoImportResult.FailedPaths); the only signature change is to the private StaticRepoImporter.WriteManifest.

Mirror-sync: npm run sync:i18n -- --ref <this PR's merged core sha> — two keys added (activity.import.itemRefusedBefore, activity.gitsync.failedNodes), no existing value re-worded.

Implementers: none — no member is added to any public interface or abstract class.

The root cause

On 2026-09-15 one file — Hosting/Deployment/Source/SelfUpdateRouting.cs — carried a literal NUL byte (0x00) inside a char literal. PostgreSQL cannot store a NUL in a text column, so the Hosting GitSync import refused that ONE row while forty others landed.

A pass whose failures were all deterministic earns ImportedWithContentErrors. That word means "every failure was a verdict about the bytes", not "everything failed" — but it was recorded on the content-addressed marker, whose whole job is to say something about the partition. Every later import read it and answered:

Imported Skipped — an earlier FULL import already recorded this exact content at
fingerprint bb9801101e859a21 (Hosting/_Activity/import-bb9801101e859a21), so the
partition was not re-read.

A claim about forty nodes, made on the evidence of one, and false: the import lost a node. Five Hosting NodeTypes stayed parked on CS0246 SelfUpdateRouting — among them Hosting/InstanceAction, so no instance action would run on the control instance at all (no Sample, Logs, HelmRelease or Roll) — and the operator's one repair, a re-import, was refused with that same sentence.

So #3146's rule is right and its GRANULARITY was the defect. Re-issuing a write the owner refused cannot help — that is real, and it cost memex-cloud 19 full passes in 3 h. But a content verdict is earned by a node, and recording a per-node fact as a per-partition verdict is what turned one unstorable byte into a frozen partition.

#4456 reads the same evidence as "applies modifications but not additions". It is the same single dropped file: nothing named it, so the shape of the loss had to be inferred. Its "a force update imports 0 nodes" is the same file being refused again — correctly, on bytes that genuinely cannot be stored.

The fix

1. The memory moves to the per-node import manifest.

WriteManifest may only claim what the run wrote. A refused node used to be recorded under its plain source token — the ledger asserting the partition holds content that is provably not in it, which is the marker's false claim one layer down, and it had to become honest before the marker's skip could be retired.

what happened to the node what the manifest now records
written its source token
refused, deterministically ! + its source token
failed, retryably nothing at all — which is what makes the next pass look again

The ! prefix is deliberately a prefix and not a schema change: a reader that does not know it compares !abc against the token it computes (abc), sees a mismatch, and re-evaluates the node — the safe direction, and exactly the behaviour before the field existed.

2. The skip moves with it. The import loop skips a node whose manifest entry is a refusal of this exact token — reported, no write attempted — and ImportedWithContentErrors stops being a fingerprint-recording outcome. The marker keeps it as a record; it is no longer a licence to answer Skipped without reading the partition. Force and Reconcile bypass the refusal memory exactly as they bypass the marker.

Both properties then hold at once: the failing write is not re-issued (no storm), and every other node is still evaluated (no freeze) — so a partition that has drifted from its marker can be repaired, which is the repair the whole-partition skip refused.

Cost: a partition holding a refused node re-evaluates on every trigger instead of short-circuiting — no node writes and no recompiles, just one subtree read, the prune evaluation and two bookkeeping writes. That is the same cost the marker already accepts for an absent or non-converged verdict.

3. An import that loses a node NAMES it. Every other thing a pass leaves behind already named its paths (written, pruned, blocked creates, held NodeTypes, refused content); failures were the one category reported as a bare number. StaticRepoImportResult.FailedPaths now carries each failed node with the write path's own reason and whether it was deterministic — #3101's argument, for nodes this time. The terminal summary names them, and the GitSync activity logs them as their own line.

4. A partial import no longer renders green. Every outcome line in GitHubActivityExtensions was written at Information whatever the import did, and RunActivity derives the activity's maxSeverity and terminal status from those levels — which is how the import that lost SelfUpdateRouting reported maxSeverity: Information, status: Succeeded. The level now follows the outcome: Failed → error, any ImportedWith… → warning.

The control

Both new behavioural tests use only pre-existing API, so the fix could be reverted and the suite still compiles.

Fix reverted (src/MeshWeaver.Graph/StaticRepoImporter.cs + the GitSync half back at main), test project rebuilt, run — 3 failed / 2 succeeded, with the defect printed verbatim:

outcome=ImportedWithContentErrors count=1 failed=1 writeRequests=3 written=[Rf9a6279eb/Good]
[StaticRepoImport] Rf9a6279eb already FAILED at b7b778c951b8aabb on content rules — skipping.
outcome=Skipped count=0 failed=0 writeRequests=0 written=[]
=== TEST FAILED: Did not expect value to be "Skipped" …

Fix restored, rebuilt, re-run — 6 succeeded / 0 failed.

suite result
ARefusedNodeDoesNotFreezeThePartitionTest + FailedImportIsNotRetriedAtTheSameFingerprintTest 6/6
MeshWeaver.Graph.Test (full) 1717/1717
MeshWeaver.Hosting.Test (full) 728/728
MeshWeaver.Documentation.Test (full) 476/476
LocalizationTest 60/60

dotnet build -c Release -warnaserror clean (0 Warning(s), 0 Error(s)) on MeshWeaver.Graph, MeshWeaver.GitSync, MeshWeaver.Documentation and each test project. check-record-signatures.py and check-type-forwards.py pass locally against origin/main.

🚨 #3146's own pins changed deliberately. They asserted the word Skipped; they now assert WriteRequests == 0 — the quantity that storm was actually made of (≈425 failing upserts plus a compile, per pass). A word can stay right while the writes come back; a request count cannot.

🚨 What this deliberately does NOT fix

Both issues list a third item, and it is the same one: a NodeType parked on a symbol whose source the import could not write still fails on CS0246 for a file that is plainly in git, with nothing in that error connecting it to the import. Split out as #4469 rather than left implied — closing two issues while one of their listed defects stays live, without saying so, is the failure mode where an answer reads like a pass.

What changed is the other direction: an operator reading the sync activity now learns which file is missing and why, and the activity is no longer green. The facts #4469 needs are already recorded here (the manifest's !<token> entries say which declared nodes a partition is missing), so it is a wiring problem, not a measurement one — with the granularity trap noted in it: scoping a compile refusal to a whole partition would re-create exactly the over-broad reading this PR removes.

Also unchanged on purpose: a remembered refusal is still reported as a failure, so MayAdvanceBaseline still holds the git baseline (the mesh genuinely is not at that commit), and VerdictIsFinal is untouched, so #3945's cheap-delivery path is unaffected.

Work products

🤖 Generated with Claude Code

…r the partition (#4459, #4456)

One literal NUL byte in `Hosting/Deployment/Source/SelfUpdateRouting.cs` took every
instance action on memex.systemorph.com offline for an evening. Postgres cannot store a
NUL in a text column, so the import refused that ONE row while forty others landed. The
pass therefore earned `ImportedWithContentErrors` — which means "every failure was
deterministic", not "everything failed" — and the content-addressed marker recorded that
verdict FOR THE PARTITION. Every later import answered:

    Skipped — an earlier FULL import already recorded this exact content at fingerprint
    bb9801101e859a21 … so the partition was not re-read

a claim about forty nodes made on the evidence of one, and false: the import lost a node.
Five Hosting NodeTypes stayed parked on CS0246 SelfUpdateRouting — among them
Hosting/InstanceAction, so no Sample, Logs, HelmRelease or Roll would run at all — and
the operator's one repair, a re-import, was refused with that same sentence.

#3146's rule is right; its GRANULARITY was the defect. A content verdict is earned by a
NODE, so the memory of "these bytes cannot be written" moves to the per-node import
manifest:

- WriteManifest may only claim what the run WROTE. A refused node used to be recorded
  under its plain source token — the ledger asserting the partition holds content that is
  provably not in it, the marker's false claim one layer down. A deterministic refusal is
  now recorded as `!<token>`; a retryable failure leaves no entry at all, which is what
  makes the next pass look again. The prefix is forward/backward compatible: a reader
  that does not know it sees a token mismatch and re-evaluates — the safe direction.
- The import loop skips a node whose manifest entry is a refusal OF THIS EXACT TOKEN,
  reported with no write attempted, so #3146's storm stays prevented per node.
- `ImportedWithContentErrors` stops being a fingerprint-recording outcome. The marker
  keeps it as a record; it is no longer a licence to answer Skipped without reading the
  partition, so a later import can create, repair and prune again. Force and Reconcile
  bypass the refusal memory exactly as they bypass the marker.

And an import that loses a node now NAMES it. Every other thing a pass leaves behind
named its paths — written, pruned, blocked creates, held NodeTypes, refused content;
failures were the one category reported as a bare number. `FailedPaths` carries each
failed node with the write path's own reason and whether it was deterministic, the
terminal summary names them, and the GitSync activity logs them on their own line. That
activity's outcome line was also written at Information whatever the import did, and
RunActivity derives the activity's severity and terminal status from those levels — which
is how an import that dropped a source node reported maxSeverity Information and status
Succeeded. The level now follows the outcome.

Control: with the fix reverted, `AContentRefusalOnOneNode_DoesNotStopTheNextImportRepairingTheRest`
and `TheRefusedWriteIsNotReIssued_AndTheIncompletenessIsStillReported` fail on
`outcome=Skipped count=0 failed=0 writeRequests=0`, the log line naming the defect
verbatim ("already FAILED at … on content rules — skipping"); restored, 6/6 pass.
MeshWeaver.Graph.Test 1717/1717, MeshWeaver.Hosting.Test 728/728,
MeshWeaver.Documentation.Test 476/476, LocalizationTest 60/60.

#3146's own pins move from asserting the word "Skipped" to asserting WriteRequests == 0 —
the quantity that storm was actually made of, and one a rename cannot make vacuous.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 16, 2026 05:13
rbuergi and others added 2 commits September 16, 2026 07:16
…y what is true of both

A content verdict can refuse an UPDATE as easily as a CREATE: a validator rule, an
invalid path or an RLS denial on a node that already exists leaves that node PRESENT and
STALE, not missing. Three of the new sentences said "is NOT in the mesh", which is right
for the incident that prompted them and wrong for the other half of the cases — and an
operator told a node is absent goes looking for the wrong thing.

They now say the mesh does not hold this CONTENT, which is exactly what is true in both:
the import's own summary, the per-node ⚠ line, the GitSync activity line, and both
catalogs.

MeshWeaver.Graph.Test import suites 43/43, LocalizationTest 60/60,
`-c Release -warnaserror` clean on MeshWeaver.Graph and MeshWeaver.GitSync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing actually returns

The argument for the '!' sigil rests on the token's alphabet, so the comment stating
that alphabet has to be checkable. ComputeNodeToken ends in Convert.ToHexString, which
returns 0-9A-F; the comment said lowercase. The argument is unaffected (no hex of either
case contains '!'), but a reader verifying it would have found the stated fact wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s at nothing (#4469)

Both issues list a third item: a NodeType parked on a symbol whose source the import
could not write fails on CS0246 for a file that is plainly in git, with nothing connecting
the error to the import that produced the state. This change fixes the other direction —
the sync activity now names the missing file and is no longer green — and does not reach
that one. Closing two issues while one of their listed defects stays live, without saying
so, is the failure mode where an answer reads like a pass.

Split out as #4469, with the note that the facts are already recorded (the manifest's
!<token> entries say which declared nodes a partition is missing) and the warning that a
fix must be scoped to the types that actually reference the missing node — refusing to
compile a whole partition would re-create the over-broad granularity this change removes.

Also states what is deliberately unchanged about the baseline: a remembered refusal is
still a failure, so MayAdvanceBaseline still holds (the mesh is not at that commit), and
VerdictIsFinal is untouched so the delivery stays cheap.

MeshWeaver.Documentation.Test 476/476 (assembly re-embedded — verified newer than the
edited page, never --no-build over a stale one).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

An unresolved critical GitSync finality issue and additional moderate findings remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR moves deterministic import refusals from partition-wide markers to per-node manifests, preventing one failed node from freezing a partition.

Changes:

  • Records refusals per node and reports failed paths.
  • Updates GitSync severity and localization.
  • Adds regression tests and documentation.
File summaries
File Review summary
test/MeshWeaver.Graph.Test/FailedImportIsNotRetriedAtTheSameFingerprintTest.cs No final findings.
test/MeshWeaver.Graph.Test/ARefusedNodeDoesNotFreezeThePartitionTest.cs Moderate (2): use a parent-child listing rather than an exact-path existence check. Nit (2): use an immutable collection.
src/MeshWeaver.Messaging.Hub/Localization/strings.en.json No final findings.
src/MeshWeaver.Messaging.Hub/Localization/strings.de.json No final findings.
src/MeshWeaver.Graph/StaticRepoImporter.cs Critical (3): content-error attempts can still persist partition-level finality and cause webhook skips. Moderate findings cover legacy manifest entries, failed-update reporting, stale-node messaging, and duplicate-path handling. Nit (2): localize the failed-path summary.
src/MeshWeaver.GitSync/GitHubActivityExtensions.cs Moderate (2): failed updates should not be reported as absent nodes.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-16-one-file-the-import-cannot-write-no-longer-freezes-the-whole-space.md No final findings.
src/MeshWeaver.Documentation/Data/Architecture/ImportMarkerRecordsConvergence.md No final findings.
src/MeshWeaver.Documentation/Data/Architecture/AContentVerdictIsPerNode.md Nit (1): align the documentation with the existing StaticRepoImport.md contract.
src/MeshWeaver.Documentation/Data/Architecture.md No final findings.
Review details

Suppressed comments (4)

src/MeshWeaver.Documentation/Data/Architecture/AContentVerdictIsPerNode.md:101

  • This new rule contradicts the existing StaticRepoImport.md contract, whose outcome table still says ImportedWithContentErrors leaves a Failed marker that skips the next same-fingerprint import. Since this page says the marker no longer licenses that skip, the canonical import documentation also needs to be updated; otherwise operators and implementers get two different behaviors for the same outcome.
**2. The skip moves with it.** The import loop skips a node whose manifest entry is a refusal *of
this exact token* — reported, with no write attempted — and `ImportedWithContentErrors` stops being
a fingerprint-recording outcome. The marker keeps it as a **record** of what the pass found; it is
no longer a licence to answer `Skipped` for the partition without reading it.

src/MeshWeaver.Graph/StaticRepoImporter.cs:3215

  • Existing manifests written before this change store failed nodes as plain tokens. This loop only recognizes the new !token; the later ordinary prevToken == token branch then treats an old failed update to an existing node as successfully imported, so the first post-upgrade pass never retries it. Add a one-time manifest migration/versioned success encoding (or otherwise invalidate old plain entries) before allowing the ordinary skip.
                    g => PartitionSourceFingerprint.ComputeNodeToken(g.First(), opts),
                    StringComparer.OrdinalIgnoreCase);
            var builder = map.ToBuilder();
            foreach (var failure in failures)
            {

src/MeshWeaver.Graph/StaticRepoImporter.cs:2127

  • This terminal summary has the same factual problem as the GitSync line: a failed WriteOne may be an update, in which case an older node is still present. Saying the path is "NOT in the mesh" and that every reference cannot compile misleads operators about what repair is needed. Use neutral failed-write wording or distinguish failed creates from failed updates.
                            // leave behind names its paths; a failure was the one reported as a bare
                            // number, and "ImportedWithContentErrors (41 node(s))" is what an operator
                            // was given while a NodeType parked on the one file that did not land.
                            // Bounded exactly like blockedNote/refusedNote so a pathological source

src/MeshWeaver.Graph/StaticRepoImporter.cs:3205

  • count.Failures can include a failed earlier attempt for a path that a later duplicate source entry successfully wrote; ImportWriteOrder deliberately preserves those duplicates, but both this map and the failure-token map use g.First(). The manifest then retains !<first-token> even though the final node landed from the later entry, so every subsequent import re-reports the stale refusal and re-writes the successful duplicate. Derive the manifest token from the final attempted/written duplicate (or resolve duplicate paths before applying refusal memory), rather than unconditionally taking the first.
        // 🚨 The token comes from the SOURCE node, never from `map`: the git-diff carve-out above
        // may just have written the PREVIOUS run's token into this entry (a node absent from the DB
        // is attempted even when it is outside the diff scope), and a refusal recorded against the
        // wrong token would be re-opened on the next pass — or, worse, honoured for bytes nobody
        // refused.
        if (failures.Count > 0)
        {
  • Files reviewed: 10/10 changed files
  • Comments generated: 6
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +233 to +240
/// <para>🚨 <b>It is NOT a fingerprint-recording outcome (issue #4459).</b> The marker keeps it
/// as a RECORD of what the pass found; it is no longer a licence to answer <c>Skipped</c> for
/// the partition without reading it. A content verdict is earned by a NODE — one file that
/// Postgres, a validator or RLS refuses — and a pass earns this word on the evidence of that one
/// node even when forty others landed, so reading it as "this whole partition cannot import"
/// froze a partition out of the mesh over a single unstorable byte. The refusal is remembered
/// per node instead, in the import manifest (see <c>StaticRepoImporter.RefusedTokenPrefix</c>),
/// which keeps #3146's storm protection — the failing write is not re-issued — without the
Comment on lines +504 to +506
return new LogMessage(
$"⚠ {result.FailedPaths.Count} node(s) did NOT land and are NOT in the mesh — "
+ $"anything referencing them will not compile: {paths}",
Failure: new FailedImport(path, RememberedRefusalReason, true),
Log: new LogMessage(
$"⚠ {path} was REFUSED at this exact content by an earlier import "
+ "and is NOT in the mesh — the same bytes break the same rule, so "
Comment on lines +235 to +239
var meshService = Mesh.ServiceProvider.GetRequiredService<IMeshService>();
await Observable.Interval(50.Milliseconds()).StartWith(0L)
.SelectMany(_ => meshService
.Query<MeshNode>(MeshQueryRequest.FromQuery($"path:{path}"))
.Take(1))
Comment on lines +2123 to +2127
var failedNote = count.Failures.Count > 0
? $" ⚠ {count.Failures.Count} node(s) that did NOT land — they are NOT in "
+ "the mesh, so anything referencing them will not compile: "
+ string.Join("; ", count.Failures.Take(BlockedPathsNamed)
.Select(f => $"{f.NodePath} ({f.Reason})"))
{
public string Partition => partition;
public bool Versioned => false;
public List<MeshNode> Nodes { get; init; } = [];
@rbuergi
rbuergi disabled auto-merge September 16, 2026 05:26
@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

526 tests   526 ✅  48s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit 3a40180.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

263 tests   263 ✅  2m 34s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit 3a40180.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

1 344 tests   1 344 ✅  3m 4s ⏱️
    2 suites      0 💤
    2 files        0 ❌

Results for commit 3a40180.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    3 files      3 suites   4m 59s ⏱️
1 718 tests 1 718 ✅ 0 💤 0 ❌
1 719 runs  1 719 ✅ 0 💤 0 ❌

Results for commit 3a40180.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    4 files      4 suites   5m 0s ⏱️
2 246 tests 2 246 ✅ 0 💤 0 ❌
2 250 runs  2 250 ✅ 0 💤 0 ❌

Results for commit 3a40180.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 653 tests   1 653 ✅  8m 58s ⏱️
    3 suites      0 💤
    3 files        0 ❌

Results for commit 3a40180.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   14 files     14 suites   25m 26s ⏱️
7 750 tests 7 750 ✅ 0 💤 0 ❌
7 755 runs  7 755 ✅ 0 💤 0 ❌

Results for commit 3a40180.

♻️ This comment has been updated with latest results.

…he existence gate was a point query

Four findings from the Copilot review, each taken on its merits.

1. StaticRepoImport.md's outcome table still read "ImportedWithContentErrors → Failed →
   **skips**, logging the recorded verdict". That is the contract this change replaces, so
   the canonical import page and the new one were promising opposite behaviour for the
   same outcome — exactly the two-homes-drift failure. The row now says "re-imports, but
   issues no write for the refused node", with the granularity argument and a pointer.

2. The test's existence gate was `path:{node}` — a POINT QUERY used to decide existence,
   which the CQRS rules forbid outright: a point read of an absent node is a routing
   NotFound that terminates the stream and opens the storm-breaker on that path. It is now
   a `scope:children` listing of the parent, `.Complete()`, shared by the wait and by a new
   assertion.

   🚨 That new assertion is the point. Waiting for a node to LEAVE a listing is satisfied
   instantly by a listing that never contained it, so the wait had to be proven able to
   fail: the test now asserts the listing SEES `Good` before deleting it. Measured — with
   the behavioural fix disabled the suite still fails on the real assertion ("Did not
   expect Skipped"), never on the new guard, so the listing genuinely observes the node.

3. A path that ALSO landed is no longer recorded as refused. A source may carry duplicate
   entries for one path (ImportWriteOrder preserves them), so one attempt can fail while
   another succeeds, and the manifest's own token comes from `g.First()` — which need not
   be the entry that won. The written set is now the authority, so a refusal is never
   stamped over a node that is in the mesh.

4. The fixture's node list is an ImmutableList, per the collections policy.

Two findings were already addressed by 1cb5046, which Copilot did not see (it reviewed
09f3fe0): the "NOT in the mesh" wording in the GitSync line and the per-node ⚠ line. A
refused UPDATE leaves an older node present, so all three sentences already say the mesh
does not hold this CONTENT.

Also names, in the doc, the residue the review found and I agree with: a manifest written
before this change stores a refused UPDATE under its plain token, so the incremental skip
still passes over it. Pre-existing — it is the very lie this change removes going forward —
and self-limiting, since any edit to that file moves its token.

Graph 1717/1717, Hosting 728/728, Documentation 476/476 (assembly verified re-embedded).
`-c Release -warnaserror` clean on MeshWeaver.Graph and every test project touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rbuergi

rbuergi commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Review response

Thanks — two of these were real and are fixed, two were already fixed in a commit you did not see, and the critical one I am declining with reasoning. Detail per finding.

✅ Fixed — StaticRepoImport.md still promised the old contract (suppressed comment, AContentVerdictIsPerNode.md:101)

Correct, and the most valuable finding here. The canonical page's outcome table still read "ImportedWithContentErrors → Failed → skips, logging the recorded verdict" — the two pages were promising opposite behaviour for the same outcome, which is the two-homes-drift failure. The row now reads "re-imports, but issues no write for the refused node", with the granularity argument and a pointer. (3a40180c6f)

✅ Fixed — the test's existence gate was a point query (ARefusedNodeDoesNotFreezeThePartitionTest.cs:239)

Correct, and it is a house-rule violation independent of this test: existence of a specific path is not a valid query use, because a point read of an absent node is a routing NotFound that terminates the stream and opens the storm-breaker on that path. It is now a scope:children listing of the parent, .Complete().

🚨 And your underlying worry — "this wait can complete while the import still sees Good" — deserved more than a shape change, because the new shape has the mirror-image vacuity: waiting for a node to leave a listing is satisfied instantly by a listing that never contained it. So the test now asserts the listing sees Good before the delete, and that guard is measured, not assumed: with the behavioural fix disabled the suite still fails on the real assertion (Did not expect value to be "Skipped"), never on the new guard.

✅ Fixed — duplicate source paths (suppressed comment, StaticRepoImporter.cs:3205)

Correct. A path that also landed is no longer recorded as refused: the written set is now the authority, so a refusal can never be stamped over a node that is in the mesh. Good catch on g.First() not necessarily being the entry that won.

✅ Already fixed in 1cb50463db — "NOT in the mesh" vs a failed UPDATE (GitHubActivityExtensions.cs:506, StaticRepoImporter.cs:1663, and the suppressed comment at 2127)

You reviewed 09f3fe0917; this landed in 1cb50463db, before your review was submitted. The reasoning is the same as yours — a content verdict can refuse an UPDATE as easily as a CREATE, leaving the node present and stale, and telling an operator it is absent sends them looking for the wrong thing. All three sentences and both catalog values now say "the mesh does NOT hold this content", which is what is true in both cases.

✅ Named, not fixed — legacy manifest entries (suppressed comment, StaticRepoImporter.cs:3215)

You are right that a manifest written before this change stores a refused update under its plain token, so the incremental skip still passes over it. But this is pre-existing, not introduced: it is precisely the lie this change removes going forward. Before this PR that same entry existed and that same skip applied.

I have named it in the doc rather than migrating, for two reasons. A migration would have to decide, for every pre-existing entry, a fact no pre-existing entry records — there is nothing stored that separates "written" from "refused" in the old encoding. And it is self-limiting in the same way the marker's absent-verdict residue is: any edit to that file moves its token and the node is evaluated again. The create case needs nothing at all — a refused create leaves no node, and the incremental skip already requires the node to be present.

❌ Declining — VerdictIsFinal / the webhook path (StaticRepoImporter.cs:240)

This one I think is wrong, and the suggested change would re-create the defect VerdictIsFinal was built to fix.

SkipReason's last arm skips only when LastAttemptedCommitSha == headSha — the same commit, re-built (a flake re-run, a manual re-dispatch). A green build of a new commit carries a different sha, does not match, and re-imports the whole source unscoped; the arm's own doc says so ("the next new commit re-attempts"). So it cannot produce the freeze this PR is about, which was permanent at every future commit.

And at the same commit, the delivery it drops is one that provably changes nothing under this PR: the refused node is skipped by the per-node memory, every other node's token is unchanged and present, so the pass would issue zero writes. That is exactly what #3945 measured the cost of — Essentials/_GitSync paying a full git fetch --depth 1 of its entire repository on every green build for 31 days and 436 commits, while 33 siblings on the same webhook were skipped for free. Making content-error attempts non-final would restore that.

The repair the issue actually names — an operator pressing Update — does not consult this predicate at all (ReimportAtCommit, noted in the same doc block), and unblocking it is what this PR does.

One thing your finding did make me check, and it is worth recording: the change makes the second pass report Failed = 1 where it previously returned Skipped with Failed = 0. MayAdvanceBaseline requires Failed == 0, so the new behaviour holds the git baseline where the old one did not obstruct advancing it — the safer direction, and correct, since the mesh genuinely is not at that commit.

↩︎ Not changed — failedNote is unkeyed (StaticRepoImporter.cs:2127)

failedNote is appended to the importer's composed terminal summary, which is unkeyed in its entirety and deliberately so: UnkeyedActivityLogMessageRatchetGuard carries a budget of 3 for this file and its own doc names "a composed multi-clause import summary" as a case no catalog can carry. prunedNote, blockedNote, refusedNote and heldNote are all in that same sentence.

The surface an operator actually reads is keyed: the GitSync activity line this PR adds is activity.gitsync.failedNodes, in both catalogs, with the paths as arguments — and the per-file ⚠ lines were already activity.import.itemFailed. So the German viewer does get a German sentence around the paths; what stays English is the internal bookkeeping summary on the attempt node, which was already so.

@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 16, 2026
@rbuergi

rbuergi commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Disposition for the one review finding the agent's table did not cover — recording it rather than letting it merge silently.

Copilot on StaticRepoImporter.cs:2133: "failedNote is persisted in the import activity as an unkeyed LogMessage, so it renders …"

Accepted as-is, deliberately. failedNote is one of seven sibling fragments interpolated into a single activity summary — preservedNote, claimedNote, prunedNote, failedNote, blockedNote, refusedNote, heldNote — and every one of the other six is unkeyed today. The new fragment follows the shape of the line it joins. Keying one of seven would leave a summary that is half-localised and read worse in both languages than either consistent choice.

Localising that whole summary is a real and separate change (one key with seven optional segments, or seven keys composed in order), and it is not this PR's subject. Where this PR adds genuinely new user-facing text it did localise — activity.import.itemRefusedBefore and activity.gitsync.failedNodes, both in strings.en.json and strings.de.json, with Mirror-sync: declared in the body.

Merging on that basis: 36/36 checks green, mergeable_state: clean, and the other six findings are fixed or declined with reasoning in the thread above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants