Skip to content

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 23:24
· 820 commits to main since this release
c1ca30c

First public release. This section covers what changed since the 2026-10-06
prep; the untagged ## [0.1.0] section below (schema v1, the core engine, the
pac → pasco rename, brand flavor, the evidence registry) is the project's
pre-release history and was never tagged or published.

Added

  • CI — offline, self-tested link gate for the Markdown corpus (#149) —
    scripts/link-check/ (checker + fixture self-test first, the scripts/dep-gate
    shape): every relative link/path in the tracked, non-hidden Markdown doc areas
    must resolve to a file or directory, every in-page anchor must match its
    destination's headings under GitHub's slug rules (the expected anchors are
    committed as github-slugger@2.0.0 output and asserted by the self-test), and
    private targets go through an allowlist that requires a reason + removal
    trigger. Offline and deterministic — external URLs are never fetched — so the
    required check cannot depend on third-party uptime. Runs after the pinned Node
    setup and before pnpm install (fail fast).

  • CI — every package type-checks its tests, and the wiring is gated (#181) —
    each package gains a tsconfig.test.json that extends its own tsconfig (single
    source for compiler options) and adds tests/**, the root *.config.ts and
    scripts/**; pnpm -r typecheck now covers the suites, so a mistyped shared
    contract (e.g. @profile-as-code/paste/conformance) fails the gate. A node-only
    scripts/typecheck-gate runs first and refuses a package whose typecheck
    script omits the test program — without it, pnpm -r typecheck silently skips
    the package and CI stays green.

  • Platform registry — per-platform metadata now has one machine-readable home (#154):
    platforms/registry.yaml carries the metadata that used to live hand-written across ≥7
    surfaces (family, deploy kind, hosts, subdomain hosts, handle hosts, status, since, issue,
    sources). pnpm gen:platforms (scripts/gen-platforms/, the D3 drift-check shape) projects
    every derived surface: packages/core/src/generated/platforms.gen.ts (the host maps + slug
    list consumed by drift.ts/types.ts, plus the derived test enumerations), the SPEC §6
    family table + count arithmetic and the README status counts between explicit
    <!-- gen:… --> … <!-- /gen --> markers. The reviewed contract is unchanged — the schema
    keeps $defs.platform.enum (D3) — and CI asserts registry ↔ enum ↔ generated agree
    (scripts/schema-check/check.py + node scripts/gen-platforms/gen.mjs --check), including
    the reverse assert: every enum member has a declared renderer or an explicit
    renderer: false ("not implemented yet"), and each renderer's deploySpec equals the
    registry deploy (D9).

  • CONTRIBUTING — escalation path and measured response latency (#148) — the
    published latency is measured, not a promise: a table over the three strata
    (n=103 merged PRs, [MI], 2026-10-08), the regenerable gh + python3
    one-liner, and the honest caveat that no external PRs exist yet so the figure
    describes maintenance cadence. Stalled = no maintainer response after 7 days;
    the escalation channel and the possible outcomes are named, and silence is not
    one of them.

  • Package READMEs + complete publication metadata (#143) — 9 new READMEs
    (D8, English: packages/markdown + the 8 renderers) with one uniform skeleton,
    since npm always packs README.md and publishing would otherwise show an empty
    package page; the 10 libraries + the CLI gain repository (+ directory),
    bugs, homepage and keywords, and the pure libraries gain
    sideEffects: false.

  • hotmart platform — schema additive v1.9, D16 operator extra #15 (49 total;
    issue #137)
    — Hotmart: digital-products storefront + memberships (the Payment Policy's
    recurring billing) + affiliate program, serving the Spanish/Portuguese-speaking markets
    (offices in Belo Horizonte, Madrid, Bogotá, Ciudad de México — [OFICIAL] footer, fetched
    2026-10-05), in the creator-monetization family. Enum
    entries in $defs.platform and the derived check.platform, ledger surface, drift host
    hotmart.com (+ HANDLE_HOSTS; the Hotmart Space profile URL shape stays [?] — no
    PLATFORM_SUBDOMAIN_HOSTS entry without a verified shape), Zod regenerated (D3). Paste
    platform (D9 — no management API documented for the profile body; developers.hotmart.com
    covers sales/products [?]): pasco build hotmart → paste → pasco status --mark.
    Fees/eligibility sourced from the official legal texts, fetched 2026-10-05: no
    monthly/annual fees ([OFICIAL] pricing); fixed fee BRL 2.49 / USD 1.00 / EUR 1.00 +
    service fee 9.9% (20% micro-transactions) ([OFICIAL] Payment Policy §5); withdrawal
    minimum BRL 2.00 + BRL 1.99 fee, 30/15-day deadlines ([OFICIAL] root FAQ #8); worldwide
    eligibility, sanctions-based exclusions only, bank account in own name in the country of
    residence after the first sale ([OFICIAL] ToU §A/§H/§2.8). EC payout rail remains
    [?]
    — carried per the #133 precedent (kofi/payhip PayPal-EC), verification item
    in the venture instance. The market-leadership framing in issue #137 was NOT carried into
    this contract text (no independent source; Hotmart's own "world's largest / Latin
    America's leader" wording lives in the skill research as attributed vendor marketing).

  • Renderer @profile-as-code/renderers-hotmart (v1.9, #137) — deterministic
    plain-text profile paste-source: exactly ONE bios row (long > medium > short), byte-stable,
    empty slice → empty artifact (skipped/unbuilt by pasco build, §15.1). Deploy spec is
    §7's manual (D9 paste platform).

  • Skill skills/hotmart/ (#137) — SKILL.md (storefront frame — "esto resuelve X", not
    tip-jar prose; no-results-promised rule per ToU §1.4) + playbook (paste flow with the
    first-paste surface-verification step) + checks.yaml (soft-cap curacional — no documented
    bio limit) + research/SOTA-2026.md (all sources fetched 2026-10-05; the fee table and
    eligibility from the official legal texts; the Space URL shape and EC payout carried
    [?] with sourcing leads).

  • M9 floor — private-first, consent-gated AI-crawler posture (SPEC §12/§14, issue #141) —
    the training-time corpus floor inverts: robots.txt on published surfaces blocks
    AI-training crawlers (CCBot / vendor AI crawlers) and user data ships without an open
    license; entering corpora or open-licensing data requires the profile owner's explicit opt-in
    (consent field — schema work deferred with Q10). Rationale: the engine is multi-tenant
    (SPEC §9), so an allow-posture decides privacy + licensing on behalf of profile owners
    (identity data entering training corpora without consent — GDPR-adjacent exposure when
    productized); the operator's own profiles may opt in — that consent is theirs to give. Inputs

    • sourced levers: docs/research/03-training-time-visibility.md (PR #135, issue #134;
      fetched 2026-10-05). No schema/code change — the consent field lands with Q10 (SPEC-first).
      docs/ROADMAP.md M9 row realigned.
  • Platform families freelance + job-boards — SPEC docs-first declaration
    (issue #138)
    — the platform families table in the SPEC (D16 block): freelance
    covers the pre-existing v1 upwork retroactively and names its backlog slice
    (freelancer, workana, fiverr — filed with the gig-storefront boundary note);
    job-boards is a backlog-only slice (infojobs, computrabajo, indeed —
    recruiter-facing CV surfaces; GEO hook = name-reputation in search results). No
    enum/schema/core/CLI changes (48 unchanged); each backlog member follows the #133
    member chain per slug when real operator demand lands, with an official eligibility
    page before any claim. Boundary notes: linkedin stays un-bucketed (hybrid),
    github-readme/github-org/researchgate stay un-declared (YAGNI), toptal is
    recorded as a reject candidate (invite-only network, [MI]).

  • Creator-monetization platform family — schema additive v1.8, D16 operator extras
    #11–#14 (48 total; issue #133)
    — buymeacoffee, liberapay, gumroad, payhip: the
    platforms where creators receive money (memberships / tips / recurrent donations /
    storefronts) alongside patreon (v1) and kofi (v1.7). The family is declared docs-first
    as creator-monetization ("Creator monetization platforms" — canonical industry term)
    in SPEC (D16/§9) and the schema descriptions — no hard schema field (zero consumer
    breakage). Enum entries in $defs.platform and the derived check.platform, ledger
    surfaces, drift hosts (buymeacoffee.com, liberapay.com, gumroad.com, payhip.com,
    with Gumroad/Liberapay subdomain profile URLs), Zod regenerated (D3). Paste platforms
    (D9 — no management API for the profile body): pasco build <platform> → paste →
    pasco status --mark. Per-platform gofundme deferred (no EC payout as organizer,
    operator decision in #133); per-platform skills remain separate follow-ups (D16 parity
    caveat) and each carries its own sourced research when authored.

  • Renderers @profile-as-code/renderers-* for the family (v1.8, #133) —
    renderers-buymeacoffee, renderers-liberapay, renderers-gumroad, renderers-payhip:
    deterministic plain-text paste-sources (exactly ONE bios row, long > medium > short),
    byte-stable, empty slice → empty artifact (skipped/unbuilt by pasco build, §15.1).
    Core fixture minimal-creator-monetization + schema-check fixture
    valid-creator-monetization.json.

  • kofi platform — schema additive v1.7, D16 operator extra #10 (44 total; issue #130) —
    Ko-fi creator page (same family as patreon): enum entries in $defs.platform and the
    derived check.platform, ledger surface, drift host ko-fi.com (+ HANDLE_HOSTS), Zod
    regenerated (D3). Paste platform (D9 — no API deploy; Ko-fi ships payment webhooks only):
    pasco build kofi → paste → pasco status --mark.

  • Renderer @profile-as-code/renderers-kofi (v1.7, #130) — deterministic plain-text
    About paste-source: exactly ONE bios row (long > medium > short), byte-stable, empty
    slice → empty artifact. Deploy spec is §7's manual (D9 paste platform).

  • Skill skills/kofi/ (#130) — SKILL.md (About: plain text, links only via Ko-fi's
    Social-links feature, ≤40-char status note) + playbook (paste checklist + rename-drift
    caveat) + checks.yaml (About soft-cap — Ko-fi documents no bio limit) + research/
    SOTA-2026.md (all sources fetched 2026-10-05).

  • schema v1.6: identity.links[].role — entity identity gets a single home (SPEC §15.4, issue #103) —
    the engine's JSON-LD had no owner: geo.ts and both renderers each assembled the entity
    block in parallel, which is how SPEC §15.4's @id requirement was never implemented — it fell
    through the seam between three copies. (The repo already solved this for hasCredential, §18.9
    credentialLdEntries, "Single source for every emitter"; this applies it to the entity.)
    New field links[].role — identity | realization (default) | aggregator, a translation
    layer
    onto schema.org:url / sameAs / mainEntityOfPage (never presented as standard
    vocabulary). Absent role = realization, so no workspace needs editing for the field itself.
    New module core/src/structured-data.ts owns @id, sameAs and the entity fields;
    geo.ts and both renderers consume it. New check geo/entity-id; geo/sameas-links now
    counts only role: realization links (it previously credited a citability signal the JSON-LD
    never emits). Weights rebalanced: entity-id 0.05 + structured-person 0.05 = the 0.10 budget
    the field-check alone held (weights sum to 1 by contract; a test enforces it).

    ⚠️ Rendered output changes for existing workspaces. Workspaces keep validating unchanged,
    but the rendered output does change — three deltas: (a) github-readme gains
    jobTitle/worksFor, which it never emitted although geo.ts computed them; (b) sameAs
    narrows to role: realization and now includes claimed socialHandles[].url — for a
    github-org workspace that is byte-identical (it already did both), but a person workspace
    with claimed handles gains them (the old github-readme did not); (c) geoCheck adds
    geo/entity-id, so a workspace declaring no role: identity loses up to 5 points.
    Re-render and review the diff.

    Design decisions (operator, 2026-09-24): an absent @id never suppresses the JSON-LD
    block — suppressing it would strip structured data from every workspace declaring no role,
    i.e. the feature would remove signal that already worked; geoCheck reports the gap instead
    (geo/entity-id), naming the actual situation (a declared-but-ineligible aggregator is reported
    as such, never as "nothing declared"). Several role: identity links → the first wins
    (declaration order): an earlier draft ranked "own domain > social network" via hostname patterns
    and was deleted — it meant a second, hand-maintained platform list beside PLATFORMS (D16)
    and a product judgement inside a regex. Classification + sources: identeco-cifereca.
    Tests: +11 (packages/renderers/github-readme/tests/role.test.ts, packages/core/tests/geo.test.ts).

  • release: release.yml crea el GitHub Release desde CHANGELOG.md (issue #58) — el
    publish a npm no anunciaba la release: el tag quedaba sin página ni notes, y el item 3 del
    gate (docs/STANDARDS.md) no se podía firmar. Job release separado y después de
    publish (una página para una versión que no llegó a npm sería peor que una tardía):
    checkout del mismo commit verificado, la sección de la versión se extrae con
    packages/cli/scripts/release-notes.mjs (match exacto — 0.1 no satisface 0.1.0 — y el
    cuerpo termina en el próximo heading ## , así [Unreleased] no se filtra) y
    gh release create … --verify-tag. Idempotente (un retry workflow_dispatch no toca una
    página existente); contents: write acotado a ese job. La sección se valida antes del
    publish (un npm publish no se deshace: descubrir las notes faltantes después dejaba la
    versión consumida con el tag vacío, el síntoma que #58 elimina). El extractor endurecido en la
    review fresh: ignora ## dentro de fences de código (antes truncaba las notes en silencio),
    no absorbe definiciones de link al final del archivo, acepta --key=value, y el main-guard
    compara realpaths y falla ruidoso en vez de no-opear (clase #81). La preparación del release
    ([Unreleased] → ## [<version>]) queda documentada en docs/ci-cd.md. Tests: 15
    (packages/cli/tests/release-notes.test.ts); runbook en docs/ci-cd.md.

  • style gate: Biome (lint + format + import order) in CI and pnpm lint (issue #59) — one tool,
    one config (biome.json), one command; CI runs it before tests (fail fast). Gate semantics:
    pnpm lint fails on Biome errors; recommended warn-level rules are reported without failing
    (Biome's default). One deliberate strictness bump: noNonNullAssertion is an error outside
    tests and off under **/tests/** (where arr[0]! is idiomatic) — both sides verified by
    flipping the rule red/green. The four intentional control-character regexes in the markdown
    escaping contract carry biome-ignore comments with reasons. Scope: Biome does not do type-aware
    linting — pnpm typecheck stays the type gate. Fix scripts are risk-split: pnpm lint:fix applies
    safe fixes only; Biome's unsafe fixes (semantics-adjacent, e.g. useTemplate,
    useLiteralKeys) are opt-in via pnpm lint:fix:unsafe and the resulting diff is reviewable.
    One-time formatter baseline applied
    (whitespace-only for schema/ + fixtures; byte-stable behavior verified: 268 tests green, D3
    gen:zod diff exit 0, schema contract self-test OK). Rationale and revisit triggers:
    docs/ci-cd.md §Why Biome.

  • Published-surface validators — publint + @arethetypeswrong/cli (issue #142):
    packages/cli gains prepublishOnly: publint — npm runs it inside
    npm publish (dry runs included), so the irreversible step self-validates its
    manifest (attw stays out of that hook on purpose: its --pack mode inherits
    npm's npm_config_dry_run=true under npm publish --dry-run and dies on the
    missing tarball, and the bin-only CLI has no types for it to check);
    ci.yml runs both validators on every PR (publint over every
    workspace package, attw --pack --profile esm-only over the packages that ship
    a types entry — the set is derived from the manifests, not a hand-maintained
    list). The typed packages are ESM-only by design, hence the profile.
    docs/ci-cd.md documents the publish chain (guard → build → verify:pack →
    prepublishOnly → publish) and the post-publish verification runbook
    (npm view + publint/attw against the published tarball).

Changed

  • Repo config & supply-chain hardening (2026-10-09 audit) — actions
    (checkout, setup-node, pnpm/action-setup) pinned to full commit SHAs;
    persist-credentials: false on every checkout whose later steps need no git
    auth (dco.yml and the release guard keep it on purpose — they run git fetch); pnpm minimumReleaseAge: 4320, blockExoticSubdeps: true and
    trustPolicy: no-downgrade; pinned tool installs (jsonschema==4.26.0,
    npm@11.6.1); runs-on: ubuntu-24.04 with timeout-minutes + concurrency;
    .gitattributes LF normalization and .github/CODEOWNERS. docs/ci-cd.md
    records what still waits on a public repo / GitHub Pro: the Actions SHA-pinning
    policy, a tag-protection ruleset for v*, secret scanning, and an environment
    approval gate on publish.

  • chokidar resolved to the attested 4.0.1, trustPolicyExclude dropped
    (#184)
    — the transitive lockfile entry (tsup's file watcher) tripped
    no-downgrade; instead of excluding the untrusted version the graph resolves
    an attested one. 4.0.1 is same-major and behaviorally identical — the
    4.0.1→4.0.3 delta is TypeScript types only ([OFICIAL] upstream releases) — and
    5.0.0 (also attested) is a breaking, ESM-only major tsup's lazy require does
    not target.

  • One-home codegen and build wiring (#152, #164, #165) — three
    hand-maintained mirrors removed: the PLATFORMS mirror is gone (#152; the list
    is now the platforms/registry.yaml projection — see the #154 entry);
    packages/cli builds from the workspace dependency graph
    (pnpm --filter "@profile-as-code/cli^..." build) instead of an eight-entry
    hand-written renderer chain (#164); the platform↔renderer mapping moves into
    each renderer's own pasco manifest block, projected into a generated CLI map
    that a parity test keeps in sync (#165).

  • dep-gate fixtures are undiscoverable data (#166) — the fixture manifests
    are committed as fixture.package.json / fixture.pnpm-workspace.yaml and
    materialized under their real names in a temp dir by scripts/dep-gate/test.mjs
    before the production check.mjs runs over them, so Dependabot can no longer
    discover (and fail on) an intentionally invalid workspace manifest. The
    production checker is unchanged and stays the tested path; exclude-paths
    remains documented as belt-and-braces for the version-update path.

  • Paste-renderer core — the invariant gets one home (#150, PR #158): the six paste
    adapters (kofi…hotmart) carried a byte-identical render body (sha256[:16]
    b00031017ee2bbc4 after comment strip). The shared body — length priority
    long > medium > short, EXACTLY ONE bios row's verbatim text + \n, "" on an empty
    slice — now lives in a dedicated shared module (pasteSource(); re-homed to
    packages/paste/ in #157 below): the
    structured-data.ts v1.6 / packages/markdown precedent — a contract shared by every
    adapter lives in one module, and adapters delegate rather than re-implement. kofi
    migrates first: its suite passes, with the overclaiming "emits plain text" fixture check
    tightened to exact-equality verbatim pass-through — identical input → identical output is
    the byte-preservation proof. pasco build output is unchanged (byte-stable; empty slice →
    unbuild, §15.1). The remaining five adapters and the six copied suites landed in #157.

  • Paste-renderer core — the seam gets its own support package; all six paste adapters
    delegate (#157)
    : the shared body moved from core/src/paste-source.ts to
    packages/paste/ (@profile-as-code/paste) — a renderer support package (the
    packages/markdown precedent), so the engine stays an engine and vitest never enters
    core's runtime. The conformance body ships as the test-only
    @profile-as-code/paste/conformance subpath; every adapter suite now invokes it
    (describePasteConformance) and keeps only its platform facts (deploySpec: "manual",
    no assets). The six copied suites and the four overclaiming "emits plain text"
    assertions are gone, and the adapter docblocks now state the honest pass-through.
    pasco build output is unchanged (byte-stable; empty slice → unbuild, §15.1).

  • Family wording normalization — creator-support → creator-monetization (#133) —
    the informal prose from #130 (SPEC D16/§9 and the kofi v1.7 description line in
    schema/profile.schema.json, this file included) now carries the canonical term;
    already-merged history keeps the old wording, and this note records the rename
    (operator decision, 2026-10-05, issue #133).

  • pasco build — empty renderer output is now a complete unbuild lifecycle (v1.7, #130)
    An empty render is not a deploy artifact, and profiles/ is a pure function of the
    workspace (§15.1)
    : the build skips BEFORE creating the directory (no litter on
    workspaces without the row) and REMOVES a previously written profiles/<platform>/
    output (README.md + assets/) when its bios row disappears — a stale file would be
    re-pasted otherwise, and pasco status never reads artifact files. The build report
    records the removals as skipped: [{platform, reason}].

  • release auth: publish migrado a trusted publishing (OIDC), sin --provenance (issue #80) —
    el camino actual usaba un granular access token con bypass 2FA, que npm retira del publish directo
    ~enero 2027 ([WEB] github.blog changelog 2026-07-08). release.yml ahora prepara el runner para
    OIDC (npm >= 11.5.1 — con Node 22 llega npm 10.x, que ni intenta el intercambio OIDC y falla con
    un E404/E403 engañoso — e imprime npm --version para descartar la causa de un vistazo) y
    elimina el flag condicional --provenance: bajo OIDC npm genera la atestación sola y la omite
    cuando el repo fuente es privado. NPM_TOKEN queda solo como fallback del primer publish
    (el que corre antes de que el paquete exista y se pueda configurar el trusted publisher); npm
    prefiere OIDC sobre el token, así que la migración no exige borrar nada en el mismo paso. Pasos
    humanos y pre-flight (workflow filename, repository.url) en docs/ci-cd.md.

Fixed

  • permanent Dependabot failure runs on main (#160) — the security job
    Dependabot opened over the dep-gate fixtures aborted on
    dependency_file_not_parseable because exclude-paths applies only to the
    version-update path ([OFICIAL] dependabot-options-reference), and the permanent
    failing run also poisoned issue-select's "main CI green" gate. The
    placeholder-version alerts are ignored by exact version and the source-map-js
    override floor documented; the class is eliminated by construction in #166.

  • hardening de semántica de vacío: geo alineada con empty=absent + strings planos vacíos interpolados + typing de
    langMap (issue #86)
    — tres follow-ups de la review fresh de #85, una familia (semántica vacío/typing alrededor de
    la regla del accesor de texto #84):

    1. geo (F1, MAJOR) — geoCheck lee el perfil crudo y divergía internamente: structuredData (lectura falsy de
      .en) trataba mission: {en: ""} como ausente, pero el check geo/structured-person creditaba
      description(mission): yes con Boolean({en: ""}) sobre el objeto. El check ahora lee de sd.fields
      (structuredData como fuente única de presencia de campos de entidad) — parity byte-idéntica para todo valor
      schema-válido poblado (logo/established/photo son url/date no-vacíos, location: "" es falsy en ambas lecturas);
      solo el caso degenerado "" deja de creditar readiness. langValues filtra "" del haystack de refuerzo
      (inerte hoy — "".includes(ancla) nunca matchea — invariante explícito para predicates futuros, sin trim).
      Pins de regresión: location: "" y role: {en: ""} cuentan como ausentes (person) y mission: {en: ""} deja de
      creditar description(mission) (brand, score 2/3 con logo+established).
    2. strings planos vacíos (F2, NIT) — sweep de strings planos sin minLength que se interpolan:
      newsletter.cadence: "" (schema-válido) producía fila pública con · colgante en github-org → "" = ausente en
      el sitio de interpolación (check truthy, regla del string vacío exacto, sin trim). El sweep encontró un segundo
      sitio vivo del mismo síntoma: items vacíos de skill.keywords[] (sin minLength) joineados crudos producían
      seams , , en github-readme → filtrados (item vacío = ausente; lista toda-vacía → sin colon, como #84).
      Sweep verificado seguro/inalcanzado: podcast.status (enum), press.outlet/experience.org/
      publications.title/venue/artifact.license (minLength: 1), publication.doi/orcidId (pattern),
      identity.handle/timezone (no renderizados raw hoy), links[].label (fallback hostnamePath de #78),
      links[].platform (miss de BADGE), arrays inertes para render (project.keywords,
      skill.geoKeywords, brandVoice, artifact.tags — pesos/selección/skill-facing), semanticAssociations
      (trim+filter previo). Revisión (post-review de #87): project.keywords NO era inerte para el
      matching de pesos — los Sets de projectWeight matcheaban items "" entre sí y creditaban el peso de
      la skill a un proyecto sin keywords (orden/featured del README alterados; verificado). Los dos sitios de
      keywords ahora comparten un helper nonEmpty (render + matching): item vacío = ausente en ambos, la regla
      del accesor se expresa una vez por archivo. Bordes pinneados: {en: "", es: "…"} es ausente para
      structured-data (en-canónico) y whitespace-only sigue siendo dato (sin trim).
    3. typing de langMap (F3, NIT — issue-first por D3) — $defs/langMap no tipaba sus valores:
      {en: 0}, {en: []}, {en: {}} eran válidos en JSON Schema mientras todo consumidor asume
      Record<string, string> (contraste: langMapList sí los tipa). Fix solo en el contrato:
      additionalProperties: {type: string} + nota en description. pnpm gen:zod regenerado — el generador ya
      materializaba el shape langMap (catchall string), así que el Zod generado queda byte-idéntico: sin cambio de
      engine, la frontera Zod ya rechazaba y ahora está pineada en load.test. Fixture
      invalid-langmap-nonstring.json en el self-test de contrato (schema-check). Los strings vacíos siguen
      schema-válidos por diseño (regla #84: vacío = ausente en el accesor).

    Byte-parity: inputs poblados byte-idénticos (tests EXPECTED sin cambios). TDD: 4 rojos (brand/geo 1, github-org 1,
    github-readme 1, schema-check fixture 1) + 2 pins verdes (frontera Zod, parity person) → verde; suite 265/265;
    schema-check OK; typecheck limpio.

  • strings vacíos en langMaps = ausentes (issue #84, opción (a) ratificada) — los valores en: "" eran
    schema-válidos (sin minLength) y los accesores los trataban como texto presente (!== undefined): filas
    degeneradas en outputs públicos (- **hub** — con dash colgante, Evidence: ([source]) con doble espacio,
    - ** ** vacío en github-org). Regla semántica única en el accesor de texto: langText (core, compile.ts) y los
    pickText de ambos renderers devuelven undefined para "" — la vacuidad cuenta como ausente en AMBOS niveles del
    lookup (un es: "" cae al en; un en: "" no emite nada). Sin ghost-fallbacks en el registro de fallbacks
    (solo se anota cuando efectivamente se devolvió texto). Sin trim (el límite de la regla es el string vacío exacto —
    " " queda como dato, candidato para un check futuro). Byte-parity: workspaces poblados byte-idénticos (tests
    byte-for-byte sin cambios); solo outputs previamente degenerados mejoran. TDD: 5 tests rojos (core 3, readme 1,
    org 1) → verde; suite 258/258.

  • github-readme: campos opcionales nunca interpolan "undefined" (issue #78) — identity.links[].label es
    opcional en el schema (solo url es requerido, y el string vacío es válido — sin minLength) y el renderer la
    interpolaba cruda: un workspace válido sin label (o con label: "") publicaba - [undefined](…) o - [](…) en
    un README público. Fallback opción (b) del issue: hostnamePath(l.url) — exactamente el label que ya usan los
    links de projects[], sin cambio de contrato. El sweep del patrón (misma clase, mismo archivo) encontró dos
    instancias más y las arregla en el mismo PR: skill.level (omisión del paréntesis) y project.status (omisión del
    segmento · \status`). Byte-parity: inputs con los tres campos poblados emiten byte-idéntico (test EXPECTED sin cambios); github-org` verificado limpio (todas sus interpolaciones opcionales están guardadas).

  • exports condition order in the five newest paste renderers
    (buymeacoffee, liberapay, gumroad, payhip — v1.8; hotmart — v1.9,
    issue #142)
    : "types" now precedes "import" in the . entry, matching
    every earlier package in the workspace. publint flags the old order as an
    error — exports conditions are order-sensitive for TypeScript resolution, so
    the types condition must come first. Caught before the first npm publish:
    none of these packages is on the registry yet.