Every link https, and a gate so it stays that way
1,237 of the URLs we ship were plain http — all of eBible.org, png.bible and alkitab.pw, with the host spelled `eBible.org` on six of them. Every one of those hosts serves https directly (sampled 12/12 at 200) and redirects http there anyway, so the plain link bought nothing but a redirect hop and a downgraded scheme in a field worker's browser, on a page that may itself be embedded over https. NORMALISED WHERE A URL ENTERS THE INDEX, not per fetcher. Doing it in the eBible fetcher first left 120 links behind — the curated scripture_url values, which fill-never-overwrite preserves and which therefore never pass through a fetcher at all. One httpsify at every emit point takes it to 3,548 https and 0 http. Scheme and HOST only. The path is left exactly as published, because a path is case-sensitive and "tidying" it would break the link rather than improve it. ★ THE GATE CAUGHT MY OWN DRIFT, ten seconds after I added the clause. Clause G failed — but as clause E, "1237 table URL(s) exist in no pin record": I had rebuilt the pins and not the table, so the two surfaces disagreed about every eBible link in the corpus. That is precisely the class this gate was written for last tick, and it found it on its first real use rather than three ticks later by hand. Clause G is poisoned in the self-test alongside the rest: 7/7 clauses now fail on demand. Verified: 0 plain-http URLs remain, six upgraded links sampled live all answer 200 over https, the coverage gate passes, and pins, table, deep-link, tooltip and list probes all still pass.
This commit is contained in:
parent
799345cba2
commit
7b27bf1b83
12 changed files with 56 additions and 19 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -22,7 +22,13 @@
|
|||
* D. the table covers EVERY code that has a resource — the "every link" claim;
|
||||
* E. every resource_url in the table exists in the pin index, so the table cannot
|
||||
* invent a link the map does not hold;
|
||||
* F. each pin's tier agrees with the URLs its code actually carries.
|
||||
* F. each pin's tier agrees with the URLs its code actually carries;
|
||||
* G. no URL is plain http. Every host we ship serves https directly and redirects
|
||||
* http there anyway, so a plain link only buys a redirect hop and a downgraded
|
||||
* scheme in a field worker's browser. 1,237 of them shipped that way until the
|
||||
* normalisation moved to the point where a URL ENTERS the index — doing it
|
||||
* per-source had left the 120 curated links behind, since fill-never-overwrite
|
||||
* preserves those and they never passed through a fetcher.
|
||||
*
|
||||
* ★ SELF-TEST: run with --poison to inject each defect in memory and confirm the clause
|
||||
* catches it. A check that has never been shown to fail proves nothing, and five of the
|
||||
|
|
@ -60,7 +66,7 @@ if (!rows?.length || !trows?.length || !Object.keys(codes).length) {
|
|||
if (POISON) {
|
||||
// one injected defect per clause, in memory only
|
||||
codes.und = { l: 'Undetermined', d: [{ s: 'Open Bible Stories', u: 'https://example.invalid/und' }] } // A
|
||||
codes.zzq = { l: '', s: 'http://ebible.org/zzq/' } // B
|
||||
codes.zzq = { l: '', s: 'http://ebible.org/zzq/' } // B + G
|
||||
trows = [{ ...trows[0], people_groups: (trows[0].people_groups || 0) + 999 }, // C
|
||||
...trows.slice(1, -1),
|
||||
{ ...trows[trows.length - 1], resource_url: 'https://example.invalid/invented' }] // E
|
||||
|
|
@ -101,6 +107,15 @@ for (const v of Object.values(codes)) {
|
|||
}
|
||||
const invented = trows.filter((t) => t.resource_url && !known.has(t.resource_url))
|
||||
if (invented.length) add('E', `${invented.length} table URL(s) exist in no pin record (e.g. ${invented[0].resource_url})`)
|
||||
// G — no plain-http URL
|
||||
const httpUrls = []
|
||||
for (const [c, v] of Object.entries(codes)) {
|
||||
for (const u of [v.s, v.f, ...(v.d || []).map((d) => d.u)]) {
|
||||
if (u && /^http:\/\//i.test(u)) httpUrls.push(`${c} ${u}`)
|
||||
}
|
||||
}
|
||||
if (httpUrls.length) add('G', `${httpUrls.length} plain-http URL(s) (e.g. ${httpUrls[0]})`)
|
||||
|
||||
// F — tier agrees with the URLs the code holds
|
||||
let fBad = 0
|
||||
for (const r of rows) {
|
||||
|
|
@ -116,7 +131,7 @@ if (POISON) {
|
|||
// resources dropped from a table titled "every link", and the most valuable clause is
|
||||
// the worst one to leave unproven. The injected codes above (und, zzq) are absent from
|
||||
// the table, so they poison D as well as A and B.
|
||||
const want = ['A', 'B', 'C', 'D', 'E', 'F']
|
||||
const want = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
|
||||
const caught = want.filter((c) => clauses.has(c))
|
||||
console.log(`COVERAGE-CONSISTENT --poison: ${caught.length}/${want.length} clause(s) caught their injected defect (${caught.join(',')})`)
|
||||
if (caught.length !== want.length) {
|
||||
|
|
|
|||
|
|
@ -52,10 +52,20 @@ function labelD43(items) {
|
|||
for (const i of items) (bySubject[i.subject] ||= []).push(i)
|
||||
return items.map((i) => ({
|
||||
s: bySubject[i.subject].length > 1 && i.owner ? `${i.subject} (${i.owner})` : i.subject,
|
||||
u: i.url,
|
||||
u: httpsify(i.url),
|
||||
}))
|
||||
}
|
||||
|
||||
// ONE normalisation for every source, applied where a URL ENTERS the index rather than
|
||||
// in each fetcher. Doing it per-source left 120 plain-http links behind — the curated
|
||||
// scripture_url values, which fill-never-overwrite preserves and which therefore never
|
||||
// passed through the catalogue's own normaliser. Scheme and host only; the path is
|
||||
// case-sensitive and is left exactly as published.
|
||||
const httpsify = (u) => {
|
||||
const m = /^https?:\/\/([^/]+)(\/.*)?$/i.exec(String(u || '').trim())
|
||||
return m ? `https://${m[1].toLowerCase()}${m[2] || ''}` : String(u || '').trim()
|
||||
}
|
||||
|
||||
const rowsOf = (p) => {
|
||||
const d = JSON.parse(readFileSync(p, 'utf8'))
|
||||
if (Array.isArray(d)) return d
|
||||
|
|
@ -81,7 +91,7 @@ try { arc = JSON.parse(readFileSync(ARC, 'utf8')).codes || {} } catch { arc = {}
|
|||
// exists it replaces that URL entirely rather than sitting alongside it.
|
||||
let share = {}
|
||||
try { share = JSON.parse(readFileSync(ARC_SHARE, 'utf8')).codes || {} } catch { share = {} }
|
||||
const filmUrl = (code, aa) => share[code] || (aa ? aa.u : '')
|
||||
const filmUrl = (code, aa) => httpsify(share[code] || (aa ? aa.u : ''))
|
||||
|
||||
// codes: one entry per ISO 639-3 that actually carries something worth linking to.
|
||||
const codes = {}
|
||||
|
|
@ -103,9 +113,9 @@ for (const r of res) {
|
|||
// link so the UI can say "Stories" rather than implying a Bible translation.
|
||||
...(dd && dd.items.length ? { d: labelD43(dd.items) } : {}),
|
||||
l: r.language || '',
|
||||
...((s0 || ebUrl) && { s: s0 || ebUrl }),
|
||||
...((s0 || ebUrl) && { s: httpsify(s0 || ebUrl) }),
|
||||
...(!s0 && ebUrl ? { sv: ee.t || '', sf: ee.full ? 1 : 0 } : {}),
|
||||
...((f || arcUrl) && { f: f || arcUrl }),
|
||||
...((f || arcUrl) && { f: httpsify(f || arcUrl) }),
|
||||
// has_audio carries a meaningful third state: 'unknown' means a pool we could not
|
||||
// query (key-blocked), NOT a no. Collapsing it to a boolean would turn "we did not
|
||||
// look" into "there is nothing" — the exact dishonesty the sheet's note guards.
|
||||
|
|
@ -142,7 +152,7 @@ for (const [c, aa] of Object.entries(arc)) {
|
|||
// An existing film link that is the ignored ?languageId= form is REPLACED where a real
|
||||
// deep link exists: keeping it would mean curation preserving a URL that goes nowhere
|
||||
// specific, which is not what the fill-never-overwrite rule is protecting.
|
||||
if (codes[c]?.f && share[c] && /[?&]languageId=/.test(codes[c].f)) { codes[c].f = share[c]; upgraded++ }
|
||||
if (codes[c]?.f && share[c] && /[?&]languageId=/.test(codes[c].f)) { codes[c].f = httpsify(share[c]); upgraded++ }
|
||||
if (codes[c]) { if (!codes[c].f) codes[c].f = filmUrl(c, aa); continue }
|
||||
codes[c] = { l: aa.n || '', f: filmUrl(c, aa), ck: 'arclight', un: 'ebible, door43, grn, api_bible, dbl, progress_bible' }
|
||||
arcOnly++
|
||||
|
|
@ -150,7 +160,7 @@ for (const [c, aa] of Object.entries(arc)) {
|
|||
for (const [c, ee] of Object.entries(eb)) {
|
||||
if (codes[c]) {
|
||||
// code exists but came from the Door43-only pass above and has no scripture yet
|
||||
if (!codes[c].s) { codes[c].s = ee.u; codes[c].sv = ee.t || ''; codes[c].sf = ee.full ? 1 : 0 }
|
||||
if (!codes[c].s) { codes[c].s = httpsify(ee.u); codes[c].sv = ee.t || ''; codes[c].sf = ee.full ? 1 : 0 }
|
||||
continue
|
||||
}
|
||||
codes[c] = { l: ee.ln || '', s: ee.u, sv: ee.t || '', sf: ee.full ? 1 : 0, ck: 'ebible', un: 'arclight, grn, api_bible, dbl, progress_bible' }
|
||||
|
|
|
|||
|
|
@ -61,6 +61,18 @@ function parseCsv(s) {
|
|||
return rows.filter((r) => r.length > 1).map((r) => Object.fromEntries(head.map((h, i) => [h, r[i] ?? ''])))
|
||||
}
|
||||
|
||||
// HTTPS, and the host lower-cased. The catalogue publishes plain http:// for all 1,237
|
||||
// of these and spells the host `eBible.org` on 6 of them. Every host here serves https
|
||||
// directly (verified 12/12 on a sample) and redirects http there anyway, so the plain
|
||||
// link only buys a redirect hop and a downgraded scheme in a field worker's browser.
|
||||
// ONLY the scheme and the HOST are normalised — the path is left exactly as published,
|
||||
// because a path is case-sensitive and "fixing" it would break the link.
|
||||
const httpsify = (u) => {
|
||||
const m = /^https?:\/\/([^/]+)(\/.*)?$/i.exec(String(u || '').trim())
|
||||
if (!m) return String(u || '').trim()
|
||||
return `https://${m[1].toLowerCase()}${m[2] || ''}`
|
||||
}
|
||||
|
||||
const num = (v) => (String(v).trim().match(/^\d+$/) ? Number(v) : 0)
|
||||
const rows = parseCsv(text)
|
||||
// badUrl is kept SEPARATE from badDomain: they are different defects and folding them
|
||||
|
|
@ -73,7 +85,7 @@ const stats = { total: rows.length, noCanon: 0, badUrl: 0, badDomain: 0, kept: 0
|
|||
const best = {}
|
||||
for (const r of rows) {
|
||||
const code = (r.languageCode || '').trim()
|
||||
const url = (r.publicationURL || '').trim()
|
||||
const url = httpsify(r.publicationURL || '')
|
||||
if (!code || code.length !== 3 || !url) continue
|
||||
const books = num(r.OTbooks) + num(r.NTbooks)
|
||||
if (books === 0) { stats.noCanon++; continue }
|
||||
|
|
|
|||
2
public/assets/tables/ebible-index.json
vendored
2
public/assets/tables/ebible-index.json
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/assets/tables/people-group-pins.json
vendored
2
public/assets/tables/people-group-pins.json
vendored
File diff suppressed because one or more lines are too long
2
public/doxa-maps/assets/tables/ebible-index.json
generated
2
public/doxa-maps/assets/tables/ebible-index.json
generated
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue