Repository navigation
Cross-repo policy: gate on MATLAB drift, and state the rule in one place #211
Description
Cross-repo proposal. Extends VH-Lab/NDR-python#21, which settled the bridge status vocabulary across NDI-python, NDR-python and DID-python. That one answered what the labels mean. This one answers what happens when the MATLAB side moves underneath them — the question #21 did not touch.
Filed here because #21 named NDI-python as the vocabulary's home repo, and this is the same class of rule.
Timing: not urgent for #210 (merged). It should land before anyone acts on #191, which is the NDI-python-shaped half of this question.
The problem: three repos, three different answers, and one of them is silence
| repo | where it is documented | what it does |
|---|---|---|
| NDR-python | spec §5 + §5a "Detecting Upstream MATLAB Changes" | gates — CI red when a bridged MATLAB file moves |
| DID-python | PORTING_INSTRUCTIONS.md, "Checking for Drift" + check table |
detects, deliberately does not gate |
| NDI-python | nothing | nothing. #209 settled commit-vs-blob and stopped there |
DID states its reasoning plainly, and it is a fair argument:
CI runs
file,hash,status,memberandmissingas gating checks, anddriftnon-gating: drift turns red when DID-matlab moves, which no commit here causes and none can fix until someone does the port.
There is also a coupling defect. NDR's §5 hardcodes a description of a different repo's policy — "This is stricter than NDI-python, which accepts any commit" — which goes stale the moment NDI-python changes. That is the same restate-it-in-three-places pattern that produced 35 blob hashes and cost us #209.
The decision: gate
These repos exist to mirror MATLAB. status is a claim about correspondence, not a task tracker: "ported" asserts this Python matches that MATLAB. When MATLAB moves, that assertion is no longer known to be true, and it stays unknown until a human looks at the diff. Red is the honest state.
DID's objection — "no commit here caused it" — is true and does not change the conclusion. The bridge's job is not to report whose fault a discrepancy is; it is to stop the repo asserting a correspondence nobody has verified.
But state it as DRIFT, not as "equals the latest commit"
NDR's §5a prose describes drift ("when MATLAB edits a bridged file CI goes red"), while its normative sentence states equality: "must be the latest commit touching its MATLAB file." Those are different rules. Measured on NDI-python main (f241b4e) against NDI-matlab main:
entries with a matlab_path:
361 carry a matlab_last_sync_hash (360 resolvable)
147 carry NO hash -> can never show drift
of the 360 checked:
60 DRIFTED (git log <hash>..HEAD -- <path> is non-empty)
163 fail an equals-latest rule (+103 beyond drift)
Those 103 are batch-sync entries: the hash is a repo-wide commit from when a batch was examined, and the file has not changed since. Nothing drifted; the claim "examined, and MATLAB has not moved" is true. Equals-latest would fail all 103 for bookkeeping rather than correctness, and the only way to clear them is to rewrite a correct record — which teaches contributors that the check is noise.
Drift is the rule that actually expresses the principle. Equality only coincides with it in a repo that never records batch-sync commits, which is why the flaw has not bitten NDR.
Proposed normative wording (drift condition, NDR's §5a resolutions, which are already right):
matlab_last_sync_hashrecords the commit an entry was last examined against. An entry drifts when NDI-matlab has commits touching itsmatlab_pathafter that hash:git log <hash>..HEAD -- <matlab_path>is non-empty. Drift fails CI. Clear it by either
- reviewing the MATLAB diff, porting the behavioural change, and bumping the hash; or
- bumping the hash, having read the diff and found nothing the Python side must follow (comment-only, formatting, an analyzer fix). Where the entry already carries a
status, say so in itsdecision_log; a regular port needs no prose — see below.Never bump a hash without doing one of those — an unexamined bump asserts an examination that did not happen, which is the failure the field exists to prevent.
The check requires a non-shallow MATLAB checkout; a shallow clone collapses history and lies about what came after.
A regular port requires no decision_log, and this must not change
Drift-gating must not turn into a blanket documentation requirement. Today decision_log is required only where a status is present (TestTheDeferralsSayWhy::test_every_status_entry_has_a_decision_log), and that scope is correct. On current main:
entries with no status (regular ports): 458
of those, with no substantive decision_log: 142
A rule requiring prose on every hash bump would fail those 142 immediately and would keep failing every routine re-sync afterwards.
It would also be wrong on principle, not merely expensive. decision_log explains a divergence. A regular port has none to explain — that is what "regular" means. Re-examining it against a newer MATLAB commit and finding nothing to follow does not create a divergence, so there is nothing for a log to say. The bumped hash is the record: it asserts "examined as of this commit", which is exactly what the field means. Forcing 458 entries to also say "looked, nothing changed" would bury the ~100 logs that carry real reasoning under boilerplate nobody reads — the same dilution the vocabulary cleanup in #210 was undoing.
So: drift gating attaches to the hash, not to the prose. No new decision_log obligation for entries without a status.
The companion rule, or a third of the bridge opts out silently
147 NDI-python entries have a matlab_path and no hash at all. They can never drift, so they claim "ported" permanently and are immune to the check. Under the reasoning above that is worse than a stale hash: same false assurance, but silent instead of red.
DID already gates exactly this and says why:
hash| Every entry with amatlab_pathhas amatlab_last_sync_hash, and that hash is a real commit. An entry without one can never show drift.
So gating drift without this is half a policy. Both rules go together. Note this is a hash requirement, not a prose requirement — consistent with the section above.
Cost
NDI-python — 60 drifted, triaged by what actually changed in the MATLAB file:
| what changed | count | what it needs |
|---|---|---|
signature / arguments block |
18 | real port review; the contract may be wrong |
| body | 40 | read the diff; mostly bump |
| comments / whitespace only | 2 | bump |
The 18 signature-changed entries all carry live Python: profile (166 changed code lines), session (69), stimulator (63), dir (58), sessiontable (48), dataset (39), dir (28), ndi_document2ndi_object (27), updateFileInfoForLocalFiles (22), ndiquery / ndiqueryAll (20 each), database (12), advanceSession, startSession, setup.lab, setup.rayolab, clocktype, RayoLabStims. That is where a stale hash is most likely hiding a real defect, so it is the right place to start.
Plus 147 entries needing a hash determined and recorded.
NDR-python: change the normative sentence in §5a from equals-latest to drift, and delete the hardcoded claim about NDI-python's policy in §5 — cross-reference instead. Its enforcement already exists.
DID-python: move drift from non-gating to gating in the CI job. Its check already exists and its docs already explain drift; only the gating decision changes.
Rollout: ratchet, not big bang
Turning the gate on in NDI-python today puts main red until all 60 are examined, and the 18 signature ones are not quick. Proposed instead:
- The check gates, with an explicit allowlist naming the currently-drifted entries and the currently-hashless ones — each line a visible IOU in the repo.
- Anything not on the list fails immediately, so no new unexamined drift can enter from today.
- Every burn-down PR deletes lines. Empty list is the finish line.
The debt becomes enumerated and counted instead of invisible. The alternative — work all 60 first, then switch on — is honest but leaves the gate off for as long as that takes, which is how NDI-python accumulated 60 in the first place.
Decision needed
@stevevanhooser — one answer for all three repos:
- Drift, not equals-latest? (recommended: yes — the 103 batch-sync entries are correct records that equality would reject)
- Gate the companion "every
matlab_pathentry has a hash" rule too? (recommended: yes — otherwise 147 entries are exempt by omission) - Ratchet with a shrinking allowlist, or clean the 60 first and then switch on? (recommended: ratchet)
- Confirm a regular port stays free of any
decision_logrequirement. (recommended: yes — 142 entries would fail otherwise, and a port with no divergence has nothing to explain)
Once settled, the rule is stated once in NDI-python's bridge spec beside the commit-vs-blob rule and the status vocabulary, and NDR-python and DID-python cross-reference it rather than restating — the discipline #210 adopted, and the one whose absence caused #209.
Activity
stevevanhooser commentedon Sep 7, 2026
Decision 1 settled: drift, and it is file-scoped
@stevevanhooser confirmed the intent — "a change is only noticed when the file changes rather than anything on the repo." That is exactly what the drift rule does, and it is worth stating plainly because the issue body could be read otherwise.
The check is path-filtered, always:
The
-- <matlab_path>restricts it to commits touching that one file. Unrelated activity in NDI-matlab never makes an entry drift, regardless of what form the recorded hash takes.The difference between the two candidate rules is the hash's FORM, not the check's scope
Both look at the file. What separates them:
markgarbageTwo real entries on
maintoday:Neither file has moved since it was examined. Both records are true. Equals-latest rejects them anyway, purely because the recorded hash is a repo-wide commit rather than the file's own — and the only way to clear the resulting red build is to rewrite a correct record with a different, equally correct value. That is how a check teaches contributors to ignore it. There are 103 such entries.
What this means for a recorded hash going forward
Either form stays legal, because drift is sound under both:
git log -1 --format=%h -- <path>) — the documented default, and the more informative of the two: it says "I examined this version of this file."HEADwhen a batch was examined together — a broader claim ("I examined as of this repo state"), and still sound: any later change to the file is caught, and changes before it fall inside what was examined.No entry needs rewriting to adopt this rule. The 103 stay as they are.
Decisions 2, 3 and 4 remain open — the companion "every
matlab_pathentry has a hash" rule (147 entries currently exempt by omission), ratchet vs clean-first for the 60-entry backlog, and confirming that a regular port carries nodecision_logobligation.Generated by Claude Code
stevevanhooser commentedon Sep 7, 2026
Decisions 2, 3 and 4 settled — all four are now implemented in #213
@stevevanhooser answered the remaining three: 2 yes, 3 ratchet for now (with the drift dealt with soon), 4 yes. Recording them here so the issue carries the whole ruling rather than a quarter of it.
2. The companion hash rule gates too — YES
Every entry naming a
matlab_pathmust carry amatlab_last_sync_hash. An entry without one can never drift, so it asserts its port is current forever and nothing can contradict it: the same false assurance as a stale hash, but silent instead of red.This check reads only this repo — no NDI-matlab checkout — so it runs in every test matrix job, not just the bridge job. I verified that from the job log rather than assuming it.
3. Ratchet — YES, for the moment
The user's words: "let's ratchet for the moment but we should very soon deal with the drift." The allowlist is a staging post, not a settlement.
tests/bridge_drift_allowlist.yamlenumerates the pre-existing debt, and everything not on it is gated today, so no new unexamined drift can enter.TestTheAllowlistOnlyShrinksfails if a listed entry no longer drifts, so a fix must delist in the same PR — the file can only shrink, and its line count is the progress metric. It cannot quietly become a permanent exemption.4. A regular port carries no
decision_logobligation — YESdecision_logexplains a divergence; a regular port has none, and re-examining one against a newer MATLAB commit and finding nothing to follow does not create one. The bumped hash is itself the record of examination. Drift gating attaches to the hash, not to the prose.This turned out to matter for our own spec, not just as a hypothetical: §3a of the bridge spec already told readers to "record the sync date in
decision_log" — a rule that contradicts decision 4, sitting in a restatement of the mechanics. #213 replaces that restatement with a pointer to §7. Exactly the failure mode this issue is about, found in the document proposing to fix it.Where the rule now lives
Section 7 of
docs/developer_notes/ndi_matlab_python_bridge.yamlis the normative statement, beside the commit-vs-blob rule (#209) and the status vocabulary (#210). NDR-python and DID-python cross-reference it rather than restating.One number changed between this issue and the PR
This issue said 147 hashless entries; #213 says 146. Not a miscount on either side — the filter got stricter in between.
normalize_matlab_pathstrips only the exact string"N/A", so"n/a (Python only)"and"(inline in getData.m)"survived it and were being treated as real MATLAB files. The path filter now requires the+ndi/package prefix, as the completeness guard already did. 507 of 528matlab_pathvalues are real paths.The allowlist-hygiene test caught this itself: the first generated list disagreed with the corrected filter and the test refused it.
Still open
profileat 166 changed code lines, thensession69,stimulator63,dir58) plus 146 hashless entries. Deliberately not in Gate MATLAB drift, with a burn-down list that can only shrink #213.driftfrom non-gating to gating. The check and the docs already exist; only the gating decision changes.Generated by Claude Code
claude commentedon Sep 8, 2026
Workstream B (
no_hash:) progress — overnight batch summary.no_hash:went 146 → 87 onmain, with seven more PRs open that take it to ~46.Merged
+fun,+gui,+util, top level)python_pathguard + 9 entries pointing at nothing+ndi/+validators/ports+ndi/+util/hex/time ports+ndi/+gui/+component/progress entriesOpen, all green and mergeable except the newest
#228 (5
+fun/+table) · #229 (7+fun/+doc) · #230 (4+docTable) · #231 (4+docTablebuilders) · #233 (13+database/+fun) · #235 (9 small+fun) · #236 (5 top level)What the exercise turned up
The point was never the hashes. Defects found by reading the Python against the MATLAB, roughly in order of severity:
Functions that could not work at all
ndi.util.getHexDiffFromFileObj—typing.IO[bytes]under@pydantic.validate_callrejects every real file object, so every call raised before the body ran (Examine and date 8 ndi.util ports; three defects, one unusable function #219).ndi.fun.docTable.openminds— queriedisa(type)for a document class named e.g."Strain"instead of matchingopenminds.openminds_type, so it selected nothing; and every row it did build held only anid(Examine the 4 large +docTable builders; openminds selected nothing #231).Silent wrong answers
mustBeIDreturnedNone— accepting an ID it had just rejected (Examine and date the 11 ndi.validators ports; fix a silent mustBeID accept #217).datestamp2datetimeread a naive datestamp as local system time: 17:00Z in New York, 03:00Z in Tokyo (Examine and date 8 ndi.util ports; three defects, one unusable function #219).identifyValidRows(df, cols, NaN)returned every row valid —NaN != NaN(Examine the five +fun/+table entries; three returned the wrong rows #228).identifyMatchingRowschose string-vs-numeric matching from the column's dtype rather than the match value's type, silently returning the rows equal to agtthreshold (Examine the five +fun/+table entries; three returned the wrong rows #228).mat2ngridrecorded a boolean'sdata_typeas"logical"where MATLAB records"ubit1"— decided by dict ordering (Examine the four ngrid/image entries; fix a data_type both sides disagreed on #227).Whole halves never ported, silently
ndi.fun.doc.diffacceptedcheckFiles,session1,session2and read none of them — reporting documents equal without comparing a byte (Examine the 7 +fun/+doc entries; port the file comparison diff never ran #229).ProgressTracker.updateMessagewas never ported (Examine the 8 gui progress entries: a dead event channel and a thin payload #222).Records that were wrong
decision_logs in+database/+fundescribed functions that do not exist — includingprojectvardef, justified as having "no equivalent concept in Python" whenprojectvar.jsonships in this repo (Examine 13 +database/+fun entries; three recorded reasons were wrong #233).name2variableNamerecords are exactly swapped: the one labelled "Exact match" matches MATLAB on none of four points; the one labelled "Python-only convenience" is MATLAB's algorithm (Examine 9 small +fun entries; two records were swapped, timestamp broke composition #235).Composition failures
ndi.fun.timestamp()produced a stringndi.util.datestamp2datetimerefuses — two functions in one package that didn't compose (Examine 9 small +fun entries; two records were swapped, timestamp broke composition #235). Only visible because Examine and date 8 ndi.util ports; three defects, one unusable function #219 stopped the parser guessing.Also: a mechanical class-rename pass had corrupted ordinary English in error messages and docstrings in five places, and — load-bearingly — in nine
python_pathvalues (#221).Deliberately not fixed, and why
Recorded in
decision_logs rather than changed: thehexDiffBytesStartOffsetbug (MATLAB has it too — diverging would break the correspondence the bridge exists to record);ubit1bit-packing (won't guess MATLAB's bit order without a MATLAB to test);element's metadata aggregation andopenminds' dependent-document expansion (need populated sessions);dateCreatedon Linux (matching MATLAB's NaT would make a test platform-dependent — a product decision).Still open
~46 entries after the current PRs:
+guitop level (8 large GUI classes),+mock(5),+utilsummaries (4),+app(3),+calc(3),+file(4),ndi.documentitself, and assorted+funsubpackages. Plus 18drifted:rows, all+ndi/+cloud/.Generated by Claude Code
claude commentedon Sep 8, 2026
Overnight progress: eight more PRs,
no_hash92 -> 11Everything below is open, green and
cleanagainstmain. Nothing is merged -- that stays yours.ndi.documentto_tablenamed its columns differently in three ways;get_fuidwas never ported+utilsummaries+mockpickFreeReferenceto fix was still here+gui+filemfdaq_epoch_channel's two halves could not compose+funplot/stimulusbar3could not run at all+fundoc/epoch/probeepochid2elementread a field that does not existndi.app.appdocReplaceIfDifferentkept documents MATLAB replacesTogether with #228-#236, that is 81 of the 92
no_hashentries examined. Eleven remain:spikeextractor,spikesorter,ndi.file.navigator, the three+calc,evaluate_fitcurve,SyncIndex,setup, and the session/datasetdiffpair -- which want #237'sget_fuidunder them first, since that is the lookup they are built on.Functions that could not work at all
bar3raisedAttributeErroron every call.plt.cm.get_cmapwas removed in matplotlib 3.9.mfdaq_epoch_channel:create_propertiesderived none of the three fields it exists to derive, so every channel came outnumber=0, group=0, dataclass=''-- andchannelgroupdecoding, which looks channels up by number, then raisedChannel number 0 found multiple times in recordfor any request at all. The two halves of the class could not be used together.epochid2elementreadelement.epoch_table. That field appears in no document schema and nothing writes it, so the lookup was empty for every epoch id ever passed in. Its type filter readdocument_class.class_list, also absent, so every element was skipped whenever a type was given.filename2epochidhas the same defect and its record (on Examine 9 small +fun entries; two records were swapped, timestamp broke composition #235) understates it.gui_v2's Database View was empty on every session: the query was built with the operator spelledregex, whichndi_queryrejects, inside a bareexcept Exception.Silent wrong answers
ndi.document.to_tablenever readndi_document2table_abbreviations.json-- which ships in this repo -- so every abbreviated field's column had a different name in the two languages. It also deleted nested fields namedfiles/depends_onat every depth where MATLAB'srmfieldreaches only the top, and collapsed a struct array into one raw-list column.compareSessionSummarysorted three field names where MATLAB sorts every array, treated''as non-empty where MATLAB'sisemptycovers it, and compared with==where MATLAB usesisequaln. This is the function the symmetry harness uses to decide whether Python and MATLAB agree.compareDatasetSummarymatched session summaries positionally where MATLAB looks each up by thesessionIdinside it, and read a missingnumSessionsas zero -- so a summary with no such field compared equal to an empty dataset.ndi.gui.data/docViewerread name/id/type/datestamp out of the legacydocument_properties.ndi_document, so every table row came out with four empty columns -- anddocViewer's dependency graph matched on that same missing id, giving no edges at all.findMixtureNamerequired the entry and the mixture to be the same length, skipped any single-component entry outright, and compared every field as a string so1never equalled1.0.appdoc.add_appdocreturned as soon as any existing document matched, and swallowed a failed removal before writing a replacement beside it.Whole halves never ported
f0_f1_responses: F0 and F1 live in separate documents; MATLAB finds the partner. The port returned one value andNonefor the other, so the pair it exists to compare could never be compared.get_fuid: not ported and not recorded, and it is whatfindFuidand bothdifffunctions are built on.gui_v2never drew a single connection between icons.Labhas no mouse routing at all -- nothing setsdrag, so dragging never starts and clicking an icon never opens its details.subject_stimulator_neuronbuilt three loose documents the session never saw.datasetSummarycould not producedocumentCounts;compareDatasetSummarycould not compare them.Records that asserted things that were not so
doc_unique_idrecorded as an "exact match" for a method that did not exist.stimulus_responserecorded asDISCREPANCY FIXEDfor a stub returning threeNones -- nowporting_deferred.navigator_epochdir'sepochidrecorded as "Added to match MATLAB" when the two languages give different epoch ids for the same data (t00001vsepoch_db4fad24..., plus a hidden sidecar file the symmetry tests already filter around).probeLocations4probeswroteprobe_location.ontology; the schema declaresontology_name.Left for you, deliberately
navigator_epochdir's epoch ids (Examine three +file entries: mfdaq_epoch_channel derived none of its own fields #241). Fixing it renames every epoch in every stored session. That is a migration, not a port.ndi.document.write(Examine ndi.document: to_table named its columns differently, get_fuid was never ported #237) means a file PREFIX in MATLAB and a complete filename here. Every caller in this tree passes a full.jsonpath, so appending an extension would rename their output.ndi.elementrefuses a name containing whitespace (Examine the five +mock entries: the reference-collision bug MATLAB fixed was still here #239) -- a rule MATLAB does not have, that MATLAB's own mock helper violates, pinned by three tests and recorded nowhere.Lab's Qt mouse routing (Examine the eight +gui entries: the Database View was empty on every session #240). Sixty lines of event code that neither CI nor my container can execute; writing it unexecuted is how a plausible wrong answer gets committed. The state machine it needs (symbol) is ported and tested.Two things worth deciding separately
ndi.guiis entirely untested in CI. PySide6 is an optional extra andndi_install.pyinstalls.[dev,tutorials], so every Qt test in the package skips on every job -- which is why four of the eight+guidefects survived. Addingguito the CI extras is one line, but it pulls Qt into every job.tests/test_ndi_install_exit_codes.pyis not hermetic. Three cases fail in my container and pass in CI:_run()stubs everything "up to step 4" but notpip_install_dependency, so step 2 shells out to a realpip install. A one-line addition to that stub list fixes it. Not touched -- it has nothing to do with the bridge.Two to report to VH-Lab
Both are MATLAB-side and I have no API access there:
ndi.fun.probe.locationhangs on any element chain deeper than one level: the descent loop readse.underlying_elementrather thancurrent_element.underlying_element, so it assigns the same object forever.ndi.gui.Data/docViewerreaddocument_properties.ndi_document, the legacy schema field, so MATLAB's own document tables will come out blank on any document built today.Generated by Claude Code
claude commentedon Sep 8, 2026
Merging these: one thing to know
Nineteen open PRs all edit the same line. Every burn-down PR removes rows from
tests/bridge_drift_allowlist.yamland updates the# no_hash: Nheader count, so the first merge makes the other eighteen conflict on that count line. That is expected and mechanical, not a sign anything is wrong.Merge them in any order you like; after each merge I can reconcile the rest with
/tmp/resolve_allowlist.py, which takes the union of removed rows and recomputes the count from the rows that actually survive -- the bridge tests themselves then check the arithmetic. A check-in is armed to do exactly that whenevermainmoves.Current state
mainat664a826and reportclean.mainis green onMATLAB ↔ Python symmetry testsagain -- the regression that followed Burn down 16 of the 40 body-drifted bridge entries #220 has cleared.SyncIndexlives insrc/ndi/cloud/sync/ndi_matlab_python_bridge.yaml, which Burn down the +ndi/+cloud/ drift cluster: allowlist 18 → 1 #234 also edits. Waiting on Burn down the +ndi/+cloud/ drift cluster: allowlist 18 → 1 #234 rather than racing it.diff (session)anddiff (dataset)are both built onndi.document.get_fuid, which Examine ndi.document: to_table named its columns differently, get_fuid was never ported #237 adds. They want doing together once Examine ndi.document: to_table named its columns differently, get_fuid was never ported #237 lands.That is the whole
no_hashsection: 92 → 3, with the last three waiting on merges rather than on examination.Generated by Claude Code
stevevanhooser commentedon Sep 8, 2026
Closing: all three repos have landed their half, verified in each repo rather than assumed
The four decisions are implemented as settled — drift rather than equals-latest, the companion hash rule gating too, a ratchet rollout, and no new
decision_logobligation on a regular port.NDI-python
tests/test_matlab_bridge_hashes.py, named explicitly by the CI job and run underNDI_BRIDGE_CHECK_STRICT=1against afetch-depth: 0checkout. 11 tests, green.The ratchet has been burnt down:
matlab_pathand no hashThe one remaining line is
downloadNdiDocuments, held open deliberately: it drifts oncd3c11673, which reconstructs a file series'ingest_locationson cloud download, and this side has no file-series support in the cloud path at all. That is #215. Bumping its hash would assert an examination whose entire finding is that the port is missing.Roughly one entry in three turned out to be hiding a real defect rather than bookkeeping —
startSessionmissing a now-requiredorganizationId,setup.labduplicating every DAQ system on a second run,waitForAllBulkUploadsported and never called, a transfer URL never checked forhttps, a document archive with nothing in it returning an empty list. Each is recorded in its entry'sdecision_log.NDR-python (VH-Lab/NDR-python#27, merged)
Verified in the repo:
docs/developer_notes/ndr_matlab_python_bridge.yaml§5a now states the drift condition, and §5 no longer carries the hardcoded claim about NDI-python's policy — replaced by a note explaining that the old sentence "characterised NDI-python's rule in a passing sentence; the sentence was a copy", with a cross-reference to this issue and to NDI-python's spec as the single home of the rule.It also records the batch-sync point that motivated choosing drift over equality: "NDI-python had 103 entries of that shape. A check that demands [equality] fails all of them."
DID-python (VH-Lab/DID-python#76, merged)
Verified in the repo:
.github/workflows/python-package.ymlreads "All six checks gate, drift included" and passes--check driftamong them.PORTING_INSTRUCTIONS.md§Drift gates states the rule, records that "Until 2026-09-07 this check ran non-gating here", and notes that nothing was drifted when the gate went on — so DID needed no burn-down, and its allowlist mechanism exists for the future rather than for a backlog.The discipline the issue asked for
The rule is stated once, in NDI-python's bridge spec, and the other two repos cross-reference it rather than restating it — which is what #210 adopted and what #209 cost us for lack of.
Closing. #215 tracks the single remaining allowlist line; #191, which asked for this check, is satisfied by the same work.
Generated by Claude Code