← 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
M src/adapters/arcgis.jsM src/sources.jsA test/auto-repoint.test.mjsM test/layer-identity.test.mjs
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 →