///
/**
* The gate checks every app runs, written once (docs/GATES.md §23).
*
* Until UIG-10 Peek and Ship each wrote them into their own
* `scripts/gates-checks.mjs`. 65 different checks for finished tickets, counted
* on 17 September: 28 were identical in both, 21 were the same check written two
* ways, 2 read each app's real page, and 14 read code only one app has. This is
* the first three groups as one list. The fourth stays in the app's own checks
* file, beside this list.
*
* Where Peek and Ship wrote one check two ways, the check here runs both, so it
* passes wherever either did: the raw-link probe with both of their texts, the
* token probes against each token config the app has.
*
* `scripts/gates-compare.mjs` holds this list to Peek's and Ship's: every check
* of theirs must be here, or be about code only that app has, or the comparison
* fails and names it.
*
* A check for a ticket not built yet is a first guess at the evidence, taken
* from the ticket's own text; the ticket that builds it changes it here.
*/
import type { CheckResult, GateCheck, GateHelpers, GateTicket } from './status'
export interface AppCheckOptions {
/** Where the app is inside the repository: `.` unless it sits in a folder of its own. */
app?: string
/** A real page of the app, relative to the app, full of elements that are not controls. */
page: string
/** The ticket the gate chain is listed under, and whether this repo owns it: UIG-3 in Peek, UIG-4 in Ship. */
chain?: { ref: string; owner: boolean }
/** The token lint configs the app has, relative to the app. */
tokenConfigs?: string[]
/** The part a hand-made column header should name (UIG-22): `Panel` since 0.37.0, `ContainerHeader` before. */
headers?: string[]
}
export const APP_TICKET_TITLES: Record = {
'UIG-2': 'The tracking rails — GATES.md, the status script, the guide committed',
'UIG-3': 'Tracer bullet — one rule, end to end, blocking in Peek',
'UIG-4': 'The same chain, blocking in Ship',
'UIG-6': 'Branch protection — the backstop, all three repos',
'UIG-7': 'Lint rule — every remaining raw element',
'UIG-8': 'Lint rule — forbid the reach',
'UIG-9': 'Lint rule — the className allow-list',
'UIG-10': 'create-app — a command that makes a new Estiva app that runs',
'UIG-13': "The registry widens to Peek's 115 and Ship's 74, with classification",
'UIG-19': 'Lock the contract in CI',
'UIG-20': 'The Claude skill, reading the registry',
'UIG-21': 'CLAUDE.md becomes an index, not a lecture',
'UIG-22': 'Fingerprint — a hand-made header row',
'UIG-23': 'Fingerprint — a hand-made empty state',
'UIG-24': 'Fingerprint — a browser tooltip where ours belongs',
'UIG-25': 'Fingerprint — a component copied out by hand',
'UIG-27': 'The components the apps had to build themselves — Link, ProgressBar, EmptyState padding',
'UIG-28': 'Close the two holes in the token contract — arbitrary values, and inline style',
'UIG-30': 'RichText — one component that draws a message\'s text, for both apps',
'UIG-32': 'Peek and Ship take their gate pieces from the package',
'UIG-33': 'Link — a whole row that is one link',
'UIG-37': 'The gates refuse a TypeScript eslint-disable comment — know the rule names, rules off',
'UIG-42': "What the gate can't see yet — a box around a part, a screen drawn by hand, two blind spots in the token check",
}
/** Run several checks as one: the first that does not pass is the answer. */
function all(h: GateHelpers, steps: (() => CheckResult | Promise)[], label: string): () => Promise {
return async () => {
for (const step of steps) {
const r = await step()
if (r.result !== 'pass') return r
}
return h.PASS(label)
}
}
export function appChecks(h: GateHelpers, { app = '.', page, chain = { ref: 'UIG-3', owner: false }, tokenConfigs = ['eslint.tokens.config.js', 'eslint.config.js'], headers = ['Panel'] }: AppCheckOptions): GateTicket[] {
const WEB = app === '.' ? '' : `${app.replace(/\/$/, '')}/`
const cwd = app === '.' ? undefined : app
const PROBE = 'src/components/__gates_probe__.tsx'
const GATES = 'eslint.gates.config.js'
const at = (rel: string) => `${WEB}${rel}`
const probe = (code: string, expect: 'error' | 'warning' | 'none', mentions?: string, file = PROBE, config = GATES) => () =>
h.lint({ ...(cwd ? { cwd } : {}), config, file, code, expect, mentions })
const component = (body: string) => `export function Probe() {\n return ${body}\n}\n`
const installed = at('node_modules/@estiva-app/ui/dist/index.d.ts')
const realPage = (): Promise => h.lint({ ...(cwd ? { cwd } : {}), config: GATES, file: page, code: h.read(at(page)), expect: 'none' })
const button = component('')
/** A line TypeScript's own lint writes; ESLint refuses it where TypeScript's plugin is unknown (UIG-37). */
const typescriptDirective = '// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const probe: any = 1\n'
const ticket = (ref: string, checks: GateCheck[], owner = false): GateTicket => ({ ref, title: APP_TICKET_TITLES[ref], owner, checks })
const tokenProbe = (code: string, expect: 'error' | 'warning' | 'none', mentions?: string, file = PROBE) =>
all(h, tokenConfigs.map((config) => probe(code, expect, mentions, file, config)), `${tokenConfigs.join(' and ')}: as expected`)
return [
ticket('UIG-2', [
{ what: 'npm run gates:status is wired', run: () => h.script('package.json', 'gates:status') },
]),
ticket(chain.ref, [
{ what: 'the gate lint config loads', run: () => h.loads(at(GATES)) },
{ what: `${at('package.json')} has lint:rules`, run: () => h.script(at('package.json'), 'lint:rules') },
{ what: 'CI runs lint:rules', run: () => h.ci('lint:rules') },
{ what: 'the committed hook runs the gate lint', run: () => h.hook('.claude/settings.json', 'gates') },
{ what: '.gates-count.json is committed and parses', run: () => h.json(h.exists('.gates-count.json') ? '.gates-count.json' : at('.gates-count.json')) },
{ what: 'a raw