Repository navigation
Education and Plugins still carry all three --resolve defects, and no guard compares a vendored gen-manifests.py to the canonical #4777
Description
#4775 fixes three ways gen-manifests.py --resolve reported ✓ … the merge can be committed over a question it had not answered, and the four byte-identical satellite copies are re-copied with it (Reinsurance#226, Crm#131, SocialMedia#209, Manufacturing#104).
Two copies are left carrying all three, and neither can be re-copied. Measured 2026-09-19 on origin/main:
| repo | md5 | lines | config? | still = (git(...) or "") |
suffix classifier | --exclude-standard |
|---|---|---|---|---|---|---|
| core canonical (before #4775) | 6cc07aa32f23… |
1404 | n/a | yes | 3× | yes |
| Reinsurance / Crm / SocialMedia / Manufacturing | 6cc07aa32f23… |
1404 | yes | yes | 3× | yes |
| MeshWeaver.Education | 1c35825e36b1… |
963 | no | yes (:700) |
3× | yes |
| MeshWeaver.Plugins | a0b4fa03603a… |
1215 | yes | yes (:856) |
3× | yes |
All three defects, same shapes, different line numbers. So the blast radius was six repositories, not five, and #4775 closes four of them.
Why a re-copy does not work for these two
Education has no scripts/gen-manifests.config.json — it predates the centralization and still carries a module-level SKIP. The canonical refuses outright rather than guessing:
✗ gen-manifests: …/scripts/gen-manifests.config.json not found. This is the PLATFORM's canonical
script and it does not guess which top-level directories are packages…
That refusal is correct (self-test case 5 exists to keep it), so adopting the canonical here means declaring the config first, keeping it equal to validate-repos.py's SKIP. Education's validate-repos.py also enumerates through is_course_dir(d) rather than a skip set, so the equality that check-skip-sets.py enforces elsewhere has to be established, not assumed.
Plugins has the config, but its vintage loads project-closure.py eagerly at module import (_CLOSURE_SPEC.loader.exec_module at module scope, :73), while the canonical loads it lazily and only for a repo that declares hashModuleSources: true. That is a behaviour difference, not a format one.
The good news for Plugins: adoption is version-neutral
The load-bearing risk in any adoption is that a different hashing surface moves a moduleVersion, which is a release event (#878: a version that does not move is built, shelved and fetched by nobody). Measured by loading both modules against Plugins' real main tree:
Plugins: old enumerates 61, canonical enumerates 60, same=False
only-old=['.agents'] only-new=[]
moduleVersion mismatches: 0 of 61
Zero of 61 versions move. The single enumeration difference is .agents — an untracked dot-directory present on disk and tracked by nothing — which is exactly the case the canonical's dot rule was added for: the older vintage would demand a manifest.lock for a scratch directory. So the port is a code reconciliation, not a release.
Nothing guards these copies
check-resolver-copy.py compares each repo's vendored scripts/resolve-platform.py against the canonical (AST, docstrings stripped, RED_FROM 2026-09-15) — and its own header names gen-manifests.py as the precedent for why: "The last time one script was vendored per repo the six copies drifted to five vintages and each fix landed in one of them (gen-manifests.py, #1426)."
The precedent still has no guard. Three vintages today; this fix reduces it to three (canonical+4, Education, Plugins) only because four were byte-identical by luck, and nothing keeps them that way.
A second subject for check-resolver-copy.py — it already takes --canonical as a parameter and compares ASTs, so COPY_PATH becoming a list is most of the work — would turn "did the fix reach every copy?" into a question CI answers instead of one a person has to remember to ask.
What would close this
- Port the three fixes into Plugins (version-neutral, measured above) — or adopt the canonical after reconciling the eager/lazy closure load.
- Declare Education's config, keep it equal to its
validate-repos.pyenumeration, then adopt the canonical. - Extend
check-resolver-copy.pytoscripts/gen-manifests.py, advisory-then-red on a dated flip, as it did for the resolver.
Until (3), a fix to the canonical reaching every copy is a manual sweep, and this issue is the evidence that manual sweeps are how a fleet gets to five vintages.
Activity
rbuergi commentedon Sep 21, 2026
Triage 2026-09-21: left open,
chore, no severityBlocked on two different things, and they are not the same kind of blocked:
scripts/gen-manifests.config.json(it predates the centralization and still carries a module-levelSKIP, and the canonical refuses rather than guessing), and Plugins' copy has diverged to 1215 lines against the canonical's 1404. Each is a per-repo port, in a repo core cannot commit to.gen-manifests.pyto the canonical" — is core work and is not blocked on anything but someone doing it. That half is the durable one: the four byte-identical satellites were re-copied with --resolve must not report a merge it could not check #4775 and could silently diverge again tomorrow, and the precedent is already in the tree (check-node-test-launcher.pyreds a vendoredrun-node-tests.py;check-resolver-copy.pyexists for the same class of question).The measurement stands as written — all three
--resolvedefects present in both remaining copies, at different line numbers (Education:700, Plugins:856), so the blast radius was six repositories and #4775 closed four.🚨 One caution for whoever writes the guard, because it is the trap this issue is itself an instance of: a guard that compares declarations rather than effective behaviour passes having checked nothing. That is exactly #4774, where
check-skip-sets.pycomparesvalidate-repos.SKIP == gen-manifests.skip_set(root)— equal declared sets, different effective enumerations, green. Agen-manifests.pycopy guard should therefore compare the file (a hash, or a shrink-only divergence ratchet naming what differs), not a list of features it is supposed to have.rbuergi commentedon Sep 22, 2026
Item 3 is #5195; items 1 and 2 are satellite acts, re-measured and named
Re-measured today on every satellite's
origin/main(never a working tree): the table holds in direction as well as distance. Education (cf0d195) and Plugins (bdfe4ce) carry all three defects at the quoted lines; the four re-copied are byte-identical to the canonical at the #4775 merge and three canonical commits behind today's (138 code lines each). Andcheck-resolver-copy.pycompares nothing today —scripts/resolve-platform.pyis absent on all six mains — so the "no guard" claim holds.#5195 (core, item 3): the guard gains a subject table;
--subject gen-manifestscompares the vendored copy to the canonical the lane already fetches at its scripts ref, with the same function-level report. Advisory by declaration, no flip date — and that is the decision, stated so it is not re-derived: the resolver's dated flip was safe because a resolver copy is deletable;node-repo-resolve-locks.ymlrefuses a repo withoutscripts/gen-manifests.pyand runs THAT copy for--resolve, so a red on drift is a red on every canonical change (four in the three days after #4775) for a file nobody may delete. The flip's precondition is the resolve lane resolving with the platform's canonical, after which a copy is optional and the subject gets itsred_from.--red-fromis refused for the subject so a workflow argument cannot schedule it by accident. What is red on every run: an unreadable canonical, an unparsable copy. A pin older than the change printsNOT comparedby name, never a silent skip. On its next validate run each satellite will print: Education706 code line(s) differ, Plugins507(incl.copy LACKS _unmerged_paths), the four138.Handed back:
scripts/gen-manifests.py(:856still = (git(...) or "");:781/:782/:878suffix classifier;:889--exclude-standardwith no--ignoredcompanion) or adopt the canonical after reconciling the eagerproject-closure.pyload at:70-73. Version-neutral per the issue's own measurement (0 of 61moduleVersions move). Note the copy also carries a fix the canonical lacks (addf16f45, "must not fail on a lock the merge DELETED") — a distance guard cannot see that direction, which is why the report names functions.scripts/gen-manifests.config.jsonequal tovalidate-repos.py's enumeration, flipcentralized-gen-manifests: true, adopt the canonical.--resolve; then the subject's flip.rbuergi commentedon Oct 9, 2026
Triage (severity backfill per Doc/Architecture/IssueTaxonomy): relabelled chore → bug, sev:M — the Education and Plugins copies of
gen-manifests.py --resolvestill print✓ … the merge can be committedover a question they did not answer (re-measured 2026-09-22 in the thread): a false pass in developer tooling. M, not H: developer-facing, with the canonical script as the workaround.rbuergi commentedon Oct 9, 2026
Closing. Both remaining copies are fixed on their
main, and the guard exists.MeshWeaver.Education. Systemorph/MeshWeaver.Education#390 merged as
e2c9070ba1. It ports all three --resolve must not report a merge it could not check #4775 fixes into this repo's own copy, the onenode-repo-resolve-locks.ymlruns for--resolve:_unmerged_pathsreturnsNonewhen git cannot answer, and the closing check refuses on it;plugin_dirs()(exactly<module>/manifest.lock);hash_files()reads block the resolve, and ignored files outside every module do not.hash_files()is untouched (--check✓, no version moves). There are three new self-test cases, and each was proven to fail (exit 1, naming its assertion) with only that fix reverted.MeshWeaver.Plugins. Already fixed. Its
scripts/gen-manifests.pyon main0b8f875478now has the canonical's shape:still = unmerged_paths(root),_unmerged_paths, andls-files --others --ignored --exclude-standardbeside the standard listing. It differs from core's canonical495a150668by 74 diff lines, all in one later canonical change: "a published tag wins over a trunk lock re-claiming its number". None of the three--resolvedefects remains.The guard. chore(ci): the resolver guard compares scripts/gen-manifests.py too — advisory by declaration, its flip is a change not a date (#4777) #5195 (merged 2026-09-22) compares a vendored
gen-manifests.pyto the canonical. It is advisory by declaration; the reason is on that PR.Not done here: adopting the canonical in Education. That needs
scripts/gen-manifests.config.jsondeclared equal tovalidate-repos.py'sis_course_direnumeration first. It is optional now that the copy is correct.