@profile-as-code/cli 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +410 -14
- package/dist/index.js +8212 -3772
- package/package.json +3 -2
- package/skills/README.md +39 -5
- package/skills/devto/SKILL.md +82 -0
- package/skills/devto/longform.yaml +33 -0
- package/skills/devto/post.md +96 -0
- package/skills/ghost/SKILL.md +85 -0
- package/skills/ghost/longform.yaml +36 -0
- package/skills/ghost/post.md +98 -0
- package/skills/github-org/SKILL.md +1 -2
- package/skills/github-org/checks.yaml +1 -1
- package/skills/github-org/playbook.md +2 -3
- package/skills/github-readme/checks.yaml +1 -1
- package/skills/github-repo-meta/SKILL.md +82 -0
- package/skills/github-repo-meta/checks.yaml +50 -0
- package/skills/github-repo-meta/playbook.md +45 -0
- package/skills/hashnode/SKILL.md +79 -0
- package/skills/hashnode/longform.yaml +33 -0
- package/skills/hashnode/post.md +90 -0
- package/skills/hotmart/checks.yaml +1 -1
- package/skills/kofi/checks.yaml +1 -1
- package/skills/listmonk/SKILL.md +83 -0
- package/skills/listmonk/longform.yaml +33 -0
- package/skills/listmonk/post.md +93 -0
- package/skills/medium/SKILL.md +86 -0
- package/skills/medium/longform.yaml +33 -0
- package/skills/medium/post.md +96 -0
- package/skills/substack/SKILL.md +83 -0
- package/skills/substack/longform.yaml +36 -0
- package/skills/substack/post.md +91 -0
- package/skills/wordpress/SKILL.md +84 -0
- package/skills/wordpress/longform.yaml +33 -0
- package/skills/wordpress/post.md +99 -0
package/README.md
CHANGED
|
@@ -58,14 +58,23 @@ commands above are the standalone path.
|
|
|
58
58
|
|---|---|---|
|
|
59
59
|
| `pasco init [dir]` | Scaffold a profile-data workspace (`content/identity.yaml`, `README.md`, `.pascorc`) — `pasco validate`-clean | ✅ |
|
|
60
60
|
| `pasco validate [--json]` | Schema **fail** (exit 1) + drift/geo **warn** (exit 0) — D12 | ✅ |
|
|
61
|
+
| `pasco doctor [--json]` | **Engine · schema · workspace** on one line (#348): the installed CLI version, the profile-contract version this build validates (from the schema `$id`), the resolved workspace and whether it loads, plus the **effective** `.pascorc` engine requirement (the strictest declaration in every config the resolution consulted, with its file). `--json` emits `{ engine, schema, workspace, requirement: { declared, met, problem?, source? } }`. Exit 0 healthy · 1 unhealthy · 2 usage; offline, no clock, no writes | ✅ v1 |
|
|
61
62
|
| `pasco build [platform] [--lang]` | Deterministic base texts per platform+lang; platforms with a renderer (the eight registered in the generated renderer map) write `profiles/<platform>/README.md` + bundled media; renderer-less manual surfaces with a bio row write `profiles/<platform>.md` (#239). Empty render = no-trace unbuild of the generated output, preserving `profiles/<platform>/variants/` (chosen-variant input, #238; pure profiles/, §15.1 — v1.7). Media in `profiles/<platform>/assets/` + `assets/manifest.json` (M8/§18.6 — see `docs/media-pipeline.md`) | ✅ |
|
|
63
|
+
| `pasco preview [--out <dir>] [--no-open] [--lang <l>] [--now <iso>] [--json]` | The **local view** (#282): **one** self-contained HTML file of the surfaces `pasco build` writes — inline CSS, no `<script>`, no external asset, no HTTP server. Sections are the published surfaces (`#github-readme`, `#linkedin`, …) plus the ones this language does not publish, named with their reason; the check state is the same report `pasco check` prints, and the text is `build`'s own render decision tree. Default `<workspace>/.pasco/preview/index.html`, then opened in the browser; `--out <dir>` writes and opens **nothing** (the CI-artifact transport). A privacy gate refuses the write (exit 1) on an absolute machine path, a credential shape, an external asset, active content, or a source outside the workspace | ✅ v1 |
|
|
62
64
|
| `pasco edit <contract-path> [--set <text> [--check]]` | Resolve **one** contract field by contract path; `--set` **writes it** (comment-preserving range splice, validated, atomic) and `--set --check` previews the **published lines the edit changes** (or says it changes nothing). Epic #247, S1–S5 landed | ✅ S1–S5 (read · preview · write · surface impact · cap pre-flight · machine contract) |
|
|
63
65
|
| `pasco diff [--json]` | Changes since the last `release-*` tag (git-based → CHANGELOG hint) | ✅ |
|
|
64
66
|
| `pasco status [platform] [--mark <ledgerKey> <surface>] [--verify] [--write-checklist] [--completeness]` | Per-entry deploy report — origin · surfaces · in-sync/stale/never · **certification** (verified/attested/unverifiable/out-of-band-drift, §19.5) + the derived **publication truth** per surface (§20.1, #242) + ledger write paths (D18, §19) | ✅ |
|
|
67
|
+
| `pasco presence [--json]` / `pasco release plan [<id>] [--json]` | **Presence board** (D23/§22, #262): every surface a release has addressed, at its newest release, with the ledger-derived state (`in-sync`/`stale`/`never`), and the **release plan** — the ordered fan-out (`repo-meta` → `profile` → `distribution`, then slug), each surface's state and the per-kind **factual spine**. Copy per channel lives in the shipped skills, not the engine | ✅ v1 |
|
|
65
68
|
| `pasco refine <platform> [--lang] [--variants N] [--choose <id>]` | Agent-driven variant flow (D10/D11/D13); the chosen body is `profiles/<platform>/variants/<id>.md` (a complete platform artifact), rendered by `pasco build` (#238) | 🚧 template + `--choose` (full flow M3) |
|
|
66
|
-
| `pasco check [platform]` | User checks from `content/checks.yaml` (schema-validated catalog) + registry rules (M7/§18.8) + media-transport rules (M8/§18.6) + offline `max-chars`/`min-chars` over headlines/bios (#240) | 🚧 offline rules today (limits + registry + media; remaining catalog M3/M4) |
|
|
67
|
-
| `pasco
|
|
68
|
-
| `pasco
|
|
69
|
+
| `pasco check [platform]` | User checks from `content/checks.yaml` (schema-validated catalog) + registry rules (M7/§18.8) + media-transport rules (M8/§18.6) + offline `max-chars`/`min-chars` over headlines/bios (#240) + `fact-present` (the built surface's fact tokens vs the SSOT render, #283) | 🚧 offline rules today (limits + registry + media + fact-present; remaining catalog M3/M4) |
|
|
70
|
+
| `pasco discover <url>... [--json] [--allow-host <hosts>]` | **Provisional profile** from public surfaces (#329, SPEC §7.10), for a prospect with no workspace: reads schema.org JSON-LD + links only (no prose mining), attaches per-fact provenance (`attested: false`) and lists `absent` fields; reports the audit's class names with discovery's own predicates (SPEC §7.10); closed host allowlist checked before any request (sequential, no retries, no credentials); **writes nothing** | ✅ v1 |
|
|
71
|
+
| `pasco audit [--json]` | Offline **diagnostic projection**, findings only (no score/prioritisation/branding, #283): three fixed classes — `dangling-ref` FAIL · `contradiction` FAIL · `unsourced-claim` WARN — exit 1 iff any FAIL; `--json` carries `{ schemaVersion: "1", scope: { declared, checked, notRun }, findings }` | ✅ v1 |
|
|
72
|
+
| `pasco entity [--json \| --jsonld [--out <file>] [--bare]]` | The **entity projection** for surfaces the engine does not render (#287): `--json` is the frozen view — `entity { id, type, name, sameAs, email }`, the structured-data `gaps` (geoCheck codes verbatim), `jsonld: null` and `faqJsonLd` (the `FAQPage` node, `null` when the workspace declares no `faq[]`); `--jsonld` is the **same assembler output** the renderers embed, as paste-ready `<script type="application/ld+json">` blocks — the entity first, then the `FAQPage` when it exists (#360). Exit 1 when `@id` is absent (never invented) | ✅ v1 |
|
|
73
|
+
| `pasco cite-check [<answers.ndjson>] [--json]` | The **citation matcher** over saved answer-engine answers (#291): reads the runner's NDJSON (stdin when no path), matches the workspace's declared entity and emits, **per answer and never aggregated**, `{ cited, confidence, spans, reasons }`. Ladder: `citation-url` (a citation on the entity's own page or its `sameAs` set — host, and the declared path or a descendant) and `exact-name` → **high**; `alias` (`identity.orgName`) and `handle` → **medium**; `partial` (any contiguous multi-token prefix, ≥ 2 tokens) → **low**, and low is never citable downstream. Refuses to guess: a one-word name with no declared disambiguator (`cited: false`, the longer capitalised reading named as `competing`), or a contrast mention, is not a citation; a declared qualifier or an adjacent capital never disqualifies a hit by itself | ✅ v1 |
|
|
74
|
+
| `pasco export ai-policy [--out <dir> \| --check] [--lang <l>]` | The M9 stance as an artifact (#290): `--out` writes `robots.txt` (**two named groups, no wildcard** — the listed retrieval crawlers allowed, the listed training crawlers disallowed, each UA commented with its registry entry/operator/source/date; the file declares that a crawler it does not name is **not restricted**), `llms.txt` (the workspace index: name, definition, canonical identity, `faq[]`, `publications[]`, with a coverage assertion) and `AI-POLICY.md` (allowed · blocked · that same explicit-list limit · who can change it · the date, quoting the stance verbatim). `--check` exits 1 on an incomplete or unsourced entry, a duplicate token (case-insensitive), a malformed/future/`>180`-day `as_of`, or a `policy.as_of` that is stale or older than its newest row; `--out` refuses to write one. The UA list is projected from the platform registry — never hardcoded in the CLI | ✅ v1 |
|
|
75
|
+
| `pasco export profile [--json] [--now <iso8601>]` | The **normalized profile** for programmatic consumers (#348): `{ schemaVersion, engine, source: { commit, generatedAt }, profile }` — `profile` is exactly what `core.load()` returns (**every root the workspace declares, plus every list root defaulted to `[]`**; an optional object root appears only when declared), never a platform slice (`compile()` makes that other object). `artifactRefs` ids resolve in `profile.artifacts[]`, `publications[].url` as declared, `deployments` included. No clock is read: `generatedAt` is `null` unless `--now` pins it, so two runs are byte-identical; `source.commit` is the workspace git HEAD (`null` outside a repo) | ✅ v1 |
|
|
76
|
+
| `pasco post <seed\|task\|choose\|check\|export\|import\|list>` | Post experiment (#264/#265, long-form #266): a seed at `posts/<seed>/` → task bundle → chosen variant → check → Postiz draft payload (API channels) or paste checklist (Substack/Ghost). Two seed formats (`short`/`longform`); a short child derived from an essay records `derivedFrom`. The CLI never calls an LLM | 🚧 experiment (not in the v1 contract; `docs/post-mvp.md`) |
|
|
77
|
+
| `pasco skills:export <dir> [--force] \| --list` | Copy the platform skills this package ships into a directory you name (`--list` shows them; no per-agent paths are invented — #254). A destination and `--list` are **separate invocations**: both together is usage (exit 2), never an ignored argument | ✅ |
|
|
69
78
|
|
|
70
79
|
The partial commands are explicit, and each one says what still owns the rest:
|
|
71
80
|
|
|
@@ -77,21 +86,384 @@ The partial commands are explicit, and each one says what still owns the rest:
|
|
|
77
86
|
#253). The full variant flow is M3 (`src/commands/refine.ts`).
|
|
78
87
|
- **`check`** — executes the offline rules that exist today: the brand handle
|
|
79
88
|
matrix (brand flavor only, D15/§17), the evidence-registry checks (M7/§18.8),
|
|
80
|
-
the media-transport rules (M8/§18.6),
|
|
89
|
+
the media-transport rules (M8/§18.6), the `max-chars` / `min-chars` rows
|
|
81
90
|
declared in `content/checks.yaml` over the headline/bio rows that exist
|
|
82
|
-
(offline, warn-class, #240)
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
(offline, warn-class, #240), and **`fact-present`** — for the surfaces whose
|
|
92
|
+
catalog declares it (`content/checks.yaml` ∪ the shipped
|
|
93
|
+
`skills/<platform>/checks.yaml`): the artifact `pasco build` last wrote is
|
|
94
|
+
compared **symmetrically** against the SSOT render — a URL, ISO
|
|
95
|
+
date or number one side carries and the other does not, or a text difference no
|
|
96
|
+
fact token explains, is a finding — WARN in `check` (D12), because a published
|
|
97
|
+
surface that disagrees with the SSOT is a fact problem, not a schema one. A
|
|
98
|
+
targeted surface with no artifact reports the rule as **not run**, never as OK. The remaining
|
|
99
|
+
per-platform rules in the catalog ship with the skills `checks.yaml` union (M3)
|
|
100
|
+
and the full `pasco check` (M4). It prints its `owner` line
|
|
101
|
+
(`src/commands/check.ts`).
|
|
85
102
|
- **`skills:export`** — copies the platform skills this package ships into the
|
|
86
103
|
directory the user names (`--list` first; an existing non-empty directory needs
|
|
87
104
|
`--force`). It replaces the earlier projection stub: the CLI never invents a
|
|
88
105
|
per-agent destination and never points at tooling the reader cannot open
|
|
89
106
|
(`src/commands/skills-export.ts`, #254).
|
|
90
|
-
- **`post`** — the
|
|
91
|
-
the `posts/` store; it never calls an LLM, emits the
|
|
92
|
-
choice, checks limits/URLs/refs
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
- **`post`** — the post experiment (#264/#265, long-form #266): a CLI
|
|
108
|
+
subcommand family over the `posts/` store; it never calls an LLM, emits the
|
|
109
|
+
agent task, records the choice, checks limits/URLs/refs (plus title/dek and
|
|
110
|
+
the word band for long-form) and exports a Postiz draft for API channels or a
|
|
111
|
+
paste checklist for Substack/Ghost. A seed is `format: short | longform`; a
|
|
112
|
+
short child derived from an essay records `derivedFrom: <essay-id>`. It reads
|
|
113
|
+
the `skills/` bundled with the package (set `PASCO_SKILLS_DIR` only to point
|
|
114
|
+
at a custom tree), and it is **not part of the v1 contract yet**
|
|
115
|
+
(`docs/post-mvp.md`).
|
|
116
|
+
|
|
117
|
+
### The audit (`pasco audit --json`, #283)
|
|
118
|
+
|
|
119
|
+
`audit` is an **offline JSON projection over existing checks** — not a second
|
|
120
|
+
analysis engine and not a scoring surface. It aggregates per identity (where
|
|
121
|
+
`check` is per document) and reports **findings only**: no score, no
|
|
122
|
+
prioritisation, no branding. Those belong to the consumer (`PROP-017 §Module
|
|
123
|
+
boundary`), and putting them in the engine would recreate the monolith the
|
|
124
|
+
module split exists to prevent.
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
pasco audit # human list, one block per finding
|
|
128
|
+
pasco audit --json # machine contract
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"schemaVersion": "1",
|
|
134
|
+
"scope": { "catalog": "shipped", "declared": ["github-readme"], "checked": ["github-readme"], "notRun": [] },
|
|
135
|
+
"findings": [
|
|
136
|
+
{
|
|
137
|
+
"class": "dangling-ref",
|
|
138
|
+
"severity": "FAIL",
|
|
139
|
+
"path": "projects[hub].links[0]",
|
|
140
|
+
"detail": "drift/url-not-in-identity — URL https://github.com/someoneelse lives on a platform host but is not in identity.links"
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`scope` is the artifact side (the SSOT-side rules always run): `catalog` is
|
|
147
|
+
`shipped` | `not-shipped` (a package without its skills tree cannot declare the
|
|
148
|
+
premise — a bundle state, not "nothing declared"); `declared` are the targeted
|
|
149
|
+
surfaces whose catalog declares the `fact-present` premise, `checked` the
|
|
150
|
+
declared ones whose built artifact was compared, `notRun` the declared ones with no
|
|
151
|
+
artifact on disk or no renderer for the flavor. **`declared: []` means the premise
|
|
152
|
+
is not declared anywhere; `checked: []` means it measured nothing** — either way the
|
|
153
|
+
audit says so instead of looking clean.
|
|
154
|
+
|
|
155
|
+
| Class | Fires when | Severity |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `dangling-ref` | a pointer does not resolve in the current view | **FAIL** |
|
|
158
|
+
| `contradiction` | two surfaces assert incompatible values for one datum | **FAIL** |
|
|
159
|
+
| `unsourced-claim` | a quantitative claim carries no evidence | **WARN** |
|
|
160
|
+
|
|
161
|
+
`schemaVersion` is the contract version: a consumer refuses a version it does not
|
|
162
|
+
know rather than guessing at the shape. `path` is a `factRef` (the row `id`
|
|
163
|
+
replaces the array index when the row declares one); a finding about a built
|
|
164
|
+
surface addresses the artifact path (`profiles/<surface>/README.md`), because a
|
|
165
|
+
fact the SSOT does not carry has no `factRef` by definition.
|
|
166
|
+
|
|
167
|
+
Exit `1` iff a `FAIL` finding exists, `0` otherwise (WARN-only included); `2` is
|
|
168
|
+
a usage error, and a schema/load failure is the standard error envelope at exit
|
|
169
|
+
`1`. The audit never touches the network, and no finding is derived from
|
|
170
|
+
`content/deployments.yaml` (the ledger is not a fact source) — it works on a
|
|
171
|
+
workspace that has neither a ledger nor a built artifact.
|
|
172
|
+
|
|
173
|
+
What it composes (`docs/SPEC.md` §7.1): `load`/schema, `reconciledDrift` (drift
|
|
174
|
+
plus the #238 variant-store reconciliation `validate` applies), `geoCheck` (its
|
|
175
|
+
`faq-coverage` premise gates the claim rows; the codes themselves map to no class),
|
|
176
|
+
the completeness `evidence` premise, and `fact-present`.
|
|
177
|
+
|
|
178
|
+
It is **deliberately stricter than `validate`/`check`** on the two FAIL classes:
|
|
179
|
+
D12 governs validity (drift facts warn, `validate` exits 0), while the audit's
|
|
180
|
+
contract is publishability and the issue freezes the class severities. A drift fact
|
|
181
|
+
in a FAIL class therefore exits 1 here and 0 there — declared in SPEC §7.1 and
|
|
182
|
+
pinned by a test. Two inputs are deliberately NOT findings, declared in §7.1:
|
|
183
|
+
`drift/url-not-in-identity` (the URL resolves — a canonicalization rule `validate`
|
|
184
|
+
warns about) and `geoCheck`'s coverage codes.
|
|
185
|
+
|
|
186
|
+
### The entity projection (`pasco entity`, #287)
|
|
187
|
+
|
|
188
|
+
`audit` diagnoses; `entity` is the artifact for a surface this engine does not
|
|
189
|
+
render (WordPress, Webflow, Next, a landing page). It exposes the **same** entity
|
|
190
|
+
the renderers embed — `core/src/structured-data.ts` is the one assembler, so there
|
|
191
|
+
is no CLI-local builder and no second truth (SPEC §7.2).
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
pasco entity --json # the frozen view: facts, @id, sameAs, gaps
|
|
195
|
+
pasco entity --jsonld # paste-ready <script type="application/ld+json"> (+ FAQPage)
|
|
196
|
+
pasco entity --jsonld --bare # the bare JSON object (node array when faq[] exists)
|
|
197
|
+
pasco entity --jsonld --out entity.html # write the block (refuses an existing file)
|
|
198
|
+
pasco entity --jsonld --out entity.html --force # replace it (atomic write)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"schemaVersion": "1",
|
|
204
|
+
"entity": { "id": null, "type": "Person", "name": "Elena Vasquez", "sameAs": ["https://github.com/elenavasquez"], "email": null },
|
|
205
|
+
"gaps": [
|
|
206
|
+
{ "code": "geo/entity-id", "detail": "no role: identity link — all 5 link(s) are role: realization (they indicate the entity, not are it). Mark the one that represents you role: identity to make the entity citable." }
|
|
207
|
+
],
|
|
208
|
+
"jsonld": null
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`entity.id` is the URL of the `role: identity` link (the first one wins) or
|
|
213
|
+
`null`: the engine **never invents an `@id`**. `gaps` are the `geoCheck` checks of
|
|
214
|
+
its structured-data section that scored below 1, with the code and detail
|
|
215
|
+
**verbatim** — no new vocabulary. `jsonld` is always `null` in `--json`; the
|
|
216
|
+
artifact has its own mode, and `faqJsonLd` carries the `FAQPage` node the same
|
|
217
|
+
mode emits when the workspace declares `faq[]` (`null` otherwise — never an empty
|
|
218
|
+
`FAQPage`, never an invented `@id`, `faq[].source` travels as
|
|
219
|
+
`acceptedAnswer.citation`). Parsing the `--jsonld` output yields exactly
|
|
220
|
+
`structuredData()`'s object (plus the `FAQPage` node when there is one) —
|
|
221
|
+
asserted by a test. `--bare` without `faq[]` stays byte-identical to the
|
|
222
|
+
pre-#360 artifact.
|
|
223
|
+
|
|
224
|
+
Both modes resolve **one** language (`--lang`, else `.pascorc` `defaults.lang`,
|
|
225
|
+
else `en`): the readiness gaps are measured in the same language the JSON-LD is
|
|
226
|
+
assembled, so a brand whose only populated `mission`/`tagline` is `es` cannot show
|
|
227
|
+
`description` as missing while the block emits it.
|
|
228
|
+
|
|
229
|
+
`--out` writes a temp file and renames it into place (atomic), and **refuses an
|
|
230
|
+
existing file** unless `--force` is passed — a mistyped path must not truncate the
|
|
231
|
+
SSOT. A write failure (existing file without `--force`, ENOENT, EACCES) is an error
|
|
232
|
+
envelope at exit `1`, never a stack.
|
|
233
|
+
|
|
234
|
+
Exit `0` when `@id` is present, `1` when it is absent (the entity has no declared
|
|
235
|
+
home; the JSON-LD is still emitted, without `@id`) or the `--out` write failed, `2`
|
|
236
|
+
for usage. It is offline and deterministic: no network, no clock, no AI.
|
|
237
|
+
|
|
238
|
+
### The citation matcher (`pasco cite-check`, #291)
|
|
239
|
+
|
|
240
|
+
Every GEO number is priced in "prompts monitored per month", and the half that
|
|
241
|
+
decides whether the number is true is one question: **did this answer actually
|
|
242
|
+
cite this entity?** The network half belongs to the runner; this command answers
|
|
243
|
+
the matching question over the answers it already saved. It is a pure function of
|
|
244
|
+
the SSOT and the input bytes — no network, no model, no clock (D19).
|
|
245
|
+
|
|
246
|
+
```sh
|
|
247
|
+
pasco cite-check answers.ndjson # one line per answer
|
|
248
|
+
cat answers.ndjson | pasco cite-check # stdin when no path is given
|
|
249
|
+
pasco cite-check answers.ndjson --json # the frozen per-answer contract
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Input — the runner's NDJSON, which this engine never produces:
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{ "engine": "chatgpt", "askedAt": "2026-10-23T15:00:00Z",
|
|
256
|
+
"question": "¿qué agencia GEO hay en Ecuador?",
|
|
257
|
+
"answer": "…", "citations": [ { "url": "https://…", "title": "…" } ] }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`answer` and `citations[]` are required; `engine`/`askedAt`/`question` are carried
|
|
261
|
+
through. A blank line is ignored; any other malformed line is exit `2` naming the
|
|
262
|
+
line and field — **never a silent skip**, because a skipped line changes a rate.
|
|
263
|
+
|
|
264
|
+
The ladder is fixed, and each rung carries its own confidence:
|
|
265
|
+
|
|
266
|
+
| Rung | What matched | Confidence |
|
|
267
|
+
|---|---|---|
|
|
268
|
+
| `citation-url` | a saved citation on the entity's own page or its `sameAs` set — host, and the declared path or a descendant (`github.com/<you>` does not match `github.com/someone-else`) | **high** |
|
|
269
|
+
| `exact-name` | the full `identity.name`, word-bounded, case- and accent-insensitive (`Domínguez` = `Dominguez`) | **high** |
|
|
270
|
+
| `alias` | `identity.orgName` | **medium** |
|
|
271
|
+
| `handle` | `identity.handle` or a `socialHandles[]` row with `status: claimed` | **medium** |
|
|
272
|
+
| `partial` | **any** contiguous multi-token prefix of the name (`DA SEO` and `DA SEO Digital` of `DA SEO Digital Studio`) — a term a higher rung already claims is not repeated | **low** |
|
|
273
|
+
|
|
274
|
+
`confidence: "low"` is **never** a citation in a downstream report — the field
|
|
275
|
+
exists so that refusal is explicit. The matcher also refuses to guess: a one-word
|
|
276
|
+
name whose sentence has no declared disambiguator (`ambiguous-term:pasco` +
|
|
277
|
+
`no-qualifier`, with the longer capitalised reading the text carries named as
|
|
278
|
+
`competing:Pasco County School District`), a mention after a contrast marker
|
|
279
|
+
(`contrast-context`), a shorter declared term inside a longer one
|
|
280
|
+
(`longer-name:…`) and a citation URL the parser rejects (`url-invalid:<url>`, a
|
|
281
|
+
missing scheme being the common case — named, never dropped) are all reported in
|
|
282
|
+
`reasons[]` as **not** citations.
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"schemaVersion": "1",
|
|
287
|
+
"entity": { "name": "Pasco", "aliases": [], "handles": ["pasco_cli"],
|
|
288
|
+
"partials": [], "hosts": ["github.com", "linkedin.com", "x.com"] },
|
|
289
|
+
"answers": [
|
|
290
|
+
{ "line": 1, "engine": "chatgpt", "askedAt": "…", "question": "…",
|
|
291
|
+
"cited": true, "confidence": "high",
|
|
292
|
+
"spans": [ { "start": 0, "end": 5, "kind": "name" } ],
|
|
293
|
+
"reasons": ["exact-name"] }
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
One row per input line, in input order, **never aggregated**: no rate, no score,
|
|
299
|
+
no total — that belongs to the report layer. A citation span is
|
|
300
|
+
`{ start: 0, end: 0, kind: "citation-url", url }` because the URL is in the
|
|
301
|
+
answer's citation list, not in its text; text spans are offsets into the original
|
|
302
|
+
answer. `entity` is the declared vocabulary the matcher resolved, so a report can
|
|
303
|
+
say what it matched without re-reading the workspace.
|
|
304
|
+
|
|
305
|
+
Exit `0` on a successful read whatever the matches (a non-citation is data) · `1`
|
|
306
|
+
when the workspace or the input path cannot be read · `2` usage, or malformed
|
|
307
|
+
NDJSON. Contract: SPEC §7.4.
|
|
308
|
+
|
|
309
|
+
### The AI policy artifacts (`pasco export ai-policy`, #290)
|
|
310
|
+
|
|
311
|
+
A profile publishes into a web where two kinds of machine reader arrive: the ones
|
|
312
|
+
whose output is a **citation** (search and answer indexing, a fetch a user asked
|
|
313
|
+
for) and the ones collecting material for **foundation-model training**. The
|
|
314
|
+
project's stance (SPEC §12 M9) is that the second kind does not get the content
|
|
315
|
+
unless the owner opts in — and until this command there was no file that said so.
|
|
316
|
+
One command now emits all three, from the workspace and the platform registry:
|
|
317
|
+
|
|
318
|
+
```sh
|
|
319
|
+
pasco export ai-policy --out ./public # robots.txt + llms.txt + AI-POLICY.md
|
|
320
|
+
pasco export ai-policy --check # exit 1 when the UA list is stale or unsourced
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
- **`robots.txt`** carries two *named* groups and never a wildcard: `ai-retrieval`
|
|
324
|
+
is `Allow: /`, `ai-training` is `Disallow: /`. Every `User-agent` line is
|
|
325
|
+
preceded by a comment naming its registry entry, operator, source URL and the
|
|
326
|
+
date that source was read, so a reader can tell **why** a crawler is in a group.
|
|
327
|
+
Because there is no `User-agent: *` group, the file restricts exactly the tokens
|
|
328
|
+
it lists: a crawler it does not name is **not restricted by it** (RFC 9309
|
|
329
|
+
§2.2.1). The artifacts **say that out loud** instead of claiming a default — the
|
|
330
|
+
list covers the crawlers whose classification has been checked against the
|
|
331
|
+
operator's own documentation, not the web.
|
|
332
|
+
- **`llms.txt`** is the workspace index in the shape the project site already
|
|
333
|
+
uses: name, one-line definition, the canonical identity URL, the `faq[]`
|
|
334
|
+
answers, the citable `publications[]`, and a dated stamp. The builder asserts
|
|
335
|
+
that **every document it claims to index appears in the text** — a document
|
|
336
|
+
missing from the index is a silent hole, not a cosmetic bug.
|
|
337
|
+
- **`AI-POLICY.md`** is the statement you send when a client asks "what is your AI
|
|
338
|
+
policy": what is allowed, what is blocked, who can change it, the date, and the
|
|
339
|
+
per-crawler table with each operator's own documentation URL.
|
|
340
|
+
|
|
341
|
+
The crawler list is **derived data**: it lives in the `ai:` block of the platform
|
|
342
|
+
registry, `pnpm gen:platforms` projects it into the engine, and the generator
|
|
343
|
+
refuses an entry with no `source` or no `as_of`. `--check` fails an unsourced
|
|
344
|
+
entry or a source older than **180 days**, and it gates the policy's own
|
|
345
|
+
`as_of` (real date, inside the window, not older than the newest row it seals) —
|
|
346
|
+
and `--out` runs the same check first, so a stale list becomes a failing command
|
|
347
|
+
instead of a file that rots. Tokens compare **case-insensitively** (RFC 9309
|
|
348
|
+
§2.2.1), so `GPTBot` and `gptbot` cannot slip in as two groups. No clock is
|
|
349
|
+
read when writing: the stamp is the registry's review date, so two runs on
|
|
350
|
+
different days produce byte-identical files.
|
|
351
|
+
|
|
352
|
+
`--out` never truncates a file it did not generate: an existing `robots.txt` that
|
|
353
|
+
is not ours needs `--force`. A refused write or a failed `--check` is exit `1`, a
|
|
354
|
+
usage error is exit `2`. The `--json` contract is frozen in SPEC §7.3.
|
|
355
|
+
|
|
356
|
+
### The normalized profile (`pasco export profile`, #348)
|
|
357
|
+
|
|
358
|
+
`build` renders platform texts and `validate` checks the workspace, but neither
|
|
359
|
+
hands a program to the **profile itself**. A site build, a service or a script
|
|
360
|
+
that needs the facts used to re-implement `load()` + `normalize()` — and each copy
|
|
361
|
+
drifted (roots hardcoded to `[]`, `publications[].url` dropped, `experience[].org`
|
|
362
|
+
mistaken for a langMap). `export profile` is that object, assembled by the same
|
|
363
|
+
`load()` every other command uses:
|
|
364
|
+
|
|
365
|
+
```sh
|
|
366
|
+
pasco export profile --json # the payload
|
|
367
|
+
pasco export profile --json --now 2026-10-28T00:00:00Z | jq . # pin the stamp
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
```json
|
|
371
|
+
{
|
|
372
|
+
"schemaVersion": "1",
|
|
373
|
+
"engine": { "name": "@profile-as-code/cli", "version": "<cli version>" },
|
|
374
|
+
"source": { "commit": "<workspace HEAD or null>", "generatedAt": null },
|
|
375
|
+
"profile": { "identity": {}, "…": "the normalized profile (see below)" }
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
- `schemaVersion` is the payload's frozen version — the same key §7.1/§7.2 use: a
|
|
380
|
+
consumer **refuses a version it does not know** instead of guessing.
|
|
381
|
+
- `source.commit` is the workspace's git `HEAD` — the value a consumer pins for
|
|
382
|
+
freshness — or `null` when the workspace is not inside a git repository.
|
|
383
|
+
- `source.generatedAt` is **`null` unless you pass `--now <iso8601>`**, which is
|
|
384
|
+
emitted verbatim after a real-calendar check. The command reads no clock, so two
|
|
385
|
+
runs over one workspace are byte-identical (a malformed or impossible `--now` —
|
|
386
|
+
`2026-02-31` — is exit `1`, never a silent "now").
|
|
387
|
+
- `profile` is the normalized `load()` object: **every root the workspace declares,
|
|
388
|
+
plus every list root defaulted to `[]`**. An optional object root (`newsletter`,
|
|
389
|
+
`podcast`, `audiences`, `active`) appears only when declared, because the
|
|
390
|
+
contract has no valid empty value for it. `artifactRefs` stay ids and resolve
|
|
391
|
+
against `profile.artifacts[]` (`load()` already refuses a dangling ref),
|
|
392
|
+
`publications[].url` is emitted as declared (never replaced by a DOI link), and
|
|
393
|
+
`deployments` is included. It is the SSOT — **not** a rendered surface, so
|
|
394
|
+
`visibility` is not filtered (renderers own §18.5) and there is no platform
|
|
395
|
+
slice (that is `pasco build`, via `compile()`).
|
|
396
|
+
|
|
397
|
+
Exit `0` ok · `1` the workspace fails to load or `--now` is malformed · `2` usage
|
|
398
|
+
(`--out`/`--check`/`--force`/`--lang` belong to `export ai-policy`). The contract
|
|
399
|
+
is frozen in SPEC §7.5.
|
|
400
|
+
|
|
401
|
+
**A consumer that only needs to validate** does not need this package at all: the
|
|
402
|
+
released schema is published as a tag-pinned URL + sha256 in
|
|
403
|
+
[`schema/consumer-contract.json`](https://github.com/ChristianLuciani/profile-as-code/blob/main/schema/consumer-contract.json).
|
|
404
|
+
The manifest's `current` block is **not** a published artifact: schema v1.15 has
|
|
405
|
+
no release tag yet (`published: false`, `artifact: null`), and the only released
|
|
406
|
+
artifact is schema **v1.13 at tag v0.3.6**, which **rejects `publication.kind`**
|
|
407
|
+
(`additionalProperties: false`) and has no `releases[]`. A consumer that needs
|
|
408
|
+
either must use a v1.15 checkout (verify its sha256) until the next release. The reading guide is
|
|
409
|
+
[docs/SCHEMA-CONTRACT.md](https://github.com/ChristianLuciani/profile-as-code/blob/main/docs/SCHEMA-CONTRACT.md) —
|
|
410
|
+
including what structural validation does **not** cover (the cross-file
|
|
411
|
+
invariants core re-checks, engine behavior, `.pascorc` and this payload's
|
|
412
|
+
envelope).
|
|
413
|
+
|
|
414
|
+
### The local view (`pasco preview`, #282)
|
|
415
|
+
|
|
416
|
+
`profile-data` is private because its data is personal, so there is no way to
|
|
417
|
+
*see* the result without publishing something — and a private fork with Pages on
|
|
418
|
+
publishes the data by default (personal plans have no access control). What was
|
|
419
|
+
missing is a **view**, not hosting:
|
|
420
|
+
|
|
421
|
+
```sh
|
|
422
|
+
pasco preview # <workspace>/.pasco/preview/index.html, then opened
|
|
423
|
+
pasco preview --no-open # write it, open nothing
|
|
424
|
+
pasco preview --out ./dist # write ./dist/index.html, open nothing (CI)
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**One file, two transports.** The artifact is the contract: one HTML document,
|
|
428
|
+
inline CSS, no `<script>`, no external asset (no font, no CDN, no badge fetch),
|
|
429
|
+
no HTTP server — it opens from `file://`, so nothing is published. A single file
|
|
430
|
+
has no base URL to resolve, which is why there are no per-platform or
|
|
431
|
+
per-language pages: one page with sections and anchors (`#github-readme`,
|
|
432
|
+
`#linkedin`, …). A remote image in an artifact (a shields.io badge) is rendered
|
|
433
|
+
as a text note instead of being fetched; workspace media is inlined as `data:`
|
|
434
|
+
URIs. The header reads `generated <date> from <N> facts` — freshness, not a
|
|
435
|
+
version — and `--now <iso8601>` pins that stamp so two runs are byte-identical.
|
|
436
|
+
|
|
437
|
+
**What it composes.** The text per platform comes from the same decision tree
|
|
438
|
+
`build` uses (active variant → renderer → shared paste source, with `build`'s
|
|
439
|
+
selection gate), and the checks come from the same report `pasco check` prints.
|
|
440
|
+
`preview` never touches `profiles/`; its default destination is
|
|
441
|
+
`<workspace>/.pasco/preview/`, untracked as long as the workspace ignores
|
|
442
|
+
`.pasco/` — a destination inside a git work tree that does not gets a warning
|
|
443
|
+
that also travels in the page's own notes.
|
|
444
|
+
|
|
445
|
+
**The privacy gate.** The public site publishes public entries only; the preview
|
|
446
|
+
carries your working data, so it **refuses** and writes **no file**:
|
|
447
|
+
|
|
448
|
+
| Rule | Refuses |
|
|
449
|
+
|---|---|
|
|
450
|
+
| `absolute-path` | a machine path (`/Users/…`, `C:\…`, `file:///…`) in the document |
|
|
451
|
+
| `credential` | a pasted token or private key (token shapes, `secret: <16+ non-space>` assignments) |
|
|
452
|
+
| `external-asset` | a `<link>`, a remote `src`, `url(https:…)`, `@import` |
|
|
453
|
+
| `active-content` | a `<script>`, an `on*=` handler, an `<iframe>`, a `javascript:` URL |
|
|
454
|
+
| `source-outside-declared-roots` | a source that resolves outside the workspace (a symlink escape) |
|
|
455
|
+
| `source-missing` · `credential-file` | a declared source that is gone; a dotfile-shaped asset (`.env`, `.netrc`, `id_rsa`, `*.pem`) |
|
|
456
|
+
|
|
457
|
+
Evidence-root media is never inlined (a per-machine path never reaches a render);
|
|
458
|
+
an oversized image is left out and listed, never silently dropped. The gate's
|
|
459
|
+
own refusal classes are exercised by the package test suite.
|
|
460
|
+
|
|
461
|
+
**Limits, stated.** `--out` into a git work tree without an ignore entry warns —
|
|
462
|
+
a preview committed by accident is the leak the command exists to avoid. The
|
|
463
|
+
preview does not edit, does not encrypt (that breaks the no-JS invariant), and
|
|
464
|
+
does not enforce: it reports the check state, `pasco check` remains the gate. The
|
|
465
|
+
CI-artifact transport is a workflow in your workspace template, `workflow_dispatch`
|
|
466
|
+
only, with no Pages; the artifact is readable by anyone with repository access.
|
|
95
467
|
|
|
96
468
|
### Contract paths (`pasco edit`, epic #247 — read · preview · write)
|
|
97
469
|
|
|
@@ -317,11 +689,22 @@ pasco status # …also prints the derived publication-t
|
|
|
317
689
|
pasco refine linkedin --variants 3 # emits the agent task template (full flow M3)
|
|
318
690
|
pasco refine linkedin --choose li-en-2 # records the chosen variant in content/active.yaml
|
|
319
691
|
pasco check github-readme
|
|
692
|
+
pasco audit # findings only: dangling-ref · contradiction · unsourced-claim
|
|
693
|
+
pasco audit --json # { schemaVersion, findings } — exit 1 iff any FAIL
|
|
694
|
+
pasco entity --json # entity facts + @id + sameAs + gaps (exit 1 without @id)
|
|
695
|
+
pasco entity --jsonld # paste-ready <script type="application/ld+json"> (+ FAQPage)
|
|
696
|
+
pasco cite-check answers.ndjson # per answer: cited? high/medium/low + why
|
|
697
|
+
pasco cite-check answers.ndjson --json # { schemaVersion, entity, answers[] }
|
|
320
698
|
pasco post seed demo --text seed.md # week-1 experiment: posts/<seed>/seed.md
|
|
699
|
+
pasco post seed essay --text essay.md --format longform # essay/newsletter seed
|
|
321
700
|
pasco post task demo --channel linkedin,x # task bundles (no LLM call)
|
|
701
|
+
pasco post task essay --format longform --channel medium,substack
|
|
702
|
+
pasco post task demo-x --format short --from essay --channel x # derived family
|
|
322
703
|
pasco post choose demo --channel linkedin --variant linkedin-1
|
|
323
704
|
pasco post check demo # limits + URLs + {{fact:...}} refs
|
|
324
|
-
pasco post export demo --target postiz #
|
|
705
|
+
pasco post export demo --target postiz # Postiz payload, or a paste checklist
|
|
706
|
+
pasco export ai-policy --out ./public # robots.txt + llms.txt + AI-POLICY.md (M9)
|
|
707
|
+
pasco export ai-policy --check # exit 1 on an unsourced or >180-day UA list
|
|
325
708
|
pasco skills:export --list # skill channels shipped with this package
|
|
326
709
|
pasco skills:export ./pasco-skills # copy them to a directory you choose
|
|
327
710
|
```
|
|
@@ -336,6 +719,7 @@ file) > current directory.
|
|
|
336
719
|
```json
|
|
337
720
|
{
|
|
338
721
|
"workspace": ".",
|
|
722
|
+
"engine": "0.4.0",
|
|
339
723
|
"defaults": { "lang": "en", "variants": 3 },
|
|
340
724
|
"diff": { "tagPattern": "release-*" },
|
|
341
725
|
"completeness": {
|
|
@@ -349,6 +733,18 @@ file) > current directory.
|
|
|
349
733
|
}
|
|
350
734
|
```
|
|
351
735
|
|
|
736
|
+
The `engine` field is the **minimum** CLI version this workspace requires
|
|
737
|
+
(#348, SPEC §7.6), written as exactly `MAJOR.MINOR.PATCH` (a lower bound, not a
|
|
738
|
+
semver range). `validate`, `build`, `export profile` and `preview` check it before doing
|
|
739
|
+
any work: an older install exits `1` with the required version, the installed
|
|
740
|
+
version and the upgrade command; a malformed value exits `1` with the accepted
|
|
741
|
+
syntax. Absent means no requirement. Every `.pascorc` the resolution consulted is
|
|
742
|
+
read — with no `--workspace` flag, the cwd `.pascorc` that resolved the workspace
|
|
743
|
+
AND the workspace's own file — and the **stricter** declaration wins; a malformed
|
|
744
|
+
one in either file is refused. `pasco doctor` reports the same effective
|
|
745
|
+
requirement in one line (with the file it came from); it does not replace the
|
|
746
|
+
gate, and it exits `1` when the workspace is unhealthy.
|
|
747
|
+
|
|
352
748
|
The `completeness` block is the policy for `pasco status --completeness` (issue
|
|
353
749
|
#102): `target` names the milestone; `langs` sets which languages text coverage is
|
|
354
750
|
measured in; `items` overrides the shipped weights per catalog id (a weight of `0`
|
|
@@ -361,7 +757,7 @@ written to the workspace; only this policy is stored.
|
|
|
361
757
|
| Code | Meaning |
|
|
362
758
|
|---|---|
|
|
363
759
|
| `0` | OK — schema valid. Drift warnings still exit 0 (D12: facts warn, schema fails). |
|
|
364
|
-
| `1` | Schema or operational failure (workspace not found, bad YAML, not a git repo for `diff`), **or a data finding that blocks the command** (`edit`: the address is not exactly one row, or the edit does not change the published artifact). |
|
|
760
|
+
| `1` | Schema or operational failure (workspace not found, bad YAML, not a git repo for `diff`), **an unmet or malformed `.pascorc` `engine` requirement** (`validate` · `build` · `export profile` · `preview`, before any work), **or a data finding that blocks the command** (`edit`: the address is not exactly one row, or the edit does not change the published artifact; `audit`: at least one `FAIL` finding; `export ai-policy`: an unsourced or stale UA list, or a refused write; `preview`: the privacy gate refused a document, or the write failed). |
|
|
365
761
|
| `2` | Usage error (unknown command/platform/lang, bad flags). |
|
|
366
762
|
|
|
367
763
|
Human output is EN by default (D8/§11); `--json` prints a machine-readable
|