fix(tooling): parse docs prop declarations with TypeScript AST

astra
dev 2 weeks ago
parent 25e49b6931
commit e648502c57

@ -197,7 +197,7 @@ Queda una condición del autor: el push (L-123).
| L-154 | «opt-in» sin calificar para announce en `docs/decisions/book-deviations.md:508` y `src/uix/sema/chans/announce.ts:13` (cierto solo para el motor desnudo) | F4 adversarial L-ADV-4 | — | trivial | ARREGLADO | pasada de documentación 2026-09-17: `book-deviations.md`, `announce.ts` y `theming/channels.md:58` califican el opt-in (motor desnudo) frente al cableado por defecto de las raíces |
| L-155 | Announce sin pin directo en attach (default ON) ni en motor desnudo (opt-in); la viñeta de attach de sema.md nombra solo `defineEngineSemantic` y omite `defineUixServices({ events: { announce: false } })`, que también sirve | F4 adversarial L-ADV-5/6 | — | pequeño | DIFERIDO | hoy ciertas (medido); solo contracts.test.ts:670-702 pinea standalone |
| L-156 | Siete handoffs de julio en minúscula (`docs/process/continue-*.md`: agente, arts-docs-reconciliation, audit-fixes, cleanroom-fixes, proof-of-human, runed-tabbable-port, with-stumbles-s2-s6) quedaron fuera del censo del plan | F6 | — | — | DATO | no llevan cabecera CONGELADO ni están transcritos aquí: son históricos (varios se declaran cerrados en su cabecera) |
| L-157 | `docs-check` I10 cosecha los identificadores del código LÍNEA A LÍNEA (`identifiersOf`), así que el conjunto de verdad sigue a cómo prettier parte o junta las listas de import/export: 219 de 267 READMEs cambiaron su conjunto (55 crecen, 76 encogen) | F5 medición de guards (8a7a31fb9) | — | pequeño | CONFIRMADO | ceguera LATENTE, no realizada: las 979 filas de prop evaluadas dan el mismo veredicto en ambas revisiones (0 volteos). Cerrarla exige cosechar del AST, no por línea |
| L-157 | `docs-check` I10 cosecha los identificadores del código LÍNEA A LÍNEA (`identifiersOf`), así que el conjunto de verdad sigue a cómo prettier parte o junta las listas de import/export: 219 de 267 READMEs cambiaron su conjunto (55 crecen, 76 encogen) | F5 medición de guards (8a7a31fb9) | — | pequeño | ARREGLADO | `docs-check` I10 usa ahora AST de TypeScript para declaraciones e imports/exports de `.ts` y bloques `<script>` de `.svelte`; prueba de formato compacto/multilínea 2/2, inyección negativa de prop fantasma rechazada y `docs:check` 0/0 tras restaurar el README. Sin cambio de runtime. |
| L-158 | `pack-census.test.ts` detecta emisiones con `source.includes("'evento'")`: solo la comilla SIMPLE. Una emisión escrita con comilla doble o backtick produce un falso «nadie lo emite» | F5 medición de guards | — | trivial | CONFIRMADO | medido: dos emisiones de chronos estaban con comilla doble y el formateo las normalizó. Solo puede dar rojo falso, nunca tapar |
| L-159 | `recipe-css-contract.test.ts` delimita el lado derecho de un `export type` con el PRIMER `;`: un infractor repartido en ≥2 miembros de un tipo objeto queda fuera del barrido | F5 medición de guards | — | trivial | CONFIRMADO | hipotético hoy: 0 fugas medidas en el árbol (ningún literal de rol vive en el cuerpo de un `export type` fuera del span) |
| L-160 | `.prettierrc` usa `endOfLine: "auto"`: un CR huérfano en un fichero hace que Prettier lo reescriba ENTERO en CR, y entonces el fichero es UNA sola línea para cualquier guard que parta por saltos de línea | F5 (8a7a31fb9) | — | pequeño | DIFERIDO | pasó en `REVIEW-theming-2026-08-24.md` (normalizado a LF ahí mismo); fijar `endOfLine: "lf"` reescribiría el árbol entero: fila aparte |

@ -170,6 +170,16 @@ completo del segundo push pasó 469/469 archivos y 5460/5460 tests, junto con
el check, build y smoke de `apps/base`. Los commits `aecf339ce` y `69162958c`
quedaron publicados en `gita/astra`.
### Guardia documental L-157 (2026-09-21)
`docs-check` I10 ya obtiene los nombres de declaraciones con el AST de
TypeScript, incluidos los bloques `<script>` de Svelte, en vez de leer una línea
cada vez. La prueba comprueba que imports, exports, tipos y destructuring dan el
mismo conjunto con formato compacto o multilínea y que comentarios, cadenas y
markup no aportan nombres. Una prop inventada en un README hizo fallar I10; el
archivo se restauró y `docs:check` volvió a 0 errores y 0 avisos. `check:gate`
terminó sin errores nuevos en `src/` ni `scripts/`. No cambia el runtime de UIX.
El [ledger de cierre](./LEDGER-cierre-2026-09.md) es la fuente vigente del estado de
trabajos anteriores; los handoffs congelados sirven de procedencia. Este plan
ordena la auditoría, **no crea otro ledger ni considera cerradas sus filas**.

