/**
* The visual editor's ONE decision module — every editability verdict, refusal
* predicate, escape rule and coordinate convention, in a single browser-safe
* file with no imports.
*
* ☠️ **Why this file exists: the system has TWO parsers by necessity and used to
* have two IMPLEMENTATIONS of every decision by accident.** The pod parses with
* oxc (native, fast enough for the tagger's per-transform hot path, byte
* offsets for MagicString splices); the cockpit parses with Babel (the only
* maintained parser that runs in a browser — `@oxc-parser/wasm` died at 0.60.0
* in 2025 — and the only one with a printer for the file plane). Neither side
* can adopt the other's parser, so for a year every predicate lived twice,
* maintained as a hand "mirror" with docblocks pleading that they stay
* byte-for-byte — and they drifted, four separate times, each drift shipping as
* an owner-facing bug: a shape one side called editable and the other refused
* is either an element the inspector won't select or a green success over a
* file nothing wrote.
*
* The fix is not one parser — it is ONE implementation. Everything here is a
* pure function over STRUCTURAL nodes ({@link AnalysisNode}) and accepts BOTH
* tree dialects: oxc's ESTree output (one `Literal` kind,
* `StaticMemberExpression`) and Babel's (split `StringLiteral` /
* `NumericLiteral` / …, `MemberExpression`). The pod's analyzer and transformer
* call these over oxc trees; the cockpit's AstEngine calls the SAME functions
* over Babel trees. `analysis-core.parity.spec.ts` parses one fixture corpus
* with both parsers and asserts identical verdicts — the drift gate that
* replaces the hand-mirror discipline.
*
* ⚠️ **Browser-safe is a hard constraint, not a preference.** The cockpit
* imports this file into a Vite SPA bundle (`@proyecta-ai/vite/analysis`), so
* nothing here may import `fs`, `path`, `oxc-parser`, `magic-string`, or
* anything else — the impure shells stay in `ast-analyzer.ts` /
* `ast-transformer.ts`.
*
* ⚠️ **Version skew is the residual, and it is bounded by design.** A pod's
* copy of this module is frozen at seed time; the cockpit's is evergreen. That
* skew existed before (as two files drifting apart in the same repo) — what is
* new is that at any single version there is exactly one behaviour, so skew is
* an honest "old pod, old rules" rather than "two current files disagree".
*/
/**
* A parsed node, structurally: the shape shared by oxc's ESTree output and
* Babel's AST for every construct these decisions touch. One declaration, so
* the writer, the analyzer's gate and the cockpit's pre-flight cannot come to
* hold three different ideas of the same tree.
*/
export interface AnalysisNode {
type: string;
start: number;
end: number;
[key: string]: unknown;
}
/**
* The `data-ve-line` / `data-ve-col` convention: BOTH are 1-based.
*
* ☠️ The cockpit's Babel `loc.start.column` is 0-BASED, and for a year the
* cockpit compared it against these 1-based coordinates raw — every lookup was
* off by one and only worked because a ±3 "drift" fallback absorbed it. Any
* consumer of a Babel `loc` must add 1 before comparing against a `data-ve-*`
* coordinate; {@link offsetToLineCol} is the tagger's own arithmetic and is
* 1-based by construction.
*/
export declare const VE_COLUMN_BASE = 1;
/** 1-based line/col of a byte offset — the tagger's own arithmetic. */
export declare function offsetToLineCol(source: string, offset: number): {
line: number;
col: number;
};
/**
* ☠️ **The column tolerance is a FALLBACK, not a match — the NEAREST opening tag
* on the line wins, and an exact column always beats it.**
*
* `component-tagger.ts` emits `offsetToLineCol(code, node.start)`, the same
* function above, so a legitimate coordinate is EXACT. The tolerance only ever
* existed as slack for a coordinate that drifted. This is the ONE home of the
* constant: the analyzer, the transformer and the cockpit's own finder used to
* carry private copies (5, 5 and 3), and the 3 was quietly absorbing the
* 0-vs-1-based column skew documented on {@link VE_COLUMN_BASE}.
*/
export declare const COLUMN_TOLERANCE = 5;
/**
* The JSX element whose opening tag sits nearest `targetLine:targetCol`
* (1-based, {@link VE_COLUMN_BASE}), with its ancestor chain.
*
* Returning the FIRST node inside the tolerance instead — pre-order, so the
* outermost — made every short wrapper swallow its own child: in
* `
` — the ordinary
* shape of every sign-in prompt, price line and footnote in a generated app —
* carries a spacer beside its one real sentence. Counting that spacer as a
* second own-text node would trip the split-text refusal and turn the element
* away.
*
* Booleans and `null` are literals that render NOTHING, so they are not text
* and are left out of the count deliberately.
*/
export declare function ownTextNodes(children: AnalysisNode[]): AnalysisNode[];
/**
* The text ONE child contributes as the element's own, or `null` when it
* contributes none — the per-child spelling of {@link ownTextNodes}, with the
* same blankness rule and the same "booleans and null render nothing" call.
* The cockpit's `buildNodeInfo` filters and concatenates with this; keeping it
* beside `ownTextNodes` is what keeps the two spellings one rule.
*/
export declare function ownTextValue(child: AnalysisNode): string | null;
/**
* The full text verdict for an element.
*
* ☠️ **`dynamic-content` and `has-children` are OPPOSITES downstream, so the
* expression test must run FIRST** — with the nesting test first the reason
* depends on which kind of child the element happens to have as well. `type` is
* `dynamic` either way; what the ordering decides is what the `reason` MEANS,
* and `shouldOpenCmsAdmin` keys on it: `
{item.name}
` is
* CMS-backed text that happens to sit next to a tag and must still say
* `dynamic-content`, while `` is code.
*/
export declare function analyzeTextContent(element: AnalysisNode): TextContentAnalysis;
export interface ElementTypeAnalysis {
type: 'static' | 'dynamic';
reason?: 'conditional-expression' | 'complex-parent' | 'map-expression';
}
/**
* Is removing this element's whole span safe, or does the SLOT it fills carry
* meaning that outlives it?
*
* `refuseUnparseableWrite` is NOT a substitute: it only catches a hole that
* stops parsing, and the worst shape parses fine — a block-bodied `.map()`
* callback left as `return ;` is valid JavaScript, so deleting ONE card
* silently deletes the entire list and reports success.
*
* ☠️ A FRAGMENT BOUNDS THE WALK EXACTLY AS AN ELEMENT DOES. Both mean "an
* enclosing JSX node already owns this subtree", so whatever expression sits
* above them produces the FRAGMENT, not this element — deleting a child of
* `{items.map((i) => (<>>))}` removes one tag from inside the
* fragment, which is safe. Without the break the walk ran past the fragment to
* the `.map`/`&&` above and refused the delete, telling the owner something
* untrue about their own source.
*/
export declare function analyzeDeletionSafety(parents: AnalysisNode[]): ElementTypeAnalysis;
/**
* The JSX grammar defines `JSXTextCharacter` as SourceCharacter *except* `{`,
* `<`, `>` and `}` — all four ordinary characters in a sentence, and this text
* is whatever the owner typed on the page (`Precios < $100`, `Horario {lunes a
* viernes}`, `2 > 1`). Measured: `<`, `>` and `}` each make the emitted file
* unparseable, and `{…}` is worse — it parses, silently becoming an expression
* container referencing an identifier that is usually not in scope, so the page
* throws at render instead of failing at build.
*
* Entities rather than an expression container (`{"…"}`), which would also be
* valid: the container shape would trip the data-bound refusal on the NEXT
* edit. `&` is deliberately NOT escaped.
*
* ONE implementation. Both writers (the pod's MagicString splice and the
* cockpit's Babel re-print) import it, so they compose the same file by
* construction rather than by a character-for-character mirror.
*/
export declare function escapeJsxText(value: string): string;
/**
* A double-quoted JSX attribute value ENDS at the first `"`, and JSX does not
* honour backslash escapes. Only the quote is escaped; `&` is left alone ON
* PURPOSE — `[&>svg]:size-4` and friends are everywhere in the template's
* classes, and Tailwind's candidate scanner reads the SOURCE text, so rewriting
* `&` to `&` would keep the runtime value correct while silently dropping
* the generated utility.
*/
export declare function escapeJsxAttributeValue(value: string): string;
/**
* The element's rendered name (`Button`, `Dialog.Trigger`). Was duplicated in
* the tagger and (dead) in the analyzer; the tagger's hot path pays nothing for
* importing it from here.
*/
export declare function getElementName(nameNode: AnalysisNode): string;
/**
* Trees the visual editor must never write — or offer.
*
* ☠️ **`src/components/admin/**` and `src/vendor/**` are SYNCED trees, not the
* app's own source.** `scripts/sync-cms-admin-template.mjs` generates them from
* `@proyecta/cms-admin-react` at seed time, and the builder already refuses its
* own FE engineer every op over them (`appBackendWriteBoundary`) — but the
* visual editor's middleware had no such line, so an owner who opened `/admin`
* inside the visual-mode preview could click-edit the admin console's own
* source. A splice there is silent drift from the generated lib: it survives on
* the pod, is committed by `autoVersionHook`, and diverges the app's console
* from every fix the lib ships after it.
*
* THREE consumers, one declaration, same shape as every boundary in this
* module: the tagger (stops stamping `data-ve-*`, so nothing in these trees is
* hoverable on newly seeded pods), `resolveComponentId` (refuses the RPCs, so
* nothing in these trees is analyzable or writable regardless of what a
* same-origin script posts), and the cockpit (drops hover/select for these
* paths, which is the only line that reaches pods seeded BEFORE this constant
* existed — their baked tagger and middleware keep the old behaviour forever).
*
* Matched on the tagger's own repo-relative, forward-slash path spelling.
*/
export declare const PROTECTED_SOURCE_TREES: readonly string[];
/** Is this repo-relative path inside a tree the visual editor must not touch? */
export declare function isProtectedSourcePath(relativePath: string | null | undefined): boolean;
//# sourceMappingURL=analysis-core.d.ts.map