Skip to content

v0.3.7

Choose a tag to compare

@github-actions github-actions released this 31 Oct 19:41
· 367 commits to main since this release
65c35dd

Added

  • pasco entity emits the FAQPage node — the answer surface's consumer
    (#360)
    — --jsonld now appends the separate FAQPage block after the entity
    block when the workspace declares faq[] (never an empty node, no invented
    @id; faq[].source travels as acceptedAnswer.citation), and --json
    exposes the same assembler node as faqJsonLd (null when undeclared). --bare
    stays the entity object alone when there is no FAQPage, and is a JSON array of
    the emitted nodes otherwise. The renderers still embed jsonLd only: the
    published-page consumer remains M5 (surface.kind, D23). SPEC §7.2/§15.4
    updated, and the emitted block has a committed Code-input fixture for the Rich
    Results release step.

  • Bilingual landing /es/ (#308) — the project page mirrors the EN landing
    in Spanish at /es/, with reciprocal hreflang (en · es · x-default) on
    both pages and the same alternates in sitemap.xml. The mirror carries no
    version claim
    — a second place to forget on every release — and
    version-check now enforces "claim-free surfaces" from the LANDINGS list in
    site-config.mjs, which is also the one home the sitemap reads. Docs stay EN
    (decision #280-B; the roadmap remains the declared ES document); site-check gates the new page
    like every other.

  • pasco discover — a provisional Profile from public surfaces (#329) —
    pasco audit needs a Profile; a prospect has none. Discovery reads only what
    a surface itself declares (schema.org JSON-LD + its links), attaches per-fact
    provenance (surface · url · retrieved · attested: false), lists absent
    fields instead of defaulting them, and runs driftCheck over the result plus
    the two classes discovery owns: dangling-ref (a same-host link that did not
    resolve) and unsourced-claim (a numeric claim with no source on the surface).
    One row per surface is kept on purpose, so a cross-surface date disagreement
    surfaces as drift/duplicate-experience instead of being averaged away.
    Transport is §19.5's: closed host allowlist checked before any request
    (registry platform hosts + --allow-host), sequential, no retries, no
    credentials, redirects refused. It writes nothing, and the provisional shape is
    not a schema version (SPEC §7.10).

  • Answer-engine surface, consumer half — structuredData() projects
    identity.definition + faq[] (#316)
    — the v1.10 contract shipped in v0.3.6
    without an emitter; structuredData() now reads the fields. Person:
    fields.description + jsonLd.description from identity.definition
    ([lang] falling to en), emitted only when declared — never invented — and
    description joins the Person considered list, whose denominator is now fixed
    at 8 (counted as populated only when declared, so a person declaring the seven
    pre-v1.10 fields and no definition moves to 7/8: the declared, honestly-accounted
    delta). Brand: definition wins over mission in the same description slot —
    a definition-less brand keeps the v1.9 mission output byte-identical. The
    FAQPage JSON-LD node (StructuredDataReadiness.faqJsonLd) exists iff
    root faq[] exists — never an empty one — and carries no @id (the engine never
    invents an anchor); a faq[].source travels as Answer.citation. No consumer
    embeds faqJsonLd yet (renderers embed jsonLd only): the emitted node is the
    readiness surface the M5 web renderer will consume.

  • pasco preview — see the data without publishing it (#282) — a private
    workspace had no way to look at the rendered result; the only options were to
    publish it or to read the base texts one by one, and Pages is not the missing
    piece (on personal plans the published site is public, so a private fork with
    Pages on publishes the data). The new command renders one self-contained HTML
    file
    — inline CSS, no <script>, no external asset, no HTTP server — whose
    sections are the surfaces pasco build would write (#github-readme,
    #linkedin, …), each with the check state, plus every surface this language
    does not publish (named with its reason, never dropped). The default writes
    <workspace>/.pasco/preview/index.html and opens it; --out <dir> writes and
    opens nothing, which is the CI-artifact transport (the workflow itself lives in
    the workspace template, not in the engine). One artifact, not a second
    implementation:
    the text is renderArtifact's decision tree (active variant →
    renderer → shared paste source, with build's selection gate) and the checks are
    the report pasco check prints — that report is now a buildCheckReport()
    function instead of a private body inside runCheck, so the two commands cannot
    drift. A privacy gate refuses the write (exit 1, and no file is left behind)
    on an absolute machine path, a credential shape, an external asset, active
    content, or a source that resolves outside the workspace: evidence-root media is
    never inlined, a remote image becomes a text note instead of a fetch, and raw
    HTML in the data is escaped rather than executed. --now <iso8601> pins the
    generated <date> stamp, so the page is byte-identical across runs. Contract in
    SPEC §7.9 (command row in §7); --help, the package README and the User Guide
    document it, and the privacy gate has its own seeded tests.

  • pasco export profile --json — the normalized profile for programmatic
    consumers (#348)
    — the engine exposed build <platform> (text slices) and
    validate, but
    no stable object a consumer could read without re-implementing load() +
    normalize(). Both real consumers paid for that copy and drifted: the CV syncer
    read 9 of 14 roots (education, languages, credentials, artifacts
    hardcoded to []), dropped an explicit publications[].url in favour of a
    DOI-derived link, and treated the invariant experience[].org as a langMap.
    The new command emits
    { schemaVersion, engine, source: { commit, generatedAt }, profile }, where
    profile is exactly the normalized core.load() object: every root the
    workspace declares, plus every list root defaulted to []
    (faq[] joins
    normalizeProfile's defaults, so the shape no longer depends on whether answers
    exist); an optional object root (newsletter, podcast, audiences,
    active) appears only when declared, because the contract has no valid empty
    value for it. artifactRefs stay ids that resolve in profile.artifacts[],
    publications[].url as declared, deployments included — never a CLI-local
    builder and never compile(profile, platform)'s platform slice (§15.4).
    schemaVersion ("1") is the frozen payload version a consumer refuses if it
    does not know (the §7.1/§7.2/§7.3 key and precedent). Deterministic by
    construction:
    no clock is read — generatedAt is null unless --now <iso8601> pins it verbatim after a real-calendar check — and source.commit is
    the workspace git HEAD (null outside a repository), so two runs over one
    workspace are byte-identical and a committed golden fixture asserts the exact
    bytes. Contract in SPEC §7.5 (command row in §7); the --help output and the
    package README document it. Separately, criterion 3 — validator parity is
    now pinned by a test: validate and build both load through core.load(), so
    they resolve the same schema and the same additionalProperties policy; the
    fixture carrying projects[].artifactRefs (the v1.12/#333 witness) is accepted
    by both, and an injected unknown row field is refused by both with identical
    violations (export profile shares that load path and is asserted with them).

  • The .pascorc engine gate and pasco doctor (#348) — a workspace can now
    declare the minimum engine it requires, and the commands that do work refuse
    early instead of failing on a contract they do not know. .pascorc gains an
    optional "engine": "MAJOR.MINOR.PATCH" field (a lower bound, exactly three
    numeric components — no v, range operator or wildcard; the repo stays
    dependency-light, so there is no semver range engine). validate, build and
    export profile check it before loading or rendering: an older install exits
    1 with the required version, the installed version and the upgrade command
    (npm i -g @profile-as-code/cli@latest); a malformed value exits 1 naming the
    field, the value and the accepted syntax. Absent means no requirement, so every
    existing workspace keeps working. Every .pascorc the resolution consulted is
    read — the cwd .pascorc that pointed at the workspace AND the workspace's own
    file — and the stricter declaration wins (a malformed one in either file is
    refused); an explicit --workspace uses only the workspace's file. pasco doctor [--json] is the reading surface — chosen over overloading --version,
    which describes the binary and must work with no workspace, while doctor
    describes the pair: one line with the installed engine, the profile-contract
    version this build validates (derived from the schema $id at pnpm gen:zod,
    never a literal), the resolved workspace and whether it loads, plus the
    effective requirement and its .pascorc (met/NOT MET/INVALID). Exit 0
    healthy · 1 unhealthy · 2 usage; offline, no clock, no writes. It reports
    the same truth the gate enforces; it does not replace the gate. Contract in SPEC
    §7.6; the .pascorc field in USER-GUIDE and the package README.

  • The published schema contract for consumers without the engine (#348) — a
    consumer that only validates a profile no longer has to install the CLI.
    schema/consumer-contract.json records the contract's URL + sha256: the
    immutable copy at a released tag
    (https://raw.githubusercontent.com/ChristianLuciani/profile-as-code/v0.3.6/schema/profile.schema.json,
    schema v1.13, sha256 275340ff…) and the committed current bytes (v1.14,
    sha256 1745ac…) with an explicit publication truth — no published artifact
    yet
    (published: false, artifact: null), because no tag carries v1.14. The
    released v1.13 artifact rejects publication.kind (additionalProperties: false), so a consumer that needs kind must use a v1.14 checkout and verify
    the sha256 until the next release; the manifest notes and the reading guide say
    so where they tell a consumer how to validate. node scripts/schema-contract/check.mjs --check runs in CI with no network: it
    recomputes the committed hash, reads the tag copy from local git, refuses a
    current publication claim it cannot join, a stale/moved artifact, a malformed
    tag/hash, or any URL that is not the exact public repo's tag-pinned raw file
    (never a floating ref or a private path, AGENTS.md rule 9). --write [--tag]
    enforces the same tag existence and reachability as --check and derives
    publication instead of inventing it. The self-test proves each refusal,
    including the CLI rows end-to-end; a pin that is no longer the newest tag only
    warns. The reading guide (docs/SCHEMA-CONTRACT.md, SPEC §7.7) states where to
    fetch it and what structural validation does not cover (the cross-file
    invariants core re-checks, engine behavior, .pascorc, the payload envelope).

  • publication.kind — the output classification moves into the contract (schema
    v1.14, #349)
    — publications[].kind is an optional, closed enum
    journal-article | preprint | conference-abstract: the structured answer to
    what kind of output is this, which until now lived outside the contract as
    per-consumer venue-string heuristics ("Carbon Trends" → peer-reviewed, "SSRN
    Electronic Journal" → preprint, "… (abstract)" → abstract) — a new venue failed a
    consumer's checker and needed hand classification. peerReviewed is derived, never
    stored:
    exactly kind == journal-article, the same derive-never-store discipline as
    published-vs-in-development (§15.3/§20.1); an exception requires a SPEC decision
    before a boolean exists. Additive: a row without kind validates and renders
    byte-identically, every existing workspace stays valid, and no renderer output
    changes. Consumers that need counts must require the field explicitly. Contract in
    SPEC §6; Zod regenerated from the schema (D3). — The consumer workspace's
    claim-sidecar heuristic becomes a schema fact (#349); publications.counts in the
    compiled profile remains #348's follow-up.

  • The seam-cohort publish is rehearsed before it is irreversible (#313) —
    scripts/release-rehearsal/ packs the five seam packages with the same client the
    release uses (pnpm, whose workspace:* rewrite is the #301 mechanism) and
    asserts, offline and deterministic, the publish ORDER against the workspace
    dependency graph, the shipped manifests (public access, exact rewrite target,
    resolvable entrypoints, repository.url), and release.yml's own mechanism —
    every publish invocation outside the --dry-run branch is pnpm publish, the
    loop iterates $ORDER, the publish job's effective permissions: grants
    id-token: write, the rehearsal barrier exists before the loop, cohort before
    CLI, and every publishable workspace member except the CLI itself (which has its
    own publish step) is in the cohort. It publishes nothing
    and holds no token; it runs on manifest PRs (release-rehearsal.yml), by hand
    (pnpm rehearse:release), and as a pre-publish barrier inside release.yml, so a
    broken manifest fails before npm consumes a version. The facts a rehearsal cannot
    create — the OIDC exchange and the five dist.attestations — are declared as
    #313's open evidence in docs/ci-cd.md §"Rehearsing the cohort publish".

  • pasco cite-check [<answers.ndjson>] — the deterministic citation matcher
    (#291)
    — every GEO offer is priced in "prompts monitored per month", and the
    half that decides whether the number is true is one question: did this answer
    cite this entity?
    The network half belongs to the runner; this command answers
    the matching question over the answers it already saved — NDJSON in (stdin by
    default), one row per answer out ({ line, cited, confidence, spans, reasons }),
    never aggregated. The ladder is fixed: citation-url (a saved citation on
    the entity's own page or its sameAs set — host plus the declared path, so
    github.com/<you> does not match github.com/someone-else) and exact-name
    (word-bounded, case- and accent-normalized through the new
    core/src/normalize.ts; geoCheck keeps its case-only comparison and its
    published score is unchanged) are high; alias
    (identity.orgName) and handle (declared handles; claimed socialHandles) are
    medium; partial (a multi-token prefix of the name) is low, and low is
    never a citation downstream. It refuses to guess: a one-word name whose
    sentence has no declared disambiguator (ambiguous-term:… + no-qualifier,
    with the longer capitalised phrase the text carries named as competing:…), a
    mention after a contrast marker (contrast-context), and a shorter declared term
    inside a longer one (longer-name:…) come back cited: false — a declared
    qualifier or an adjacent capital never disqualifies a hit by itself. Malformed
    NDJSON is exit 2, naming the 1-based line
    and the field — never a silent skip, because a skipped line changes a rate —
    and a citation URL that does not parse (a missing scheme is the common case) is
    named url-invalid:<url> rather than dropped, for the same reason;
    while a non-citation is exit 0: this is a measurement, not a gate. No network,
    no model, no clock (D19). Contract in SPEC §7.4. — The engine half of
    answer-engine citation measurement.

  • pasco export ai-policy [--out <dir> | --check] — the training stance as an
    artifact (#290)
    — SPEC §12 M9 fixed the position (published surfaces block
    AI-training crawlers by default, retrieval must stay open) and no file said so.
    One command now generates all three from the workspace and the platform
    registry: robots.txt with two named groups (ai-retrieval → Allow: /,
    ai-training → Disallow: /) and a comment per User-agent naming its
    registry entry, operator, source and read date — never a wildcard, and the file
    declares the limit that comes with that: it restricts exactly the tokens it
    lists, so a crawler it does not name is not restricted by it (RFC 9309 §2.2.1);
    llms.txt, the workspace index (name · definition · canonical role: identity
    URL · faq[] · publications[]) in the shape the project site already uses,
    with a coverage assertion that fails when a document the builder claims to
    index is missing from the text; and AI-POLICY.md, the statement you send a
    client — allowed, blocked, who can change it, the date, the stance quoted
    verbatim, and each crawler's own source URL. The crawler list is derived
    data
    : it lives in the ai: block of platforms/registry.yaml and
    pnpm gen:platforms projects it into the engine (a hardcoded UA list would rot
    without a failing command); the generator refuses an entry with no source, an
    unknown family, a duplicate token or a stance the SPEC does not state.
    --check exits 1 on an incomplete or unsourced entry, a duplicate token
    (case-insensitive), an as_of that is malformed, future-dated or older than
    180 days, or a policy.as_of that is stale/future-dated or predates the
    newest row it seals — and --out runs the same check before writing, so rot is
    a failing command. The project site's own robots.txt is generated from the
    same ai: block, so the engine's published surface states the same policy.
    --out also refuses to truncate a file pasco did not generate without
    --force, and reads no clock (the stamp is the registry date, so outputs are
    byte-identical across runs). Offline; contract in SPEC §7.3. — M9's artifact
    in SPEC §12.

  • pasco entity [--json | --jsonld] — the entity projection for surfaces the
    engine does not render (#287)
    — audit diagnoses, entity is the fix. The
    schema.org entity the renderers embed is exposed as an artifact: --json is a
    frozen, schemaVersion-stamped view { entity: { id, type, name, sameAs, email }, gaps, jsonld: null } (gaps are the structured-data geoCheck codes
    that scored below 1, verbatim — no new vocabulary), and --jsonld is the
    same assembler output serialized as a paste-ready
    <script type="application/ld+json"> block (--bare = bare object, --out <file> = write it atomically; an existing file is refused without --force).
    One assembler: core/src/structured-data.ts — the CLI never re-derives the
    entity (SPEC §7.2). Both modes resolve one lang (--lang, else .pascorc defaults.lang), so geoCheck(profile, lang) measures gaps in the language the
    block emits. @id is never invented: it is the URL of the role: identity link
    or null, and exit 1 when absent / 0 when present. Offline and
    deterministic. — Answer-engine surface in SPEC §12.

  • pasco audit [--json] — the offline diagnostic projection (#283) — a
    per-identity aggregate over the checks that already exist, emitting findings
    only
    (no score, no prioritisation, no branding — SPEC §7.1). Three fixed
    classes with fixed severities: dangling-ref (a pointer that does not resolve)
    and contradiction (two surfaces disagreeing on one datum) are FAIL; an
    unsourced-claim (a quantitative claim with no evidence) is WARN. Exit 1 iff
    a FAIL exists; --json carries { schemaVersion: "1", scope: { catalog, declared, checked, notRun }, findings: [{ class, severity, path, detail }] } where
    scope is the artifact side (so "measured nothing" is distinguishable from
    "clean") and path is a factRef (row id in place of the index) or the artifact
    path for a surface finding. Offline: no network, and no finding is derived from
    content/deployments.yaml. It is deliberately stricter than validate/check
    on the FAIL classes, and drift/url-not-in-identity (the URL resolves) plus
    geoCheck's coverage codes are declared no-class rules, in SPEC §7.1. D23 is operator-ratified; its
    normative entry into the SPEC is pending in #262. — M11 in SPEC §12.

  • fact-present — the rule the ledger recorded as missing (PROP-017 §Risks F2) — executed by pasco check for the surfaces whose catalog declares it
    (workspace content/checks.yaml ∪ the shipped skills/<platform>/checks.yaml):
    the artifact pasco build last wrote is compared against the SSOT render
    symmetrically — a URL, ISO date or number the surface adds or omits, and a
    text difference no fact token explains. All are WARN in check (D12: facts
    warn) and classified by pasco audit (a URL the surface adds is dangling-ref;
    every other disagreement is contradiction). No artifact = not run, and
    declared: [] = no premise declared — never a silent OK.

  • pasco post — long-form seeds and the short family (#266) — post.yaml
    gains format: short | longform (the format belongs to the seed) and
    derivedFrom: <seed-id>; a short child derived from an essay via
    post task <child> --format short --from <essay> has no seed.md and links
    back to the parent. New skills/<channel>/longform.yaml for substack,
    ghost, medium, devto, hashnode, wordpress, listmonk carries
    fields (required title/dek), an editorial word band, and
    export: api | paste. post check validates the long-form front matter,
    word count, SSOT URLs (canonical included), dangling fact refs and a dangling
    derivedFrom; post export writes the Postiz settings mapping for API
    channels and a paste-checklist.md for Substack/Ghost. Posts stay outside
    profile.schema.json (D17); images are not a v0 field.

Fixed

  • The release rehearsal no longer passes on non-barriers (#313) — four fresh
    passes (#347, #363 and its second and third rounds) found the gate's own checks
    tautological, each time by mutation rather than by reading its output. A job-level
    permissions: override fell through to the workflow default and the gate read
    id-token: write from a grant the job had replaced: flow style
    ({contents: read}), the scalar spellings (read-all / write-all), a key
    written with a space before the colon, and finally any shape the gate could not
    read (a multi-line flow mapping, an anchor, an alias) — which is now denied
    instead of resolving through the default. The barrier was located by "does the
    step text contain the path", so an echo, a constant-false if: (with or without
    a trailing comment), and a continue-on-error (true, TRUE, ${{ true }}, a
    trailing comment — on the step or on the barrier's job) passed green; a shell tail
    that discards the status (|| true, a command after the barrier, set +e, a pipe
    into tee, a background &) also passed, as did a barrier reached only through
    cmd || node <path>; and a flat step index counted a barrier in a prior job
    without needs: as "before the cohort". The gate now reads the job's own
    permissions: in the three single-key spellings at the job's indent (normalizing
    whitespace in the entries, so id-token : write still grants), requires
    node <path> as the step's last unconditional shell segment with the status
    observable, reads the barrier's run: commands (compact - run: form included)
    rather than its text, and requires the barrier in the publish job or in a job
    publish transitively needs:. It is not a YAML or shell parser:
    docs/ci-cd.md §"Rehearsing the cohort publish" declares the three outcomes
    (refused, unreadable-and-denied, unproven-and-accepted) and names the residual —
    an if:/continue-on-error resolved only at runtime is accepted, not proved.
    Each class has a self-test that fails against the revision it closes; the fixture
    case pins the fixture shape (the barrier job must land BEFORE publish:) as well
    as the refusal.

Planned (roadmap)

  • Renderers: linkedin, upwork, x, researchgate, substack (M4)
  • CLI pasco: the full refine flow (M3)
  • Per-platform skills v1 from SOTA research (M3)
  • Web renderer (M5)

Internal

  • The seam cohort publishes through the workflow for the first time (#313, #377) —
    @profile-as-code/core goes 0.1.1 → 0.1.2, so the release event has a seam member to
    publish instead of five skips. The cohort step now requests provenance explicitly
    (pnpm publish --provenance) on both branches, and release-rehearsal asserts the
    request per invocation — the last flag wins, a flag in a comment does not count, and
    both branches must use pnpm. Until this release the five seam packages on the registry
    came from a manual staged publish: no gitHead, no dist.attestations.