@neurowire/tap-wizard
Deterministic tap authoring and healing (version 0.1.0). Candidate selectors from page structure, a preview through the real engine, and a verification gate no tap is written without. See taps.
npm install @neurowire/tap-wizardDepends on core and ingest. It does not depend on @neurowire/taps: the wizard authors taps, it does not carry them.
No model in the loop
Nothing here calls an LLM, behind a flag or otherwise, and nothing needs an API key. The only network call in the package is the single page fetch in openTapSession.
Candidate suggestion
const TAP_FIELDS: readonly ['item', 'title', 'link', 'date', 'summary', 'author', 'tags']
type TapField = (typeof TAP_FIELDS)[number]
type TapCandidates = Record<TapField, string[]>
function suggestCandidates(html: string, seed?: FeedTemplate): TapCandidates
function emptyCandidates(): TapCandidates
function countMatches(html: string, selector: string): numbersuggestCandidates ranks selectors per field, best first, purely from structure:
item:article/li/div/sectionelements containing ana[href], keyed astag.first-classand ranked by repeat count. Only selectors repeating 3 to 300 times qualify; class-less selectors are dropped (a baredivmatches far too much),is-/has-/js-state classes are skipped when keying, and so are classes that are not plain CSS identifiers (md:flex,w-1/2,text-[13px]), which would produce a selector that throws when parsed.- every other field: read inside the FIRST element matching the best item selector, so a candidate is relative to an item exactly as
applyTemplateexpects.
A seed (typically proposeTemplate(...).template) is candidate zero for each field it fills. Per-field caps: 6 for item and title, 5 for the rest.
countMatches returns 0 for a blank or invalid selector rather than throwing.
Preview
interface TapPreviewEntry {
title: string; link: string; date: string
summary: string; author: string; tags: string[]
}
interface TapPreview {
matched: number
entries: TapPreviewEntry[]
error?: string
}
function previewTemplate(doc: RawDocument, template: FeedTemplate): Promise<TapPreview>Runs ingestDocument with an explicit template, which short-circuits every other resolution step, so the preview IS the engine output and cannot drift from what a real fetch would produce. It never throws: a bad selector comes back as error.
With item chosen but title still blank there is nothing to extract, so matched reports the raw item match count and entries is empty. That is what gives step one of the walkthrough feedback before a title exists.
The verification gate
interface VerifyCheck { name: string; ok: boolean; detail?: string }
interface VerifyReport {
score: number // share of applicable checks that passed, 0..1
matched: number
checks: VerifyCheck[]
ok: boolean // score >= threshold and no hard check failed
}
interface VerifyOptions {
minItems?: number // default 3
minTitleRate?: number // default 0.8
minDateRate?: number // default 0.5
minHostRate?: number // default 0.5
threshold?: number // default 0.75
}
function verifyTemplate(
doc: RawDocument,
template: FeedTemplate,
options?: VerifyOptions,
): Promise<VerifyReport>The checks, and which are hard failures, are listed under the verification gate. A template whose selector cannot be applied at all comes back with a single failed extract check and a score of 0.
The session
interface TapStep { field: TapField; label: string; required: boolean; hint: string }
const TAP_STEPS: readonly TapStep[]
interface TapSession {
readonly doc: RawDocument
readonly steps: readonly TapStep[]
readonly template: FeedTemplate
readonly seed: FeedTemplate | undefined
candidates(field: TapField): string[]
choose(field: TapField, selector: string): Promise<TapPreview>
preview(): Promise<TapPreview>
verify(options?: VerifyOptions): Promise<VerifyReport>
}
function createTapSession(doc: RawDocument): TapSession
function openTapSession(url: string, options?: OpenTapSessionOptions): Promise<TapSession>
function autoComplete(session: TapSession): Promise<TapPreview>createTapSession analyzes the page once (proposeTemplate for the seed, suggestCandidates for the alternatives) and prefills host and feedTitle, neither of which is a selector a human should have to type. item and title start blank.
A pick never refetches. The document is held for the life of the session and re-applied on every choose, which is what keeps the walkthrough instant and a noisy site from being hammered. openTapSession is the only function in the package that touches the network, exactly once.
choose with a blank selector clears an optional field and leaves a required one blank. template and candidates() hand out copies, so a caller cannot mutate the session.
import { openTapSession } from '@neurowire/tap-wizard'
const session = await openTapSession('https://example.com/blog')
await session.choose('item', session.candidates('item')[0])
const preview = await session.choose('title', 'h2.post-title')
const report = await session.verify()
if (report.ok) console.log(JSON.stringify(session.template, null, 2))autoComplete accepts the top candidate for every field the page offers, skipping the ones with no suggestion. It is what the CLI's --yes runs, and it decides nothing about saving: that stays the gate's call.