@ -0,0 +1,56 @@
import ts from 'typescript';
/** Declared names used by the README prop-table guard, independent of formatting. */
export function identifiersOfSource(source: string, file: string): Set<string> {
const scripts = file.endsWith('.svelte')
? [...source.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)].map((match) => match[1])
: [source];
const names = new Set<string>();
function addName(name: ts.Node | undefined): void {
if (name && (ts.isIdentifier(name) || ts.isStringLiteral(name))) names.add(name.text);
}
for (const script of scripts) {
const root = ts.createSourceFile(
file + '.ts',
script,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS
);
function visit(node: ts.Node): void {
if (
ts.isPropertySignature(node) ||
ts.isPropertyDeclaration(node) ||
ts.isMethodSignature(node) ||
ts.isMethodDeclaration(node) ||
ts.isGetAccessorDeclaration(node) ||
ts.isSetAccessorDeclaration(node) ||
ts.isPropertyAssignment(node) ||
ts.isShorthandPropertyAssignment(node) ||
ts.isParameter(node) ||
ts.isVariableDeclaration(node) ||
ts.isImportClause(node) ||
ts.isImportSpecifier(node) ||
ts.isExportSpecifier(node) ||
ts.isNamespaceImport(node) ||
ts.isClassDeclaration(node) ||
ts.isInterfaceDeclaration(node) ||
ts.isTypeAliasDeclaration(node) ||
ts.isFunctionDeclaration(node) ||
ts.isEnumDeclaration(node) ||
ts.isEnumMember(node)
) {
addName(node.name);
} else if (ts.isBindingElement(node)) {
addName(node.propertyName);
addName(node.name);
}
ts.forEachChild(node, visit);
}
visit(root);
}
return names;
}

@ -56,6 +56,7 @@ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { join, dirname, resolve, relative, sep } from 'node:path';
import { UIX_ALIASES } from '../uix.aliases.js';
import { identifiersOfSource } from './docs-check-identifiers';
import { generateVocabulariesDoc } from './docs-vocabularies';
const REPO = resolve(import.meta.dirname, '..');
@ -968,15 +969,10 @@ const identifierCache = new Map<string, Set<string>>();
function identifiersOf(file: string): Set<string> {
const cached = identifierCache.get(file);
if (cached) return cached;
const out = new Set<string>();
if (existsSync(file) && statSync(file).isFile()) {
for (const line of read(file).split('\n')) {
const m = line.match(
/^\s*(?:export\s+)?(?:public\s+|private\s+|protected\s+|readonly\s+)*'?([A-Za-z_$][\w$]*)'?\s*\??\s*[:=,)]/
);
if (m) out.add(m[1]);
}
}
const out =
existsSync(file) && statSync(file).isFile()
? identifiersOfSource(read(file), file)
: new Set<string>();
identifierCache.set(file, out);
return out;
}

@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import { identifiersOfSource } from '../../scripts/docs-check-identifiers';
describe('README prop declaration index', () => {
it('finds the same declarations in inline and wrapped TypeScript', () => {
const inline = `
import { source, other as local } from './source';
export { source, local as exported };
export type Props = { value?: string; onValueChange?: () => void };
const { open, disabled: isDisabled } = $props<Props>();
`;
const wrapped = `
import {
source,
other as local
} from './source';
export {
source,
local as exported
};
export type Props = {
value?: string;
onValueChange?: () => void;
};
const {
open,
disabled: isDisabled
} = $props<Props>();
`;
expect(identifiersOfSource(inline, 'types.ts')).toEqual(
identifiersOfSource(wrapped, 'types.ts')
);
expect(identifiersOfSource(inline, 'types.ts')).toEqual(
new Set([
'source',
'local',
'exported',
'Props',
'value',
'onValueChange',
'open',
'disabled',
'isDisabled'
])
);
});
it('reads Svelte scripts but not markup, comments or string contents', () => {
const source = `
<script lang="ts">
// phantom?: string
const note = 'invented: true';
let { value, disabled: isDisabled } = $props<{ value: string; disabled?: boolean }>();
</script>
<div data-visual="mark" />
`;
const names = identifiersOfSource(source, 'example.svelte');
expect(names).toEqual(new Set(['note', 'value', 'disabled', 'isDisabled']));
expect(names.has('phantom')).toBe(false);
expect(names.has('invented')).toBe(false);
expect(names.has('visual')).toBe(false);
});
});
Loading…
Cancel
Save

Powered by TurnKey Linux.