Skip to content

Latest commit

 

History

History
156 lines (134 loc) · 37.3 KB

File metadata and controls

156 lines (134 loc) · 37.3 KB

Roadmap — sii (TypeScript)

The single source of truth for "are we there yet?". Every user-facing surface (CLI verb + MCP tool) gets a row here; tick it ✅ when the work merges.

For the engineering detail behind the 📋 / 💭 / 🔒 rows — spike plans, blocking decisions, unobserved contracts and known limitations — see BACKLOG.md. That file is subordinate to this one: status lives here, detail lives there.

Where we're going

One @dostertags/sii-core engine, two surfaces:

  • @dostertags/sii-cli — the terminal surface (and what Claude Code drives via Bash).
  • @dostertags/sii-mcp — a stdio MCP server, the integration point for Claude Code AND Claude Desktop (both connect to stdio MCP servers).

Together they cover the day-to-day a Chilean contador or single taxpayer performs against SII: authenticate, read identity + contributor data, query the RCV, prepare and read F29 / F22, read BHE/DTE, and pull composite summaries — with a single-account, operate-centric identity model (ADR-005).

Non-goals: multi-tenant SaaS; custodying a third party's Clave; the cert sandbox; any code on third-party SII libraries (ADR-004); an embedded plugin (dropped — CLI + MCP only).

Status legend

Meaning
Shipped, tested + live-validated

Foundations (must land before feature surfaces)

