[object Object]

← back to La Socrata Ingester

feat(la-data): verified auto-repoint for drifting NavigateLA layer ids

482ed58641684b9ab4c073396cd16519a2c2af4c · 2026-09-11 11:46:35 -0700 · Steve Abrams

Steve's call (2026-09-11, DTD option B/D). NavigateLA layer ids are positional, so
LA inserting one layer renumbers everything below it. That happened on 09-05 and
again on 09-11 ("Special Event Permits" at position 52, 452 -> 453 layers). The
fail-loud-only posture ratified on 09-10 was right at the time but, at that
recurrence rate, means a roughly weekly outage where GIS layers sit stale until a
human repoints. This reversal is deliberate and the new fact is the recurrence.

New policy: the pinned id is a HINT. On a name mismatch the adapter FINDS the layer
by exact name and PROVES it before reading a single row; it still fails loud on
anything it cannot prove.

  resolveLayerIdentity(src) — two discovery paths, proof required either way:
    1. service catalog (one cached GET) — covers any drift distance
    2. bounded +/-N window scan of per-layer endpoints — the fallback for a STALE
       catalog, which was MEASURED on 09-11: it served a 452-layer body (old index)
       while per-layer endpoints already served 453, so resolving off the catalog
       ALONE would have returned the WRONG layer. A catalog hit is never trusted on
       its own; it is always re-proved on /MapServer/<id>?f=json, the plane /query
       follows.

Proof required to MOVE: exact normalized name + type 'Feature Layer' + a field
fingerprint (the source's oidField present, and at least one nameField when it
declares any). That closes codex's stated risk — a replacement dataset reusing the
exact name — and the documented layer-73 trap (same name, Group Layer). Measured
9/9 live before enforcing, so it cannot false-alarm today.

Fails loud, never guesses: a rename, zero matches, MORE THAN ONE proved match (the
window is scanned whole, so ambiguity is detected rather than masked), drift beyond
the window with no catalog, or right-name-wrong-schema on the pinned id (which
never triggers a search — a replaced dataset needs a human). Deliberate asymmetry:
metadata with no field list keeps a WORKING pin but is never enough to MOVE.

LIVE PROOF, not just mocks: replaying the pre-fix ids against the real MapServer
auto-repoints 9/9 to the correct layer via the catalog; with the catalog forced
unavailable, the window scan recovers 419->420 and correctly fails loud on a drift
of 419. Had this shipped a day earlier, today's 04:30 run would have self-healed.

test/auto-repoint.test.mjs — 10 checks, 8 of them NEGATIVE controls (Group Layer,
wrong schema, ambiguity, rename, schema drift, kill switch, unpinned passthrough).
Check 9 earned its keep immediately: it caught that LA_NO_AUTO_REPOINT and
LA_REPOINT_WINDOW were module-load consts, so the kill switch silently did nothing.
Both now read at call time.

Also closes a coverage regression the contrarian red-team found in 314f1dc: the old
check 6 caught two sources sharing one layer id only as a side effect of asserting
exact ids, and dropping it traded a static catch for a runtime-only one. Asserted
directly now (check 7b), with a negative control.

