You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
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).