Status Item Notes ADR
CFD scaffolding docs, ADRs, slash commands, CI validate-context ADR-001
Monorepo skeleton pnpm workspaces + TS project references; core + cli + mcp stubs ADR-002
Install + build green pnpm install + tsc -b (strict) + eslint + prettier + 39 vitest tests, all green ADR-002
NodeNext build module/moduleResolution → NodeNext; .js on relative imports; tsc -b output runs directly on Node (no bundler), verified via the built sii binary ADR-009
Seams spine PortalDriver / SecretStore / KeyValueStore / AuditSink / Clock interfaces + Node defaults + in-memory fakes ADR-003
Embeddable core (0.2.0) Pure main barrel (no node:*/playwright at import time); ./node subpath = composition root (createNodeRuntime(overrides?) + Node adapters); playwright OPTIONAL peer, lazy-loaded. Verified with an external npm consumer without playwright. ADR-016
rut + config + audit Pure-core modules: Mod-11 RUT, prod hostnames, secret-dropping audit ADR-004
Output contract — JSON by default @dostertags/sii-core returns JSON-serializable objects (the library contract); the CLI emits them as JSON by default (--human for text) via the shared emit(data, humanFn); MCP already JSON. STDOUT pure (pipeable to jq), header/diagnostics STDERR human-only. (#35) ADR-012
🚧 auth + identity base core logic + Playwright driver + CLI surface landed & tested; CLI real-SII validated (#5) — login/status/--refresh/logout; MCP next ADR-005, ADR-006
Node Playwright PortalDriver Headed login / headless cookies-only restore wired into createNodeRuntime; real-SII login validated (#5) — landed on Mi-SII off zeusr.sii.cl, cookies-only restore + DatosCntrNow read confirmed ADR-006, ADR-008
Operate reach (representación) Spike #15 RESOLVED: RCV = body-RUT (--rut reaches a representada); F22 / F29 / BHE = session-keyed (authorize by the principal — represented RUT does NOT reach; read self, no --rut). BHE re-confirmed TS-live 2026-06-30 (#20). ADR-005
Operable fetch getDcvEmpresasAutorizadas wired into login (best-effort → [self] on failure) + PortalSession.requestJson seam. Live-validated 2026-06-28 (real session → 1 empresa + self; the .sii.cl cookie covers www4, no SPA nav). ADR-005

Identity & auth (the operate-centric center — ADR-005)

Status CLI MCP Spec
🚧 sii auth login (browser) / --console auth_login (no password arg) CLI real-SII validated (#5); MCP tool built + tested (no password arg; delegates to the browser flow). --console (ADR-010): RUT + hidden Clave in the terminal → headless form-fill → same cookies-only session, Clave never stored. CLI-only. Headed login persists ~/.sii/session.json (0600, no secret).
🚧 sii auth status [--refresh] auth_status / Resource sii://session CLI real-SII validated (#5); MCP tool + resource built + tested. Local read (who am I, operating-as); refresh=true reads DatosCntrNow live.
sii auth logout auth_logout Real-SII validated (#5): server-side close (best-effort, redirect off autTermino.cgi) + local wipe. MCP tool built + tested (#11) — no secret, so MCP-eligible (ADR-006); no input args. Switching accounts = logout→login.
🚧 sii operate <rut|alias> / --self / --list operate (rut/self/list) / Resources sii://operating, sii://operable CLI built + tested incl. --list (operable set with self/current markers); MCP operate tool incl. list=true (#23) + sii://operable resource. Validated against the operable set; always visible. Alias TBD.
sii whoami (fuller profile 📋) whoami Built + live-validated (#70, CLI + MCP) — the authenticated principal's own identity: razón social/nombre + email, on top of the RUT. Session-keyed (reads the login principal's DatosCntrNow live; ignores the operate pointer, no --rut). MCP whoami tool (readOnly) whose description declares the PII exposure to the model; audit records the read only (rut), never the values (ADR-006). Follow-up: domicilio (from direcciones[], needs a live capture) + a fuller profile (raw).

Read surfaces

Status CLI MCP Spec
🚧 sii rcv summary / list / all (match 📋) rcv_summary / rcv_list / rcv_all Built + tested (#17; all #77, PR #78) — the domain-read template: getResumen aggregates + getDetalle rows, withSession + body-RUT (--rut/operate), curated+raw. all (#77): rcvListAll fans out getDetalle over every DTE type the resumen reports in ONE session (flat "todos los documentos" table, each row tagged with its codigoTipoDoc); per-type RcvErrorrejectedTypes + incomplete while the rest return (mirrors f22Historial), paced, never retries. Wire contract ported (cited), not yet live-revalidated from TS (all verified vs fakes only). match (folio reconciliation) deferred.
🚧 sii f29 formulario <periodo> / overview <desde> <hasta>|<año> / status <periodo> f29_formulario / f29_overview / f29_status Fase 1 shipped + live-validated (#18, PR #43); Fase 2 deferred. Robust SDI-JSON (no GWT-RPC): formulario = the IVA propuesta códigos labeled (glosa) + grouped (fuente:"propuesta"); overview = per-month position across a date range (estado/folio/fecha + declared total = "lo que pagué"); status = raw estado of one month. Código taxonomy (f29-codigos.ts, 157 códigos) observed from the form HTML; unobserved → otros (surfaced). Session-keyed (no --rut; rejects a representing pointer up front). Fase 2 (deferred, own PR+ADR): the presented form's full balance via rfiInternet GWT-RPC (fuente:"presentada" + computed resumen) — mapped in the spike, gated on a headless PoC.
sii f22 status [año] f22_status Built + live-validated (#19, #32) — the session-keyed template: buscaDeclVgte (decls+estado) → f22Compacto (código grid). No año → multi-year overview; with año → flat estado detail. Session-keyed (no --rut; reads the principal — spike #15: body-RUT does NOT reach it). PII-safe (identity/bank códigos + raw dropped). es-CL monto parsing fixed; live-validated from TS (AT 2023–2026).
sii f22 formulario <año> (#27 → #36) f22_formulario Built + live-validated (#32 then #37). The complete form, grouped into the lines a contador reads: ingresos (rentas / base imponible / honorarios), deducciones (gastos / cotizaciones / pérdidas), retenciones · PPM · créditos, resultado (impuesto / devolución / giro), + a visible otros — sign-preserving. Reads f22Compacto (the real form; f22Completo is noisier, not richer → not used). DENYLIST of identity/bank PII only (authoritative from codigosFormato.codigosCabecera); an allowlist was tried + REJECTED (hid real honorarios/retenciones). Shipped first as status --full (#32), then split into its own verb (#37). Taxonomy in portal/f22-codigos.ts.
sii f22 observaciones <año> [--folio] (#26) f22_observaciones Built + live-validated (#26, 2026-06-29)situacionObservacion(periodo,rut,dv,folio)[{codigo,descripcion,url}]: the observación códigos (B102, G37…) + glosa + SII ayuda URL. Endpoint located first-hand via a live spike (no Python equivalent), cited in sii-contract/f22.md. Folio resolved from buscaDeclVgte (vigente) or --folio. Session-keyed (no --rut). PII-safe: rows are non-PII (no header-código exclusion, no raw).
sii f22 historial <año> (#28) f22_historial Built + live-validated (#28, 2026-06-29)buscaEventos(periodo,rut,dv,folio) → the per-folio event timeline ("sección Historial"): declaración recibida, devoluciones, giros de Tesorería, rectificatorias, fechas. Endpoint located first-hand via a live probe (folio REQUIRED; sent as strings), cited in sii-contract/f22.md. Default reads ALL folios of the año (paced) and sorts most-recent-first; --folio scopes to one. Session-keyed (no --rut). PII-safe: rows are non-PII (event code + glosa verbatim + carta refs), no raw.
sii bte list <periodo> (#20) bte_list Built + live-validated (#20, 2026-06-30). Monthly boletas de honorarios for a período — --recibidas / --emitidas (default). First inline-JS-map facade: reads the legacy loa.sii.cl/cgi_IMT/ CGIs' xml_values + arr_informe_mensual via PortalSession.goto/evaluate (NOT requestJson), paginated + paced. Curated boleta, NO raw (live BUG-1: the row mixes counterparty data with the taxpayer's OWN identity on both sides — emitidas usuemisor, recibidas nombre_receptor — so BTE joins F22/F29's no-raw camp). Session-keyed (ADR-005, spike #15 answered #62 + re-confirmed TS-live: rut_arrastre rides the principal; no --rut, rejects a representing pointer up front). es-CL monto parsing. Live-validated 2026-06-30 (3 RUTs: persona/empresa/worker — emitidas + recibidas + the no-raw PII fix). Wire contract sii-contract/bte.md.
sii bpst list <periodo> (ADR-029) bpst_list Built + live-validated (2026-07-29). Boleta de Prestación de Servicios de Terceros Electrónica — boletas a company EMITS on behalf of a low-skilled/informal worker (agrícola, bodega, reparto). --recibidas / --emitidas (default). A DIFFERENT SII product from bte above despite SII's own UI calling it "BTE" (naming collision documented in ADR-029) — a THIRD wire shape: zeus.sii.cl/cvc_cgi/bte/, form-POST → plain HTML (PortalSession.requestForm, not goto/evaluate), in-house table parser. Per-page (*)Totales, not per-month — the facade walks every page and sums (a real month under-counted by 44.5% reading page 1 alone). Curated, NO raw (same own-identity-mixed-with-counterparty shape as bte). Session-keyed (no RUT field in the query form). EMITIDAS data-row-validated (2 real months, exact-match arithmetic); RECIBIDAS header-only observed (no real row seen — flagged for revalidation). Wire contract sii-contract/bpst.md.
sii dte authorized <rut> (#21) dte_authorized Built + tested (#21). The FIRST public, login-free surface: ee_empresa_rut palena CGI → curated DteAutorizados (razón social, resolución, authorized-docs grid + DD-MM-YYYY dates), or a clean negative (autorizado:false + SII's verbatim message). No withSession — reached via the new PortalDriver.requestPublic seam (unauthenticated text-HTTP, Node fetch, charset-aware; ADR-014). Any RUT (counterparties incl.); no operate/--rut concept; audited rut=<subject> (no rutAuth). In-house HTML table parser (no third-party lib). Wire contract ported from sii-py (cited; sii-contract/dte-authorized.md), not yet TS-live-revalidated.
sii peticiones list (#74) peticiones_list Built + live-validated (#74, 2026-07-03) — the FIRST GWT-RPC surface (ADR-020). A taxpayer's SISPAD peticiones administrativas + their state timeline: número, materia, estado actual, per-transition fecha + SII's verbatim note (what's pending / why). Early-warning SII doesn't push (e.g. "Peticion en espera de Antecedentes"). Cold authenticated POST to www3.sii.cl/sispadinternet/peticion via the new PortalSession.requestText seam; body-RUT (operable-set gate, like RCV). In-house GWT-RPC codec portal/gwt.ts decodes the //OK[…] graph schema-directed; the per-type schema (gwt-schema.ts, 109 classes) is derived from the compiled permutation deserializers (Path B — samples proved incomplete), keyed by class name → recompile still resolves, changed class ⇒ "scraper roto". Policy hash self-heals; //EX verbatim. PII: NO raw, tight allowlist; functionary suffix stripped from the glosa; audit = rut + count only. Schema regenerable via sii-contract/peticiones-schema-extract.py. Writing NEW petitions = future write surface.
🚧 sii iva <periodo> iva Built + tested (ADR-022); live-validated 2026-07-28 in a full read-only audit against a real taxpayer (16 consecutive months). Monthly VAT cuadre: RCV IVA (débito = ventas, crédito = compras) vs the F29 propuesta's own totals códigos 538 / 537 (falls back to component 511 for crédito — 538/537 are absent from every observed propuesta), with the per-DTE-type breakdown so a mismatch can be located. The live audit surfaced and fixed a real bug: ladoFrom summed RCV resumen rows straight, double-counting Notas de Crédito (tipo 61) instead of netting them — 15 of 16 months showed a phantom "descuadre" that was this tool's own aggregation error, not an SII rejection or a books problem (see ADR-022 Revisions, BACKLOG.md §3.5, IVA_CACHE_VERSION bumped 1→2). Facade-level join inside ONE withSession — composing rcvSummary+f29Formulario would launch three Chromium instances. Session-keyed, self only: RCV is body-RUT but F29 is not, so a representing pointer is rejected up front (joining them would splice one entity's ledger onto another's return). Missing propuesta / empty month = data, not error; null never coerced to 0. fuente:'propuesta' — reconciles against SII's suggestion, not the filed form (ADR-013 Fase 2 still deferred; a user-reported SII-native cross-check, "Consulta Integral de Informe F29", is an unconfirmed spike candidate — BACKLOG.md §7).
🚧 sii renta <anio> renta Built + tested (ADR-022); NOT yet live-validated. Annual pack: the F22 of año tributario N (folio, estado, contador grouping) + the twelve F29 months of commercial year N-1 — the year offset is the domain rule and is asserted in tests. One session for all 14 POSTs, paced. Session-keyed. Gap: CLI/MCP surfaces have no dedicated test files (core task does).
sii honorarios <anio> honorarios Built + tested (ADR-023) + live-validated (2026-07-29, ADR-029). Annual honorarios cuadre motivated by a live B92 observación: retención on BHE recibidas + BPST emitidas (ADR-029) vs F29 código 151 (Art. 42 Nº2 via Art. 74 Nº2), month by month, + the F22 honorarios códigos surfaced alongside. Three traps encoded: only retencionReceptor reconciles (retencionEmisor is the contractor's own, reported not declarable); código 153 (Art. 48, directorio) is excluded; BPST (a company emitting on behalf of a low-skilled worker) is a SECOND, previously-missing retención source — omitting it produced a real, persistent, large false "descuadre" for a live taxpayer (a real taxpayer account: −$6.03M/−$1.66M/−$1.21M across 2023–2025), traced live to exact-match precision and fixed by adding bpstRetenido to the reconciled total. The F22 leg surfaces without adjudicating — código 1412 is named by B92 but absent from the observed taxonomy, so it returns verbatim with enTaxonomia:false and no delta is computed (ADR-004: never assert an unobserved rule). Empty-month verdict rule: absent-vs-absent balances, one-sided non-zero is a discrepancy. anio = commercial year; F22 read is AT anio+1. Heaviest read in the repo (~26 POSTs + BHE/BPST pagination). Live-validated 2026-07-29: 2023 and 2025 reconcile EXACTLY (12/12 months); 2024 reconciles 11/12 months with one small residual ($28.829, Feb) still open.
📋 sii notificaciones notificaciones Citaciones Art. 63 / liquidaciones / notificaciones. Spike partially done: routing solved — linkexterno() on Mi SII resolves the app to corenoti1Internet, whose host emin.sii.cl is an internal alias for the public www4.sii.cl (proven: the same table lists propuestaf29ui under emin while portal/f29.ts reaches it on www4). Both www4.sii.cl/corenoti1Internet/ and www4.sii.cl/seeui/see/internet confirmed valid (login wall echoed them as post-login destinations). Payload not yet captured — blocked on session expiry (~10h). Mi SII's own CViewCarta.cgi (POST opc=<n>) is a fifth wire shape: JSON with a {codigoError, descripcionError, sysdate, …} envelope, NOT the SDI respEstado one — needs its own zod schema. Carta PDFs via www3.sii.cl/Coreca1Internet/MuestraPdf?, keyed by the codCarta already exposed in f22 historial.
sii dj status (ADR-030) dj_status Built + live-validated (2026-08-03). Estado de Declaraciones Juradas for the last N años tributarios (default 6, max 15) — which DJ (1835, 1847, 1879, 1887, 1926 balance 8 columnas, 1948 retiros/remesas/dividendos) are Aceptada/Observada per año. A SIXTH wire shape: plain REST JSON on www2.sii.cl/app/djconsulta/<rut>/recurso/v1/dj/, RUT embedded in the URL path — NOT the SDI-JSON family despite living alongside F22/F29. Load-bearing gotcha (cost real production time to find): a direct POST 401s — the endpoint requires a goto to the UI bootstrap page + a paced wait before the SPA's own async session setup completes; found live, fixed, documented in docs/sii-contract/dj-status.md. Each call covers a ROLLING 3-YEAR WINDOW (not a single año) with a real "out of range" error below periodo 2022 (observed) — the multi-year sweep treats that as a soft stop past the first call, never on the first. Session-keyed, self only (RUT-in-path reach for a represented empresa NOT confirmed — open item). No PII curation trade-off — every field is own metadata or an opaque id, no raw needed.
sii dj historial <año> / sii dj detalle <año> <dj> <key> (ADR-031) dj_historial / dj_detalle Built + live-validated (2026-08-03) — closes NEXT-SPIKES-PLAN.md candidates 2–3 (DJ 1943 RLI + DJ 1926 balance) with ONE facade: this taxpayer's DJ 1943 does not exist; DJ 1926 alone carries BOTH the RLI breakdown and the 8-column balance. dj historial = getDjIntegralAnualDetalle (periodo sent as a bare NUMBER — the OPPOSITE convention from dj status's string requirement) — folio/estado/tipoDeclaracion per DJ, including anuladas/rectificatorias (live-validated against a REAL rectificatoria: this taxpayer's own DJ 1926 AT2025, folio 599626 anulada same-day, re-filed as 599782). dj detalle = detalle — the actual form CONTENT, a RENDER-ORIENTED nested table (sections → sub-tables → rows → cells), NOT a flat códigos array; deliberately NOT semantically parsed (column meaning differs per DJ number and is only knowable by reading the aligned header row — building a parser from one observation risked a silently wrong peso figure) — curated FAITHFULLY instead, redacting any cell SII itself flags contactoRut:true. Live-validated end-to-end against the real taxpayer's real AT2025 balance (24KB response, 3 sections, real RLI figures down to the peso). Discovery method: read the SPA's own public JS bundle for endpoint names, then confirmed exact request shapes by patching window.fetch/XMLHttpRequest and clicking the real UI. Session-keyed, self only. Wire contract sii-contract/dj-detalle.md.
sii notificaciones list [--correspondencia] / sii notificaciones detalle <id> <tipo> (ADR-032) notificaciones_list / notificaciones_detalle Built + live-validated (2026-08-03) — closes NEXT-SPIKES-PLAN.md candidate 4. Notificaciones formales ("Mis Notificaciones Vigentes": citaciones, liquidaciones, resoluciones — the audit-motivating "¿hay una citación Art. 63 vigente?" answer) and Correspondencia (cartas/avisos informales), both served by Mi SII's own legacy CViewCarta.cgi (misiir.sii.cl) — a SEVENTH wire shape: form-POST (opc=<n>) answering JSON, envelope {estado, glosa, data, metaData} distinct from every envelope already in this codebase. No bootstrap goto needed (unlike DJ) — a cold POST works directly. Corrects a prior session's hypothesis: the GWT app at corenoti1Internet (linkexterno(4)) is NOT the read surface — reading Mi SII's own misii.min.js live showed its real tooltip is "Solicitar, actualizar o revocar correo para notificaciones" (a write flow managing the alert email address); the actual read link ("Mis Notificaciones Vigentes") fires a plain call_option(201)opc=41. No GWT decoder was needed, avoiding the ADR-020-style risk entirely. estado: 1403 ("No existen registros") is a legitimate empty result — for Empresa A (a real taxpayer account) this itself answers the audit question: no formal notification on file as of 2026-08-03. Detail exposes the letter's HTML body (own-identity content, same posture as whoami) while dropping base64 attachment/certificate payloads. Session-keyed, self only (no represented-empresa reach observed). opc=43 (formal detail) unconfirmed with real data — this taxpayer has zero formal notifications. Wire contract sii-contract/notificaciones.md.
sii monitor — barrido ampliado (ADR-037) + plazo/anclaje reales (ADR-038) monitor Ampliado + live-validado (2026-08-04) — cierra G17. El barrido pasa de 3 a 7 familias de señales: suma DJ observadas (ADR-030), notificaciones formales vigentes (ADR-032), giros con saldo (ADR-034) y requisitos de la Ley COT incumplidos (ADR-035). En la primera corrida en vivo encontró 3 DJ observadas que el proyecto no podía ver (1835 AT2025, 1847 AT2024, 1948 AT2024). El criterio de deuda es el SALDO, no el estado — marcar por Estado:"Vigente" habría dado alarma falsa en los 3 giros del contribuyente (ADR-034). Respeta ADR-024 al pie: el snapshot guarda solo identificadores y estados — id→tipo de notificación (nunca la descripción), y el monto del giro NO se persiste; pinneado por tests. Un snapshot anterior no dispara falsos "nuevos"; un fallo de una señal no aborta el barrido. ADR-038 (2026-08-05) cierra los dos falsos positivos que ADR-037 dejó documentados sin corregir: el mes F29 inmediatamente anterior ya NO se marca alta antes de vencer su plazo real (día 12, o 20 vía autorizadoDeclararDia20, opc=112) — se degrada a info mientras el plazo sigue abierto, y a alta una vez vencido; sin el dato (perfil no disponible) se mantiene el comportamiento previo. El anclaje de severidad pre-inicio para F22/F29 ahora usa fechaInicioActividades real (opc=112) en vez de la heurística "primer año/mes con declaración detectado" — la heurística queda como respaldo cuando el perfil no está disponible. 596 tests. Live-validado 2026-08-05 tras renovar sesión: F29 2026-07info ("el plazo día 20 todavía no vence"); F22 AT2019info citando fechaInicioActividades=2019-06-19 real (no la heurística); errores: [].
sii dte acuse <periodo> (ADR-036) dte_acuse Construido + live-validado (2026-08-04) — cierra PARCIALMENTE G3 (acuse de DTE, Ley 19.983). Estado de acuse de los DTE recibidos, agrupado por evento, con los sin-evento aislados y desglosados por tipo. No es una fachada nueva: el estado ya viaja en el RCV (detEventoReceptor) — campos que se curaban desde siempre pero cuyos valores nunca se documentaron. Observados en vivo sobre 1.859 documentos: A (no reclamado en plazo), C (recibo otorgado), P (contado). La app oficial registrorechazodtej6ui NO es una bandeja de pendientes — es consulta de UN folio; SII no publica lista de pendientes, así que la pregunta se DERIVA. ⚠️ diasDesdeRecepcion es un dato, NO un veredicto legal: el disparador del Art. 3 depende de la recepción de mercaderías, que SII no expone. ⚠️ sinEventoPorTipo evita un falso positivo real: el 100 % de los sin-evento observados fueron notas de crédito y una DIN, nunca facturas. Body-RUT (hereda RCV: --rut alcanza una representada). ⛔ Nunca acepta ni reclama.
sii situacion (ADR-035) situacion_tributaria Construido + live-validado (2026-08-04) — cierra G5 y parte de G8 de COVERAGE-GAP-ANALYSIS.md. Una sola lectura responde: régimen vigente (observados M14A semi integrado, REGS, FAEL, SGGC), fecha de inicio de actividades, actividades económicas (CIIU + afecto IVA), documentos timbrados, y cumplimiento de la Ley COT con cada requisito marcado (NDIR/DQDT/ATPJ) + requisitosIncumplidos ya filtrado. Compone TRES opc del CGI de Mi SII (118/22/112) en una sesión. Hallazgo: CViewCarta.cgi NO tiene un envelope común — seis formas distintas entre diez opc; y las etiquetas del MISII_OPC_MAP mienten (opc=29 "Alertas" son reportes de destino de impuestos; opc=8 "Situación Tributaria" viene VACÍO). fechaInicioActividades es el campo que CONVENTIONS.md daba por inexistente y por el que monitor.ts usa una heurística — ahora observado, y confirma que la heurística acertaba. PII: nunca expone socios/representantes/direcciones (RUT, nombres, % de participación, RUT del arrendador) — solo su CONTEO; descarta el id del funcionario SII. Normaliza dos ligaduras PUA con las que SII corrompe su propio texto. Contrato sii-contract/situacion.md.
sii giros list [--anio] [--years] (ADR-034) giros_list Construido + live-validado (2026-08-04) — cierra la brecha G1 de COVERAGE-GAP-ANALYSIS.md (deuda fiscal), la pregunta N°1 de un cliente. Verificado contra los 3 giros reales del contribuyente en dos años distintos (2026: 1 giro; 2021: 2 giros), coincidencia exacta campo a campo. Giros emitidos al titular: folio, formulario de origen (ej. el F29 del período), fechas de emisión/vencimiento, estado, moneda y saldo, con saldoTotal sumado. Primera fachada del repo que lee el DOM RENDERIZADO manejando una SPA — desviación deliberada de ADR-020, decidida por MODO DE FALLA: rfiInternet habla GWT protocolo 7 (peticiones habla el 5; longs en base64, literales con comillas simples que rompen el JSON.parse del códec, registro de deserializadores distinto ⇒ habría que re-derivar el extractor de esquema desde cero), y un esquema mal derivado devuelve un monto o una fecha de vencimiento silenciosamente errados sobre deuda fiscal; la tabla renderizada se mapea por nombre de cabecera y falla ruidosamente ("scraper roto"). ⛔ SOLO LECTURA pinneada por test: la misma página expone "Pagar" y enlaza pagogiro-ui — un test verifica que la expresión enviada al navegador no menciona "Pagar". ⚠️ Estado:"Vigente" se observó junto a Saldo:$0 en los 3 giros reales — "Vigente" NO significa impago; declarado en el tipo, la ayuda del CLI y la descripción MCP. Transporte GWT-RPC resuelto y documentado (replay en frío + el header X-GWT-Permutation que lo desbloquea) pero no usado — queda listo para el F29 presentado. Contrato sii-contract/giros.md.
🚧 sii carpeta historial [--anio] (ADR-033) carpeta_tributaria_historial Built + tested (fakes); closes NEXT-SPIKES-PLAN.md candidate 5 (the last of the five original candidates) — wire contract live-captured cleanly 3× (once via a real browser click intercepting XHR, twice via direct API calls), but the FINAL pre-close live re-verification of the built binary failed with a consistent HTTP 401 after ~10 calls to the endpoint within ~20 minutes — re-verify live, spaced out over time, before relying on this for time-sensitive work (see the ADR and docs/audits/2026-08-03-spike5-carpeta-tributaria.md). Historial of "Carpeta Tributaria Regular" credit-application dossiers the taxpayer has GENERATED — código/clave carpeta, fechas de generación/vencimiento/anulación, estado (verbatim, no closed enum), institución, and the declared RECEPTOR (can be a THIRD PARTY, e.g. a named bank contact — real PII exposure, declared explicitly at every surface, same posture as peticiones_list) plus carpParametros (which dossier sections were disclosed, parsed from a JSON-encoded string). An EIGHTH wire shape, same www2.sii.cl REST-JSON-with-RUT-in-path family as DJ (#6) — same bootstrap (goto UI page + pace) precondition, but response is a BARE JSON ARRAY, no wrapper envelope. READ ONLY by design: never touches Generar Carpeta Tributaria Regular (creates a new dossier) or the page's own "Anular" action — both are write actions explicitly out of scope. Session-keyed, self only. Wire contract sii-contract/carpeta-tributaria.md.
sii bancario resumen (ADR-039) bancario_resumen Construido + live-validado (2026-08-05) — cierra E1, señalada como la mayor oportunidad no explotada del proyecto. Abonos bancarios por semestre × banco × tipo de cuenta — el insumo del cruce de fiscalización que compara depósitos bancarios contra ventas declaradas. Mismo CGI que situacion/notificaciones (CViewCarta.cgi), opc=119, otro envelope propio (listCarpetaTributarianombre de clave engañoso, sin relación con ADR-033, documentado explícitamente para no confundirlos). Verificado en vivo contra los 3 semestres reales del contribuyente (1.433/1.485/1.543 abonos, $619.8M/$656.5M/$700.4M), confirmando una cita de una sesión anterior sin log propio. periodoDj y semestre pasan VERBATIM, sin decodificar — se observó periodoDj:202502 y periodoDj:202507 con la MISMA glosa de semestre, una inconsistencia real que ADR-004 prohíbe resolver por adivinanza. Session-keyed, self only (sin --rut, sin parámetro de RUT observado). rutTitularC2/dvTitularC2 se descartan del tipo curado (siempre el RUT de la sesión). Recibo de auditoría: rut + cantidad de semestres, NUNCA un monto. Contrato sii-contract/bancario.md.
sii profile [--incluir-terceros] (ADR-041) profile Construido + live-validado (2026-08-05) — cierra P8 y BACKLOG §3.4 (domicilio). Perfil del titular vía opc=112 (mismo endpoint que situacion, otra proyección): identidad, actividades, timbrajes y domicilio propio SIEMPRE. Socios y representantes (RUT, nombre, % participación) requieren --incluir-terceros/incluirTerceros:true — sin eso, solo el conteo, igual que situacion. rutPropietario/dvPropietario (el arrendador) también quedan tras el mismo opt-in. Verificado en vivo: 3 socios reales (33.33% c/u) y 1 representante, invisibles sin el flag, exactos con el flag. Recibo de auditoría: rut + incluyoTerceros + conteos, NUNCA un RUT/nombre de tercero. Contrato sii-contract/situacion.md.
🚧 sii carta pdf <codCarta> --out <ruta> (ADR-042) — (CLI-only, MCP deliberadamente diferido) Arquitectura construida + tests verdes contra fakes; NO live-validado. Primera fachada que usa una seam binaria (PortalSession.requestBinary, nunca decodifica el body a texto). Descarga el PDF de una carta/notificación por codCarta (ya expuesto por f22 historial). Verificación en vivo (2026-08-06) FALLÓ: los tres codCarta reales probados devolvieron HTTP 500 (una página de error genérica del framework legacy, no un mensaje de parámetro faltante) — la URL MuestraPdf?codCarta=<n> era una hipótesis derivada de un global JS (jwscarta) nunca confirmada capturando el enlace real de la UI. Queda como ítem abierto: recapturar el enlace real antes de confiar en este comando.

Write surfaces (each needs its own ADR for legal weight)

Status Surface Notes
🔒 f29 submit / f22 submit File monthly/annual returns.
🚧 sii bte emit (#60) / bte_emit+bte_emit_preview First WRITE surface — MERGED (ADR-017, PR #61). Issue a BHE via the TMBECN_* form-POST flow (Clave, no cert; session-keyed). CLI --dry-run/--confirm; MCP bte_emit (the first destructiveHint) + bte_emit_preview. Preview LIVE-VALIDATED (both retención modes); the ISSUE step (step 4) is coded to the 2026-07-02 capture but its live validation + the v0.3.0 tag are #62. Retención server-side; audit = folio only.
🔒 dte emit / dte accept Issue DTEs; accept per Ley 19.983. Blocked on the cert auth layer.
🔒 bte anular (#63) Annul an issued BHE — needs its own live capture (follow-up to bte emit).

MCP-specific structure (best practices — ADR-003)

The MCP server is the surface that lands the project in Claude Code and Claude Desktop, so structure it to the spec. The stdio server is built (@dostertags/sii-mcp, buildServer(runtime) + stdio main), tested with an in-memory client (no SII), and binary-smoke-validated (initialize handshake):

  • Resources (read-only context): ✅ sii://session, sii://operating, sii://operable, sii://config. NOT tools — the model reads them to orient.
  • Tools (actions): ✅ auth_login (no password — delegates to the browser flow), auth_logout (no args — best-effort server close + local wipe), auth_status (refresh), operate (rut/self/list); read surfaces rcv_summary / rcv_list / rcv_all (body-RUT; rcv_all = single-session detalle fan-out over all DTE types, #77) + f22_status (anio/folio/years, estado, session-keyed) + f22_formulario (anio/folio, the complete grouped form, #37) + f22_observaciones + f29_formulario / f29_overview / f29_status (session-keyed, #18 Fase 1) + dte_authorized (rut, PUBLIC/login-free, #21) + bte_list (periodo/recibidas, session-keyed BHE, #20) + bpst_list (periodo/recibidas, session-keyed BPST — a different SII product from bte_list despite the name collision, ADR-029) + dj_status (years, session-keyed, Estado de Declaraciones Juradas, ADR-030) + dj_historial (anio) + dj_detalle (anio/dj/key/seccion/pagina, faithful render-oriented table, ADR-031) + notificaciones_list (categoria, session-keyed, Notificaciones formales / Correspondencia via Mi SII's own CViewCarta.cgi, ADR-032) + notificaciones_detalle (id/tipo/categoria; description declares the own-identity exposure in the letter body) + carpeta_tributaria_historial (anio, session-keyed, historial of issued Carpeta Tributaria Regular dossiers, ADR-033; description declares the receptor may be a third party) + giros_list (anio/years, session-keyed, deuda fiscal formal leída del DOM renderizado, ADR-034; la descripción advierte que "Vigente" NO significa impago y que la tool nunca paga) + situacion_tributaria (sin args, session-keyed, régimen + inicio de actividades + actividades + timbrajes + cumplimiento Ley COT, ADR-035; la descripción declara que NO expone socios, representantes ni direcciones) + dte_acuse (periodo/rut, BODY-RUT, estado de acuse de DTE recibidos derivado del RCV, ADR-036; la descripción advierte que diasDesdeRecepcion no es un veredicto legal y que la tool nunca acepta ni reclama) + whoami (no args, the authenticated account's razón social/nombre + email, session-keyed — #70; description declares the PII exposure to the model) + peticiones_list (rut, body-RUT, SISPAD peticiones administrativas via GWT-RPC — #74; description declares the PII exposure) + bancario_resumen (sin args, session-keyed, abonos bancarios por semestre × banco × tipo de cuenta, ADR-039; la descripción declara que expone montos agregados propios, el insumo del cruce de fiscalización bancario) + profile (incluirTerceros, session-keyed, ADR-041; la descripción declara que ese flag expone socios/representantes — PII de terceros — y el RUT del arrendador del domicilio), all readOnlyHint. Each is a thin call into a @dostertags/sii-core task; future writes get destructiveHint. auth_logout is MCP-eligible because it carries no secret (ADR-006). New modules register their tools via tools/<mod>.ts (register<Mod>Tools) — append-only.
  • Prompts (workflow templates): 📋 "revisar IVA del mes", "preparar renta", "conciliar folio" — deferred until the read surfaces they orchestrate land.

How to keep this current

Tick rows ✅ on merge; add issue links when a 📋 row gets an issue; update the ADR column when a decision gates/unblocks a row; resolve spikes (💭 → ADOPT adds rows / REJECT strikes them). Do NOT list internal core modules here — those live in docs/ARCHITECTURE.md.