4/4 suites pass. TK-10955. $0 local (free public ArcGIS GETs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CocwcUw6gFt3tr4CZJmra6

Files touched

Diff

commit 482ed58641684b9ab4c073396cd16519a2c2af4c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 11 11:46:35 2026 -0700

    feat(la-data): verified auto-repoint for drifting NavigateLA layer ids
    
    Steve's call (2026-09-11, DTD option B/D). NavigateLA layer ids are positional, so
    LA inserting one layer renumbers everything below it. That happened on 09-05 and
    again on 09-11 ("Special Event Permits" at position 52, 452 -> 453 layers). The
    fail-loud-only posture ratified on 09-10 was right at the time but, at that
    recurrence rate, means a roughly weekly outage where GIS layers sit stale until a
    human repoints. This reversal is deliberate and the new fact is the recurrence.
    
    New policy: the pinned id is a HINT. On a name mismatch the adapter FINDS the layer
    by exact name and PROVES it before reading a single row; it still fails loud on
    anything it cannot prove.
    
      resolveLayerIdentity(src) — two discovery paths, proof required either way:
        1. service catalog (one cached GET) — covers any drift distance
        2. bounded +/-N window scan of per-layer endpoints — the fallback for a STALE
           catalog, which was MEASURED on 09-11: it served a 452-layer body (old index)
           while per-layer endpoints already served 453, so resolving off the catalog
           ALONE would have returned the WRONG layer. A catalog hit is never trusted on
           its own; it is always re-proved on /MapServer/<id>?f=json, the plane /query
           follows.
    
    Proof required to MOVE: exact normalized name + type 'Feature Layer' + a field
    fingerprint (the source's oidField present, and at least one nameField when it
    declares any). That closes codex's stated risk — a replacement dataset reusing the
    exact name — and the documented layer-73 trap (same name, Group Layer). Measured
    9/9 live before enforcing, so it cannot false-alarm today.
    
    Fails loud, never guesses: a rename, zero matches, MORE THAN ONE proved match (the
    window is scanned whole, so ambiguity is detected rather than masked), drift beyond
    the window with no catalog, or right-name-wrong-schema on the pinned id (which
    never triggers a search — a replaced dataset needs a human). Deliberate asymmetry:
    metadata with no field list keeps a WORKING pin but is never enough to MOVE.
    
    LIVE PROOF, not just mocks: replaying the pre-fix ids against the real MapServer
    auto-repoints 9/9 to the correct layer via the catalog; with the catalog forced
    unavailable, the window scan recovers 419->420 and correctly fails loud on a drift
    of 419. Had this shipped a day earlier, today's 04:30 run would have self-healed.
    
    test/auto-repoint.test.mjs — 10 checks, 8 of them NEGATIVE controls (Group Layer,
    wrong schema, ambiguity, rename, schema drift, kill switch, unpinned passthrough).
    Check 9 earned its keep immediately: it caught that LA_NO_AUTO_REPOINT and
    LA_REPOINT_WINDOW were module-load consts, so the kill switch silently did nothing.
    Both now read at call time.
    
    Also closes a coverage regression the contrarian red-team found in 314f1dc: the old
    check 6 caught two sources sharing one layer id only as a side effect of asserting
    exact ids, and dropping it traded a static catch for a runtime-only one. Asserted
    directly now (check 7b), with a negative control.
    
    4/4 suites pass. TK-10955. $0 local (free public ArcGIS GETs).
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01CocwcUw6gFt3tr4CZJmra6
---
 src/adapters/arcgis.js       | 155 +++++++++++++++++++++++++++++++++++++++++--
 src/sources.js               |  11 +++
 test/auto-repoint.test.mjs   | 122 ++++++++++++++++++++++++++++++++++
 test/layer-identity.test.mjs |  13 +++-
 4 files changed, 293 insertions(+), 8 deletions(-)

diff --git a/src/adapters/arcgis.js b/src/adapters/arcgis.js
index bd7b9fa..4e5ee6d 100644
--- a/src/adapters/arcgis.js
+++ b/src/adapters/arcgis.js
@@ -39,13 +39,154 @@ async function fetchArcgis(url, { tries = 5 } = {}) {
 // ingesting at all — one metadata GET per source per run ($0, public API).
 const normName = (s) => String(s ?? '').trim().toLowerCase().replace(/\s+/g, ' ');
 
+// ── Verified auto-repoint (TK-10955, Steve 2026-09-11) ──────────────────────
+// Positional ids drifted twice in six days (2026-09-05, 2026-09-11), so fail-loud-only
+// meant a weekly outage. Policy is now: on a name mismatch, FIND the layer by exact name,
+// PROVE it on the authoritative plane, then ingest — and still fail loud on anything we
+// cannot prove (a rename, no match, or an ambiguous match).
+//
+// Two discovery paths, in order:
+//   1. the service catalog (…/MapServer?f=json) — one cached GET, covers any drift distance;
+//   2. a bounded ±N window scan of per-layer endpoints — the fallback for when the catalog
+//      is STALE, which was MEASURED on 2026-09-11: it served a 452-layer body (old index)
+//      while per-layer endpoints already served 453. Resolving off the catalog ALONE would
+//      have returned the wrong layer, so a catalog hit is never trusted on its own.
+// Either way the candidate is re-proved on /MapServer/<id>?f=json — the plane /query
+// follows — before a single row is read.
+const LAYER_ENDPOINT_RE = /^(.*)\/(\d+)$/;
+// Read at CALL time, not module-load time. As module-level consts these were baked in at
+// import and the kill switch silently did nothing — caught by its own negative test.
+const repointWindow = () => Math.max(0, Number(process.env.LA_REPOINT_WINDOW ?? 10));
+const autoRepointDisabled = () => process.env.LA_NO_AUTO_REPOINT === '1';
+
+const catalogCache = new Map(); // baseUrl -> Promise<catalog|null>, one GET per process
+function fetchCatalog(baseUrl, fetcher) {
+  if (!catalogCache.has(baseUrl)) {
+    catalogCache.set(baseUrl, Promise.resolve(fetcher(`${baseUrl}?f=json`)).catch(() => null));
+  }
+  return catalogCache.get(baseUrl);
+}
+export function _resetCatalogCache() { catalogCache.clear(); } // tests only
+
+const nameMatches = (meta, src) => normName(meta?.name) === normName(src.expectName);
+
+// Field/type fingerprint. Closes the "a replacement dataset reuses the exact name" risk:
+// a same-named layer that lacks our oidField (or every one of our nameFields) is NOT ours.
+// Returns 'ok' | 'unverifiable' | 'mismatch'. 'unverifiable' means the metadata carried no
+// field list, so we could not measure — deliberately NOT folded into 'ok'.
+function fingerprint(meta, src) {
+  if (normName(meta?.type) !== 'feature layer') return 'mismatch';
+  const list = Array.isArray(meta?.fields) ? meta.fields : null;
+  if (!list || list.length === 0) return 'unverifiable';
+  const fields = new Set(list.map((f) => f?.name));
+  if (!fields.has(src.oidField || 'OBJECTID')) return 'mismatch';
+  const nameFields = Array.isArray(src.nameFields) ? src.nameFields : [];
+  if (nameFields.length && !nameFields.some((f) => fields.has(f))) return 'mismatch';
+  return 'ok';
+}
+
+async function probeLayer(baseUrl, id, fetcher) {
+  if (!Number.isInteger(id) || id < 0) return null;
+  try { return await fetcher(`${baseUrl}/${id}?f=json`); } catch { return null; }
+}
+
+// A candidate is accepted ONLY on full proof: exact name AND a verified fingerprint.
+// 'unverifiable' is never enough to MOVE onto a new layer (asymmetry is intentional —
+// see resolveLayerIdentity for why the pinned id is allowed to stay on 'unverifiable').
+const candidateProved = (meta, src) => !!meta && nameMatches(meta, src) && fingerprint(meta, src) === 'ok';
+
+async function findLayerByName(baseUrl, pinnedId, src, fetcher) {
+  const tried = [];
+  const cat = await fetchCatalog(baseUrl, fetcher);
+  const catHits = (cat?.layers ?? []).filter(
+    (l) => normName(l?.name) === normName(src.expectName) && normName(l?.type) === 'feature layer'
+  );
+  if (catHits.length === 1) {
+    const id = catHits[0].id;
+    tried.push(`catalog->${id}`);
+    if (candidateProved(await probeLayer(baseUrl, id, fetcher), src)) return { id, via: 'catalog', tried };
+    tried.push(`catalog->${id} FAILED re-proof (stale catalog?)`);
+  } else if (catHits.length > 1) {
+    tried.push(`catalog ambiguous (${catHits.length} layers named "${src.expectName}")`);
+  } else {
+    tried.push('catalog: no exact-name Feature Layer');
+  }
+
+  // Fallback: scan a bounded window on the authoritative per-layer plane. Scans the WHOLE
+  // window (does not stop at the first hit) so an ambiguous match is detected, not masked.
+  const hits = [];
+  const win = repointWindow();
+  for (let d = 1; d <= win; d++) {
+    for (const id of [pinnedId + d, pinnedId - d]) {
+      if (candidateProved(await probeLayer(baseUrl, id, fetcher), src)) hits.push(id);
+    }
+  }
+  tried.push(`window +/-${win}: ${hits.length} proved match(es)${hits.length ? ` [${hits.join(', ')}]` : ''}`);
+  if (hits.length === 1) return { id: hits[0], via: `window+/-${win}`, tried };
+  return { id: null, ambiguous: hits.length > 1, tried };
+}
+
+// Resolve the endpoint a source should actually page. Returns { endpoint, repointed, from,
+// to, via }. Throws the loud drift error when the layer cannot be PROVED anywhere.
+export async function resolveLayerIdentity(src, fetcher = fetchArcgis) {
+  if (!src.expectName) return { endpoint: src.endpoint, repointed: false }; // opt-in
+
+  const pinned = await fetcher(`${src.endpoint}?f=json`);
+  if (nameMatches(pinned, src)) {
+    const fp = fingerprint(pinned, src);
+    if (fp === 'mismatch') {
+      // Right name, wrong shape: the dataset behind our label changed. Never auto-move on
+      // this — a human must look, because "same name, different schema" is how a
+      // replacement dataset would impersonate ours.
+      throw new Error(
+        `ArcGIS layer schema drift: ${src.endpoint} still reports "${pinned?.name}" but no longer ` +
+        `matches this source's shape (type=${pinned?.type}, oidField=${src.oidField || 'OBJECTID'}, ` +
+        `nameFields=${JSON.stringify(src.nameFields ?? [])}). Upstream replaced the dataset — ` +
+        `do NOT ingest; review the source definition.`
+      );
+    }
+    // fp === 'unverifiable' (metadata carried no field list) keeps the PINNED id, matching
+    // pre-existing behaviour. We decline to break a working pin on missing metadata, but we
+    // still refuse to MOVE to a new layer on the same weak evidence.
+    return { endpoint: src.endpoint, repointed: false };
+  }
+
+  const m = LAYER_ENDPOINT_RE.exec(src.endpoint);
+  const driftMsg =
+    `ArcGIS layer identity drift: ${src.endpoint} is now "${pinned?.name ?? '(unnamed)'}" ` +
+    `but this source expects "${src.expectName}".`;
+  if (autoRepointDisabled() || !m) {
+    throw new Error(`${driftMsg} Upstream re-indexed its layer ids — repoint the layer id (verify by NAME), do NOT ingest.`);
+  }
+
+  const [, baseUrl, pinnedIdStr] = m;
+  const found = await findLayerByName(baseUrl, Number(pinnedIdStr), src, fetcher);
+  if (found.id == null) {
+    throw new Error(
+      `${driftMsg} Could not PROVE where it moved, so nothing was ingested. Tried: ${found.tried.join('; ')}. ` +
+      (found.ambiguous
+        ? 'Multiple layers matched — ambiguous, a human must choose.'
+        : 'A rename, a removal, or a shift beyond the search window — repoint by hand (verify by NAME).')
+    );
+  }
+
+  const endpoint = `${baseUrl}/${found.id}`;
+  console.error(
+    `  ⚠ AUTO-REPOINT ${pinnedIdStr} -> ${found.id} for "${src.expectName}" (via ${found.via}; ` +
+    `name + schema re-proved on the per-layer endpoint). Upstream shifted its positional ids; ` +
+    `update the pinned id in src/sources.js.`
+  );
+  return { endpoint, repointed: true, from: Number(pinnedIdStr), to: found.id, via: found.via };
+}
+
+// Back-compat wrapper: throws on drift, never repoints. Kept because it is the narrow
+// "is this layer what we think it is" assertion the tests exercise directly.
 export async function assertLayerIdentity(src, fetcher = fetchArcgis) {
-  if (!src.expectName) return; // opt-in: unpinned sources keep prior behaviour
+  if (!src.expectName) return;
   const meta = await fetcher(`${src.endpoint}?f=json`);
-  const actual = meta?.name;
-  if (normName(actual) !== normName(src.expectName)) {
+  if (normName(meta?.name) !== normName(src.expectName)) {
     throw new Error(
-      `ArcGIS layer identity drift: ${src.endpoint} is now "${actual ?? '(unnamed)'}" ` +
+      `ArcGIS layer identity drift: ${src.endpoint} is now "${meta?.name ?? '(unnamed)'}" ` +
       `but this source expects "${src.expectName}". Upstream re-indexed its layer ids — ` +
       `repoint the layer id (verify by NAME), do NOT ingest.`
     );
@@ -66,12 +207,12 @@ export async function assertLayerIdentity(src, fetcher = fetchArcgis) {
 // opts: since, pageSize, maxRows, fullScan, whereOverride
 export async function* arcgisPages(src, opts = {}) {
   const { since = null, pageSize = 2000, maxRows = Infinity, fullScan = false, whereOverride = null } = opts;
-  const base = `${src.endpoint}/query`;
+  // Resolve BEFORE building the query url: a verified auto-repoint changes the endpoint.
+  const resolved = await resolveLayerIdentity(src);
+  const base = `${resolved.endpoint}/query`;
   const oidField = src.oidField || 'OBJECTID';
   const geom = !!src.geometry;
 
-  await assertLayerIdentity(src); // fail loud if the layer id no longer means what we think
-
   // base filters (everything except the oid cursor)
   const filters = [];
   if (whereOverride) filters.push(whereOverride);
diff --git a/src/sources.js b/src/sources.js
index 2a2b8d2..436aa82 100644
--- a/src/sources.js
+++ b/src/sources.js
@@ -188,6 +188,13 @@ export const SOURCES = {
 //              NavigateLA sources failed loud at preflight and ingested NOTHING, so
 //              there was no corruption to clean up. Repointed +1 again (see ids below).
 //
+// POLICY since 2026-09-11 (Steve's call): the pinned ids below are a HINT, not the
+// contract. On a name mismatch the adapter FINDS the layer by exact name, PROVES it on the
+// authoritative per-layer endpoint (name + type + field fingerprint), logs an AUTO-REPOINT
+// warning and continues — and still fails loud on a rename, an ambiguous match, or a match
+// it cannot prove. Fail-loud-only was correct on 09-10 but became a roughly weekly outage
+// once the drift recurred. Kill switch: LA_NO_AUTO_REPOINT=1. See src/adapters/arcgis.js.
+//
 // TRAP, learned 2026-09-11 — do NOT resolve an id by name from the SERVICE CATALOG
 // (/MapServer?f=json) alone. That response is cacheable and was observed serving a
 // STALE body (452 layers, old index) while the per-layer endpoints already served the
@@ -209,6 +216,10 @@ const gisSrc = (layer, layerId, nameFields, expectName, extra = {}) => ({
   platform: 'arcgis', endpoint: `${NAV}/${layerId}`, geometry: true,
   table: 'la_gis_features', conflict: ['layer', 'oid'],
   expectName,
+  // Surfaced (not just closed over by gisFeat) so the adapter's schema fingerprint can
+  // check them: a same-named REPLACEMENT dataset that lacks every one of our nameFields
+  // is not ours, and must never be auto-repointed onto.
+  nameFields,
   map: gisFeat(layer, nameFields), ...extra,
 });
 
diff --git a/test/auto-repoint.test.mjs b/test/auto-repoint.test.mjs
new file mode 100644
index 0000000..7481f8a
--- /dev/null
+++ b/test/auto-repoint.test.mjs
@@ -0,0 +1,122 @@
+// TK-10955: zero-network, zero-database proof of the VERIFIED auto-repoint policy
+// (Steve 2026-09-11, option B/D). Positional NavigateLA layer ids shifted +1 twice in six
+// days, so fail-loud-only meant a roughly weekly outage. The policy is now: find the layer
+// by exact name, PROVE it on the authoritative per-layer plane, then ingest — and still
+// fail loud on anything we cannot prove.
+//
+// Every check below is a NEGATIVE control except 1 and 2: the point of this suite is that
+// the resolver REFUSES to move without proof, not merely that it moves when it can.
+import assert from 'node:assert/strict';
+import { resolveLayerIdentity, _resetCatalogCache } from '../src/adapters/arcgis.js';
+
+const NAV = 'https://maps.lacity.org/arcgis/rest/services/Mapping/NavigateLA/MapServer';
+const src = (id, extra = {}) => ({
+  endpoint: `${NAV}/${id}`, expectName: 'Council Districts',
+  nameFields: ['District_Name'], ...extra,
+});
+// A fake upstream: map of id -> layer metadata, plus the service catalog at '?f=json'.
+const F = (name, { type = 'Feature Layer', fields = ['OBJECTID', 'District_Name'] } = {}) =>
+  ({ name, type, fields: fields.map((n) => ({ name: n })) });
+function upstream(layers, { catalog = true, catalogLayers = null } = {}) {
+  return async (url) => {
+    if (url === `${NAV}?f=json`) {
+      if (!catalog) throw new Error('catalog unavailable');
+      const src = catalogLayers ?? layers;
+      return { layers: Object.entries(src).map(([id, m]) => ({ id: Number(id), name: m.name, type: m.type })) };
+    }
+    const m = /\/(\d+)\?f=json$/.exec(url);
+    const hit = m && layers[m[1]];
+    if (!hit) throw new Error(`no layer ${m && m[1]}`);
+    return hit;
+  };
+}
+const run = (s, f) => { _resetCatalogCache(); return resolveLayerIdentity(s, f); };
+
+// 1. Pinned id still correct → no repoint, endpoint untouched.
+let r = await run(src(419), upstream({ 419: F('Council Districts') }));
+assert.equal(r.repointed, false);
+assert.equal(r.endpoint, `${NAV}/419`);
+
+// 2. The real 2026-09-11 shift: pinned 419 drifted, truth moved to 420 → repoint via catalog.
+r = await run(src(419), upstream({
+  419: F('Contract Administration Inspection Districts'),
+  420: F('Council Districts'),
+}));
+assert.equal(r.repointed, true);
+assert.equal(r.to, 420);
+assert.equal(r.via, 'catalog');
+assert.equal(r.endpoint, `${NAV}/420`);
+
+// 3. STALE CATALOG (measured live on 2026-09-11: catalog served the OLD index while
+//    per-layer endpoints served the new one). The catalog says 419, but 419 re-proves as
+//    something else, so the catalog hit must be REJECTED and the window scan must find 420.
+r = await run(src(419), upstream(
+  { 419: F('Contract Administration Inspection Districts'), 420: F('Council Districts') },
+  { catalogLayers: { 419: F('Council Districts'), 420: F('Previous Council Districts') } }
+));
+assert.equal(r.repointed, true);
+assert.equal(r.to, 420, 'a stale catalog must not decide the id');
+assert.match(r.via, /^window/);
+
+// 4. NEGATIVE — a GROUP LAYER with our exact name must never be repointed onto.
+//    (This is the documented layer-73 trap: same name, wrong type.)
+await assert.rejects(
+  () => run(src(419), upstream({
+    419: F('Contract Administration Inspection Districts'),
+    420: F('Council Districts', { type: 'Group Layer' }),
+  })),
+  /Could not PROVE/,
+  'a same-named Group Layer is not a valid repoint target'
+);
+
+// 5. NEGATIVE — a same-named REPLACEMENT dataset lacking our fields must be refused.
+await assert.rejects(
+  () => run(src(419), upstream({
+    419: F('Contract Administration Inspection Districts'),
+    420: F('Council Districts', { fields: ['FID', 'SOMETHING_ELSE'] }),
+  })),
+  /Could not PROVE/,
+  'a same-named layer missing oidField/nameFields is not ours'
+);
+
+// 6. NEGATIVE — AMBIGUITY (two proved matches) must fail loud, not pick one.
+await assert.rejects(
+  () => run(src(419), upstream({
+    419: F('Contract Administration Inspection Districts'),
+    420: F('Council Districts'),
+    418: F('Council Districts'),
+  }, { catalog: false })),
+  /ambiguous|Could not PROVE/i,
+  'two matching layers must fail loud'
+);
+
+// 7. NEGATIVE — a genuine RENAME (our name exists nowhere) must fail loud.
+await assert.rejects(
+  () => run(src(419), upstream({ 419: F('Something Entirely Different') }, { catalog: false })),
+  /Could not PROVE/,
+  'a rename must fail loud, never silently follow'
+);
+
+// 8. NEGATIVE — SCHEMA drift on the pinned id (right name, wrong shape) must fail loud and
+//    must NOT wander off looking for another layer.
+await assert.rejects(
+  () => run(src(419), upstream({ 419: F('Council Districts', { fields: ['FID'] }) })),
+  /layer schema drift/,
+  'same name + wrong schema means the dataset was replaced'
+);
+
+// 9. The kill switch still works: LA_NO_AUTO_REPOINT=1 restores fail-loud-only.
+process.env.LA_NO_AUTO_REPOINT = '1';
+await assert.rejects(
+  () => run(src(419), upstream({ 419: F('Contract Administration Inspection Districts'), 420: F('Council Districts') })),
+  /repoint the layer id/,
+  'the kill switch must restore pure fail-loud'
+);
+delete process.env.LA_NO_AUTO_REPOINT;
+
+// 10. Unpinned sources (no expectName) are untouched — the guard stays opt-in.
+r = await run({ endpoint: `${NAV}/5` }, async () => { throw new Error('must not be called'); });
+assert.equal(r.repointed, false);
+assert.equal(r.endpoint, `${NAV}/5`);
+
+console.log('✔ verified auto-repoint: 10/10 checks pass (no network, no database)');
diff --git a/test/layer-identity.test.mjs b/test/layer-identity.test.mjs
index 97108a7..26f5ae7 100644
--- a/test/layer-identity.test.mjs
+++ b/test/layer-identity.test.mjs
@@ -71,6 +71,17 @@ for (const [name, src] of navSources) {
   assert.match(src.endpoint.slice(NAV_PREFIX.length), /^\d+$/, `${name} must end in a numeric layer id`);
 }
 
+// 7b. Two sources must never point at the SAME numeric layer id either. The OLD check 6
+// caught this ONLY as a side effect of asserting each source's exact id; dropping that
+// assertion traded a static catch for a runtime-only one, so assert it directly. (Found by
+// the contrarian red-team on TK-10955 — a real coverage regression, not a wash.)
+const seenId = new Map();
+for (const [name, src] of navSources) {
+  const id = src.endpoint.slice(NAV_PREFIX.length);
+  assert.ok(!seenId.has(id), `${name} and ${seenId.get(id)} both point at layer id ${id}`);
+  seenId.set(id, name);
+}
+
 // 7. Two sources must never expect the SAME upstream layer name — that would mean one
 // of them is mislabelled, and a name-based repoint could not tell them apart.
 const seen = new Map();
@@ -80,4 +91,4 @@ for (const [name, src] of navSources) {
   seen.set(key, name);
 }
 
-console.log('✔ layer-identity guard: 7/7 checks pass (no network, no database)');
+console.log('✔ layer-identity guard: 8/8 checks pass (no network, no database)');

← 314f1dc fix(la-data): repoint 9 NavigateLA layers after a SECOND ups  ·  back to La Socrata Ingester  ·  fix(la-data): don't cache a failed catalog fetch; report pro 28fcd49 →