Skip to content

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 04 Nov 19:10
· 253 commits to main since this release
ec1be50

Added

  • release plan --json carries order on every row (#421, SPEC §7.8) — the
    human plan prints a 1-based ORDER column, but the machine payload had no
    equivalent: a consumer had to count array indices to reproduce it, an implicit
    contract it could get wrong. Each surfaces[] row now carries order
    (1..n, the array position), and the human column reads the same field instead
    of recomputing index + 1. Additive: the array order is unchanged and no
    consumer breaks.
  • discover --observations — the human reads the surface the machine cannot
    (#412, SPEC §7.10)
    — discover reads only schema.org Person/Organization
    JSON-LD, which a prospect's real surfaces do not publish (measured at 4560ae1:
    LinkedIn http-999, ResearchGate http-403, ORCID/X/Instagram no Person —
    0 of 5 usable), so the audit's classes had no input for the person it sells an
    audit to. --observations <file> takes YAML rows read by hand — surface,
    field (name · link · experience), value, url, retrieved_at — and
    turns each into the SAME SurfaceObservation the transport produces, so
    provisionalProfile, provenance, absent[] and discoveryFindings run
    unchanged. Rows make NO request (the url is a provenance pointer) and enter
    first, so a declared name beats a machine-read one. --json is payload v2
    (via: fetch|manual; a manual surface's status is null — no request, and a
    fabricated 200 would be a claim). A name declared differently is a
    contradiction at identity.name with both sources cited: the profile holds
    one name, and dropping the second hid the disagreement the audit exists to find.
    A malformed row is refused with its number: an impossible date (2026-02-31,
    2020-13, 2026-02-00), a non-http URL, an unknown key, and a second
    experience row for the assembly's own (surface, org) key (experienceId, so
    ACME/Acme collide and École/Cole do not) fail with the row. That key
    holds ONE row by design, so a second role at one org is refused rather than
    dropped, and a surface the transport also reads resolves to the machine row's
    key: supply one or the other.
  • achievements[] — a career impact log with metrics, targets and capture-first
    evidence (#446, D24, schema v1.16)
    — the contract recorded what a person DID
    (experience[].highlights, free text: no value, unit, baseline, target or link)
    and what can be PROVEN (projects[].evidence[], project-scoped and requiring a
    public source, so an internal outcome had no home), but not what CHANGED — so
    the effect was not captured when it happened and could not be reconstructed
    later. One ROOT block, never per-row outcomes[]: each row states a statement,
    is a metric (value/target/baseline/unit/direction) or a milestone,
    points at the units it belongs to (refs, block:id, resolved by core) and
    declares how the result is known (basis: measured | estimated | reported
    | attested) plus a REQUIRED visibility, so an internal outcome never becomes
    public by omission. A target without a value is a planned goal, and the
    status (planned | measured | achieved | partial) is DERIVED by the one
    achievementStatus() function, never stored — a stored status could disagree
    with the numbers it describes. load fails on duplicate ids, a dangling
    refs/artifactId/credentialId, a kind ↔ metric mismatch and a
    public→private registry leak (§18.5), including through refs[].
    pasco achievements [--targets] [--json] is the read-only log view (grouped by
    the first ref, with the derived status, the formatted metric, the basis and the
    pointer; scope counts the rows the view returned); pasco audit reports a
    public measured/attested row with no pointer as unsourced-claim (WARN) and
    never as a finding for reported. Additive: a workspace without the block
    validates and renders byte-identically — pasco export profile emits
    achievements: [] for it, the same loaded-shape rule every list root follows,
    which is why the root carries no minItems: an explicitly empty array stays
    schema-valid, and "declare the block only when you have rows" is an authoring
    convention the engine does not enforce.
    SPEC §5 D24 + §6 + §7 + §23, schema/README.md, docs/SCHEMA-CONTRACT.md and
    schema/consumer-contract.json (v1.16, unpublished) document the change, and
    completeness counts achievements[].statement in language coverage.
  • pasco edit reaches the answer-engine surface — and --create seeds a
    missing root (#435)
    — two addresses join the contract-path grammar with the
    same rules as the existing ones (exactly one row, --check preview, cap
    pre-flight): identity.definition.<lang> and
    faq[id=<slug>,lang=<l>].q|.a / faq[id=<slug>].source. faq[] rows are
    selected by their stable id (a repeated id is ambiguous and refused, never
    index-addressed) and q/a are langMaps, so their lang selector is
    required; an optional value the row does not carry is a 0-row address. The
    preview is the field's real published surface: identity.definition shows the
    entity JSON-LD line the github-readme/github-org artifacts embed, and
    faq[] shows the FAQPage node pasco entity --jsonld emits (no
    pasco build artifact carries it). Both are pre-flighted against the
    contract's own caps (160 / 90 / 320 chars per value, v1.10 #288): unlike a
    platform's soft cap, over one is a schema failure, so the pre-flight warns and
    the write is refused by validation. The byte-range splice cannot invent a
    root, so --create <root> is a SEPARATE operation: it writes a schema-valid
    template for a root the workspace does not declare yet (faq: 3 placeholder
    pairs, the schema minimum — never invented facts), refuses an existing root
    under any extension case (idempotent, nothing written), takes the same write
    lock and validation gate over a temp copy, runs the same cap pre-flight, and
    --check previews the seed. --list gains identity.definition and faq
    scopes. --help, the CLI README, the user guide and SPEC §7 document the new
    addresses and the seeded root.

Fixed

  • check and preview name the catalog they actually read (dogfood
    2026-11-04)
    — on a real workspace with no content/checks.yaml
    (profile-data), check printed user checks (content/checks.yaml, schema-validated catalog) over the four rows of DEFAULT_CHECKS, and
    preview captioned the same rows declared limits (content/checks.yaml): two
    published surfaces attributing the caps to a file the engine never read — the
    provenance class #455 closed in the docs, still live in runtime output.
    load() merges every content/*.y{a,}ml by root property and reports no
    source, so a filename is a claim the engine cannot make (false for an empty
    checks.yaml, false for a workspace using checks.yml). The rule "the
    workspace's catalog, else the package's conservative caps (#240)" now has ONE
    home (check-catalog.ts) instead of four copied ternaries, and every surface
    derives its wording from catalogSource(): check says workspace checks (declared in content/, …) or workspace checks: none declared — the conservative caps shipped with pasco apply (#240); preview's table captions
    declared limits (content/) or declared limits (caps shipped with pasco, #240). Tests cover the four states — declared, absent, an empty file, and rows
    in checks.yml — and assert that no surface prints content/checks.yaml.
    Values, check --json and every exit code are unchanged; the only payload
    movement is preview --json's bytes (the caption's own length).
  • post --help names which channels ship short rules and which are long-form
    (#422)
    — pasco post task <id> --channel devto exited 2 naming the file it
    wanted, so a reader learned the split by provoking the error. The help and
    docs/USER-GUIDE.md §Posts now name the short-rule channels (linkedin, x)
    and the long-form set (devto, ghost, hashnode, listmonk, medium,
    substack, wordpress); no flag and no runtime discovery. A test reads both
    sets from skills/, so a channel added later fails CI instead of drifting.
  • The engine floor is declared at birth, and an old engine's failure names the
    upgrade (#434)
    — pasco init now writes "engine": "<installed CLI version>"
    into .pascorc (in the gate's MAJOR.MINOR.PATCH grammar), so a scaffolded
    workspace can no longer leave the gate off by default; a build whose version
    carries a pre-release suffix declares no floor at all (reducing it to the
    numeric triple would pin the scaffold to a version @latest may not publish).
    When schema validation fails on an Unrecognized key, pasco validate adds
    ONE hint line naming the likely cause (data newer than the engine) and the
    remedy (npm i -g @profile-as-code/cli@latest, then pasco doctor) — as
    error.hint in --json too — unless the engine gate already named the
    mismatch (installed older than the declared floor). The verdict is unchanged: exit 1, same violations. init's
    version comes from one shared read (packages/cli/src/version.ts), never a
    literal, and docs/USER-GUIDE.md §3/§8.2.1 now state that the floor is
    declared at birth, not by hand.
  • pasco doctor names the engine-requirement state instead of letting
    "absent" look met (#436)
    — the line now reads engine requirement absent — no .pascorc declares one for a workspace with no declaration, alongside met,
    NOT MET and INVALID, and --json carries the same value as
    requirement.state (absent | met | NOT MET | INVALID). The exit code is
    unchanged: an absent declaration remains a healthy workspace, but the premise it
    actually read is now stated, never implied by a green exit.
  • The completeness label names the policy's langs, not a hardcoded
    (en/es/fr) (#436)
    — COMPLETENESS_CATALOG owns the base label and the
    report appends the langs the effective policy declared, so a workspace with
    completeness.langs: ["en","es"] reads Language coverage (en/es) on the
    human checklist and in --json, matching the detail that already named those
    langs. The shipped default still renders (en/es/fr).
  • unsourced-claim only fires on a number that carries a quantity marker
    (#437)
    — isQuantitativeClaim, the one predicate shared by audit,
    post check and discover, now requires a currency/percent sign adjacent to
    the number, a trailing +/magnitude suffix, or a unit word right after it.
    A bare run of digits is no longer a claim, so prose years (Aug 2024) and
    ages (76) stop producing warnings whose only honest clear was deleting a
    true private fact or inventing a source; a four-digit calendar year followed
    by a function word (or preceded by a month name) stays a date, while
    2000 users is still a count. The docstring, docs/SPEC.md §7.1 and
    docs/post-mvp.md now state this boundary, and a table in
    packages/cli/tests/claims.test.ts pins both sides of it.
  • geo/freshness reads a publication year at year granularity (#438) — the
    newest publications[].year is mapped to ${year}-12-31, so a publication of
    the current year was reported as a future-dated fact for the rest of that
    year; the check now compares a year-precision fact at year granularity (it
    cannot be future-dated before Jan 1 of the following year) and reports the year
    itself, never the synthetic December edge. A genuinely future year (2099) is
    still reported, and a current-year publication is no longer future-dated, so it can
    evidence freshness again instead of going red exactly when it is most accurate
    (a genuinely future year alongside it still fails the signal — the anomaly wins). SPEC §8 updated.
  • pasco discover names the surfaces that qualify instead of a dead end
    (#418)
    — the refusal for a page with no entity name now says that discovery
    reads only schema.org Person/Organization JSON-LD, that a GitHub profile
    (and a product/page node such as SoftwareApplication) carries none, and to
    point at a personal site or page that declares one; --help and the tutorial
    say the same. The matcher and the provisional-profile semantics are unchanged.
  • pasco discover reports only claims about the entity, not page chrome
    (#419)
    — a quantitative sentence now counts only when it names the entity
    (declared name, its words, or a passed surface's handle) or is first-person;
    identical sentences are reported once, navigation segments (/about,
    /blog, /team, …) and lowercase name particles (de, van, dos) are not
    subjects, short tokens match case-sensitively, and block boundaries no longer
    merge a page title into the first sentence (nor split prose wrapped in the
    HTML source). isQuantitativeClaim stays the one shared predicate with
    post/audit.
  • pasco release plan no longer calls a valid changedUnit "not an SSOT
    unit" (#408)
    — a changed unit that IS an SSOT unit but has no deploy record
    on a surface is a pending fan-out (never), not a malformed release row; the
    message sent the operator to correct a datum that was already correct. The
    plan payload now separates the two causes additively — unknownUnits (a key
    with no SSOT unit behind it: the row IS wrong) and missingUnits (a valid
    unit still owed a record) — and the human plan names each. SPEC §7.8/§22
    updated.
  • pasco refine's agent template renders a langMap role and names the
    addressed platform's variant ids (#415)
    — a bilingual
    experience[].role was interpolated raw into the fact sheet the executing
    agent works from, so it read [object Object]; the template now resolves it
    with the language rule the text fields already use (the requested --lang,
    falling back to en — textOf gained the same optional language, one home
    for the read). The Variant id convention examples were hardcoded gh-en-*
    for every platform; they are derived from the prefix and language the
    convention line prints (linkedin-en-1, …), and the shipped github-readme
    skill + playbook now document that same slug dialect — they said
    gh-<lang>-<n>, so the template and the skill it points at would have
    disagreed (operator decision, 2026-11-01: the slug is the one dialect; the
    hotmart/kofi skills and the user guide already used it). While reconciling,
    the same line's variant count turned out to trust .pascorc's
    defaults.variants unchecked — 0 printed "Write 0 variant(s)" and a
    non-number collapsed the examples — so parseVariants now normalizes an
    unusable fallback to the documented default (3) in one place, which also
    covers pasco post.
  • one prefix policy on the human error surfaces (#416) — report() printed
    <command>: … without the pasco the success lines carry, and the top-level
    catch added a usage: to UsageError messages that already started with
    one, so a usage error read usage: usage: pasco skills:export …. The label
    now has one home per axis: humanCommand() (pasco <command>) on the
    reported error path, and the catch strips a leading usage: before adding
    its own. --json envelopes are unchanged.

Internal

  • The CLI goes 0.3.7 → 0.4.0 and the core seam 0.1.2 → 0.1.3 — the cycle since
    v0.3.7 includes schema v1.16 (D24 achievements[]) and the presence fan-out's
    order field (#421). The core bump publishes the seam member that carries
    ReleasePlanSurface.order and the v1.16 load/types to direct core consumers:
    the cohort step skips a version already live on the registry, so an unchanged
    0.1.2 would leave the published core describing the previous tree. paste and
    github are live at 0.1.0 with an exact core@0.1.1 dependency and are NOT
    bumped here, so their installers keep the earlier core until one of those packages
    gets a release of its own (a bump reopens the 48 h trusted-publisher window per
    package, docs/ci-cd.md). The CLI is bin-only and bundles core, so pasco
    itself is unaffected. Both versions publish through the tagged workflow with
    provenance (#313/#377).