Repository navigation
v0.3.7
Added
-
pasco entityemits theFAQPagenode — the answer surface's consumer
(#360) —--jsonldnow appends the separateFAQPageblock after the entity
block when the workspace declaresfaq[](never an empty node, no invented
@id;faq[].sourcetravels asacceptedAnswer.citation), and--json
exposes the same assembler node asfaqJsonLd(nullwhen 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 embedjsonLdonly: 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 reciprocalhreflang(en·es·x-default) on
both pages and the same alternates insitemap.xml. The mirror carries no
version claim — a second place to forget on every release — and
version-checknow enforces "claim-free surfaces" from theLANDINGSlist 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-checkgates the new page
like every other. -
pasco discover— a provisional Profile from public surfaces (#329) —
pasco auditneeds aProfile; 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), listsabsent
fields instead of defaulting them, and runsdriftCheckover the result plus
the two classes discovery owns:dangling-ref(a same-host link that did not
resolve) andunsourced-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 asdrift/duplicate-experienceinstead 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.descriptionfromidentity.definition
([lang]falling toen), emitted only when declared — never invented — and
descriptionjoins the Personconsideredlist, 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:definitionwins overmissionin the same description slot —
a definition-less brand keeps the v1.9 mission output byte-identical. The
FAQPageJSON-LD node (StructuredDataReadiness.faqJsonLd) exists iff
rootfaq[]exists — never an empty one — and carries no@id(the engine never
invents an anchor); afaq[].sourcetravels asAnswer.citation. No consumer
embedsfaqJsonLdyet (renderers embedjsonLdonly): 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 surfacespasco buildwould 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.htmland 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 isrenderArtifact's decision tree (active variant →
renderer → shared paste source, withbuild's selection gate) and the checks are
the reportpasco checkprints — that report is now abuildCheckReport()
function instead of a private body insiderunCheck, 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 exposedbuild <platform>(text slices) and
validate, but
no stable object a consumer could read without re-implementingload()+
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 explicitpublications[].urlin favour of a
DOI-derived link, and treated the invariantexperience[].orgas a langMap.
The new command emits
{ schemaVersion, engine, source: { commit, generatedAt }, profile }, where
profileis exactly the normalizedcore.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.artifactRefsstay ids that resolve inprofile.artifacts[],
publications[].urlas declared,deploymentsincluded — never a CLI-local
builder and nevercompile(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 —generatedAtisnullunless--now <iso8601>pins it verbatim after a real-calendar check — andsource.commitis
the workspace gitHEAD(nulloutside 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--helpoutput and the
package README document it. Separately, criterion 3 — validator parity is
now pinned by a test:validateandbuildboth load throughcore.load(), so
they resolve the same schema and the sameadditionalPropertiespolicy; the
fixture carryingprojects[].artifactRefs(the v1.12/#333 witness) is accepted
by both, and an injected unknown row field is refused by both with identical
violations (export profileshares that load path and is asserted with them). -
The
.pascorcengine gate andpasco 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..pascorcgains an
optional"engine": "MAJOR.MINOR.PATCH"field (a lower bound, exactly three
numeric components — nov, range operator or wildcard; the repo stays
dependency-light, so there is no semver range engine).validate,buildand
export profilecheck it before loading or rendering: an older install exits
1with the required version, the installed version and the upgrade command
(npm i -g @profile-as-code/cli@latest); a malformed value exits1naming the
field, the value and the accepted syntax. Absent means no requirement, so every
existing workspace keeps working. Every.pascorcthe resolution consulted is
read — the cwd.pascorcthat 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--workspaceuses 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, whiledoctor
describes the pair: one line with the installed engine, the profile-contract
version this build validates (derived from the schema$idatpnpm gen:zod,
never a literal), the resolved workspace and whether it loads, plus the
effective requirement and its.pascorc(met/NOT MET/INVALID). Exit0
healthy ·1unhealthy ·2usage; 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.pascorcfield 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.jsonrecords 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, sha256275340ff…) and the committedcurrentbytes (v1.14,
sha2561745ac…) with an explicit publication truth — no published artifact
yet (published: false,artifact: null), because no tag carries v1.14. The
released v1.13 artifact rejectspublication.kind(additionalProperties: false), so a consumer that needskindmust 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 --checkruns in CI with no network: it
recomputes the committed hash, reads the tag copy from local git, refuses a
currentpublication 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--checkand 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[].kindis 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-consumervenue-string heuristics ("Carbon Trends" → peer-reviewed, "SSRN
Electronic Journal" → preprint, "… (abstract)" → abstract) — a new venue failed a
consumer's checker and needed hand classification.peerReviewedis derived, never
stored: exactlykind == 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 withoutkindvalidates 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;Zodregenerated from the schema (D3). — The consumer workspace's
claim-sidecar heuristic becomes a schema fact (#349);publications.countsin 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, whoseworkspace:*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-runbranch ispnpm publish, the
loop iterates$ORDER, the publish job's effectivepermissions: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 insiderelease.yml, so a
broken manifest fails before npm consumes a version. The facts a rehearsal cannot
create — the OIDC exchange and the fivedist.attestations— are declared as
#313's open evidence indocs/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 itssameAsset — host plus the declared path, so
github.com/<you>does not matchgithub.com/someone-else) andexact-name
(word-bounded, case- and accent-normalized through the new
core/src/normalize.ts;geoCheckkeeps its case-only comparison and its
published score is unchanged) are high;alias
(identity.orgName) andhandle(declared handles; claimedsocialHandles) 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 ascompeting:…), a
mention after a contrast marker (contrast-context), and a shorter declared term
inside a longer one (longer-name:…) come backcited: false— a declared
qualifier or an adjacent capital never disqualifies a hit by itself. Malformed
NDJSON is exit2, 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
namedurl-invalid:<url>rather than dropped, for the same reason;
while a non-citation is exit0: 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.txtwith two named groups (ai-retrieval→Allow: /,
ai-training→Disallow: /) and a comment perUser-agentnaming 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 · canonicalrole: 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; andAI-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 theai:block ofplatforms/registry.yamland
pnpm gen:platformsprojects it into the engine (a hardcoded UA list would rot
without a failing command); the generator refuses an entry with nosource, an
unknown family, a duplicate token or a stance the SPEC does not state.
--checkexits1on an incomplete or unsourced entry, a duplicate token
(case-insensitive), anas_ofthat is malformed, future-dated or older than
180 days, or apolicy.as_ofthat is stale/future-dated or predates the
newest row it seals — and--outruns the same check before writing, so rot is
a failing command. The project site's ownrobots.txtis generated from the
sameai:block, so the engine's published surface states the same policy.
--outalso 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) —auditdiagnoses,entityis the fix. The
schema.org entity the renderers embed is exposed as an artifact:--jsonis a
frozen,schemaVersion-stamped view{ entity: { id, type, name, sameAs, email }, gaps, jsonld: null }(gaps are the structured-datageoCheckcodes
that scored below 1, verbatim — no new vocabulary), and--jsonldis 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 onelang(--lang, else.pascorc defaults.lang), sogeoCheck(profile, lang)measures gaps in the language the
block emits.@idis never invented: it is the URL of therole: identitylink
ornull, and exit1when absent /0when 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)
andcontradiction(two surfaces disagreeing on one datum) are FAIL; an
unsourced-claim(a quantitative claim with no evidence) is WARN. Exit1iff
a FAIL exists;--jsoncarries{ schemaVersion: "1", scope: { catalog, declared, checked, notRun }, findings: [{ class, severity, path, detail }] }where
scopeis the artifact side (so "measured nothing" is distinguishable from
"clean") andpathis 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 thanvalidate/check
on the FAIL classes, anddrift/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 bypasco checkfor the surfaces whose catalog declares it
(workspacecontent/checks.yaml∪ the shippedskills/<platform>/checks.yaml):
the artifactpasco buildlast 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 incheck(D12: facts
warn) and classified bypasco audit(a URL the surface adds isdangling-ref;
every other disagreement iscontradiction). 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
gainsformat: 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 noseed.mdand links
back to the parent. Newskills/<channel>/longform.yamlforsubstack,
ghost,medium,devto,hashnode,wordpress,listmonkcarries
fields(requiredtitle/dek), an editorial word band, and
export: api | paste.post checkvalidates the long-form front matter,
word count, SSOT URLs (canonical included), dangling fact refs and a dangling
derivedFrom;post exportwrites the Postiz settings mapping for API
channels and apaste-checklist.mdfor 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: writefrom 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 anecho, a constant-falseif:(with or without
a trailing comment), and acontinue-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
intotee, 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
withoutneeds: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, soid-token : writestill grants), requires
node <path>as the step's last unconditional shell segment with the status
observable, reads the barrier'srun:commands (compact- run:form included)
rather than its text, and requires the barrier in thepublishjob or in a job
publishtransitivelyneeds:. 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 —
anif:/continue-on-errorresolved 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 BEFOREpublish:) as well
as the refusal.
Planned (roadmap)
- Renderers: linkedin, upwork, x, researchgate, substack (M4)
- CLI
pasco: the fullrefineflow (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/coregoes 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, andrelease-rehearsalasserts 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: nogitHead, nodist.attestations.