Move the console onto ReUI PRO
Install ReUI (MCP, skill, @reui registry, Tailwind v4, shadcn base-nova) and move the shared UI onto it: Button, Tag, Check, Input/Textarea, Segmented, Alert, Panel, Dialog, Tooltip, Menu and Select (Popover), Toast (sonner), the Table frame (DataGrid), the Dashboard KPI strip, the shnt Ops palette, Geist and the type scale. Design only: behaviour, texts and roles are unchanged. Tests that asserted the old DOM were updated; the tests now format numbers in English wherever they run. docs/reui-migration-plan.md records the plan and its decisions. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
f3531c64f9
680 files changed
+139836
No files matched your search
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
Have we dropped any of upstream's destinations?
|
||||
|
||||
The review plan said "open the same screen in `technitium-ui-ref` and compare by
|
||||
eye". Things slip past the eye, and they did, big ones: the About panel had lost
|
||||
eight of its nine links, and the whole `body` footer, with the author's support
|
||||
and donation addresses inside it.
|
||||
|
||||
This does it by list, and without a browser: upstream's console is a single
|
||||
`index.html` with ALL its panels in the markup, so its destinations are read from
|
||||
the HTML; ours are read from the application code. No new dependencies (putting
|
||||
Playwright in `package.json` would show up in the pull request diff), and
|
||||
therefore runnable anywhere with the reference console up.
|
||||
|
||||
node dev/check-parity-controls.mjs
|
||||
REF=http://other:5381 node dev/check-parity-controls.mjs
|
||||
|
||||
What it does NOT answer, worth having in front of you before believing a green:
|
||||
|
||||
- **It searches ALL the code, not the screen in question.** If an explanation
|
||||
appears in two dialogs and falls out of one, this stays green. Verified: the
|
||||
DNSKEY TTL text was deliberately removed from "Sign Zone" and it said nothing,
|
||||
because the same text lives in "DNSSEC Properties". It bites on what exists in
|
||||
only one place, which is how things actually get lost: the About panel lost
|
||||
eight links and none of them was anywhere else.
|
||||
- Whether a destination present in both points to the same thing from the
|
||||
equivalent screen.
|
||||
- Whether a button that exists in both does the same thing. For behaviour there
|
||||
is `check-parity-actions.sh`, which compares server state.
|
||||
*/
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const REF = process.env.REF ?? 'http://127.0.0.1:5381'
|
||||
const SOURCE = new URL('../src/', import.meta.url).pathname
|
||||
|
||||
/* Destinations upstream has and that must NOT be here, with their reason. Every
|
||||
line is a decision, not an oversight: if it is not justified, it is a finding. */
|
||||
const EXCUSED = new Map([
|
||||
// Today this list is deliberately empty: it is left written so that whoever
|
||||
// adds an excuse has to reason it out here.
|
||||
])
|
||||
|
||||
function files(dir) {
|
||||
const out = []
|
||||
for (const n of readdirSync(dir)) {
|
||||
const p = join(dir, n)
|
||||
if (statSync(p).isDirectory()) out.push(...files(p))
|
||||
else if (/\.(tsx?|css)$/.test(n)) out.push(p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const normalise = (u) => u.replace(/\/$/, '').replace(/^http:/, 'https:')
|
||||
|
||||
/** The four entities that appear in upstream's texts. */
|
||||
const decode = (t) =>
|
||||
t
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'|'/g, "'")
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
/*
|
||||
The source WITHOUT comments.
|
||||
|
||||
A comment is not interface: nobody reads it on screen. While they were in Spanish
|
||||
it made no difference, because they could not match an English sentence from
|
||||
upstream; once translated to English it stopped making no difference: a comment
|
||||
quoting the help text it explains would make a REALLY missing help text match,
|
||||
and this check would go green for the opposite reason to the one it exists for.
|
||||
|
||||
They are stripped before comparing. String literals in the code stay, since that
|
||||
is exactly where the text being searched for lives.
|
||||
*/
|
||||
const withoutComments = (t) =>
|
||||
t.replaceAll(/\/\*[\s\S]*?\*\//g, ' ').replaceAll(/^\s*\/\/[^\n]*/gm, ' ')
|
||||
|
||||
const ours = files(SOURCE)
|
||||
.filter((f) => !/\.test\./.test(f))
|
||||
.map((f) => withoutComments(readFileSync(f, 'utf8')))
|
||||
.join('\n')
|
||||
|
||||
const html = await fetch(`${REF}/`).then((r) => {
|
||||
if (!r.ok) throw new Error(`the reference console answers ${r.status} at ${REF}`)
|
||||
return r.text()
|
||||
})
|
||||
|
||||
/* Upstream's destinations, with the screen they appear on. */
|
||||
const theirs = new Map()
|
||||
for (const m of html.matchAll(/href="((?:https?:|mailto:)[^"]+)"/g)) {
|
||||
const url = normalise(m[1])
|
||||
if (!theirs.has(url)) theirs.set(url, [])
|
||||
/* The nearest container with an id, searching backwards. It is a HINT, not a
|
||||
truth: upstream's markup does not close a modal before the next one starts,
|
||||
so it serves to go and find it, not to cite it. */
|
||||
const before = html.slice(0, m.index)
|
||||
const near = [...before.matchAll(/id="(modal\w+|\w*TabPane\w+|footer)"/g)].pop()
|
||||
theirs.get(url).push(near ? near[1] : '?')
|
||||
}
|
||||
|
||||
/*
|
||||
The application's text, reduced to words.
|
||||
|
||||
Comparing markup does not work here. JSX splits sentences (`Add{' '}<code>!</code>
|
||||
character`) and many explanations travel as an ATTRIBUTE (`help="The duration for
|
||||
which…"`), so stripping the tags to clean up takes with it the very text you came
|
||||
looking for. Both produced false positives: the first accused the ACL text of
|
||||
being lost, the second three more that had been in place for months.
|
||||
|
||||
Reducing both sides to bare words (no punctuation, no case, no symbols) removes
|
||||
all of that difference. It adds noise (class names, attributes), but noise can
|
||||
only produce a false NEGATIVE across eight consecutive words, and that does not
|
||||
happen.
|
||||
*/
|
||||
const asWords = (t) =>
|
||||
' ' +
|
||||
t
|
||||
.toLowerCase()
|
||||
.replace(/"|'|'/g, ' ')
|
||||
.replace(/[^a-z0-9]+/g, ' ')
|
||||
.trim() +
|
||||
' '
|
||||
|
||||
/* JSX writes upstream's inline markup as markup too (`bind to <code>[::]</code>
|
||||
local`), and the tag names would sit between the words as `code code`. Only
|
||||
the attribute-free inline tags are dropped: a tag WITH attributes may be
|
||||
carrying the very `help="…"` being looked for. Links are the exception: their
|
||||
attributes are addresses, never explanations. */
|
||||
const prose = asWords(
|
||||
ours
|
||||
.replace(/<\/?(?:code|b|i|em|strong|kbd|br\s*\/?)>/g, ' ')
|
||||
.replace(/<\/?(?:Link|External|a)\b[^>]*>/g, ' ')
|
||||
.replace(/\{' '\}/g, ' ')
|
||||
// A bare JSX expression (`{hosts.doh}`) is a value filled at runtime, not
|
||||
// prose; its name would otherwise sit between the words. Upstream's side
|
||||
// drops its runtime labels the same way (see NOTES).
|
||||
.replace(/\{[A-Za-z_$][\w.$]*\}/g, ' ')
|
||||
// So is a template literal (`{`proxy_set_header ${realIpHeader} $remote_addr;`}`),
|
||||
// where upstream has a `<code id="lbl…">` its script fills.
|
||||
.replace(/\{`[^`]*`\}/g, ' '),
|
||||
)
|
||||
|
||||
const missing = []
|
||||
for (const [url] of theirs) {
|
||||
if (EXCUSED.has(url)) continue
|
||||
// the destination is searched for as-is and without the trailing slash: in the
|
||||
// code it may be wrapped by the formatter, but a URL is never split.
|
||||
const raw = url.replace(/^https:/, 'http:')
|
||||
if (ours.includes(url) || ours.includes(raw) || ours.includes(`${url}/`)) continue
|
||||
missing.push(url)
|
||||
}
|
||||
|
||||
const total = theirs.size
|
||||
for (const url of missing) {
|
||||
console.log(` MISSING ${url} (upstream, near: ${[...new Set(theirs.get(url))].join(', ')})`)
|
||||
}
|
||||
|
||||
console.log(
|
||||
missing.length === 0
|
||||
? `DESTINATION PARITY: all ${total} of upstream's are in the new console.`
|
||||
: `DESTINATION PARITY: ${missing.length} of ${total} missing.`,
|
||||
)
|
||||
|
||||
/*
|
||||
And the help texts. Upstream does not mark them with a class of its own: they are
|
||||
`div`s carrying the Bootstrap grid offset (`col-sm-offset-N col-sm-M`), which
|
||||
turns out to be a reliable signature because nothing else goes there.
|
||||
|
||||
They are compared by a run of words from the middle, not by the whole sentence:
|
||||
the beginning and the end are what gets reworded most while laying out, and what
|
||||
matters is whether the explanation is there or not.
|
||||
*/
|
||||
const HELP = [...html.matchAll(/<div class="col-sm-offset-\d+ col-sm-\d+"[^>]*>([\s\S]*?)<\/div>/g)]
|
||||
/* A `<span id="lbl…">` or `<code id="lbl…">` inside a note is a placeholder
|
||||
`loadDnsSettings` overwrites on every load (main.js:1369-1372: the DoH, DoT,
|
||||
DoQ and DoH(S) addresses). Its initial text is not contract, so it is not
|
||||
compared. */
|
||||
.map((m) => m[1].replace(/<(span|code) id="lbl\w+">[\s\S]*?<\/\1>/g, ' '))
|
||||
.map((t) => t.replace(/<[^>]*>/g, ' ').replace(/ /g, ' '))
|
||||
.map((t) => decode(t).replace(/\s+/g, ' ').trim())
|
||||
.filter((t) => t.length > 40)
|
||||
|
||||
const missingHelp = HELP.filter((t) => {
|
||||
const words = asWords(t).trim().split(' ')
|
||||
if (words.length < 12) return false
|
||||
// three runs spread out: if NONE is there, the explanation is not there
|
||||
const runs = [words.slice(2, 10), words.slice(6, 14), words.slice(-8)]
|
||||
return !runs.some((p) => prose.includes(` ${p.join(' ')} `))
|
||||
})
|
||||
|
||||
console.log('')
|
||||
for (const t of missingHelp) console.log(` MISSING help: ${t.slice(0, 120)}…`)
|
||||
console.log(
|
||||
missingHelp.length === 0
|
||||
? `HELP PARITY: all ${HELP.length} of upstream's texts are present.`
|
||||
: `HELP PARITY: ${missingHelp.length} of ${HELP.length} missing.`,
|
||||
)
|
||||
|
||||
/*
|
||||
And each field's EXAMPLE, which is what upstream puts in `placeholder`.
|
||||
|
||||
Added after finding thirty-four of them lost, and some were not decoration: the
|
||||
one on "Add Zone" read `example.com or 192.168.0.0/24 or 2001:db8::/64`, which is
|
||||
how you discover that field takes a reverse zone in CIDR form, and the one on
|
||||
"Certificate Association Data" carried the example hex and the PEM alternative.
|
||||
Without them the field does not explain what it expects.
|
||||
|
||||
It compares by VALUE against the whole source, with the same limitation as the
|
||||
other two checks and for the same reason: working out which component each field
|
||||
ends up in would require understanding the JSX. That lets through the case where
|
||||
the example is present but on another field (it happened with "confirm password",
|
||||
which existed in "Add User" and was missing from "Change Password"), so a value
|
||||
repeated across several fields has to be looked at by eye. What it does catch,
|
||||
and what was needed, is the wholesale loss.
|
||||
*/
|
||||
const EXAMPLES = new Map()
|
||||
for (const m of html.matchAll(/<(?:input|textarea)[^>]*>/g)) {
|
||||
const ph = /placeholder="([^"]*)"/.exec(m[0])
|
||||
if (!ph) continue
|
||||
const id = /id="([^"]*)"/.exec(m[0])
|
||||
/* Upstream's image was built on Windows and its multi-line examples carry
|
||||
CRLF; our tree is LF. It is the same normalisation `dev/check-parity.sh`
|
||||
does, and without it the four multi-line examples came out as lost over a
|
||||
carriage return. */
|
||||
const value = decode(ph[1]).replace(/\r\n/g, '\n')
|
||||
if (value.trim().length < 1) continue
|
||||
if (!EXAMPLES.has(value)) EXAMPLES.set(value, id ? id[1] : '?')
|
||||
}
|
||||
|
||||
/* The forwarder example is rewritten by `zone.js:139-152` depending on the
|
||||
protocol, and the same is done here in `forwarderExample`: the one in the HTML
|
||||
is only the initial value. */
|
||||
const DYNAMIC = new Set(['8.8.8.8'])
|
||||
|
||||
const missingExample = [...EXAMPLES].filter(
|
||||
([value]) => !DYNAMIC.has(value) && !ours.includes(value.split('\n')[0]),
|
||||
)
|
||||
|
||||
console.log('')
|
||||
for (const [value, id] of missingExample) {
|
||||
console.log(` MISSING example for ${id}: ${value.split('\n')[0].slice(0, 60)}`)
|
||||
}
|
||||
console.log(
|
||||
missingExample.length === 0
|
||||
? `EXAMPLE PARITY: all ${EXAMPLES.size} of upstream's are present.`
|
||||
: `EXAMPLE PARITY: ${missingExample.length} of ${EXAMPLES.size} missing.`,
|
||||
)
|
||||
|
||||
/*
|
||||
The notes and the explanations that are NOT on the grid offset.
|
||||
|
||||
HELP above only reads `col-sm-offset-*` divs, and on 2026-09-30 it said "all 110
|
||||
of upstream's texts are present" against a v15.5.1 whose new LDAP pane was not
|
||||
here at all: its eleven "Note!"/"Warning!" paragraphs are `<p>`, and its field
|
||||
explanations are `<div style="padding-top: 5px;">`. Neither shape was read. Both
|
||||
are now, with the same three-runs comparison.
|
||||
*/
|
||||
const NOTES = [
|
||||
...html.matchAll(/<p>([\s\S]*?)<\/p>/g),
|
||||
...html.matchAll(/<div style="padding-top: 5px;[^"]*">([\s\S]*?)<\/div>/g),
|
||||
// and the third shape, the Zone Options tabs': `<div><b>Note!</b> …</div>`
|
||||
...html.matchAll(/<div>\s*(<b>(?:Note|Warning)!<\/b>[\s\S]*?)<\/div>/g),
|
||||
]
|
||||
/* A `<span id="lbl…">` or `<code id="lbl…">` inside a note is a placeholder
|
||||
`loadDnsSettings` overwrites on every load (main.js:1369-1372: the DoH, DoT,
|
||||
DoQ and DoH(S) addresses). Its initial text is not contract, so it is not
|
||||
compared. */
|
||||
.map((m) => m[1].replace(/<(span|code) id="lbl\w+">[\s\S]*?<\/\1>/g, ' '))
|
||||
.map((t) => t.replace(/<[^>]*>/g, ' ').replace(/ /g, ' '))
|
||||
.map((t) => decode(t).replace(/\s+/g, ' ').trim())
|
||||
.filter((t, i, all) => t.length > 40 && all.indexOf(t) === i)
|
||||
|
||||
/* Texts upstream has and this console deliberately does not, with the reason.
|
||||
Each entry is a run of words, matched whole. */
|
||||
const EXCUSED_NOTES = [
|
||||
// deviation 1 in CONVENTIONS.md: Amber is not offered. No note names it today;
|
||||
// the entry stays so that one which does is excused for that reason and no other.
|
||||
'amber theme',
|
||||
]
|
||||
|
||||
/* A run only proves its own note when no other upstream note shares it. v15.6.1
|
||||
split the LDAP server note into "On Windows, the domain name used for LDAP
|
||||
Server above ..." and "On Linux, the domain name used for LDAP Server above
|
||||
...", and the Linux one passed on the run it shares with the Windows one while
|
||||
it was not here at all. */
|
||||
const noteWords = NOTES.map((t) => asWords(t))
|
||||
const distinctive = (run) => noteWords.filter((w) => w.includes(` ${run} `)).length === 1
|
||||
|
||||
const missingNotes = NOTES.filter((t) => {
|
||||
const words = asWords(t).trim().split(' ')
|
||||
if (words.length < 12) return false
|
||||
if (EXCUSED_NOTES.some((e) => asWords(t).includes(` ${e} `) && words.length < 40)) return false
|
||||
const all = [words.slice(2, 10), words.slice(6, 14), words.slice(-8)].map((p) => p.join(' '))
|
||||
const runs = all.filter(distinctive)
|
||||
/* And its last eight words, always. A run proves the note is there, not that it
|
||||
is whole: v15.6.2 added "and require a manual resync" to the end of the SOA
|
||||
serial warning, and every run of the old text still matched. */
|
||||
const end = words.slice(-8).join(' ')
|
||||
return !(runs.length ? runs : all).some((p) => prose.includes(` ${p} `)) || !prose.includes(` ${end} `)
|
||||
})
|
||||
|
||||
console.log('')
|
||||
for (const t of missingNotes) console.log(` MISSING note: ${t.slice(0, 120)}…`)
|
||||
console.log(
|
||||
missingNotes.length === 0
|
||||
? `NOTE PARITY: all ${NOTES.length} of upstream's notes and explanations are present.`
|
||||
: `NOTE PARITY: ${missingNotes.length} of ${NOTES.length} missing.`,
|
||||
)
|
||||
|
||||
/*
|
||||
And the short texts of a control: a radio's option, a modal's title, an entry of
|
||||
the account menu.
|
||||
|
||||
None of the passes above reads them (they are neither links, nor help, nor
|
||||
examples, nor notes), so a whole dialog could go missing and every one of them
|
||||
stay green. It is exactly what happened to `Change Theme`: absent by decision
|
||||
until 2026-10-01, and nothing here could have told whether it came back whole.
|
||||
They are compared as whole runs of words, against our source without comments.
|
||||
*/
|
||||
const accountMenu = html.slice(html.indexOf('<ul class="dropdown-menu">'))
|
||||
const CONTROL_TEXTS = [
|
||||
...[...html.matchAll(/<input type="radio"[^>]*>\s*([^<]+?)\s*<\/label>/g)].map((m) => ['radio', m[1]]),
|
||||
...[...html.matchAll(/<h4 class="modal-title"[^>]*>([^<]+)<\/h4>/g)].map((m) => ['modal title', m[1]]),
|
||||
...[...accountMenu.slice(0, accountMenu.indexOf('</ul>')).matchAll(/>([^<>]+)<\/a>/g)].map((m) => ['account menu', m[1]]),
|
||||
]
|
||||
.map(([kind, t]) => [kind, decode(t).replace(/\s+/g, ' ').trim()])
|
||||
.filter(([, t], i, all) => t.length > 0 && all.findIndex(([, u]) => u === t) === i)
|
||||
|
||||
/* Upstream's control texts this console deliberately does not have, with the reason. */
|
||||
const EXCUSED_TEXTS = new Map([
|
||||
['Amber Theme', 'deviation 1 in CONVENTIONS.md: Amber is not offered in Change Theme'],
|
||||
])
|
||||
|
||||
const missingText = CONTROL_TEXTS.filter(([, t]) => !EXCUSED_TEXTS.has(t) && !prose.includes(asWords(t)))
|
||||
|
||||
console.log('')
|
||||
for (const [kind, t] of missingText) console.log(` MISSING ${kind}: ${t}`)
|
||||
for (const [t, why] of EXCUSED_TEXTS) console.log(` EXCUSED ${t} (${why})`)
|
||||
console.log(
|
||||
missingText.length === 0
|
||||
? `CONTROL TEXT PARITY: all ${CONTROL_TEXTS.length - EXCUSED_TEXTS.size} of upstream's radio options, modal titles and account-menu entries are present; ${EXCUSED_TEXTS.size} excused (EXCUSED_TEXTS).`
|
||||
: `CONTROL TEXT PARITY: ${missingText.length} of ${CONTROL_TEXTS.length} missing.`,
|
||||
)
|
||||
|
||||
/*
|
||||
And the other direction, which none of the checks above can see: a field this
|
||||
console still draws after upstream REMOVED it. v15.5 dropped Auto Prefetch; its
|
||||
two fields stayed here, still required, and Settings could not be saved at all
|
||||
against the new server, with every parity check green, because every check
|
||||
asked "is upstream's text here?" and never "is our text still upstream's?".
|
||||
|
||||
Every `label="…"` in the source must exist, word for word, somewhere in
|
||||
upstream's page.
|
||||
*/
|
||||
/* Upstream builds part of its interface from JavaScript (row menus, the Apps
|
||||
buttons, the DHCP and Logs tab strips), so its scripts are read too. */
|
||||
const scripts = await Promise.all(
|
||||
[...html.matchAll(/<script src="(js\/[^"?]+)/g)].map((m) =>
|
||||
fetch(`${REF}/${m[1]}`).then((r) => (r.ok ? r.text() : '')),
|
||||
),
|
||||
)
|
||||
const theirWords = asWords([html, decode(html), ...scripts].join(' '))
|
||||
const OUR_LABELS = new Set()
|
||||
/* Where each label is written, for the Blocking exception below. */
|
||||
const LABEL_FILES = new Map()
|
||||
for (const f of files(SOURCE).filter((f) => !/\.test\.|fixture/.test(f))) {
|
||||
for (const m of withoutComments(readFileSync(f, 'utf8')).matchAll(/(?<![-\w])label="([^"{}]{3,})"/g)) {
|
||||
OUR_LABELS.add(m[1])
|
||||
LABEL_FILES.set(m[1], [...new Set([...(LABEL_FILES.get(m[1]) ?? []), f])])
|
||||
}
|
||||
}
|
||||
/* Accessible names this console gives to controls upstream leaves unnamed: a
|
||||
menu button's `aria-label` and a tab strip's. They are read by a screen
|
||||
reader, never drawn, so they are not interface text upstream could own. */
|
||||
const ACCESSIBLE_NAMES = new Set([
|
||||
'Blocking options',
|
||||
'Blocking sections',
|
||||
'Zone actions',
|
||||
'DNSSEC actions',
|
||||
'DHCP sections',
|
||||
'Logs sections',
|
||||
'Settings sections',
|
||||
])
|
||||
/*
|
||||
The Blocking section is the deliberate exception that adds a screen
|
||||
(CONVENTIONS.md, "Deliberate deviations"), so it has words upstream does not.
|
||||
They are named here one by one, each with where it is written and where its file
|
||||
declares it as ours, and each passes ONLY while every file that writes it is
|
||||
under `screens/blocking/`. Anywhere else in the console it is still a stray, and
|
||||
a label that is not in this list is a stray even inside the section.
|
||||
*/
|
||||
const BLOCKING = join(SOURCE, 'screens/blocking/')
|
||||
const BLOCKING_OWN_LABELS = new Map([
|
||||
['Block List Domains', 'Overview figure, blocking/Overview.tsx:361; declared ours at Overview.tsx:35'],
|
||||
['Your Rules', 'Overview figure, blocking/Overview.tsx:365; declared ours at Overview.tsx:35'],
|
||||
['Tree', "the Tree view's Blocked/Allowed selector, blocking/Rules.tsx:329; declared ours at Rules.tsx:37-38"],
|
||||
['More lists', "Quick Add's group of the console's own catalogue, blocking/BlockLists.tsx:508; declared ours at BlockLists.tsx:48-49"],
|
||||
['Listed as', 'Check a domain, the list entry that matched, blocking/DomainCheck.tsx:107; declared ours at DomainCheck.tsx:15'],
|
||||
['Rule', 'Check a domain, the rule that matched, blocking/DomainCheck.tsx:117; declared ours at DomainCheck.tsx:15'],
|
||||
['Add to group', 'the group a new list goes into, blocking/BlockLists.tsx:666; declared ours at BlockLists.tsx:48'],
|
||||
])
|
||||
const onlyInBlocking = (label) => (LABEL_FILES.get(label) ?? []).every((f) => f.startsWith(BLOCKING))
|
||||
|
||||
const labels = [...OUR_LABELS]
|
||||
const upstreamLabels = labels.filter((l) => theirWords.includes(asWords(l)))
|
||||
const accessible = labels.filter((l) => !upstreamLabels.includes(l) && ACCESSIBLE_NAMES.has(l))
|
||||
const blockingOwn = labels.filter(
|
||||
(l) => !upstreamLabels.includes(l) && !accessible.includes(l) && BLOCKING_OWN_LABELS.has(l) && onlyInBlocking(l),
|
||||
)
|
||||
const strayLabels = labels.filter((l) => !upstreamLabels.includes(l) && !accessible.includes(l) && !blockingOwn.includes(l))
|
||||
|
||||
console.log('')
|
||||
for (const l of blockingOwn) console.log(` BLOCKING OWN label: ${l} (${BLOCKING_OWN_LABELS.get(l)})`)
|
||||
for (const l of strayLabels) console.log(` NOT UPSTREAM label: ${l}`)
|
||||
console.log(
|
||||
strayLabels.length === 0
|
||||
? `LABEL PARITY: ${upstreamLabels.length} of our ${labels.length} labels exist upstream; ` +
|
||||
`${accessible.length} are accessible names upstream leaves unnamed (ACCESSIBLE_NAMES) and ` +
|
||||
`${blockingOwn.length} are the Blocking section's own (BLOCKING_OWN_LABELS).`
|
||||
: `LABEL PARITY: ${strayLabels.length} of ${labels.length} labels are not upstream's.`,
|
||||
)
|
||||
|
||||
process.exit(
|
||||
missing.length + missingHelp.length + missingExample.length + missingNotes.length + missingText.length + strayLabels.length === 0
|
||||
? 0
|
||||
: 1,
|
||||
)
|
||||
Reference in new issue
Block a user