@profile-as-code/cli 0.3.0 → 0.3.5

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/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync6 } from "fs";
4
+ import { readFileSync as readFileSync10 } from "fs";
5
5
  import { parseArgs } from "util";
6
6
 
7
7
  // src/commands/build.ts
8
- import { copyFileSync, mkdirSync, rmSync, writeFileSync } from "fs";
9
- import { dirname as dirname2, join as join4 } from "path";
8
+ import { copyFileSync, existsSync as existsSync5, mkdirSync, rmSync, writeFileSync } from "fs";
9
+ import { dirname as dirname2, join as join6 } from "path";
10
10
 
11
11
  // ../core/dist/ledger.js
12
12
  import { createHash } from "crypto";
@@ -182,6 +182,7 @@ function textOf(v) {
182
182
  return typeof v === "string" ? v : v.en ?? "";
183
183
  }
184
184
  var PLATFORMS = PLATFORM_SLUGS;
185
+ var LANGS = ["en", "es", "fr"];
185
186
 
186
187
  // ../core/dist/ledger.js
187
188
  var LEDGER_META = /* @__PURE__ */ new Set(["id", "origin"]);
@@ -212,6 +213,86 @@ function hashUnit(unit) {
212
213
  }
213
214
  var LEDGER_KEY_PATTERN = /^(identity|positioning|keywords)$|^(bios|headlines|skills|socialHandles|experience|projects|publications|education|press):.+$/;
214
215
  var ROW_BLOCKS = ["experience", "projects", "publications", "education", "press"];
216
+ function reasonField(reason) {
217
+ return reason === void 0 ? {} : { reason };
218
+ }
219
+ function declaredReason(capability) {
220
+ if (capability === void 0 || capability.extractor === void 0)
221
+ return "no-extractor";
222
+ if (capability.rung === "walled")
223
+ return "walled";
224
+ if (capability.rung === "walled-con-api")
225
+ return "walled-con-api";
226
+ if (capability.artifactUrl === null || capability.artifactUrl === void 0) {
227
+ return "no-artifact-url";
228
+ }
229
+ return void 0;
230
+ }
231
+ function certifiedSurface(record) {
232
+ return record === void 0 ? { state: "attested" } : { ...record };
233
+ }
234
+ function certificationRollup(surfaces) {
235
+ const entries = Object.entries(surfaces).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
236
+ if (entries.length === 0)
237
+ return { state: "attested" };
238
+ if (entries.every(([, c]) => c.state === "verified"))
239
+ return { state: "verified" };
240
+ if (entries.every(([, c]) => c.state === "verified" || c.state === "attested")) {
241
+ return { state: "attested" };
242
+ }
243
+ const drift = entries.find(([, c]) => c.state === "out-of-band-drift");
244
+ const lead = drift ?? entries.find(([, c]) => c.state === "unverifiable");
245
+ return { state: lead?.[1].state ?? "unverifiable", ...reasonField(lead?.[1].reason) };
246
+ }
247
+ function certificationVerdict(record, live) {
248
+ if (live === null)
249
+ return { state: "unverifiable" };
250
+ if (live.status < 200 || live.status >= 300)
251
+ return { state: "unverifiable" };
252
+ const anchor = record.certification?.artifactHash;
253
+ if (anchor === void 0)
254
+ return { state: "unverifiable", reason: "unattributed" };
255
+ if (live.digest === void 0)
256
+ return { state: "unverifiable", reason: "unattributed" };
257
+ return live.digest === anchor ? { state: "verified" } : { state: "out-of-band-drift", reason: "live-mismatch" };
258
+ }
259
+ function attemptRecord(verdict, evidence) {
260
+ if (verdict.state === "verified") {
261
+ return {
262
+ state: "verified",
263
+ ...evidence.artifactHash !== void 0 ? { artifactHash: evidence.artifactHash } : {},
264
+ method: "fetch",
265
+ ...evidence.fetchedAt !== void 0 ? { fetchedAt: evidence.fetchedAt } : {},
266
+ ...evidence.liveHash !== void 0 ? { liveHash: evidence.liveHash } : {},
267
+ ...evidence.evidenceUrl !== void 0 ? { evidenceUrl: evidence.evidenceUrl } : {}
268
+ };
269
+ }
270
+ if (verdict.state === "out-of-band-drift") {
271
+ return {
272
+ state: "out-of-band-drift",
273
+ reason: verdict.reason ?? "live-mismatch",
274
+ ...evidence.artifactHash !== void 0 ? { artifactHash: evidence.artifactHash } : {},
275
+ ...evidence.liveHash !== void 0 ? { liveHash: evidence.liveHash } : {},
276
+ method: "fetch",
277
+ ...evidence.fetchedAt !== void 0 ? { fetchedAt: evidence.fetchedAt } : {},
278
+ ...evidence.evidenceUrl !== void 0 ? { evidenceUrl: evidence.evidenceUrl } : {}
279
+ };
280
+ }
281
+ return {
282
+ state: "unverifiable",
283
+ reason: verdict.reason ?? "network-error",
284
+ ...evidence.artifactHash !== void 0 ? { artifactHash: evidence.artifactHash } : {},
285
+ ...evidence.fetchedAt !== void 0 || evidence.evidenceUrl !== void 0 ? { method: "fetch" } : {},
286
+ ...evidence.fetchedAt !== void 0 ? { fetchedAt: evidence.fetchedAt } : {},
287
+ ...evidence.evidenceUrl !== void 0 ? { evidenceUrl: evidence.evidenceUrl } : {}
288
+ };
289
+ }
290
+ function attestedRecord(anchor) {
291
+ return { state: "attested", ...anchor !== void 0 ? { artifactHash: anchor } : {} };
292
+ }
293
+ function fetchFailureReason(status) {
294
+ return status === 0 ? "network-error" : `http-${status}`;
295
+ }
215
296
  var ROW_CONFIG = {
216
297
  experience: {
217
298
  block: "experience",
@@ -289,6 +370,7 @@ function deployStatus(profile) {
289
370
  const statuses = units.map((unit) => {
290
371
  const records = ledger[unit.key] ?? {};
291
372
  const surfaces = {};
373
+ const certification = {};
292
374
  let stale = false;
293
375
  let deployed = 0;
294
376
  for (const [surface, record] of Object.entries(records)) {
@@ -297,11 +379,14 @@ function deployStatus(profile) {
297
379
  surfaces[surface] = inSync ? "in-sync" : "stale";
298
380
  if (!inSync)
299
381
  stale = true;
382
+ certification[surface] = certifiedSurface(record.certification);
300
383
  }
384
+ const rollup = certificationRollup(certification);
301
385
  return {
302
386
  ...unit,
303
387
  surfaces,
304
- state: stale ? "stale" : deployed > 0 ? "in-sync" : "never"
388
+ state: stale ? "stale" : deployed > 0 ? "in-sync" : "never",
389
+ certification: deployed === 0 ? null : { state: rollup.state, surfaces: certification, ...reasonField(rollup.reason) }
305
390
  };
306
391
  });
307
392
  return { units: statuses, untracked: untrackedRows(profile), removed };
@@ -598,7 +683,7 @@ import { parseDocument } from "yaml";
598
683
 
599
684
  // ../core/dist/generated/schema.zod.js
600
685
  import { z } from "zod";
601
- var profileSchema = z.object({ "identity": z.object({ "name": z.string().min(1), "email": z.string().regex(new RegExp(`^[^@\\s"']+@[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+$`)).describe("v1.5 (additive, issue #71): contact email of the person/brand (schema.org `email` on Person/Organization). A fact like name/location \u2014 NOT a link: renderers emit it as a mailto: href in the Links section and as the JSON-LD `email` property; `sameAs` stays page-URLs-only. Lives OUTSIDE identity.links[] by design \u2014 the ^https?:// rule of $defs/url (issue #53) stays strict on every URL field, so a mailto: row in links[] fails validation (D12). RFC-5322-adjacent (issue #71 review): local part lax (no whitespace/quotes), domain hostname-shaped (labels [A-Za-z0-9-], no empty/trailing/consecutive dots \u2014 unicode domains must use punycode).").optional(), "handle": z.string().describe("v1.1: primary handle (e.g. X @cluciani_ve).").optional(), "timezone": z.string().describe("v1.1: IANA timezone (e.g. America/Guayaquil).").optional(), "orgName": z.string().min(1).describe("v1.1 (additive, brand): legal/registered org name when it differs from the display name (name = display, e.g. 'Topia Press').").optional(), "logo": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "brandVoice": z.object({ "tone": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "values": z.array(z.string()).optional(), "avoid": z.array(z.string()).optional() }).strict().describe("v1.1 (additive, brand): tone matrix seed \u2014 overall tone (langMap), core values, words/tones to avoid. Per-platform voice refinement lives in the platform skill (\xA717).").optional(), "mission": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "tagline": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "established": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01).").optional(), "location": z.string().optional(), "photo": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "links": z.array(z.object({ "platform": z.string().optional(), "label": z.string().optional(), "role": z.enum(["identity", "realization", "aggregator"]).describe("v1.6 (#103): the ROLE this link plays with respect to the entity \u2014 a translation layer of this engine onto schema.org, never standard vocabulary. `identity` \u2192 schema.org:url (the page IS the entity; strong authority; the one eligible for JSON-LD @id). `realization` (DEFAULT when absent) \u2192 schema.org:sameAs (the page indirectly indicates the entity; weak authority; these \u2014 and only these \u2014 form the sameAs set). `aggregator` \u2192 schema.org:mainEntityOfPage (link-in-bio / redirector that aggregates references to the entity; medium authority; never eligible for @id). Absence keeps every pre-v1.6 workspace VALIDATING unchanged (additive, dev-methodology \xA73) \u2014 it does NOT keep the output byte-identical: v1.6 also makes github-readme emit jobTitle/worksFor, narrows sameAs to realization links (+ claimed handles), and adds the geo/entity-id gap report.").optional(), "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.") }).strict()).describe("Canonical link set. Renderers and skills read URLs ONLY from here \u2014 no per-platform URL duplication (drift source). v1.6 (#103): items carry an optional `role` distinguishing the entity's own page (schema.org:url) from pages that merely indicate it (schema.org:sameAs) or aggregate references to it (schema.org:mainEntityOfPage).").optional() }).strict(), "flavor": z.enum(["person", "brand"]).describe("v1.1 (additive): workspace flavor (D15). person (default, absent = person) or brand. Brand enables the org identity variant, the socialHandles/newsletter/podcast/press/audiences blocks and Organization GEO (\xA717).").optional(), "socialHandles": z.array(z.object({ "platform": z.enum(["github-readme", "linkedin", "upwork", "x", "researchgate", "substack", "patreon", "github-org", "bluesky", "instagram", "tiktok", "youtube", "spotify", "reddit", "mastodon", "soundcloud", "facebook", "linkedin-page", "instagram-standalone", "warpcast", "lemmy", "discord", "slack", "skool", "whop", "pinterest", "dribbble", "medium", "devto", "hashnode", "wordpress", "gmb", "listmonk", "twitch", "threads", "telegram", "nostr", "vk", "kick", "moltbook", "ghost", "mewe", "tumblr", "kofi", "buymeacoffee", "liberapay", "gumroad", "payhip", "hotmart"]).describe("Platform set (ONE source, dedup). v1: person surfaces. v1.1 (additive, brand \xA717): brand surfaces + the socialHandles matrix key space. v1.2 (additive, D16): Postiz parity + operator extras \u2014 43 platforms; new slugs Postiz-aligned verbatim (linkedin-page, instagram-standalone, devto, gmb, warpcast, moltbook, mewe, tumblr); ghost is an operator extra Postiz lacks (docs.postiz.com llms.txt: 34 providers, fetched 2026-09-02). v1.7 (issue #130): kofi \u2014 Ko-fi creator page, operator extra #10, 44 total; creator-monetization family, paste flow (D9). v1.8 (issue #133): buymeacoffee, liberapay, gumroad, payhip \u2014 creator monetization platforms (D16 family), operator extras #11\u2013#14, 48 total; paste flow (D9). v1.9 (issue #137): hotmart \u2014 Hotmart digital-products storefront + memberships (Members Area / recurring billing), operator extra #15, 49 total; creator-monetization family, paste flow (D9)."), "handle": z.string().min(1), "status": z.enum(["claimed", "pending"]), "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "note": z.string().optional() }).strict().describe("v1.1 (additive, brand): one row of the brand handle matrix (platform \u2192 handle + claim status). Seed source: the operator's brand-handles availability sweep (\xA717).")).describe("v1.1 (additive, brand): the brand handle matrix \u2014 one row per platform with claim status. Seeded from the operator's brand-handles availability sweep (\xA717).").optional(), "newsletter": z.object({ "platform": z.enum(["substack", "ghost"]).describe("v1.1 (additive, brand): newsletter hosts the engine models (Substack now, Ghost later)."), "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole."), "name": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "description": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "cadence": z.string().optional() }).strict().describe("v1.1 (additive, brand): the brand's newsletter surface. The name/description double as the GEO-visible text (RSS/LLM-readable).").optional(), "podcast": z.object({ "title": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())), "platform": z.enum(["spotify", "rss"]).describe("v1.1 (additive, brand): podcast distribution modeled in v1.1 (Spotify for Creators / standalone RSS).").optional(), "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "rss": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "description": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "status": z.enum(["planned", "live"]).optional() }).strict().describe("v1.1 (additive, brand): the brand's podcast \u2014 show metadata + RSS (GEO-citable via RSS).").optional(), "press": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional(), "origin": z.literal("template").describe("v1.3 (D18, \xA719.2): provenance \u2014 `template` marks the fictional examples shipped by profile-data-template. Absent = user-authored.").optional(), "outlet": z.union([z.string().min(1), langMapKeys(z.object({ "en": z.string() }).catchall(z.string()))]), "title": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())), "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole."), "date": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01)."), "artifactRefs": z.array(z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.")).describe("v1.4 (D17/\xA718.1): references `artifacts[].id` \u2014 the work behind this mention. press keeps its domain (no duplication); targets resolved by core (load fails on a dangling ref).").optional() }).strict().describe("v1.1 (additive, brand): a media mention \u2014 outlet, headline, resolvable URL, date (verifiable claims, \xA78).")).describe("v1.1 (additive, brand): media mentions of the brand (verifiable, dated).").optional(), "audiences": z.record(z.string(), langMapKeys(z.object({ "en": z.string() }).catchall(z.string()))).describe("v1.1 (additive, brand): brand audiences \u2014 reader profiles (not recruiters). Audience name \u2192 per-lang angle (same shape as positioning.audiences; brand-facing).").optional(), "positioning": z.object({ "statement": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "audiences": z.record(z.string(), langMapKeys(z.object({ "en": z.string() }).catchall(z.string()))).describe("Audience name \u2192 per-lang angle (e.g. recruiter / client / academia).").optional(), "geo": z.object({ "semanticAssociations": z.array(z.string()).min(3).max(5).describe("3-5 anchors an LLM should associate with the person (e.g. 'f\xEDsica', 'innovaci\xF3n educativa', 'soberan\xEDa de IA'). Seeded without stuffing (\xA78).").optional(), "citationsGoal": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional() }).strict().optional() }).strict().optional(), "bios": z.array(z.object({ "platform": z.enum(["github-readme", "linkedin", "upwork", "x", "researchgate", "substack", "patreon", "github-org", "bluesky", "instagram", "tiktok", "youtube", "spotify", "reddit", "mastodon", "soundcloud", "facebook", "linkedin-page", "instagram-standalone", "warpcast", "lemmy", "discord", "slack", "skool", "whop", "pinterest", "dribbble", "medium", "devto", "hashnode", "wordpress", "gmb", "listmonk", "twitch", "threads", "telegram", "nostr", "vk", "kick", "moltbook", "ghost", "mewe", "tumblr", "kofi", "buymeacoffee", "liberapay", "gumroad", "payhip", "hotmart"]).describe("Platform set (ONE source, dedup). v1: person surfaces. v1.1 (additive, brand \xA717): brand surfaces + the socialHandles matrix key space. v1.2 (additive, D16): Postiz parity + operator extras \u2014 43 platforms; new slugs Postiz-aligned verbatim (linkedin-page, instagram-standalone, devto, gmb, warpcast, moltbook, mewe, tumblr); ghost is an operator extra Postiz lacks (docs.postiz.com llms.txt: 34 providers, fetched 2026-09-02). v1.7 (issue #130): kofi \u2014 Ko-fi creator page, operator extra #10, 44 total; creator-monetization family, paste flow (D9). v1.8 (issue #133): buymeacoffee, liberapay, gumroad, payhip \u2014 creator monetization platforms (D16 family), operator extras #11\u2013#14, 48 total; paste flow (D9). v1.9 (issue #137): hotmart \u2014 Hotmart digital-products storefront + memberships (Members Area / recurring billing), operator extra #15, 49 total; creator-monetization family, paste flow (D9)."), "lang": z.enum(["en", "es", "fr"]).describe("Supported languages. EN required (international recruiters), ES native, FR desirable (D8)."), "length": z.enum(["short", "medium", "long"]), "text": z.string().min(1) }).strict()).describe("Curated bio per platform+lang+length. Generated by `pasco refine`, chosen by the user, stored here.").optional(), "headlines": z.array(z.object({ "platform": z.enum(["github-readme", "linkedin", "upwork", "x", "researchgate", "substack", "patreon", "github-org", "bluesky", "instagram", "tiktok", "youtube", "spotify", "reddit", "mastodon", "soundcloud", "facebook", "linkedin-page", "instagram-standalone", "warpcast", "lemmy", "discord", "slack", "skool", "whop", "pinterest", "dribbble", "medium", "devto", "hashnode", "wordpress", "gmb", "listmonk", "twitch", "threads", "telegram", "nostr", "vk", "kick", "moltbook", "ghost", "mewe", "tumblr", "kofi", "buymeacoffee", "liberapay", "gumroad", "payhip", "hotmart"]).describe("Platform set (ONE source, dedup). v1: person surfaces. v1.1 (additive, brand \xA717): brand surfaces + the socialHandles matrix key space. v1.2 (additive, D16): Postiz parity + operator extras \u2014 43 platforms; new slugs Postiz-aligned verbatim (linkedin-page, instagram-standalone, devto, gmb, warpcast, moltbook, mewe, tumblr); ghost is an operator extra Postiz lacks (docs.postiz.com llms.txt: 34 providers, fetched 2026-09-02). v1.7 (issue #130): kofi \u2014 Ko-fi creator page, operator extra #10, 44 total; creator-monetization family, paste flow (D9). v1.8 (issue #133): buymeacoffee, liberapay, gumroad, payhip \u2014 creator monetization platforms (D16 family), operator extras #11\u2013#14, 48 total; paste flow (D9). v1.9 (issue #137): hotmart \u2014 Hotmart digital-products storefront + memberships (Members Area / recurring billing), operator extra #15, 49 total; creator-monetization family, paste flow (D9)."), "lang": z.enum(["en", "es", "fr"]).describe("Supported languages. EN required (international recruiters), ES native, FR desirable (D8)."), "text": z.string().min(1) }).strict()).describe("Headline per platform+lang (the 1-line under the name).").optional(), "skills": z.array(z.object({ "name": z.string().min(1), "level": z.enum(["novice", "intermediate", "advanced", "expert"]).optional(), "keywords": z.array(z.string()).optional(), "geoKeywords": z.array(z.string()).describe("Terms a generative engine should match this skill with (GEO layer, D7).").optional(), "platformWeights": z.record(z.string(), z.number().gte(0).lte(1)).describe("How much this skill matters per platform (0..1). Feeds renderer keyword placement.").optional() }).strict()).optional(), "experience": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional(), "origin": z.literal("template").describe("v1.3 (D18, \xA719.2): provenance \u2014 `template` marks the fictional examples shipped by profile-data-template. Absent = user-authored.").optional(), "role": z.union([z.string().min(1), langMapKeys(z.object({ "en": z.string() }).catchall(z.string()))]).describe("v1: string. v1.1 (additive): langMap \u2014 bilingual role titles."), "org": z.string().min(1).describe("Proper noun \u2014 invariant across languages (e.g. 'USFQ', 'Nimbus Labs'). Not a langMap: institutions are never translated (SPEC \xA711)."), "start": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01)."), "end": z.union([z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01)."), z.literal("present")]).optional(), "highlights": z.union([langMapKeys(z.object({ "en": z.string() }).catchall(z.string())), langMapKeys(z.object({ "en": z.array(z.string()) }).catchall(z.array(z.string())))]).describe("v1: langMap of strings (separator-delimited). v1.1 (additive): langMap of string arrays (bullets).").optional() }).strict().describe("start <= end is NOT expressible in JSON Schema \u2014 enforced by core.driftCheck (\xA712 note).")).optional(), "education": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional(), "origin": z.literal("template").describe("v1.3 (D18, \xA719.2): provenance \u2014 `template` marks the fictional examples shipped by profile-data-template. Absent = user-authored.").optional(), "degree": z.union([z.string().min(1), langMapKeys(z.object({ "en": z.string() }).catchall(z.string()))]).describe("v1.1: degree title (string or bilingual langMap)."), "institution": z.string().min(1).describe("Proper noun \u2014 invariant across languages (e.g. 'Universidad Central de Venezuela'). Not a langMap: institutions are never translated (SPEC \xA711)."), "field": z.union([z.string(), langMapKeys(z.object({ "en": z.string() }).catchall(z.string()))]).describe("v1.1: field of study (string or bilingual langMap).").optional(), "start": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01)."), "end": z.union([z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01)."), z.literal("present")]).optional(), "note": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional() }).strict().describe("v1.1 (additive): formal education entry.")).describe("v1.1 (additive): formal education. v1 workspaces keep education inside experience[] with role = degree.").optional(), "languages": z.array(z.object({ "code": z.enum(["en", "es", "fr"]).describe("Supported languages. EN required (international recruiters), ES native, FR desirable (D8)."), "level": z.enum(["native", "fluent", "advanced", "intermediate", "basic"]) }).strict()).describe("v1.1 (additive): spoken languages. v1 workspaces document them as comments in skills.yaml.").optional(), "projects": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional(), "origin": z.literal("template").describe("v1.3 (D18, \xA719.2): provenance \u2014 `template` marks the fictional examples shipped by profile-data-template. Absent = user-authored.").optional(), "name": z.string().min(1), "status": z.enum(["active", "maintained", "archived", "experimental", "planning"]).optional(), "description": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "links": z.array(z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.")).optional(), "keywords": z.array(z.string()).optional(), "evidence": z.array(z.object({ "claim": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())), "source": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole."), "verified": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01).").optional(), "credentialId": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional(), "artifactId": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional() }).strict().describe("Verifiable claim with source. Anti-pattern: unverifiable claims (dropped by cite-aware systems, \xA78).")).optional() }).strict()).optional(), "publications": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional(), "origin": z.literal("template").describe("v1.3 (D18, \xA719.2): provenance \u2014 `template` marks the fictional examples shipped by profile-data-template. Absent = user-authored.").optional(), "title": z.string().min(1), "venue": z.string().min(1), "year": z.number().int().gte(1900).lte(2100), "authors": z.string().describe("v1.1: author string (e.g. 'Luciani, C.' or 'Zambrano et al.').").optional(), "doi": z.string().regex(new RegExp("^10\\.\\d{4,9}/\\S+$")).optional(), "orcidId": z.string().regex(new RegExp("^\\d{4}-\\d{4}-\\d{4}-\\d{3}[\\dX]$")).optional(), "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "artifactRefs": z.array(z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.")).describe("v1.4 (D17/\xA718.1): references `artifacts[].id` \u2014 the dataset/code behind this paper. publications keep their domain (no duplication); targets resolved by core (load fails on a dangling ref).").optional() }).strict().describe("DOI/ORCID must be resolvable \u2014 GEO citability anchor (LLMs resolve them).")).describe("Peer-reviewed / citable output. DOI/ORCID resolvable \u2192 GEO citability.").optional(), "keywords": z.object({ "seo": z.record(z.string(), z.array(z.string())).describe("Per-platform SEO keyword sets (fed by each platform skill's research).").optional(), "geo": z.object({ "associations": z.array(z.string()).optional(), "claims": z.array(z.string()).optional() }).strict().optional() }).strict().optional(), "checks": z.array(z.object({ "id": z.string().optional(), "platform": z.enum(["all", "github-readme", "linkedin", "upwork", "x", "researchgate", "substack", "patreon", "github-org", "bluesky", "instagram", "tiktok", "youtube", "spotify", "reddit", "mastodon", "soundcloud", "facebook", "linkedin-page", "instagram-standalone", "warpcast", "lemmy", "discord", "slack", "skool", "whop", "pinterest", "dribbble", "medium", "devto", "hashnode", "wordpress", "gmb", "listmonk", "twitch", "threads", "telegram", "nostr", "vk", "kick", "moltbook", "ghost", "mewe", "tumblr", "kofi", "buymeacoffee", "liberapay", "gumroad", "payhip", "hotmart"]).describe("Dedup contract: MUST equal ['all', ...$defs/platform.enum] \u2014 scripts/schema-check asserts it (D16)."), "rule": z.enum(["max-chars", "min-chars", "url-live", "fact-present", "geo-consistent", "lang-present", "link-in-identity", "handle-claimed", "handle-consistent"]).describe("Rule catalog (extensible). Skills' checks.yaml use the same catalog; `pasco check` runs the union. v1.1 (brand \xA717): handle-claimed (a claimed handle must resolve to a URL) and handle-consistent (handle matrix coherent across surfaces)."), "value": z.union([z.number(), z.string(), z.boolean(), z.array(z.string())]), "note": z.string().optional() }).strict()).describe("User-level declarative checks; unioned with the platform skill's checks.yaml by `pasco check`.").optional(), "active": z.record(z.string(), z.object({ "lang": z.enum(["en", "es", "fr"]).describe("Supported languages. EN required (international recruiters), ES native, FR desirable (D8)."), "variantId": z.string().min(1), "chosenAt": z.string().datetime({ offset: true }) }).strict().describe("The chosen variant id \u2014 user picked it (D11); variant body lives in the workspace variants store.")).describe("Which bio/headline variant is LIVE per platform (D11: user picks, engine records).").optional(), "deployments": z.record(z.string(), z.record(z.string(), z.object({ "deployedAt": z.string().datetime({ offset: true }), "contentHash": z.string().regex(new RegExp("^[a-f0-9]{64}$")), "source": z.enum(["manual", "git", "api"]) }).strict().describe("v1.3 (D18, \xA719.1): one confirmed deploy of a trackable unit on one surface. contentHash = SHA-256 of the canonical serialization (sorted keys, sans id/origin) of the unit AT deploy time \u2014 staleness is exact and release-tag-independent.")).describe("v1.3 (D18): surface \u2192 deploy record. Surface keys are the ONE platform enum (D16) \u2014 also core-checked post-Zod (gen-zod drops propertyNames).")).describe("v1.3 (D18, \xA719): per-entry deploy ledger \u2014 ledgerKey \u2192 surface \u2192 record. Keys are constrained above for JSON Schema consumers; the generated Zod drops propertyNames, so core re-checks key patterns, surface enums, duplicate ids and ambiguous composite keys post-Zod (D12 fail class). Only `pasco` mutates this block (same write rule as refine --choose, D14).").optional(), "credentials": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked."), "kind": z.enum(["certificate", "title", "award", "patent"]).describe("D17/\xA718.2: third-party attestation kind."), "title": z.string().min(1).describe("Credential name (proper noun \u2192 invariant string, \xA711)."), "issuer": z.string().min(1).describe("Third-party attester (proper noun \u2192 invariant string, \xA711)."), "credentialId": z.string().min(1).describe("Issuer-side identifier (e.g. certificate serial). Certificates carry PII \u2014 visibility governs exposure (\xA718.5).").optional(), "verifyUrl": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "issued": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01).").optional(), "expires": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01).").optional(), "visibility": z.enum(["public", "unlisted", "private"]).describe("D17/\xA718.5: first-class visibility. Renderers emit only `public`; `unlisted` is shared on request (client DMs); `private` never leaves the workspace. Absent = public (the pre-registry profile was public by design \u2014 absence must not hide silently).").optional() }).strict().describe("v1.4 (D17, \xA718.2): a third-party attestation. JSON-LD payoff (\xA718.9): `Person.hasCredential` \u2192 `EducationalOccupationalCredential` (public only).")).describe("v1.4 (D17, \xA718): the proof layer \u2014 third-party attestations (certificates, titles, awards, patents). Pointer-first: the workspace declares pointers, the engine never owns binaries (D14). Naming discipline (\xA718.1): never called evidence.").optional(), "artifacts": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked."), "kind": z.enum(["portfolio", "gallery", "showreel", "dataset", "document", "code"]).describe("D17/\xA718.2: work/media kind."), "title": z.string().min(1).describe("Work title (proper noun \u2192 invariant string, \xA711)."), "description": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "locator": z.object({ "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "path": z.string().min(1).describe("In-workspace `media/\u2026` or external under PASCO_EVIDENCE_ROOT \u2014 capability matrix (\xA718.3): NEVER emitted as a public URL while the workspace is private (D14); reaches public surfaces only via renderer assets/ output (M8).").optional() }).strict().describe("Exactly one of `url` | `path` (\xA718.3 capability matrix). The oneOf expresses the XOR for JSON Schema consumers; the generated Zod cannot express required-without-property, so core re-checks the XOR post-Zod (D12 fail class, ledger precedent)."), "checksum": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("Immutable artifacts only (\xA718.6): recorded at compile for byte-stable bundling (M8).").optional(), "license": z.string().min(1).describe("Usage rights for the work (e.g. CC-BY-4.0).").optional(), "tags": z.array(z.string()).describe("Multi-area selection (\xA718.9): renderers select by platform + lang + tags; one workspace, many areas.").optional(), "visibility": z.enum(["public", "unlisted", "private"]).describe("D17/\xA718.5: first-class visibility. Renderers emit only `public`; `unlisted` is shared on request (client DMs); `private` never leaves the workspace. Absent = public (the pre-registry profile was public by design \u2014 absence must not hide silently).").optional() }).strict().describe("v1.4 (D17, \xA718.2): a work or media item. JSON-LD payoff (\xA718.9): surfaces as CreativeWork/ImageGallery/VideoObject/Dataset per platform (renderer-curated, D5).")).describe("v1.4 (D17, \xA718): works/media items (portfolio, gallery, showreel, dataset, document, code), pointer-first via `locator`. Naming discipline (\xA718.1): never called evidence \u2014 canonical blocks keep their domains (publications/press are NOT registry kinds, they reference registry ids).").optional() }).strict().describe("Contract source (D3) for a profile-data workspace. The core `load()` parses content/*.yaml, merges them into a single Profile and validates against THIS schema. Facts (dates, DOIs, URLs, ORCID) are language-independent and are the GEO anchor. Every text field is a langMap ({lang: value}) with EN required (D8, \xA711). Additive since v1 (semver minor): every earlier workspace still validates. v1.1: role as langMap (org stays invariant string), highlights as string[] per lang, publication.authors/url, root languages + education, identity.handle/timezone, patreon in checks; brand flavor (D15/\xA717) \u2014 `flavor`, org identity variant (orgName/logo/brandVoice/mission/tagline/established), socialHandles/newsletter/podcast/press/audiences blocks, brand platforms, handle checks. New in v1.2 (D16): platform set = Postiz parity + operator extras \u2014 43 platforms, one shared enum for both flavors, single-source dedup guard (check.platform). New in v1.3 (D18): per-entry deploy ledger \u2014 `deployments` (ledgerKey \u2192 surface \u2192 deployedAt/contentHash/source), optional stable `id`s on row blocks (experience/projects/publications/education/press), `origin: template` provenance flag. New in v1.4 (D17): evidence registry \u2014 `credentials[]` (third-party attestations: certificate/title/award/patent) and `artifacts[]` (works/media: portfolio/gallery/showreel/dataset/document/code), pointer-first (\xA718); `evidenceItem` gains `credentialId`/`artifactId` cross-refs, `publications[]`/`press[]` gain `artifactRefs`. Naming discipline (\xA718.1): the registry is never called evidence \u2014 `projects[].evidence[]` stays claims-only. New in v1.5 (issue #71): `identity.email` \u2014 the contact email as a first-class fact (schema.org `email` on Person/Organization), NOT a link \u2014 `links[]` stays http(s)-only (mailto: rejected, D12); a mailto: row in links[] broke the template and every real workspace after v1.4's scheme boundary (issue #53). New in v1.6 (issue #103): `identity.links[].role` \u2014 the link ROLE (`identity` | `realization` | `aggregator`), a translation layer onto schema.org (`url` | `sameAs` | `mainEntityOfPage`); absent role = `realization`, so every pre-v1.6 workspace stays VALIDATING unchanged (the rendered output does change \u2014 see \xA715.4). The JSON-LD `@id` is derived from the `role: identity` link and is ABSENT when no link carries it (never invented). Entity assembly moves to a single home (core/src/structured-data.ts), consumed by geoCheck and every renderer \u2014 the \xA718.9 credentialLdEntries precedent. New in v1.7 (issue #130): `kofi` \u2014 Ko-fi creator page as operator extra #10 (44 platforms), creator-monetization family; paste platform, no API deploy (D9). New in v1.8 (issue #133): `buymeacoffee`, `liberapay`, `gumroad`, `payhip` \u2014 the creator monetization platforms family (D16), operator extras #11\u2013#14 (48 platforms); the family is declared in SPEC and descriptions (docs-first), not as a hard schema field; all four are paste platforms, no API deploy (D9). New in v1.9 (issue #137): `hotmart` \u2014 the Hotmart digital-products storefront + memberships (Members Area / recurring billing) serving the Spanish/Portuguese-speaking markets, in the `creator-monetization` family as operator extra #15 (49 platforms); paste platform, no API deploy (D9) \u2014 no management API is documented for the profile body (developers.hotmart.com covers sales/products [?], fetched 2026-10-05).");
686
+ var profileSchema = z.object({ "identity": z.object({ "name": z.string().min(1), "email": z.string().regex(new RegExp(`^[^@\\s"']+@[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+$`)).describe("v1.5 (additive, issue #71): contact email of the person/brand (schema.org `email` on Person/Organization). A fact like name/location \u2014 NOT a link: renderers emit it as a mailto: href in the Links section and as the JSON-LD `email` property; `sameAs` stays page-URLs-only. Lives OUTSIDE identity.links[] by design \u2014 the ^https?:// rule of $defs/url (issue #53) stays strict on every URL field, so a mailto: row in links[] fails validation (D12). RFC-5322-adjacent (issue #71 review): local part lax (no whitespace/quotes), domain hostname-shaped (labels [A-Za-z0-9-], no empty/trailing/consecutive dots \u2014 unicode domains must use punycode).").optional(), "handle": z.string().describe("v1.1: primary handle (e.g. X @cluciani_ve).").optional(), "timezone": z.string().describe("v1.1: IANA timezone (e.g. America/Guayaquil).").optional(), "orgName": z.string().min(1).describe("v1.1 (additive, brand): legal/registered org name when it differs from the display name (name = display, e.g. 'Topia Press').").optional(), "logo": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "brandVoice": z.object({ "tone": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "values": z.array(z.string()).optional(), "avoid": z.array(z.string()).optional() }).strict().describe("v1.1 (additive, brand): tone matrix seed \u2014 overall tone (langMap), core values, words/tones to avoid. Per-platform voice refinement lives in the platform skill (\xA717).").optional(), "mission": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "tagline": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "established": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01).").optional(), "location": z.string().optional(), "photo": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "links": z.array(z.object({ "platform": z.string().optional(), "label": z.string().optional(), "role": z.enum(["identity", "realization", "aggregator"]).describe("v1.6 (#103): the ROLE this link plays with respect to the entity \u2014 a translation layer of this engine onto schema.org, never standard vocabulary. `identity` \u2192 schema.org:url (the page IS the entity; strong authority; the one eligible for JSON-LD @id). `realization` (DEFAULT when absent) \u2192 schema.org:sameAs (the page indirectly indicates the entity; weak authority; these \u2014 and only these \u2014 form the sameAs set). `aggregator` \u2192 schema.org:mainEntityOfPage (link-in-bio / redirector that aggregates references to the entity; medium authority; never eligible for @id). Absence keeps every pre-v1.6 workspace VALIDATING unchanged (additive, dev-methodology \xA73) \u2014 it does NOT keep the output byte-identical: v1.6 also makes github-readme emit jobTitle/worksFor, narrows sameAs to realization links (+ claimed handles), and adds the geo/entity-id gap report.").optional(), "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.") }).strict()).describe("Canonical link set. Renderers and skills read URLs ONLY from here \u2014 no per-platform URL duplication (drift source). v1.6 (#103): items carry an optional `role` distinguishing the entity's own page (schema.org:url) from pages that merely indicate it (schema.org:sameAs) or aggregate references to it (schema.org:mainEntityOfPage).").optional() }).strict(), "flavor": z.enum(["person", "brand"]).describe("v1.1 (additive): workspace flavor (D15). person (default, absent = person) or brand. Brand enables the org identity variant, the socialHandles/newsletter/podcast/press/audiences blocks and Organization GEO (\xA717).").optional(), "socialHandles": z.array(z.object({ "platform": z.enum(["github-readme", "linkedin", "upwork", "x", "researchgate", "substack", "patreon", "github-org", "bluesky", "instagram", "tiktok", "youtube", "spotify", "reddit", "mastodon", "soundcloud", "facebook", "linkedin-page", "instagram-standalone", "warpcast", "lemmy", "discord", "slack", "skool", "whop", "pinterest", "dribbble", "medium", "devto", "hashnode", "wordpress", "gmb", "listmonk", "twitch", "threads", "telegram", "nostr", "vk", "kick", "moltbook", "ghost", "mewe", "tumblr", "kofi", "buymeacoffee", "liberapay", "gumroad", "payhip", "hotmart"]).describe("Platform set (ONE source, dedup). v1: person surfaces. v1.1 (additive, brand \xA717): brand surfaces + the socialHandles matrix key space. v1.2 (additive, D16): Postiz parity + operator extras \u2014 43 platforms; new slugs Postiz-aligned verbatim (linkedin-page, instagram-standalone, devto, gmb, warpcast, moltbook, mewe, tumblr); ghost is an operator extra Postiz lacks (docs.postiz.com llms.txt: 34 providers, fetched 2026-09-02). v1.7 (issue #130): kofi \u2014 Ko-fi creator page, operator extra #10, 44 total; creator-monetization family, paste flow (D9). v1.8 (issue #133): buymeacoffee, liberapay, gumroad, payhip \u2014 creator monetization platforms (D16 family), operator extras #11\u2013#14, 48 total; paste flow (D9). v1.9 (issue #137): hotmart \u2014 Hotmart digital-products storefront + memberships (Members Area / recurring billing), operator extra #15, 49 total; creator-monetization family, paste flow (D9)."), "handle": z.string().min(1), "status": z.enum(["claimed", "pending"]), "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "note": z.string().optional() }).strict().describe("v1.1 (additive, brand): one row of the brand handle matrix (platform \u2192 handle + claim status). Seed source: the operator's brand-handles availability sweep (\xA717).")).describe("v1.1 (additive, brand): the brand handle matrix \u2014 one row per platform with claim status. Seeded from the operator's brand-handles availability sweep (\xA717).").optional(), "newsletter": z.object({ "platform": z.enum(["substack", "ghost"]).describe("v1.1 (additive, brand): newsletter hosts the engine models (Substack now, Ghost later)."), "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole."), "name": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "description": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "cadence": z.string().optional() }).strict().describe("v1.1 (additive, brand): the brand's newsletter surface. The name/description double as the GEO-visible text (RSS/LLM-readable).").optional(), "podcast": z.object({ "title": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())), "platform": z.enum(["spotify", "rss"]).describe("v1.1 (additive, brand): podcast distribution modeled in v1.1 (Spotify for Creators / standalone RSS).").optional(), "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "rss": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "description": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "status": z.enum(["planned", "live"]).optional() }).strict().describe("v1.1 (additive, brand): the brand's podcast \u2014 show metadata + RSS (GEO-citable via RSS).").optional(), "press": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional(), "origin": z.literal("template").describe("v1.3 (D18, \xA719.2): provenance \u2014 `template` marks the fictional examples shipped by profile-data-template. Absent = user-authored.").optional(), "outlet": z.union([z.string().min(1), langMapKeys(z.object({ "en": z.string() }).catchall(z.string()))]), "title": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())), "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole."), "date": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01)."), "artifactRefs": z.array(z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.")).describe("v1.4 (D17/\xA718.1): references `artifacts[].id` \u2014 the work behind this mention. press keeps its domain (no duplication); targets resolved by core (load fails on a dangling ref).").optional() }).strict().describe("v1.1 (additive, brand): a media mention \u2014 outlet, headline, resolvable URL, date (verifiable claims, \xA78).")).describe("v1.1 (additive, brand): media mentions of the brand (verifiable, dated).").optional(), "audiences": z.record(z.string(), langMapKeys(z.object({ "en": z.string() }).catchall(z.string()))).describe("v1.1 (additive, brand): brand audiences \u2014 reader profiles (not recruiters). Audience name \u2192 per-lang angle (same shape as positioning.audiences; brand-facing).").optional(), "positioning": z.object({ "statement": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "audiences": z.record(z.string(), langMapKeys(z.object({ "en": z.string() }).catchall(z.string()))).describe("Audience name \u2192 per-lang angle (e.g. recruiter / client / academia).").optional(), "geo": z.object({ "semanticAssociations": z.array(z.string()).min(3).max(5).describe("3-5 anchors an LLM should associate with the person (e.g. 'f\xEDsica', 'innovaci\xF3n educativa', 'soberan\xEDa de IA'). Seeded without stuffing (\xA78).").optional(), "citationsGoal": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional() }).strict().optional() }).strict().optional(), "bios": z.array(z.object({ "platform": z.enum(["github-readme", "linkedin", "upwork", "x", "researchgate", "substack", "patreon", "github-org", "bluesky", "instagram", "tiktok", "youtube", "spotify", "reddit", "mastodon", "soundcloud", "facebook", "linkedin-page", "instagram-standalone", "warpcast", "lemmy", "discord", "slack", "skool", "whop", "pinterest", "dribbble", "medium", "devto", "hashnode", "wordpress", "gmb", "listmonk", "twitch", "threads", "telegram", "nostr", "vk", "kick", "moltbook", "ghost", "mewe", "tumblr", "kofi", "buymeacoffee", "liberapay", "gumroad", "payhip", "hotmart"]).describe("Platform set (ONE source, dedup). v1: person surfaces. v1.1 (additive, brand \xA717): brand surfaces + the socialHandles matrix key space. v1.2 (additive, D16): Postiz parity + operator extras \u2014 43 platforms; new slugs Postiz-aligned verbatim (linkedin-page, instagram-standalone, devto, gmb, warpcast, moltbook, mewe, tumblr); ghost is an operator extra Postiz lacks (docs.postiz.com llms.txt: 34 providers, fetched 2026-09-02). v1.7 (issue #130): kofi \u2014 Ko-fi creator page, operator extra #10, 44 total; creator-monetization family, paste flow (D9). v1.8 (issue #133): buymeacoffee, liberapay, gumroad, payhip \u2014 creator monetization platforms (D16 family), operator extras #11\u2013#14, 48 total; paste flow (D9). v1.9 (issue #137): hotmart \u2014 Hotmart digital-products storefront + memberships (Members Area / recurring billing), operator extra #15, 49 total; creator-monetization family, paste flow (D9)."), "lang": z.enum(["en", "es", "fr"]).describe("Supported languages. EN required (international recruiters), ES native, FR desirable (D8)."), "length": z.enum(["short", "medium", "long"]), "text": z.string().min(1) }).strict()).describe("Curated bio per platform+lang+length. Generated by `pasco refine`, chosen by the user, stored here.").optional(), "headlines": z.array(z.object({ "platform": z.enum(["github-readme", "linkedin", "upwork", "x", "researchgate", "substack", "patreon", "github-org", "bluesky", "instagram", "tiktok", "youtube", "spotify", "reddit", "mastodon", "soundcloud", "facebook", "linkedin-page", "instagram-standalone", "warpcast", "lemmy", "discord", "slack", "skool", "whop", "pinterest", "dribbble", "medium", "devto", "hashnode", "wordpress", "gmb", "listmonk", "twitch", "threads", "telegram", "nostr", "vk", "kick", "moltbook", "ghost", "mewe", "tumblr", "kofi", "buymeacoffee", "liberapay", "gumroad", "payhip", "hotmart"]).describe("Platform set (ONE source, dedup). v1: person surfaces. v1.1 (additive, brand \xA717): brand surfaces + the socialHandles matrix key space. v1.2 (additive, D16): Postiz parity + operator extras \u2014 43 platforms; new slugs Postiz-aligned verbatim (linkedin-page, instagram-standalone, devto, gmb, warpcast, moltbook, mewe, tumblr); ghost is an operator extra Postiz lacks (docs.postiz.com llms.txt: 34 providers, fetched 2026-09-02). v1.7 (issue #130): kofi \u2014 Ko-fi creator page, operator extra #10, 44 total; creator-monetization family, paste flow (D9). v1.8 (issue #133): buymeacoffee, liberapay, gumroad, payhip \u2014 creator monetization platforms (D16 family), operator extras #11\u2013#14, 48 total; paste flow (D9). v1.9 (issue #137): hotmart \u2014 Hotmart digital-products storefront + memberships (Members Area / recurring billing), operator extra #15, 49 total; creator-monetization family, paste flow (D9)."), "lang": z.enum(["en", "es", "fr"]).describe("Supported languages. EN required (international recruiters), ES native, FR desirable (D8)."), "text": z.string().min(1) }).strict()).describe("Headline per platform+lang (the 1-line under the name).").optional(), "skills": z.array(z.object({ "name": z.string().min(1), "level": z.enum(["novice", "intermediate", "advanced", "expert"]).optional(), "keywords": z.array(z.string()).optional(), "geoKeywords": z.array(z.string()).describe("Terms a generative engine should match this skill with (GEO layer, D7).").optional(), "platformWeights": z.record(z.string(), z.number().gte(0).lte(1)).describe("How much this skill matters per platform (0..1). Feeds renderer keyword placement.").optional() }).strict()).optional(), "experience": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional(), "origin": z.literal("template").describe("v1.3 (D18, \xA719.2): provenance \u2014 `template` marks the fictional examples shipped by profile-data-template. Absent = user-authored.").optional(), "role": z.union([z.string().min(1), langMapKeys(z.object({ "en": z.string() }).catchall(z.string()))]).describe("v1: string. v1.1 (additive): langMap \u2014 bilingual role titles."), "org": z.string().min(1).describe("Proper noun \u2014 invariant across languages (e.g. 'USFQ', 'Nimbus Labs'). Not a langMap: institutions are never translated (SPEC \xA711)."), "start": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01)."), "end": z.union([z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01)."), z.literal("present")]).optional(), "highlights": z.union([langMapKeys(z.object({ "en": z.string() }).catchall(z.string())), langMapKeys(z.object({ "en": z.array(z.string()) }).catchall(z.array(z.string())))]).describe("v1: langMap of strings (separator-delimited). v1.1 (additive): langMap of string arrays (bullets).").optional() }).strict().describe("start <= end is NOT expressible in JSON Schema \u2014 enforced by core.driftCheck (\xA712 note).")).optional(), "education": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional(), "origin": z.literal("template").describe("v1.3 (D18, \xA719.2): provenance \u2014 `template` marks the fictional examples shipped by profile-data-template. Absent = user-authored.").optional(), "degree": z.union([z.string().min(1), langMapKeys(z.object({ "en": z.string() }).catchall(z.string()))]).describe("v1.1: degree title (string or bilingual langMap)."), "institution": z.string().min(1).describe("Proper noun \u2014 invariant across languages (e.g. 'Universidad Central de Venezuela'). Not a langMap: institutions are never translated (SPEC \xA711)."), "field": z.union([z.string(), langMapKeys(z.object({ "en": z.string() }).catchall(z.string()))]).describe("v1.1: field of study (string or bilingual langMap).").optional(), "start": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01)."), "end": z.union([z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01)."), z.literal("present")]).optional(), "note": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional() }).strict().describe("v1.1 (additive): formal education entry.")).describe("v1.1 (additive): formal education. v1 workspaces keep education inside experience[] with role = degree.").optional(), "languages": z.array(z.object({ "code": z.enum(["en", "es", "fr"]).describe("Supported languages. EN required (international recruiters), ES native, FR desirable (D8)."), "level": z.enum(["native", "fluent", "advanced", "intermediate", "basic"]) }).strict()).describe("v1.1 (additive): spoken languages. v1 workspaces document them as comments in skills.yaml.").optional(), "projects": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional(), "origin": z.literal("template").describe("v1.3 (D18, \xA719.2): provenance \u2014 `template` marks the fictional examples shipped by profile-data-template. Absent = user-authored.").optional(), "name": z.string().min(1), "status": z.enum(["active", "maintained", "archived", "experimental", "planning"]).optional(), "description": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "links": z.array(z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.")).optional(), "keywords": z.array(z.string()).optional(), "evidence": z.array(z.object({ "claim": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())), "source": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole."), "verified": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01).").optional(), "credentialId": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional(), "artifactId": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional() }).strict().describe("Verifiable claim with source. Anti-pattern: unverifiable claims (dropped by cite-aware systems, \xA78).")).optional() }).strict()).optional(), "publications": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.").optional(), "origin": z.literal("template").describe("v1.3 (D18, \xA719.2): provenance \u2014 `template` marks the fictional examples shipped by profile-data-template. Absent = user-authored.").optional(), "title": z.string().min(1), "venue": z.string().min(1), "year": z.number().int().gte(1900).lte(2100), "authors": z.string().describe("v1.1: author string (e.g. 'Luciani, C.' or 'Zambrano et al.').").optional(), "doi": z.string().regex(new RegExp("^10\\.\\d{4,9}/\\S+$")).optional(), "orcidId": z.string().regex(new RegExp("^\\d{4}-\\d{4}-\\d{4}-\\d{3}[\\dX]$")).optional(), "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "artifactRefs": z.array(z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked.")).describe("v1.4 (D17/\xA718.1): references `artifacts[].id` \u2014 the dataset/code behind this paper. publications keep their domain (no duplication); targets resolved by core (load fails on a dangling ref).").optional() }).strict().describe("DOI/ORCID must be resolvable \u2014 GEO citability anchor (LLMs resolve them).")).describe("Peer-reviewed / citable output. DOI/ORCID resolvable \u2192 GEO citability.").optional(), "keywords": z.object({ "seo": z.record(z.string(), z.array(z.string())).describe("Per-platform SEO keyword sets (fed by each platform skill's research).").optional(), "geo": z.object({ "associations": z.array(z.string()).optional(), "claims": z.array(z.string()).optional() }).strict().optional() }).strict().optional(), "checks": z.array(z.object({ "id": z.string().optional(), "platform": z.enum(["all", "github-readme", "linkedin", "upwork", "x", "researchgate", "substack", "patreon", "github-org", "bluesky", "instagram", "tiktok", "youtube", "spotify", "reddit", "mastodon", "soundcloud", "facebook", "linkedin-page", "instagram-standalone", "warpcast", "lemmy", "discord", "slack", "skool", "whop", "pinterest", "dribbble", "medium", "devto", "hashnode", "wordpress", "gmb", "listmonk", "twitch", "threads", "telegram", "nostr", "vk", "kick", "moltbook", "ghost", "mewe", "tumblr", "kofi", "buymeacoffee", "liberapay", "gumroad", "payhip", "hotmart"]).describe("Dedup contract: MUST equal ['all', ...$defs/platform.enum] \u2014 scripts/schema-check asserts it (D16)."), "rule": z.enum(["max-chars", "min-chars", "url-live", "fact-present", "geo-consistent", "lang-present", "link-in-identity", "handle-claimed", "handle-consistent"]).describe("Rule catalog (extensible). Skills' checks.yaml use the same catalog; `pasco check` runs the union. v1.1 (brand \xA717): handle-claimed (a claimed handle must resolve to a URL) and handle-consistent (handle matrix coherent across surfaces)."), "value": z.union([z.number(), z.string(), z.boolean(), z.array(z.string())]), "note": z.string().optional(), "surface": z.enum(["headline", "bio", "both"]).describe("Additive (#240): which text rows a char-limit rule applies to. Absent = both. A title cap can target `headline` without flagging the bio.").optional() }).strict()).describe("User-level declarative checks; unioned with the platform skill's checks.yaml by `pasco check`.").optional(), "active": z.record(z.string(), z.object({ "lang": z.enum(["en", "es", "fr"]).describe("Supported languages. EN required (international recruiters), ES native, FR desirable (D8)."), "variantId": z.string().min(1), "chosenAt": z.string().datetime({ offset: true }) }).strict().describe("The chosen variant id \u2014 user picked it (D11); variant body lives in the workspace variants store.")).describe("Which bio/headline variant is LIVE per platform (D11: user picks, engine records).").optional(), "deployments": z.record(z.string(), z.record(z.string(), z.object({ "deployedAt": z.string().datetime({ offset: true }), "contentHash": z.string().regex(new RegExp("^[a-f0-9]{64}$")), "source": z.enum(["manual", "git", "api"]), "certification": z.union([z.object({ "state": z.literal("verified"), "artifactHash": z.string().regex(new RegExp("^[a-f0-9]{64}$")), "method": z.literal("fetch"), "fetchedAt": z.string().datetime({ offset: true }), "liveHash": z.string().regex(new RegExp("^[a-f0-9]{64}$")), "evidenceUrl": z.string().url().regex(new RegExp("^https?://")).min(7).optional() }).strict().describe("\xA719.5 C1: a deterministic pass fetched the live artifact, applied the surface's extractor, and the live digest EQUALS the stored anchor. The fetch evidence is required \u2014 a `verified` with no fetch behind it is not evidence."), z.object({ "state": z.literal("attested"), "artifactHash": z.string().regex(new RegExp("^[a-f0-9]{64}$")).optional() }).strict().describe("\xA719.5 C2: the human `--mark`. Certified by ASSERTION, never rendered as a bare `in-sync`. `artifactHash` is the anchor written at confirmation when the surface has an extractor; a mark on a surface without one records no anchor (the declared forward-only v1 limit, \xA719.5)."), z.object({ "state": z.literal("unverifiable"), "reason": z.string().regex(new RegExp("^(walled|walled-con-api|host-not-allowed|network-error|no-artifact-url|no-extractor|unattributed|http-[0-9]{3})$")).describe("\xA719.5: the declared reasons, the attempt reasons, and `http-<code>` for the ACTUAL status the pass observed. `http-` accepts ANY 3-digit code \u2014 an enum of just http-403/http-429 made a routine 404/500 write a ledger the next `load()` rejects (review F1). One alternation rather than `enum \u222A pattern`: the Zod generator cannot express that union (it degrades to an untyped schema, which its safety net refuses), so the alternatives are pinned by the pattern and by fixtures instead."), "artifactHash": z.string().regex(new RegExp("^[a-f0-9]{64}$")).optional(), "method": z.literal("fetch").optional(), "fetchedAt": z.string().datetime({ offset: true }).optional(), "evidenceUrl": z.string().url().regex(new RegExp("^https?://")).min(7).optional() }).strict().describe("\xA719.5: no deterministic check is possible, or one was attempted and did not complete. DECLARED reasons (walled \xB7 walled-con-api \xB7 no-extractor \xB7 no-artifact-url) are the PASS's knowledge \u2014 it states them for its own output and writes nothing, so a workspace needs no ledger row per walled platform and the file keeps only durable evidence; ATTEMPTED ones (http-<code> \xB7 network-error \xB7 unattributed \xB7 host-not-allowed) are RECORDED with the fetch evidence. `liveHash` is impossible here \u2014 a comparison would make the state lie; `artifactHash` is allowed because it is the deploy ANCHOR, not a comparison, and a failed check must not destroy it (\xA719.5: the anchor is left unchanged)."), z.object({ "state": z.literal("out-of-band-drift"), "reason": z.literal("live-mismatch"), "artifactHash": z.string().regex(new RegExp("^[a-f0-9]{64}$")), "liveHash": z.string().regex(new RegExp("^[a-f0-9]{64}$")), "method": z.literal("fetch"), "fetchedAt": z.string().datetime({ offset: true }), "evidenceUrl": z.string().url().regex(new RegExp("^https?://")).min(7) }).strict().describe("\xA719.5: a C1 check COMPLETED and contradicts the ledger \u2014 the live digest differs from the anchor. Positive contrary evidence, never a strength of confirmation: the report must not fold it into a green state. Both digests and the fetch evidence are required so the contradiction is auditable.")]).optional() }).strict().describe("v1.3 (D18, \xA719.1): one confirmed deploy of a trackable unit on one surface. contentHash = SHA-256 of the canonical serialization (sorted keys, sans id/origin) of the unit AT deploy time \u2014 staleness is exact and release-tag-independent. v1.10 (issue #132, \xA719.5): the optional `certification` block adds the second, orthogonal dimension.")).describe("v1.3 (D18): surface \u2192 deploy record. Surface keys are the ONE platform enum (D16) \u2014 also core-checked post-Zod (gen-zod drops propertyNames).")).describe("v1.3 (D18, \xA719): per-entry deploy ledger \u2014 ledgerKey \u2192 surface \u2192 record. Keys are constrained above for JSON Schema consumers; the generated Zod drops propertyNames, so core re-checks key patterns, surface enums, duplicate ids and ambiguous composite keys post-Zod (D12 fail class). Only `pasco` mutates this block (same write rule as refine --choose, D14).").optional(), "credentials": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked."), "kind": z.enum(["certificate", "title", "award", "patent"]).describe("D17/\xA718.2: third-party attestation kind."), "title": z.string().min(1).describe("Credential name (proper noun \u2192 invariant string, \xA711)."), "issuer": z.string().min(1).describe("Third-party attester (proper noun \u2192 invariant string, \xA711)."), "credentialId": z.string().min(1).describe("Issuer-side identifier (e.g. certificate serial). Certificates carry PII \u2014 visibility governs exposure (\xA718.5).").optional(), "verifyUrl": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "issued": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01).").optional(), "expires": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01).").optional(), "visibility": z.enum(["public", "unlisted", "private"]).describe("D17/\xA718.5: first-class visibility. Renderers emit only `public`; `unlisted` is shared on request (client DMs); `private` never leaves the workspace. Absent = public (the pre-registry profile was public by design \u2014 absence must not hide silently).").optional() }).strict().describe("v1.4 (D17, \xA718.2): a third-party attestation. JSON-LD payoff (\xA718.9): `Person.hasCredential` \u2192 `EducationalOccupationalCredential` (public only).")).describe("v1.4 (D17, \xA718): the proof layer \u2014 third-party attestations (certificates, titles, awards, patents). Pointer-first: the workspace declares pointers, the engine never owns binaries (D14). Naming discipline (\xA718.1): never called evidence.").optional(), "artifacts": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9._:-]*$")).min(1).describe("v1.3 (D18, \xA719.2): stable entry id \u2014 the ledger address. Unique per block (core-checked, fails validation; JSON Schema cannot express uniqueness). Optional: rows without id are listed by `pasco status` as untracked."), "kind": z.enum(["portfolio", "gallery", "showreel", "dataset", "document", "code"]).describe("D17/\xA718.2: work/media kind."), "title": z.string().min(1).describe("Work title (proper noun \u2192 invariant string, \xA711)."), "description": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "locator": z.object({ "url": z.string().url().regex(new RegExp("^https?://")).min(7).describe("http(s) ONLY (issue #53): the SSOT validates the scheme at the boundary so renderers emit any SSOT URL verbatim with zero per-emit filtering. `format: uri` alone admits non-http schemes (javascript:, data:, vbscript:) \u2014 the pattern is the assertion; format stays as annotation for schema-aware consumers. Case-sensitive per ECMA-262 regex (JSON Schema patterns carry no flags); an uppercase scheme fails loudly (D12 fail class), it is not a security hole.").optional(), "path": z.string().min(1).describe("In-workspace `media/\u2026` or external under PASCO_EVIDENCE_ROOT \u2014 capability matrix (\xA718.3): NEVER emitted as a public URL while the workspace is private (D14); reaches public surfaces only via renderer assets/ output (M8).").optional() }).strict().describe("Exactly one of `url` | `path` (\xA718.3 capability matrix). The oneOf expresses the XOR for JSON Schema consumers; the generated Zod cannot express required-without-property, so core re-checks the XOR post-Zod (D12 fail class, ledger precedent)."), "checksum": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("Immutable artifacts only (\xA718.6): recorded at compile for byte-stable bundling (M8).").optional(), "license": z.string().min(1).describe("Usage rights for the work (e.g. CC-BY-4.0).").optional(), "tags": z.array(z.string()).describe("Multi-area selection (\xA718.9): renderers select by platform + lang + tags; one workspace, many areas.").optional(), "visibility": z.enum(["public", "unlisted", "private"]).describe("D17/\xA718.5: first-class visibility. Renderers emit only `public`; `unlisted` is shared on request (client DMs); `private` never leaves the workspace. Absent = public (the pre-registry profile was public by design \u2014 absence must not hide silently).").optional() }).strict().describe("v1.4 (D17, \xA718.2): a work or media item. JSON-LD payoff (\xA718.9): surfaces as CreativeWork/ImageGallery/VideoObject/Dataset per platform (renderer-curated, D5).")).describe("v1.4 (D17, \xA718): works/media items (portfolio, gallery, showreel, dataset, document, code), pointer-first via `locator`. Naming discipline (\xA718.1): never called evidence \u2014 canonical blocks keep their domains (publications/press are NOT registry kinds, they reference registry ids).").optional() }).strict().describe("Contract source (D3) for a profile-data workspace. The core `load()` parses content/*.yaml, merges them into a single Profile and validates against THIS schema. Facts (dates, DOIs, URLs, ORCID) are language-independent and are the GEO anchor. Every text field is a langMap ({lang: value}) with EN required (D8, \xA711). Additive since v1 (semver minor): every earlier workspace still validates. v1.1: role as langMap (org stays invariant string), highlights as string[] per lang, publication.authors/url, root languages + education, identity.handle/timezone, patreon in checks; brand flavor (D15/\xA717) \u2014 `flavor`, org identity variant (orgName/logo/brandVoice/mission/tagline/established), socialHandles/newsletter/podcast/press/audiences blocks, brand platforms, handle checks. New in v1.2 (D16): platform set = Postiz parity + operator extras \u2014 43 platforms, one shared enum for both flavors, single-source dedup guard (check.platform). New in v1.3 (D18): per-entry deploy ledger \u2014 `deployments` (ledgerKey \u2192 surface \u2192 deployedAt/contentHash/source), optional stable `id`s on row blocks (experience/projects/publications/education/press), `origin: template` provenance flag. New in v1.4 (D17): evidence registry \u2014 `credentials[]` (third-party attestations: certificate/title/award/patent) and `artifacts[]` (works/media: portfolio/gallery/showreel/dataset/document/code), pointer-first (\xA718); `evidenceItem` gains `credentialId`/`artifactId` cross-refs, `publications[]`/`press[]` gain `artifactRefs`. Naming discipline (\xA718.1): the registry is never called evidence \u2014 `projects[].evidence[]` stays claims-only. New in v1.5 (issue #71): `identity.email` \u2014 the contact email as a first-class fact (schema.org `email` on Person/Organization), NOT a link \u2014 `links[]` stays http(s)-only (mailto: rejected, D12); a mailto: row in links[] broke the template and every real workspace after v1.4's scheme boundary (issue #53). New in v1.6 (issue #103): `identity.links[].role` \u2014 the link ROLE (`identity` | `realization` | `aggregator`), a translation layer onto schema.org (`url` | `sameAs` | `mainEntityOfPage`); absent role = `realization`, so every pre-v1.6 workspace stays VALIDATING unchanged (the rendered output does change \u2014 see \xA715.4). The JSON-LD `@id` is derived from the `role: identity` link and is ABSENT when no link carries it (never invented). Entity assembly moves to a single home (core/src/structured-data.ts), consumed by geoCheck and every renderer \u2014 the \xA718.9 credentialLdEntries precedent. New in v1.7 (issue #130): `kofi` \u2014 Ko-fi creator page as operator extra #10 (44 platforms), creator-monetization family; paste platform, no API deploy (D9). New in v1.8 (issue #133): `buymeacoffee`, `liberapay`, `gumroad`, `payhip` \u2014 the creator monetization platforms family (D16), operator extras #11\u2013#14 (48 platforms); the family is declared in SPEC and descriptions (docs-first), not as a hard schema field; all four are paste platforms, no API deploy (D9). New in v1.9 (issue #137): `hotmart` \u2014 the Hotmart digital-products storefront + memberships (Members Area / recurring billing) serving the Spanish/Portuguese-speaking markets, in the `creator-monetization` family as operator extra #15 (49 platforms); paste platform, no API deploy (D9) \u2014 no management API is documented for the profile body (developers.hotmart.com covers sales/products [?], fetched 2026-10-05).");
602
687
  var LANG_KEYS = ["en", "es", "fr"];
603
688
  function langMapKeys(schema) {
604
689
  return schema.superRefine((value, ctx) => {
@@ -849,6 +934,179 @@ function load(workspace) {
849
934
  return normalizeProfile(profile);
850
935
  }
851
936
 
937
+ // ../core/dist/selection.js
938
+ var SELECTION_BLOCKS = [
939
+ "experience",
940
+ "projects",
941
+ "publications",
942
+ "education",
943
+ "press",
944
+ "credentials",
945
+ "artifacts",
946
+ "skills"
947
+ ];
948
+ var TAG_CAPABLE = /* @__PURE__ */ new Set(["artifacts"]);
949
+ function entriesOf(profile, block) {
950
+ const raw = profile[block];
951
+ return Array.isArray(raw) ? raw : [];
952
+ }
953
+ function keyOf(block, entry) {
954
+ if (block === "skills" && typeof entry.name === "string" && entry.name !== "") {
955
+ return `skills:${entry.name}`;
956
+ }
957
+ if (typeof entry.id === "string" && entry.id !== "")
958
+ return `${block}:${entry.id}`;
959
+ return void 0;
960
+ }
961
+ function labelOf(block, entry, index) {
962
+ for (const field of ["id", "name", "title"]) {
963
+ const value = entry[field];
964
+ if (typeof value === "string" && value !== "")
965
+ return value;
966
+ }
967
+ return `${block}[${index}]`;
968
+ }
969
+ function tagsOf(entry) {
970
+ const raw = entry.tags;
971
+ return Array.isArray(raw) ? raw.filter((t) => typeof t === "string") : [];
972
+ }
973
+ var intersects = (a, b) => a.some((value) => b.has(value));
974
+ function resolveView(policy, view) {
975
+ if (policy === void 0)
976
+ return {};
977
+ return policy.views?.[view] ?? policy.default ?? {};
978
+ }
979
+ function selectFacts(profile, view, platform, lang, policy) {
980
+ const rule = resolveView(policy, view);
981
+ const mode = rule.mode ?? "all";
982
+ const excludeKeys = new Set(rule.excludeKeys ?? []);
983
+ const excludeTags = new Set(rule.excludeTags ?? []);
984
+ const includeTags = new Set(rule.tags ?? []);
985
+ const selected = {
986
+ experience: 0,
987
+ projects: 0,
988
+ publications: 0,
989
+ education: 0,
990
+ press: 0,
991
+ credentials: 0,
992
+ artifacts: 0,
993
+ skills: 0
994
+ };
995
+ const omitted = [];
996
+ const kept = /* @__PURE__ */ new Map();
997
+ const keptRefs = [];
998
+ const omittedRefs = [];
999
+ const allKeys = /* @__PURE__ */ new Set();
1000
+ const allTags = /* @__PURE__ */ new Set();
1001
+ let totalEntries = 0;
1002
+ for (const block of SELECTION_BLOCKS) {
1003
+ const entries = entriesOf(profile, block);
1004
+ totalEntries += entries.length;
1005
+ const survivors = [];
1006
+ entries.forEach((entry, index) => {
1007
+ const key = keyOf(block, entry);
1008
+ if (key !== void 0)
1009
+ allKeys.add(key);
1010
+ const tags = tagsOf(entry);
1011
+ for (const tag of tags)
1012
+ allTags.add(tag);
1013
+ if (key !== void 0 && excludeKeys.has(key)) {
1014
+ omitted.push({
1015
+ block,
1016
+ key,
1017
+ label: labelOf(block, entry, index),
1018
+ reason: "excluded-by-key"
1019
+ });
1020
+ omittedRefs.push({ block, entry });
1021
+ return;
1022
+ }
1023
+ if (TAG_CAPABLE.has(block)) {
1024
+ if (intersects(tags, excludeTags)) {
1025
+ omitted.push({
1026
+ block,
1027
+ key: key ?? `${block}[${index}]`,
1028
+ label: labelOf(block, entry, index),
1029
+ reason: "excluded-by-tag"
1030
+ });
1031
+ omittedRefs.push({ block, entry });
1032
+ return;
1033
+ }
1034
+ if (mode === "tags" && !intersects(tags, includeTags)) {
1035
+ omitted.push({
1036
+ block,
1037
+ key: key ?? `${block}[${index}]`,
1038
+ label: labelOf(block, entry, index),
1039
+ reason: "not-tagged"
1040
+ });
1041
+ omittedRefs.push({ block, entry });
1042
+ return;
1043
+ }
1044
+ }
1045
+ survivors.push(entry);
1046
+ keptRefs.push({ block, entry });
1047
+ });
1048
+ kept.set(block, survivors);
1049
+ selected[block] = survivors.length;
1050
+ }
1051
+ const findings = [];
1052
+ for (const key of excludeKeys) {
1053
+ if (!allKeys.has(key)) {
1054
+ findings.push({
1055
+ code: "selection/unknown-key",
1056
+ severity: "error",
1057
+ message: `view "${view}" excludes "${key}", which addresses no entry \u2014 check the \xA719.2 key (an entry needs an "id" to be addressable)`
1058
+ });
1059
+ }
1060
+ }
1061
+ for (const tag of /* @__PURE__ */ new Set([...includeTags, ...excludeTags])) {
1062
+ if (!allTags.has(tag)) {
1063
+ findings.push({
1064
+ code: "selection/unknown-tag",
1065
+ severity: "warn",
1066
+ message: `view "${view}" names tag "${tag}", which no ${[...TAG_CAPABLE].join("/")} entry carries`
1067
+ });
1068
+ }
1069
+ }
1070
+ if (omitted.length > 0) {
1071
+ const shown = omitted.slice(0, 5).map((o) => `${o.key} (${o.reason})`).join(", ");
1072
+ const more = omitted.length > 5 ? ` and ${omitted.length - 5} more` : "";
1073
+ findings.push({
1074
+ code: "selection/omits-facts",
1075
+ severity: "warn",
1076
+ message: `view "${view}" omits ${omitted.length} entr${omitted.length === 1 ? "y" : "ies"}: ${shown}${more} \u2014 subset-only by design (D22); run \`pasco status --selection ${view}\` for the full report`
1077
+ });
1078
+ }
1079
+ if (totalEntries > 0 && keptRefs.length === 0) {
1080
+ findings.push({
1081
+ code: "selection/empty-facts",
1082
+ severity: "warn",
1083
+ message: `view "${view}" selects ZERO entries while the workspace has ${totalEntries} \u2014 check its mode/tags`
1084
+ });
1085
+ }
1086
+ const anchors = profile.positioning?.geo?.semanticAssociations ?? [];
1087
+ for (const anchor of anchors) {
1088
+ const needle = anchor.toLowerCase();
1089
+ const haystack = (refs) => refs.some((r) => JSON.stringify(r.entry).toLowerCase().includes(needle));
1090
+ if (!haystack(keptRefs) && haystack(omittedRefs)) {
1091
+ findings.push({
1092
+ code: "selection/anchor-omitted",
1093
+ severity: "warn",
1094
+ message: `GEO anchor "${anchor}" is reinforced only by entries view "${view}" omits \u2014 it does not reach this surface`
1095
+ });
1096
+ }
1097
+ }
1098
+ const filtered = { ...profile };
1099
+ for (const block of SELECTION_BLOCKS) {
1100
+ if (profile[block] === void 0)
1101
+ continue;
1102
+ filtered[block] = kept.get(block) ?? [];
1103
+ }
1104
+ return {
1105
+ profile: filtered,
1106
+ report: { view, platform, lang, mode, selected, omitted, findings }
1107
+ };
1108
+ }
1109
+
852
1110
  // ../core/dist/structured-data.js
853
1111
  var DEFAULT_LINK_ROLE = "realization";
854
1112
  function linkRole(link) {
@@ -1002,16 +1260,18 @@ function compile(profile, platform, lang = "en", policy) {
1002
1260
  brandAudiences[name] = text;
1003
1261
  }
1004
1262
  const platformChecks = (profile.checks ?? []).filter((c) => c.platform === "all" || c.platform === platform);
1263
+ const selection = policy?.selection === void 0 ? void 0 : selectFacts(profile, policy.view ?? platform, platform, lang, policy.selection);
1264
+ const facts = selection?.profile ?? profile;
1005
1265
  return {
1006
1266
  platform,
1007
1267
  lang,
1008
1268
  flavor: profile.flavor ?? "person",
1009
1269
  // facts — untouched
1010
- identity: profile.identity,
1011
- experience: profile.experience ?? [],
1012
- projects: profile.projects ?? [],
1013
- publications: profile.publications ?? [],
1014
- skills: profile.skills ?? [],
1270
+ identity: facts.identity,
1271
+ experience: facts.experience ?? [],
1272
+ projects: facts.projects ?? [],
1273
+ publications: facts.publications ?? [],
1274
+ skills: facts.skills ?? [],
1015
1275
  socialHandles: profile.socialHandles ?? [],
1016
1276
  newsletter: profile.newsletter,
1017
1277
  podcast: profile.podcast,
@@ -1029,18 +1289,19 @@ function compile(profile, platform, lang = "en", policy) {
1029
1289
  },
1030
1290
  checks: platformChecks,
1031
1291
  // Evidence registry (v1.4/D17, §18) — facts, pass through untouched.
1032
- credentials: profile.credentials ?? [],
1033
- artifacts: profile.artifacts ?? [],
1292
+ credentials: facts.credentials ?? [],
1293
+ artifacts: facts.artifacts ?? [],
1034
1294
  ...profile.active?.[platform] ? { active: profile.active[platform] } : {},
1035
1295
  // v1.6 (#103): entity structured-data identity — assembled ONCE here, so no
1036
1296
  // renderer re-derives `@id`/`sameAs` (SPEC §15.4, the §18.9 precedent).
1037
- structuredData: structuredData(profile, lang),
1297
+ structuredData: structuredData(facts, lang),
1038
1298
  // Issue #102: the completeness report travels WITH the slice (like
1039
1299
  // structuredData) so a renderer emits the number verbatim and never
1040
1300
  // recomputes it. `policy` is the workspace's `.pascorc` policy when the CLI
1041
1301
  // passes it, so the artifact and `pasco status --completeness` agree
1042
1302
  // (review of #205, finding 3); omitted ⇒ the shipped default.
1043
1303
  completeness: completeness(normalizeProfile(profile), policy),
1304
+ ...selection === void 0 ? {} : { selection: selection.report },
1044
1305
  fallbacks
1045
1306
  };
1046
1307
  }
@@ -1090,10 +1351,10 @@ function identityOrcid(profile) {
1090
1351
  }
1091
1352
  return void 0;
1092
1353
  }
1093
- function groupBy(items, keyOf) {
1354
+ function groupBy(items, keyOf2) {
1094
1355
  const groups = /* @__PURE__ */ new Map();
1095
1356
  items.forEach((item, index) => {
1096
- const key = keyOf(item);
1357
+ const key = keyOf2(item);
1097
1358
  const bucket = groups.get(key) ?? [];
1098
1359
  bucket.push(index);
1099
1360
  groups.set(key, bucket);
@@ -1460,7 +1721,9 @@ function geoCheck(profile) {
1460
1721
  // Mirrors `ready = Boolean(name)` exactly (structured-data.ts). The old string still
1461
1722
  // claimed "need name + ≥1 sameAs link" — the predicate it no longer uses, i.e. the
1462
1723
  // same contradiction as the `ready` bug, one line away from its own fix.
1463
- `schema.org ${sd.entityType} JSON-LD ${sd.ready ? "ready for the web renderer" : "not ready (need a name)"}.`,
1724
+ // #241: the @id gap is a user-facing fact, not a JSON-only field — name the missing
1725
+ // anchor and its weight in the same line the score prints.
1726
+ `schema.org ${sd.entityType} JSON-LD ${sd.ready ? "ready for the web renderer" : "not ready (need a name)"}.${sd.fields.id === void 0 ? ` Missing canonical @id: declare a role: identity link on a page or profile (+${Math.round((checks.find((c) => c.code === "geo/entity-id")?.weight ?? 0.05) * 100)} overall).` : ""}`,
1464
1727
  checks.filter((c) => c.code.startsWith("geo/sameas") || c.code.startsWith("geo/structured") || c.code === "geo/entity-id")
1465
1728
  ),
1466
1729
  readiness: sd
@@ -1597,6 +1860,18 @@ function mediaManifest(platform, lang, assets) {
1597
1860
  };
1598
1861
  }
1599
1862
 
1863
+ // ../paste/dist/index.js
1864
+ var LENGTH_PRIORITY = ["long", "medium", "short"];
1865
+ function pasteSource(p) {
1866
+ for (const length of LENGTH_PRIORITY) {
1867
+ const bio = p.bios.find((b) => b.length === length);
1868
+ if (bio !== void 0)
1869
+ return `${bio.text}
1870
+ `;
1871
+ }
1872
+ return "";
1873
+ }
1874
+
1600
1875
  // src/contract.ts
1601
1876
  var UsageError = class extends Error {
1602
1877
  constructor(message) {
@@ -1633,18 +1908,6 @@ var EXIT = {
1633
1908
  USAGE: 2
1634
1909
  };
1635
1910
 
1636
- // ../paste/dist/index.js
1637
- var LENGTH_PRIORITY = ["long", "medium", "short"];
1638
- function pasteSource(p) {
1639
- for (const length of LENGTH_PRIORITY) {
1640
- const bio = p.bios.find((b) => b.length === length);
1641
- if (bio !== void 0)
1642
- return `${bio.text}
1643
- `;
1644
- }
1645
- return "";
1646
- }
1647
-
1648
1911
  // ../renderers/buymeacoffee/dist/index.js
1649
1912
  function render(p) {
1650
1913
  return { text: pasteSource(p) };
@@ -1658,6 +1921,235 @@ function mdText(text) {
1658
1921
  return text.replace(/[\\[\]]/g, "\\$&");
1659
1922
  }
1660
1923
 
1924
+ // ../artifact/dist/index.js
1925
+ var H1 = /^# /;
1926
+ var CREATED = /* @__PURE__ */ new WeakSet();
1927
+ function escapeRegExp(value) {
1928
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1929
+ }
1930
+ var SURFACE_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1931
+ function assertSurface(surface) {
1932
+ if (!SURFACE_SLUG.test(surface)) {
1933
+ throw new Error(`not a surface slug (D16): ${JSON.stringify(surface)} \u2014 the generic projection must know whose generated footer bounds the region`);
1934
+ }
1935
+ }
1936
+ function footerMarker(surface) {
1937
+ return new RegExp(`^<!-- generated by profile-as-code pasco build ${escapeRegExp(surface)}(?![\\w-])`);
1938
+ }
1939
+ function normalizeTransport(text) {
1940
+ return text.replace(/\r\n?/g, "\n").split("\n").map((line2) => line2.replace(/^\uFEFF+/, ""));
1941
+ }
1942
+ function projectArtifact(text, surface) {
1943
+ assertSurface(surface);
1944
+ const lines2 = normalizeTransport(text);
1945
+ const terminus = footerMarker(surface);
1946
+ const footer = lines2.findLastIndex((line2) => terminus.test(line2));
1947
+ const limit = footer === -1 ? lines2.length - 1 : footer;
1948
+ const header = lines2.findLastIndex((line2, index) => index <= limit && H1.test(line2));
1949
+ const bounded = header !== -1;
1950
+ const from = bounded ? header : 0;
1951
+ const to = bounded || footer === -1 ? limit : lines2.length - 1;
1952
+ const artifact = lines2.slice(from, to + 1);
1953
+ let first = 0;
1954
+ let last = artifact.length - 1;
1955
+ while (first <= last && artifact[first]?.trim() === "")
1956
+ first += 1;
1957
+ while (last >= first && artifact[last]?.trim() === "")
1958
+ last -= 1;
1959
+ return artifact.slice(first, last + 1).join("\n");
1960
+ }
1961
+ function createArtifactExtractor(surface) {
1962
+ assertSurface(surface);
1963
+ const extractor = Object.defineProperty((text) => projectArtifact(text, surface), "surface", { value: surface, enumerable: true });
1964
+ CREATED.add(extractor);
1965
+ return extractor;
1966
+ }
1967
+
1968
+ // ../github/dist/index.js
1969
+ var CREATED2 = /* @__PURE__ */ new WeakSet();
1970
+ function assertBinding(binding) {
1971
+ const { surface, linkPlatforms } = binding;
1972
+ if (surface === "" || !linkPlatforms.has(surface)) {
1973
+ throw new Error(`incoherent GitHub surface binding: ${JSON.stringify(surface)} is not in its own linkPlatforms \u2014 a surface whose own slug cannot name it would read "no-artifact-url" for an artifact it publishes`);
1974
+ }
1975
+ }
1976
+ var OWNER = /^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/;
1977
+ var OWNER_MAX = 39;
1978
+ function ownerSlug(value) {
1979
+ if (RESERVED_BY_PREFIX.has(value.toLowerCase()))
1980
+ return null;
1981
+ return value.length <= OWNER_MAX && OWNER.test(value) ? value : null;
1982
+ }
1983
+ function ownerFromHandle(handle) {
1984
+ if (handle === void 0)
1985
+ return null;
1986
+ return ownerSlug(handle.startsWith("@") ? handle.slice(1) : handle);
1987
+ }
1988
+ var route = (prefix, kind, why, observed) => Object.freeze({ prefix, kind, why, observed });
1989
+ var RESERVED_ROUTES = Object.freeze([
1990
+ // --- the route's next segment IS the account's own page ---------------------
1991
+ route("orgs", "owner-after-prefix", "GitHub's organization landing route: the page at /orgs/<org> IS the org's own page, so the owner is the segment after the prefix (the shipped F2 fix of #212).", "bare /orgs \u2192 404 (no index page) \xB7 /orgs/vercel \u2192 200 \u2192 github.com/vercel"),
1992
+ route("sponsors", "owner-after-prefix", "GitHub Sponsors: /sponsors/<account> is that account's sponsored profile and GitHub resolves it to the account's own page, so the owner is the segment after the prefix.", "/sponsors/<account> \u2192 200 \u2192 github.com/<account>; bare /sponsors \u2192 200 (the Sponsors landing at /open-source/sponsors)"),
1993
+ // --- GitHub's own discovery, marketplace and marketing pages ---------------
1994
+ route("topics", "no-owner", "GitHub Topics: /topics/<topic> is a hub aggregating many repositories, not an account's page.", "/topics/typescript \u2192 200 (a topic hub)"),
1995
+ route("collections", "no-owner", "GitHub Collections: /collections/<slug> is a curated list of repositories, not an account.", "/collections \u2192 200 (the collections index)"),
1996
+ route("marketplace", "no-owner", "GitHub Marketplace: /marketplace/<listing> is an App/Action listing page; a listing is not an account.", "bare /marketplace \u2192 400 (bot-gated) \xB7 /marketplace/actions/checkout \u2192 200 (an Action listing)"),
1997
+ route("apps", "no-owner", "GitHub Apps: /apps/<slug> is an App's public page; an App is not a user or an org, so no owner artifact exists for it.", "bare /apps \u2192 400 followed (bot-gated at /marketplace) \xB7 without `-L`: 302 \u2192 github.com/marketplace, which IS the route evidence"),
1998
+ route("trending", "no-owner", "GitHub Trending: GitHub's own leaderboard page.", "/trending \u2192 200"),
1999
+ route("explore", "no-owner", "GitHub Explore: GitHub's own discovery hub.", "/explore \u2192 200"),
2000
+ route("discover", "no-owner", "A GitHub discovery alias that resolves to Explore, not to an account.", "/discover \u2192 200 \u2192 github.com/explore"),
2001
+ route("search", "no-owner", "GitHub Search: a query page over the whole site, owned by no account.", "/search \u2192 200"),
2002
+ route("events", "no-owner", "GitHub's events/webinars index (a marketing page), not an account.", "/events \u2192 200 \u2192 github.com/resources/events"),
2003
+ route("education", "no-owner", "GitHub Education: GitHub's own program page.", "/education \u2192 200"),
2004
+ route("partners", "no-owner", "GitHub Partner Program: GitHub's own program page.", "/partners \u2192 200"),
2005
+ route("customers", "no-owner", "GitHub's customer-stories page, an alias of GitHub's own marketing surface.", "/customers \u2192 200 \u2192 github.com/customer-stories"),
2006
+ route("business", "no-owner", "GitHub's business/Enterprise marketing alias, not an account.", "/business \u2192 200 \u2192 github.com/enterprise"),
2007
+ route("enterprise", "no-owner", "GitHub Enterprise: GitHub's own product page.", "/enterprise \u2192 200"),
2008
+ route("enterprises", "no-owner", "GitHub Enterprise Cloud accounts live at /enterprises/<slug>: an enterprise account is not a user or org owner, and the route itself is GitHub's.", "bare /enterprises \u2192 404 signed out (the per-slug page is GitHub-gated)"),
2009
+ route("security", "no-owner", "GitHub Security: GitHub's own security hub.", "/security \u2192 200"),
2010
+ route("about", "no-owner", "GitHub About: GitHub's own page.", "/about \u2192 200"),
2011
+ route("pricing", "no-owner", "GitHub Pricing: GitHub's own plans page.", "/pricing \u2192 200"),
2012
+ route("features", "no-owner", "GitHub Features: GitHub's own product tour.", "/features \u2192 200"),
2013
+ route("contact", "no-owner", "GitHub Support: GitHub's own contact page.", "/contact \u2192 200 \u2192 help.github.com"),
2014
+ route("home", "no-owner", "GitHub's signed-in home page.", "/home \u2192 200"),
2015
+ route("copilot", "no-owner", "GitHub Copilot: GitHub's own product page.", "/copilot \u2192 200"),
2016
+ // --- the marketing, program and doc-hosted pages (review of PR #232, F1) ----
2017
+ // Admitted by the case-variant SIGNATURE, not by guessing: GitHub serves a
2018
+ // lowercase page whose `og:type` is not `profile`, while the all-caps spelling is
2019
+ // a 404 — a route, because an ACCOUNT answers in every casing (`/WWW` and
2020
+ // `/WWW`==`/www`, `/Profile`/`/PROFILE`, probed). The fresh review named 8 of
2021
+ // these; the same sweep found 18 more, and it is what keeps the live accounts it
2022
+ // touched (`www`, `actions`, `desktop`, `docs`, `dependabot`, `students`, …) OUT:
2023
+ // their uppercase spelling returns a profile. `observed` records both halves of
2024
+ // the signature for these rows; the sweep command is in the README.
2025
+ route("open-source", "no-owner", "GitHub's open-source program landing \u2014 also where the bare `/sponsors` redirect lands, which is why `/sponsors` alone is not an owner either.", "bare /open-source \u2192 200 (object) \xB7 /OPEN-SOURCE \u2192 404"),
2026
+ route("readme", "no-owner", "The README Project: GitHub's own editorial hub about maintainers.", "bare /readme \u2192 200 (object) \xB7 /README \u2192 404"),
2027
+ route("team", "no-owner", "GitHub for Teams: GitHub's own marketing page.", "bare /team \u2192 200 (object) \xB7 /TEAM \u2192 404"),
2028
+ route("solutions", "no-owner", "GitHub Solutions: GitHub's own by-industry landing page.", "bare /solutions \u2192 200 (object) \xB7 /SOLUTIONS \u2192 404"),
2029
+ route("why-github", "no-owner", "GitHub's 'why GitHub' marketing page.", "bare /why-github \u2192 200 (object) \xB7 /WHY-GITHUB \u2192 404"),
2030
+ route("trust-center", "no-owner", "GitHub Trust Center: GitHub's own compliance and assurance page.", "bare /trust-center \u2192 200 (object) \xB7 /TRUST-CENTER \u2192 404"),
2031
+ route("repositories", "no-owner", "A GitHub alias that resolves to the Trending leaderboard, not a repository index an owner could be read from.", "bare /repositories \u2192 200 \u2192 /trending \xB7 /REPOSITORIES \u2192 404"),
2032
+ route("users", "no-owner", "A GitHub alias to the signed-in home; there is no user index at the bare segment to read an owner from.", "bare /users \u2192 200 (object) \xB7 /USERS \u2192 404"),
2033
+ route("mobile", "no-owner", "GitHub Mobile: GitHub's own product page.", "bare /mobile \u2192 200 (object) \xB7 /MOBILE \u2192 404"),
2034
+ route("pages", "no-owner", "GitHub Pages: GitHub's own product site (pages.github.com).", "bare /pages \u2192 200 \u2192 pages.github.com \xB7 /PAGES \u2192 404"),
2035
+ route("wiki", "no-owner", "GitHub Wikis: GitHub's own feature page.", "bare /wiki \u2192 200 (object) \xB7 /WIKI \u2192 404"),
2036
+ route("discussions", "no-owner", "GitHub Discussions: the viewer's cross-repository discussions hub.", "bare /discussions \u2192 200 \u2192 login?return_to=/discussions \xB7 /DISCUSSIONS \u2192 404"),
2037
+ route("site-policy", "no-owner", "GitHub's site-policy corpus, served from docs.github.com.", "bare /site-policy \u2192 200 \u2192 docs.github.com/en/site-policy \xB7 /SITE-POLICY \u2192 404"),
2038
+ route("resources", "no-owner", "GitHub Resources: GitHub's own content hub.", "bare /resources \u2192 200 (object) \xB7 /RESOURCES \u2192 404"),
2039
+ route("guides", "no-owner", "GitHub Guides, served from docs.github.com.", "bare /guides \u2192 200 \u2192 docs.github.com \xB7 /GUIDES \u2192 404"),
2040
+ route("support", "no-owner", "GitHub Support, served from help.github.com.", "bare /support \u2192 200 \u2192 help.github.com \xB7 /SUPPORT \u2192 404"),
2041
+ route("status", "no-owner", "GitHub Status: GitHub's own availability page.", "bare /status \u2192 200 \xB7 /STATUS \u2192 404"),
2042
+ route("blog", "no-owner", "The GitHub Blog (github.blog).", "bare /blog \u2192 200 \u2192 github.blog \xB7 /BLOG \u2192 404"),
2043
+ route("nonprofit", "no-owner", "GitHub for Nonprofits, served from socialimpact.github.com.", "bare /nonprofit \u2192 200 \u2192 socialimpact.github.com \xB7 /NONPROFIT \u2192 404"),
2044
+ route("advisories", "no-owner", "The GitHub Advisory Database: GitHub's own security-advisories index.", "bare /advisories \u2192 200 (object) \xB7 /ADVISORIES \u2192 404"),
2045
+ route("spark", "no-owner", "GitHub Spark: GitHub's own product page.", "bare /spark \u2192 200 (object) \xB7 /SPARK \u2192 404"),
2046
+ route("press", "no-owner", "GitHub Press, served under /about/press.", "bare /press \u2192 200 \u2192 /about/press \xB7 /PRESS \u2192 404"),
2047
+ route("social-impact", "no-owner", "GitHub Social Impact: GitHub's own program page.", "bare /social-impact \u2192 200 (object) \xB7 /SOCIAL-IMPACT \u2192 404"),
2048
+ route("universe", "no-owner", "GitHub Universe: GitHub's own conference site (githubuniverse.com).", "bare /universe \u2192 200 \u2192 githubuniverse.com \xB7 /UNIVERSE \u2192 404"),
2049
+ route("sitemap", "no-owner", "GitHub's own sitemap index.", "bare /sitemap \u2192 200 \xB7 /SITEMAP \u2192 404"),
2050
+ route("terms", "no-owner", "GitHub's Terms of Service, served from docs.github.com.", "bare /terms \u2192 200 \u2192 docs.github.com/en/site-policy/github-terms/\u2026 \xB7 /TERMS \u2192 404"),
2051
+ // --- account and session routes -------------------------------------------
2052
+ route("settings", "no-owner", "Account settings are per-viewer, not a page belonging to a named account (/settings/profile is the canonical example from #227).", "/settings \u2192 200 \u2192 github.com/login?return_to=/settings"),
2053
+ route("account", "no-owner", "GitHub's own account entry point, an alias of settings.", "/account \u2192 200 \u2192 github.com/login?return_to=/account"),
2054
+ route("login", "no-owner", "GitHub's sign-in form.", "/login \u2192 200"),
2055
+ route("logout", "no-owner", "GitHub's sign-out action.", "/logout \u2192 200 \u2192 github.com/"),
2056
+ route("join", "no-owner", "GitHub's sign-up funnel.", "/join \u2192 403 \u2192 github.com/signup"),
2057
+ route("signup", "no-owner", "GitHub's sign-up form.", "/signup \u2192 403 (bot-gated)"),
2058
+ route("new", "no-owner", "GitHub's new-repository entry point for the viewer.", "/new \u2192 200 \u2192 github.com/login?return_to=/new"),
2059
+ route("notifications", "no-owner", "The viewer's notification inbox, not an account's page.", "/notifications \u2192 200 \u2192 github.com/login?return_to=/notifications"),
2060
+ route("dashboard", "no-owner", "The viewer's dashboard, not an account's page.", "/dashboard \u2192 200 \u2192 github.com/login?return_to=/dashboard"),
2061
+ route("stars", "no-owner", "DELIBERATE, and the one row whose evidence would also support owner-after: /stars/<account> IS that account's starred list. #227 decides null and this row follows it \u2014 the conservative direction (an honest no-artifact-url, never a wrong URL). Flipping the kind is one word of data plus a corpus case.", "/stars/<account> \u2192 200 (titled with the account); bare /stars \u2192 200 \u2192 github.com/login?return_to=/stars"),
2062
+ route("pulls", "no-owner", "The viewer's cross-repository pull-request inbox.", "/pulls \u2192 200 \u2192 github.com/login?return_to=/pulls"),
2063
+ route("issues", "no-owner", "The viewer's cross-repository issue inbox.", "bare /issues \u2192 404 signed out (the signed-in global list)"),
2064
+ route("codespaces", "no-owner", "The viewer's Codespaces hub.", "/codespaces \u2192 200 \u2192 github.com/login?return_to=/codespaces"),
2065
+ route("organizations", "no-owner", "GitHub's organization management routes (/organizations/<org>/settings/\u2026); the bare segment is not an account page.", "bare /organizations \u2192 404 signed out"),
2066
+ route("gists", "no-owner", "Gists live on a separate host; /gists is a GitHub redirect, not an account.", "/gists \u2192 200 \u2192 gist.github.com/starred")
2067
+ ]);
2068
+ var RESERVED_BY_PREFIX = new Map(RESERVED_ROUTES.map((row) => [row.prefix, row]));
2069
+ function decodeSegment(segment) {
2070
+ try {
2071
+ return decodeURIComponent(segment);
2072
+ } catch {
2073
+ return null;
2074
+ }
2075
+ }
2076
+ function ownerFromUrl(url) {
2077
+ let parsed;
2078
+ try {
2079
+ parsed = new URL(url);
2080
+ } catch {
2081
+ return null;
2082
+ }
2083
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
2084
+ return null;
2085
+ if (parsed.hostname !== "github.com" && parsed.hostname !== "www.github.com")
2086
+ return null;
2087
+ const segments = parsed.pathname.split("/").filter((part) => part !== "");
2088
+ const first = segments[0];
2089
+ if (first === void 0)
2090
+ return null;
2091
+ const decodedFirst = decodeSegment(first);
2092
+ if (decodedFirst === null)
2093
+ return null;
2094
+ const reserved = RESERVED_BY_PREFIX.get(decodedFirst.toLowerCase());
2095
+ if (reserved !== void 0) {
2096
+ if (reserved.kind === "no-owner")
2097
+ return null;
2098
+ const ownerSegment = segments[1];
2099
+ if (ownerSegment === void 0)
2100
+ return null;
2101
+ const decodedOwner = decodeSegment(ownerSegment);
2102
+ return decodedOwner === null ? null : ownerSlug(decodedOwner);
2103
+ }
2104
+ return ownerSlug(decodedFirst);
2105
+ }
2106
+ function resolveGithubOwner(profile, binding) {
2107
+ assertBinding(binding);
2108
+ const { surface, linkPlatforms } = binding;
2109
+ for (const link of profile.identity.links ?? []) {
2110
+ if (link.platform !== void 0 && !linkPlatforms.has(link.platform))
2111
+ continue;
2112
+ const owner = ownerFromUrl(link.url);
2113
+ if (owner !== null)
2114
+ return owner;
2115
+ }
2116
+ for (const handle of profile.socialHandles ?? []) {
2117
+ if (handle.platform !== surface || handle.status !== "claimed")
2118
+ continue;
2119
+ const owner = (handle.url !== void 0 ? ownerFromUrl(handle.url) : null) ?? ownerFromHandle(handle.handle);
2120
+ if (owner !== null)
2121
+ return owner;
2122
+ }
2123
+ return null;
2124
+ }
2125
+ function createGithubOwnerResolver(binding) {
2126
+ assertBinding(binding);
2127
+ const linkPlatforms = Object.freeze([...binding.linkPlatforms].sort());
2128
+ const declared = {
2129
+ surface: binding.surface,
2130
+ linkPlatforms: new Set(linkPlatforms)
2131
+ };
2132
+ const resolver = Object.defineProperty((profile) => resolveGithubOwner(profile, declared), "surface", { value: binding.surface, enumerable: true });
2133
+ Object.defineProperty(resolver, "linkPlatforms", { value: linkPlatforms, enumerable: true });
2134
+ CREATED2.add(resolver);
2135
+ return resolver;
2136
+ }
2137
+
2138
+ // ../renderers/github-org/dist/artifact.js
2139
+ var fetchHosts = ["raw.githubusercontent.com"];
2140
+ var GITHUB_LINK_PLATFORMS = /* @__PURE__ */ new Set(["github", "github-org"]);
2141
+ var githubOwner = createGithubOwnerResolver({
2142
+ surface: "github-org",
2143
+ linkPlatforms: GITHUB_LINK_PLATFORMS
2144
+ });
2145
+ function artifactUrl(profile) {
2146
+ const org = githubOwner(profile);
2147
+ if (org === null)
2148
+ return null;
2149
+ return `https://raw.githubusercontent.com/${org}/.github/HEAD/profile/README.md`;
2150
+ }
2151
+ var extractArtifact = createArtifactExtractor("github-org");
2152
+
1661
2153
  // ../renderers/github-org/dist/index.js
1662
2154
  var BADGE = {
1663
2155
  github: { color: "181717", logo: "github", label: "GitHub" },
@@ -1778,6 +2270,21 @@ function render2(p, opts = {}) {
1778
2270
  ` };
1779
2271
  }
1780
2272
 
2273
+ // ../renderers/github-readme/dist/artifact.js
2274
+ var fetchHosts2 = ["raw.githubusercontent.com"];
2275
+ var GITHUB_LINK_PLATFORMS2 = /* @__PURE__ */ new Set(["github", "github-readme"]);
2276
+ var githubOwner2 = createGithubOwnerResolver({
2277
+ surface: "github-readme",
2278
+ linkPlatforms: GITHUB_LINK_PLATFORMS2
2279
+ });
2280
+ function artifactUrl2(profile) {
2281
+ const owner = githubOwner2(profile);
2282
+ if (owner === null)
2283
+ return null;
2284
+ return `https://raw.githubusercontent.com/${owner}/${owner}/HEAD/README.md`;
2285
+ }
2286
+ var extractArtifact2 = createArtifactExtractor("github-readme");
2287
+
1781
2288
  // ../renderers/github-readme/dist/index.js
1782
2289
  function credentialLine(c) {
1783
2290
  const years = c.issued !== void 0 ? `, ${c.issued.slice(0, 4)}${c.expires !== void 0 ? `\u2013${c.expires.slice(0, 4)}` : ""}` : "";
@@ -1848,6 +2355,19 @@ function projectWeight(p, skills) {
1848
2355
  }
1849
2356
  return best;
1850
2357
  }
2358
+ var STATUS_RANK = {
2359
+ active: 0,
2360
+ maintained: 1,
2361
+ experimental: 2,
2362
+ planning: 3,
2363
+ archived: 4
2364
+ };
2365
+ function statusRank(status) {
2366
+ return status === void 0 ? 5 : STATUS_RANK[status] ?? 5;
2367
+ }
2368
+ function byteOrder(a, b) {
2369
+ return a < b ? -1 : a > b ? 1 : 0;
2370
+ }
1851
2371
  function render3(p, opts = {}) {
1852
2372
  const lang = opts.lang ?? p.lang;
1853
2373
  if (opts.lang !== void 0 && opts.lang !== p.lang && p.structuredData?.jsonLd !== void 0) {
@@ -1876,7 +2396,7 @@ function render3(p, opts = {}) {
1876
2396
  out.push("", badges.join(" "));
1877
2397
  if (p.statement !== void 0)
1878
2398
  out.push("", `**${p.statement}**`);
1879
- const projects = [...p.projects].sort((a, b) => projectWeight(b, p.skills) - projectWeight(a, p.skills) || a.name.localeCompare(b.name));
2399
+ const projects = [...p.projects].sort((a, b) => statusRank(a.status) - statusRank(b.status) || projectWeight(b, p.skills) - projectWeight(a, p.skills) || byteOrder(a.name, b.name));
1880
2400
  if (projects.length > 0) {
1881
2401
  out.push("", "## Projects");
1882
2402
  for (const project of projects.slice(0, 6)) {
@@ -2025,10 +2545,124 @@ function line(text) {
2025
2545
  process.stdout.write(`${text}
2026
2546
  `);
2027
2547
  }
2548
+ function warn2(text) {
2549
+ if (jsonMode) return;
2550
+ process.stderr.write(`warning: ${text}
2551
+ `);
2552
+ }
2553
+
2554
+ // src/publication.ts
2555
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
2556
+ import { join as join5 } from "path";
2557
+
2558
+ // src/limits.ts
2559
+ function limitRows(profile) {
2560
+ return [
2561
+ ...(profile.headlines ?? []).map((h) => ({
2562
+ label: `headline ${h.platform}/${h.lang}`,
2563
+ platform: h.platform,
2564
+ surface: "headline",
2565
+ text: h.text,
2566
+ path: `headlines:${h.platform}:${h.lang}`
2567
+ })),
2568
+ ...(profile.bios ?? []).map((b) => ({
2569
+ label: `bio ${b.platform}/${b.lang}/${b.length}`,
2570
+ platform: b.platform,
2571
+ surface: "bio",
2572
+ text: b.text,
2573
+ path: `bios:${b.platform}:${b.lang}:${b.length}`
2574
+ }))
2575
+ ];
2576
+ }
2577
+ function limitFindings(profile, checks, platform) {
2578
+ const rows = limitRows(profile);
2579
+ const findings = [];
2580
+ for (const check of checks) {
2581
+ if (check.rule !== "max-chars" && check.rule !== "min-chars") continue;
2582
+ if (platform !== void 0 && check.platform !== "all" && check.platform !== platform) continue;
2583
+ if (typeof check.value !== "number" || !Number.isFinite(check.value)) {
2584
+ findings.push({
2585
+ severity: "warn",
2586
+ code: check.rule,
2587
+ message: `check ${check.id ?? "(no id)"} has a non-numeric ${check.rule} value \u2014 skipped`,
2588
+ path: `checks:${check.id ?? "?"}`
2589
+ });
2590
+ continue;
2591
+ }
2592
+ for (const row of rows) {
2593
+ if (platform !== void 0 && row.platform !== platform) continue;
2594
+ if (check.platform !== "all" && check.platform !== row.platform) continue;
2595
+ const surface = check.surface ?? "both";
2596
+ if (surface !== "both" && surface !== row.surface) continue;
2597
+ const length = [...row.text].length;
2598
+ const over = check.rule === "max-chars" && length > check.value;
2599
+ const under = check.rule === "min-chars" && length < check.value;
2600
+ if (!over && !under) continue;
2601
+ findings.push({
2602
+ severity: "warn",
2603
+ code: check.rule,
2604
+ message: `${row.label} has ${length} chars, ${over ? "over max" : "under min"} ${check.value}${check.note === void 0 ? "" : ` \u2014 ${check.note}`}`,
2605
+ path: row.path
2606
+ });
2607
+ }
2608
+ }
2609
+ return findings;
2610
+ }
2611
+
2612
+ // src/variant-store.ts
2613
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
2614
+ import { join as join3 } from "path";
2615
+ var SAFE_VARIANT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
2616
+ function isSafeVariantId(id) {
2617
+ return SAFE_VARIANT_ID.test(id) && !id.includes("..");
2618
+ }
2619
+ function variantBodyPath(workspace, platform, variantId) {
2620
+ return join3(workspace, "profiles", platform, "variants", `${variantId}.md`);
2621
+ }
2622
+ function hasVariantBody(workspace, platform, variantId) {
2623
+ if (!isSafeVariantId(variantId)) return false;
2624
+ try {
2625
+ const path = variantBodyPath(workspace, platform, variantId);
2626
+ if (!statSync2(path).isFile()) return false;
2627
+ return readFileSync3(path, "utf8").trim().length > 0;
2628
+ } catch {
2629
+ return false;
2630
+ }
2631
+ }
2632
+ function resolveVariantBody(workspace, slice) {
2633
+ const active = slice.active;
2634
+ if (active === void 0) return {};
2635
+ const id = active.variantId;
2636
+ if (!isSafeVariantId(id)) {
2637
+ return {
2638
+ warning: `active variant id ${JSON.stringify(id)} is not a safe path segment \u2014 deterministic fallback`
2639
+ };
2640
+ }
2641
+ if (active.lang !== slice.lang) {
2642
+ return {
2643
+ warning: `active variant ${id} is for ${active.lang}, building ${slice.lang} \u2014 deterministic fallback`
2644
+ };
2645
+ }
2646
+ const path = variantBodyPath(workspace, slice.platform, id);
2647
+ try {
2648
+ if (!statSync2(path).isFile()) {
2649
+ return {
2650
+ warning: `active variant ${id} at ${path} is not a regular file \u2014 deterministic fallback`
2651
+ };
2652
+ }
2653
+ const text = readFileSync3(path, "utf8");
2654
+ if (text.trim() === "") {
2655
+ return { warning: `active variant ${id} is empty at ${path} \u2014 deterministic fallback` };
2656
+ }
2657
+ return { id, text };
2658
+ } catch {
2659
+ return { warning: `active variant ${id} has no body at ${path} \u2014 deterministic fallback` };
2660
+ }
2661
+ }
2028
2662
 
2029
2663
  // src/workspace.ts
2030
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2031
- import { dirname, join as join3, resolve as resolve2 } from "path";
2664
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
2665
+ import { dirname, join as join4, resolve as resolve2 } from "path";
2032
2666
  var WORKSPACE_NAME = "profile-data";
2033
2667
  function sanitizeCompletenessPolicy(raw) {
2034
2668
  if (raw === void 0) return {};
@@ -2042,6 +2676,45 @@ function sanitizeCompletenessPolicy(raw) {
2042
2676
  ...items.length > 0 ? { items } : {}
2043
2677
  };
2044
2678
  }
2679
+ function sanitizeSelectionPolicy(raw) {
2680
+ if (raw === void 0) return void 0;
2681
+ const strings = (v) => {
2682
+ if (!Array.isArray(v)) return void 0;
2683
+ const kept = v.filter((x) => typeof x === "string" && x !== "");
2684
+ return kept.length > 0 ? kept : void 0;
2685
+ };
2686
+ const rule = (r) => {
2687
+ if (r === void 0) return void 0;
2688
+ const mode = r.mode === "all" || r.mode === "tags" ? r.mode : void 0;
2689
+ const tags = strings(r.tags);
2690
+ const excludeTags = strings(r.excludeTags);
2691
+ const excludeKeys = strings(r.excludeKeys);
2692
+ const platform = typeof r.platform === "string" && r.platform !== "" ? r.platform : void 0;
2693
+ const lang = typeof r.lang === "string" && r.lang !== "" ? r.lang : void 0;
2694
+ if (mode === void 0 && tags === void 0 && excludeTags === void 0 && excludeKeys === void 0 && platform === void 0 && lang === void 0) {
2695
+ return void 0;
2696
+ }
2697
+ return {
2698
+ ...mode === void 0 ? {} : { mode },
2699
+ ...tags === void 0 ? {} : { tags },
2700
+ ...excludeTags === void 0 ? {} : { excludeTags },
2701
+ ...excludeKeys === void 0 ? {} : { excludeKeys },
2702
+ ...platform === void 0 ? {} : { platform },
2703
+ ...lang === void 0 ? {} : { lang }
2704
+ };
2705
+ };
2706
+ const views = {};
2707
+ for (const [name, r] of Object.entries(raw.views ?? {})) {
2708
+ const clean = rule(r);
2709
+ if (clean !== void 0) views[name] = clean;
2710
+ }
2711
+ const def = rule(raw.default);
2712
+ if (def === void 0 && Object.keys(views).length === 0) return void 0;
2713
+ return {
2714
+ ...def === void 0 ? {} : { default: def },
2715
+ ...Object.keys(views).length > 0 ? { views } : {}
2716
+ };
2717
+ }
2045
2718
  var ConfigError = class extends Error {
2046
2719
  constructor(message) {
2047
2720
  super(message);
@@ -2049,9 +2722,9 @@ var ConfigError = class extends Error {
2049
2722
  }
2050
2723
  };
2051
2724
  function readPascorc(dir) {
2052
- const pascorcPath = join3(dir, ".pascorc");
2725
+ const pascorcPath = join4(dir, ".pascorc");
2053
2726
  if (!existsSync3(pascorcPath)) {
2054
- const legacy = join3(dir, ".pacrc");
2727
+ const legacy = join4(dir, ".pacrc");
2055
2728
  if (existsSync3(legacy)) {
2056
2729
  throw new ConfigError(
2057
2730
  `legacy .pacrc found at ${legacy} \u2014 rename it to .pascorc (the bin was renamed pac \u2192 pasco; issue #38)`
@@ -2061,7 +2734,7 @@ function readPascorc(dir) {
2061
2734
  }
2062
2735
  let raw;
2063
2736
  try {
2064
- raw = readFileSync3(pascorcPath, "utf8");
2737
+ raw = readFileSync4(pascorcPath, "utf8");
2065
2738
  } catch (err) {
2066
2739
  throw new ConfigError(`cannot read ${pascorcPath}: ${err.message}`);
2067
2740
  }
@@ -2093,6 +2766,155 @@ function defaultsLang(pascorc, requested) {
2093
2766
  return pascorc.defaults?.lang ?? "en";
2094
2767
  }
2095
2768
 
2769
+ // src/publication.ts
2770
+ function artifactPath(workspace, surface) {
2771
+ return RENDERERS[surface] === void 0 ? join5(workspace, "profiles", `${surface}.md`) : join5(workspace, "profiles", surface, "README.md");
2772
+ }
2773
+ function builtArtifact(workspace, surface) {
2774
+ const path = artifactPath(workspace, surface);
2775
+ return existsSync4(path) ? readFileSync5(path, "utf8") : void 0;
2776
+ }
2777
+ function publishedSurfaces(workspace) {
2778
+ const dir = join5(workspace, "profiles");
2779
+ if (!existsSync4(dir)) return [];
2780
+ const known = new Set(PLATFORMS);
2781
+ const out = [];
2782
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
2783
+ const surface = entry.isDirectory() ? existsSync4(join5(dir, entry.name, "README.md")) ? entry.name : void 0 : entry.name.endsWith(".md") ? entry.name.slice(0, -".md".length) : void 0;
2784
+ if (surface !== void 0 && known.has(surface)) out.push(surface);
2785
+ }
2786
+ return out;
2787
+ }
2788
+ function createArtifactResolver(args) {
2789
+ let assets;
2790
+ const media = () => assets ??= collectMedia(args.profile, args.workspace).assets;
2791
+ const policy = sanitizeCompletenessPolicy(args.completenessPolicy);
2792
+ const selection = sanitizeSelectionPolicy(args.selectionPolicy);
2793
+ return {
2794
+ built: (surface) => builtArtifact(args.workspace, surface),
2795
+ render: (surface, lang) => {
2796
+ if (surface === "github-org" && args.profile.flavor !== "brand") return void 0;
2797
+ const renderer = RENDERERS[surface];
2798
+ const slice = compile(args.profile, surface, lang, {
2799
+ ...policy,
2800
+ ...selection === void 0 ? {} : { selection, view: surface }
2801
+ });
2802
+ const variant = resolveVariantBody(args.workspace, slice);
2803
+ if (variant.text !== void 0) return variant.text;
2804
+ return renderer === void 0 ? pasteSource(slice) : renderer(slice, media()).text;
2805
+ }
2806
+ };
2807
+ }
2808
+ function candidateLangs(primary) {
2809
+ return [primary, ...LANGS.filter((lang) => lang !== primary)];
2810
+ }
2811
+ var GENERATED_COMPLETENESS = /^Profile completeness: -?\d+(?:\.\d+)?% — \d+\/\d+$/;
2812
+ function factIdentity(text) {
2813
+ return text.split("\n").filter((line2) => !GENERATED_COMPLETENESS.test(line2)).join("\n");
2814
+ }
2815
+ function artifactTruth(resolver, surface, langs) {
2816
+ const built = resolver.built(surface);
2817
+ if (built === void 0) return "absent";
2818
+ const identity = factIdentity(built);
2819
+ for (const lang of langs) {
2820
+ const render9 = resolver.render(surface, lang);
2821
+ if (render9 !== void 0 && factIdentity(render9) === identity) return "matches-SSOT";
2822
+ }
2823
+ return "diverges";
2824
+ }
2825
+ function driftingSurfaces(reportData) {
2826
+ const drifting = /* @__PURE__ */ new Set();
2827
+ for (const unit of reportData.units) {
2828
+ for (const [surface, cert] of Object.entries(unit.certification?.surfaces ?? {})) {
2829
+ if (cert.state === "out-of-band-drift") drifting.add(surface);
2830
+ }
2831
+ }
2832
+ return drifting;
2833
+ }
2834
+ function ledgerSurfaces(reportData) {
2835
+ const out = {};
2836
+ for (const unit of reportData.units) {
2837
+ for (const [surface, state] of Object.entries(unit.surfaces)) {
2838
+ const entry = out[surface] ?? { state: "in-sync", recorded: 0, stale: 0 };
2839
+ entry.recorded += 1;
2840
+ if (state === "stale") {
2841
+ entry.state = "stale";
2842
+ entry.stale += 1;
2843
+ }
2844
+ out[surface] = entry;
2845
+ }
2846
+ }
2847
+ return out;
2848
+ }
2849
+ function isRowFinding(finding2) {
2850
+ return finding2.path.startsWith("headlines:") || finding2.path.startsWith("bios:");
2851
+ }
2852
+ function axisFindings(findings) {
2853
+ return findings.filter((finding2) => !isRowFinding(finding2) || finding2.code === "max-chars");
2854
+ }
2855
+ function publicationTruth(args) {
2856
+ const resolver = createArtifactResolver({
2857
+ workspace: args.workspace,
2858
+ profile: args.profile,
2859
+ completenessPolicy: args.completenessPolicy,
2860
+ selectionPolicy: args.selectionPolicy
2861
+ });
2862
+ const ledger = ledgerSurfaces(args.reportData);
2863
+ const drifting = driftingSurfaces(args.reportData);
2864
+ const surfaces = [.../* @__PURE__ */ new Set([...Object.keys(ledger), ...publishedSurfaces(args.workspace)])].filter((s) => PLATFORMS.includes(s)).sort();
2865
+ const langs = candidateLangs(args.lang);
2866
+ return surfaces.map((surface) => {
2867
+ const ledgerEntry = ledger[surface];
2868
+ const findings = axisFindings(limitFindings(args.profile, args.checks, surface));
2869
+ const local = artifactTruth(resolver, surface, langs);
2870
+ const drifted = drifting.has(surface);
2871
+ const artifact = drifted || local === "diverges" ? "diverges" : local === "absent" ? "absent" : "matches-SSOT";
2872
+ const artifactReason = artifact === "matches-SSOT" ? void 0 : drifted ? "live-drift" : local === "absent" ? "no-artifact" : "local-mismatch";
2873
+ return {
2874
+ surface,
2875
+ sync: ledgerEntry?.state ?? "never",
2876
+ artifact,
2877
+ ...artifactReason === void 0 ? {} : { artifactReason },
2878
+ limits: findings.some((f) => isRowFinding(f)) ? "over(cap)" : "ok",
2879
+ units: { recorded: ledgerEntry?.recorded ?? 0, stale: ledgerEntry?.stale ?? 0 },
2880
+ findings
2881
+ };
2882
+ });
2883
+ }
2884
+ function publicationOfCaller(args, profile, reportData, defaultChecks) {
2885
+ const checks = profile.checks.length > 0 ? profile.checks : defaultChecks;
2886
+ return publicationTruth({
2887
+ workspace: args.workspace,
2888
+ profile,
2889
+ reportData,
2890
+ checks,
2891
+ lang: args.lang ?? "en",
2892
+ completenessPolicy: args.completenessPolicy,
2893
+ selectionPolicy: args.selectionPolicy
2894
+ }).filter((t) => args.platformArg === void 0 || t.surface === args.platformArg);
2895
+ }
2896
+ function publicationCoverageLine(publication) {
2897
+ const total = publication.length;
2898
+ const matching = publication.filter((t) => t.artifact === "matches-SSOT").length;
2899
+ const over = publication.filter((t) => t.limits === "over(cap)").length;
2900
+ const diverging = publication.filter((t) => t.artifact === "diverges").length;
2901
+ const absent = publication.filter((t) => t.artifact === "absent").length;
2902
+ const noun = total === 1 ? "surface" : "surfaces";
2903
+ const verb = total === 1 ? "matches" : "match";
2904
+ return [
2905
+ `${matching}/${total} ${noun} ${verb} the SSOT`,
2906
+ `${over} over cap`,
2907
+ ...diverging > 0 ? [`${diverging} diverged`] : [],
2908
+ ...absent > 0 ? [`${absent} absent`] : []
2909
+ ].join(" \xB7 ");
2910
+ }
2911
+ function publicationCoverageOrNone(publication) {
2912
+ return publication.length === 0 ? "no surface published yet" : publicationCoverageLine(publication);
2913
+ }
2914
+ function publicationLine(truth) {
2915
+ return `${truth.surface}: ${truth.sync} \xB7 artifact: ${truth.artifact} \xB7 limits: ${truth.limits} \xB7 units: ${truth.units.recorded} recorded${truth.units.stale > 0 ? ` (${truth.units.stale} stale)` : ""}`;
2916
+ }
2917
+
2096
2918
  // src/commands/build.ts
2097
2919
  function runBuild(args) {
2098
2920
  let profile;
@@ -2113,36 +2935,128 @@ function runBuild(args) {
2113
2935
  }
2114
2936
  throw err;
2115
2937
  }
2116
- const platforms = args.platformArg !== void 0 ? [parsePlatform(args.platformArg)] : [...PLATFORMS];
2117
- const lang = parseLang(defaultsLang(args.pascorc, args.langArg));
2938
+ const selectionPolicy = sanitizeSelectionPolicy(args.pascorc.selection);
2939
+ const viewName = args.viewArg;
2940
+ let viewRule;
2941
+ if (viewName !== void 0) {
2942
+ viewRule = selectionPolicy?.views?.[viewName] ?? selectionPolicy?.default;
2943
+ if (viewRule === void 0) {
2944
+ throw new ConfigError(
2945
+ `unknown view "${viewName}" \u2014 declare it in .pascorc "selection.views" (D22/\xA721), or build the platform positionally: pasco build <platform>`
2946
+ );
2947
+ }
2948
+ }
2949
+ const positionalPlatform = args.platformArg !== void 0 ? parsePlatform(args.platformArg) : void 0;
2950
+ let platform = positionalPlatform;
2951
+ if (viewRule?.platform !== void 0) {
2952
+ const declared = viewRule.platform;
2953
+ if (!PLATFORMS.includes(declared)) {
2954
+ throw new ConfigError(
2955
+ `view "${viewName}" declares unknown platform "${declared}" (allowed: ${PLATFORMS.join(", ")})`
2956
+ );
2957
+ }
2958
+ const viewPlatform = declared;
2959
+ if (positionalPlatform !== void 0 && positionalPlatform !== viewPlatform) {
2960
+ throw new ConfigError(
2961
+ `view "${viewName}" targets "${viewPlatform}" but the command names "${positionalPlatform}" \u2014 drop one`
2962
+ );
2963
+ }
2964
+ platform = viewPlatform;
2965
+ }
2966
+ if (viewName !== void 0 && platform === void 0) {
2967
+ throw new ConfigError(
2968
+ `view "${viewName}" declares no platform \u2014 add "platform" to its rule or name one: pasco build <platform> --view ${viewName}`
2969
+ );
2970
+ }
2971
+ const platforms = platform !== void 0 ? [platform] : [...PLATFORMS];
2972
+ const lang = parseLang(defaultsLang(args.pascorc, args.langArg ?? viewRule?.lang));
2118
2973
  const completenessPolicy = sanitizeCompletenessPolicy(args.pascorc.completeness);
2974
+ const selection = selectionPolicy;
2119
2975
  const slices = platforms.map(
2120
- (p) => compile(profile, p, lang, completenessPolicy)
2976
+ (p) => compile(profile, p, lang, {
2977
+ ...completenessPolicy,
2978
+ ...selection === void 0 ? {} : { selection, view: viewName ?? p }
2979
+ })
2121
2980
  );
2981
+ const selFindings = slices.flatMap(
2982
+ (s) => (s.selection?.findings ?? []).map((f) => ({ platform: s.platform, ...f }))
2983
+ );
2984
+ const selErrors = selFindings.filter((f) => f.severity === "error");
2985
+ if (selErrors.length > 0) {
2986
+ return report("build", false, EXIT.FAIL, {
2987
+ error: {
2988
+ kind: "selection",
2989
+ message: `${selErrors.length} selection error(s) \u2014 ${selErrors[0]?.message}`
2990
+ },
2991
+ data: { selection: selFindings }
2992
+ });
2993
+ }
2122
2994
  const bundle = collectMedia(profile, args.workspace);
2123
2995
  const written = [];
2124
2996
  const skipped = [];
2997
+ const warnings = [];
2998
+ const renderedVariants = /* @__PURE__ */ new Map();
2125
2999
  for (const s of slices) {
2126
- const renderer = RENDERERS[s.platform];
2127
- if (renderer === void 0) continue;
2128
3000
  if (s.platform === "github-org" && profile.flavor !== "brand") continue;
3001
+ const variant = resolveVariantBody(args.workspace, s);
3002
+ if (variant.warning !== void 0) {
3003
+ warnings.push({ platform: s.platform, reason: variant.warning });
3004
+ }
3005
+ if (variant.text !== void 0 && variant.id !== void 0) {
3006
+ const renderer2 = RENDERERS[s.platform];
3007
+ const path2 = artifactPath(args.workspace, s.platform);
3008
+ if (renderer2 !== void 0) {
3009
+ rmSync(join6(args.workspace, "profiles", `${s.platform}.md`), { force: true });
3010
+ rmSync(join6(args.workspace, "profiles", s.platform, "assets"), {
3011
+ recursive: true,
3012
+ force: true
3013
+ });
3014
+ }
3015
+ mkdirSync(dirname2(path2), { recursive: true });
3016
+ writeFileSync(path2, variant.text);
3017
+ renderedVariants.set(s.platform, variant.id);
3018
+ written.push({ platform: s.platform, path: path2, variant: variant.id });
3019
+ continue;
3020
+ }
3021
+ const renderer = RENDERERS[s.platform];
3022
+ if (renderer === void 0) {
3023
+ const path2 = artifactPath(args.workspace, s.platform);
3024
+ const text = pasteSource(s);
3025
+ if (text === "") {
3026
+ skipped.push({
3027
+ platform: s.platform,
3028
+ reason: "empty paste source \u2014 no bio row for this platform+lang; platform output removed"
3029
+ });
3030
+ rmSync(path2, { force: true });
3031
+ continue;
3032
+ }
3033
+ mkdirSync(dirname2(path2), { recursive: true });
3034
+ writeFileSync(path2, text);
3035
+ written.push({ platform: s.platform, path: path2 });
3036
+ continue;
3037
+ }
3038
+ rmSync(join6(args.workspace, "profiles", `${s.platform}.md`), { force: true });
2129
3039
  const result = renderer(s, bundle.assets);
2130
- const dir = join4(args.workspace, "profiles", s.platform);
3040
+ const path = artifactPath(args.workspace, s.platform);
3041
+ const dir = dirname2(path);
2131
3042
  if (result.text === "") {
2132
3043
  skipped.push({
2133
3044
  platform: s.platform,
2134
3045
  reason: "empty render \u2014 no bios row for this platform+lang; platform output removed"
2135
3046
  });
2136
- rmSync(dir, { recursive: true, force: true });
3047
+ const variantsDir = join6(dir, "variants");
3048
+ const keepVariants = existsSync5(variantsDir);
3049
+ rmSync(join6(dir, "README.md"), { force: true });
3050
+ rmSync(join6(dir, "assets"), { recursive: true, force: true });
3051
+ if (!keepVariants) rmSync(dir, { recursive: true, force: true });
2137
3052
  continue;
2138
3053
  }
2139
3054
  mkdirSync(dir, { recursive: true });
2140
- const path = join4(dir, "README.md");
2141
3055
  writeFileSync(path, result.text);
2142
3056
  let bundled = 0;
2143
3057
  const referenced = result.assets ?? [];
2144
3058
  if (referenced.length > 0) {
2145
- const assetsDir = join4(dir, "assets");
3059
+ const assetsDir = join6(dir, "assets");
2146
3060
  rmSync(assetsDir, { recursive: true, force: true });
2147
3061
  const byTarget = new Map(bundle.assets.map((a) => [a.target, a]));
2148
3062
  const copied = referenced.map((target) => {
@@ -2152,13 +3066,13 @@ function runBuild(args) {
2152
3066
  `renderer "${s.platform}" declared asset "${target}" without a bundle entry \u2014 renderers may only reference collectMedia() targets (M8 contract)`
2153
3067
  );
2154
3068
  }
2155
- const dest = join4(assetsDir, ...asset.target.split("/").slice(1));
3069
+ const dest = join6(assetsDir, ...asset.target.split("/").slice(1));
2156
3070
  mkdirSync(dirname2(dest), { recursive: true });
2157
3071
  copyFileSync(asset.source, dest);
2158
3072
  return asset;
2159
3073
  });
2160
3074
  writeFileSync(
2161
- join4(assetsDir, "manifest.json"),
3075
+ join6(assetsDir, "manifest.json"),
2162
3076
  `${JSON.stringify(mediaManifest(s.platform, s.lang, copied), null, 2)}
2163
3077
  `
2164
3078
  );
@@ -2184,7 +3098,9 @@ function runBuild(args) {
2184
3098
  })),
2185
3099
  written,
2186
3100
  skipped,
2187
- media: bundle.findings
3101
+ warnings,
3102
+ media: bundle.findings,
3103
+ selection: selFindings
2188
3104
  }
2189
3105
  });
2190
3106
  }
@@ -2194,7 +3110,9 @@ function runBuild(args) {
2194
3110
  [${s.platform}]`);
2195
3111
  if (s.statement !== void 0) line(` statement: ${s.statement}`);
2196
3112
  if (s.active !== void 0)
2197
- line(` active: ${s.active.variantId} (chosen ${s.active.chosenAt})`);
3113
+ line(
3114
+ ` active: ${s.active.variantId} (chosen ${s.active.chosenAt})${renderedVariants.get(s.platform) !== void 0 ? " \u2014 rendered" : " \u2014 deterministic fallback"}`
3115
+ );
2198
3116
  for (const b of s.bios) line(` bio (${b.length}): ${b.text}`);
2199
3117
  for (const h of s.headlines) line(` headline: ${h.text}`);
2200
3118
  if (s.bios.length === 0 && s.headlines.length === 0)
@@ -2204,21 +3122,64 @@ function runBuild(args) {
2204
3122
  if (written.length > 0) {
2205
3123
  section("deploy artifacts written");
2206
3124
  for (const w of written)
2207
- line(` ${w.path}${w.assets !== void 0 ? ` (+${w.assets} asset(s) in assets/)` : ""}`);
3125
+ line(
3126
+ ` ${w.path}${w.assets !== void 0 ? ` (+${w.assets} asset(s) in assets/)` : ""}${w.variant !== void 0 ? ` (variant ${w.variant})` : ""}`
3127
+ );
2208
3128
  }
2209
3129
  if (skipped.length > 0) {
2210
3130
  section(`empty renders removed (${skipped.length}) \u2014 pure profiles/ (\xA715.1)`);
2211
3131
  for (const k of skipped) line(` [${k.platform}] ${k.reason}`);
2212
3132
  }
3133
+ if (warnings.length > 0) {
3134
+ section(`variant warnings (${warnings.length})`);
3135
+ for (const w of warnings) line(` [${w.platform}] ${w.reason}`);
3136
+ }
2213
3137
  if (bundle.findings.length > 0) {
2214
3138
  section(`media pipeline (M8/\xA718.6): ${bundle.findings.length} finding(s)`);
2215
3139
  for (const f of bundle.findings) line(` ${f.severity}: [${f.code}] ${f.message} (${f.path})`);
2216
3140
  }
3141
+ if (selFindings.length > 0) {
3142
+ section(`fact selection (D22/\xA721): ${selFindings.length} finding(s)`);
3143
+ for (const f of selFindings) line(` ${f.severity}: [${f.code}] [${f.platform}] ${f.message}`);
3144
+ }
2217
3145
  return EXIT.OK;
2218
3146
  }
2219
3147
 
3148
+ // src/default-checks.ts
3149
+ import { parse } from "yaml";
3150
+ var DEFAULT_CHECKS_YAML = `# checks.yaml \u2014 conservative offline char caps (#240).
3151
+ # Every row carries its source, date and provenance tier in note; [?] means the
3152
+ # number is curated/third-party, not the platform's own documented limit.
3153
+ # The rule engine is offline: pasco check warns, it never fails (D12).
3154
+ - id: github-readme-headline-max
3155
+ platform: github-readme
3156
+ rule: max-chars
3157
+ value: 280
3158
+ surface: headline
3159
+ note: "[?] editorial soft cap (skills/github-readme, SOTA-2026 \xA71, fetched 2026-10-05) \u2014 the README one-liner is read in ~30s."
3160
+ - id: kofi-bio-max
3161
+ platform: kofi
3162
+ rule: max-chars
3163
+ value: 500
3164
+ surface: bio
3165
+ note: "[?] curated soft cap; Ko-fi documents no About limit (skills/kofi, SOTA-2026 \xA71, fetched 2026-10-05)."
3166
+ - id: hotmart-bio-max
3167
+ platform: hotmart
3168
+ rule: max-chars
3169
+ value: 500
3170
+ surface: bio
3171
+ note: "[?] curated soft cap; Hotmart documents no profile-body limit (skills/hotmart, SOTA-2026 \xA76, fetched 2026-10-05)."
3172
+ - id: upwork-title-max
3173
+ platform: upwork
3174
+ rule: max-chars
3175
+ value: 50
3176
+ surface: headline
3177
+ note: "[?] third-party guidance (snipework, 2026-03-08, https://snipework.com/blog/upwork-character-limits-2026) \u2014 confirm against Upwork's own docs before relying."
3178
+ `;
3179
+ var DEFAULT_CHECKS = parse(DEFAULT_CHECKS_YAML);
3180
+
2220
3181
  // src/commands/check.ts
2221
- var CHECK_OWNER = "M6: handle-matrix rules (brand, offline) executed; M7 (D17/\xA718.8): registry checks (credential-expiry, no-private-in-public-output) executed; M8 (\xA718.6): media-transport rules executed; remaining catalog ships with M3/M4 (SPEC \xA77)";
3182
+ var CHECK_OWNER = "M6: handle-matrix rules (brand, offline) executed; M7 (D17/\xA718.8): registry checks (credential-expiry, no-private-in-public-output) executed; M8 (\xA718.6): media-transport rules executed; limits: max-chars/min-chars over existing rows executed (offline); remaining catalog ships with M3/M4 (SPEC \xA77)";
2222
3183
  var HANDLE_CODES = /* @__PURE__ */ new Set([
2223
3184
  "drift/duplicate-handle",
2224
3185
  "drift/handle-claimed-not-linked",
@@ -2244,7 +3205,8 @@ function runCheck(args) {
2244
3205
  }
2245
3206
  throw err;
2246
3207
  }
2247
- const checks = profile.checks.filter(
3208
+ const baseChecks = profile.checks.length > 0 ? profile.checks : DEFAULT_CHECKS;
3209
+ const checks = baseChecks.filter(
2248
3210
  (c) => platform === void 0 || c.platform === "all" || c.platform === platform
2249
3211
  );
2250
3212
  const handleIssues = profile.flavor === "brand" ? driftCheck(profile).filter((i) => HANDLE_CODES.has(i.code)) : [];
@@ -2257,6 +3219,7 @@ function runCheck(args) {
2257
3219
  }));
2258
3220
  const registry = registryCheck(profile);
2259
3221
  const media = collectMedia(profile, args.workspace).findings;
3222
+ const limits = limitFindings(profile, checks, platform);
2260
3223
  const failErrors = [...registry, ...media].filter((f) => f.severity === "error");
2261
3224
  if (isJsonMode()) {
2262
3225
  return report("check", failErrors.length === 0, failErrors.length === 0 ? EXIT.OK : EXIT.FAIL, {
@@ -2267,12 +3230,18 @@ function runCheck(args) {
2267
3230
  // Per-rule, machine-readable: handle rules exist for brand only (D15/§17),
2268
3231
  // registry rules run for every flavor (D17/§18.8). A single boolean would
2269
3232
  // overload two different facts.
2270
- implemented: { handleMatrix: profile.flavor === "brand", registry: true, media: true },
3233
+ implemented: {
3234
+ handleMatrix: profile.flavor === "brand",
3235
+ registry: true,
3236
+ media: true,
3237
+ limits: true
3238
+ },
2271
3239
  checks,
2272
3240
  handleMatrix,
2273
3241
  handleIssues,
2274
3242
  registry,
2275
3243
  media,
3244
+ limits,
2276
3245
  ownedBy: CHECK_OWNER
2277
3246
  }
2278
3247
  });
@@ -2299,6 +3268,15 @@ function runCheck(args) {
2299
3268
  if (media.length === 0) {
2300
3269
  line(" media transport: OK (no findings)");
2301
3270
  }
3271
+ section(
3272
+ `text limits (max-chars/min-chars, offline)${platform !== void 0 ? ` for ${platform}` : ""}: ${limits.length} finding(s)`
3273
+ );
3274
+ for (const finding2 of limits) {
3275
+ line(` warn: [${finding2.code}] ${finding2.message} (${finding2.path})`);
3276
+ }
3277
+ if (limits.length === 0) {
3278
+ line(" text limits: OK (no findings)");
3279
+ }
2302
3280
  if (profile.flavor === "brand") {
2303
3281
  section("handle matrix (brand, \xA717)");
2304
3282
  if (profile.socialHandles.length === 0) {
@@ -2329,12 +3307,13 @@ function runCheck(args) {
2329
3307
  );
2330
3308
  for (const c of checks) {
2331
3309
  const note = c.note !== void 0 ? ` note="${c.note}"` : "";
3310
+ const executed = c.rule === "max-chars" || c.rule === "min-chars";
2332
3311
  line(
2333
- ` [${c.id ?? "?"}] platform=${c.platform} rule=${c.rule} value=${JSON.stringify(c.value)}${note}`
3312
+ ` [${c.id ?? "?"}] platform=${c.platform} rule=${c.rule} value=${JSON.stringify(c.value)}${note} \u2014 ${executed ? "executed" : "not executed"}`
2334
3313
  );
2335
3314
  }
2336
3315
  line(
2337
- " not executed \u2014 rule execution ships with the skills checks.yaml union (M3) and pasco check (M4)"
3316
+ " max-chars/min-chars executed above; the remaining rules ship with the skills checks.yaml union (M3) and pasco check (M4)"
2338
3317
  );
2339
3318
  line(` owner: ${CHECK_OWNER}`);
2340
3319
  return failErrors.length === 0 ? EXIT.OK : EXIT.FAIL;
@@ -2403,8 +3382,8 @@ function runDiff(args) {
2403
3382
  }
2404
3383
  const headSha = head.out.trim();
2405
3384
  const commits = lines(git(root, ["log", "--format=%H%x1f%s", `${base}..HEAD`])).map((entry) => {
2406
- const sep = entry.indexOf("");
2407
- return sep === -1 ? { hash: entry, subject: "" } : { hash: entry.slice(0, sep), subject: entry.slice(sep + 1) };
3385
+ const sep2 = entry.indexOf("");
3386
+ return sep2 === -1 ? { hash: entry, subject: "" } : { hash: entry.slice(0, sep2), subject: entry.slice(sep2 + 1) };
2408
3387
  });
2409
3388
  const rootReal = realpathSync2(root);
2410
3389
  const wsReal = realpathSync2(args.workspace);
@@ -2459,8 +3438,8 @@ function runDiff(args) {
2459
3438
  }
2460
3439
 
2461
3440
  // src/commands/init.ts
2462
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
2463
- import { join as join5, resolve as resolve3 } from "path";
3441
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
3442
+ import { join as join7, resolve as resolve3 } from "path";
2464
3443
  var FLAVOR_BRAND_TEMPLATE = `# ${WORKSPACE_NAME} brand workspace \u2014 content/ flavor (D15/\xA717)
2465
3444
  # The file value IS the root value: a bare scalar (file name = root property).
2466
3445
  brand
@@ -2550,8 +3529,14 @@ private by default and never leaves your control (D14: integration by configured
2550
3529
 
2551
3530
  ## Files
2552
3531
 
2553
- - \`content/*.yaml\` \u2014 the profile (file name = root property, see schema/README.md)
2554
- - \`profiles/\` \u2014 generated outputs (created by renderers; committed: they ARE the deploy artifacts)
3532
+ Three top-level directories, three grains \u2014 nothing nests one inside another:
3533
+ \`content/\` is identity, \`profiles/\` is generated output (its \`variants/\` subdir is input),
3534
+ \`posts/\` is the stream.
3535
+
3536
+ - \`content/*.yaml\` \u2014 identity facts, the SSOT (file name = root property, see schema/README.md)
3537
+ - \`profiles/\` \u2014 generated outputs (created by pasco build; committed: they ARE the deploy artifacts)
3538
+ - \`profiles/<platform>/variants/\` \u2014 chosen variant bodies (INPUT, #238; preserved by build)
3539
+ - \`posts/\` \u2014 the stream, created by \`pasco post\` (#264): seeds + per-channel projections; never identity facts
2555
3540
  - \`.pascorc\` \u2014 workspace config (defaults)
2556
3541
 
2557
3542
  ## Workflow
@@ -2573,8 +3558,14 @@ checks drift and renders brand surfaces. Private by default (D14: configured pat
2573
3558
 
2574
3559
  ## Files
2575
3560
 
2576
- - \`content/*.yaml\` \u2014 the brand profile (file name = root property, see schema/README.md)
2577
- - \`profiles/\` \u2014 generated outputs (github-org README is the first deploy artifact)
3561
+ Three top-level directories, three grains \u2014 nothing nests one inside another:
3562
+ \`content/\` is identity, \`profiles/\` is generated output (its \`variants/\` subdir is input),
3563
+ \`posts/\` is the stream.
3564
+
3565
+ - \`content/*.yaml\` \u2014 the brand profile, identity facts (file name = root property, see schema/README.md)
3566
+ - \`profiles/\` \u2014 generated outputs (github-org README is the first deploy artifact; pasco build also writes manual-paste files)
3567
+ - \`profiles/<platform>/variants/\` \u2014 chosen variant bodies (INPUT, #238; preserved by build)
3568
+ - \`posts/\` \u2014 the stream, created by \`pasco post\` (#264): seeds + per-channel projections; never identity facts
2578
3569
  - \`.pascorc\` \u2014 workspace config (defaults)
2579
3570
 
2580
3571
  ## Workflow
@@ -2604,21 +3595,21 @@ var BRAND_FILES = {
2604
3595
  };
2605
3596
  function runInit(args) {
2606
3597
  const dir = resolve3(args.cwd, args.dir ?? ".");
2607
- const contentDir = join5(dir, "content");
3598
+ const contentDir = join7(dir, "content");
2608
3599
  const brand = args.flavor === "brand";
2609
- if (existsSync4(contentDir)) {
3600
+ if (existsSync6(contentDir)) {
2610
3601
  return report("init", false, EXIT.FAIL, {
2611
3602
  error: { kind: "exists", message: `refusing to scaffold: ${contentDir} already exists` }
2612
3603
  });
2613
3604
  }
2614
- const files = brand ? BRAND_FILES : { "identity.yaml": IDENTITY_TEMPLATE };
3605
+ const files = brand ? { ...BRAND_FILES, "checks.yaml": DEFAULT_CHECKS_YAML } : { "identity.yaml": IDENTITY_TEMPLATE, "checks.yaml": DEFAULT_CHECKS_YAML };
2615
3606
  const targets = [
2616
- ...Object.entries(files).map(([name, content]) => ({ path: join5(contentDir, name), content })),
2617
- { path: join5(dir, "README.md"), content: brand ? BRAND_README_TEMPLATE : README_TEMPLATE },
2618
- { path: join5(dir, ".pascorc"), content: PASCORC_TEMPLATE }
3607
+ ...Object.entries(files).map(([name, content]) => ({ path: join7(contentDir, name), content })),
3608
+ { path: join7(dir, "README.md"), content: brand ? BRAND_README_TEMPLATE : README_TEMPLATE },
3609
+ { path: join7(dir, ".pascorc"), content: PASCORC_TEMPLATE }
2619
3610
  ];
2620
3611
  for (const t of targets) {
2621
- if (existsSync4(t.path)) {
3612
+ if (existsSync6(t.path)) {
2622
3613
  return report("init", false, EXIT.FAIL, {
2623
3614
  error: { kind: "exists", message: `refusing to overwrite existing file: ${t.path}` }
2624
3615
  });
@@ -2640,26 +3631,228 @@ function runInit(args) {
2640
3631
  });
2641
3632
  }
2642
3633
 
2643
- // src/commands/refine.ts
2644
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2645
- import { dirname as dirname3, join as join6 } from "path";
2646
- import { fileURLToPath } from "url";
3634
+ // src/commands/post.ts
3635
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync5, writeFileSync as writeFileSync4 } from "fs";
3636
+ import { dirname as dirname4, join as join10 } from "path";
3637
+ import { parseDocument as parseDocument3 } from "yaml";
3638
+
3639
+ // src/post-store.ts
3640
+ import { createHash as createHash3 } from "crypto";
3641
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
3642
+ import { join as join8 } from "path";
2647
3643
  import { parseDocument as parseDocument2, stringify } from "yaml";
2648
- var REFINE_OWNER = "M3 SOTA research per platform \u2192 skills v1 + the full refine flow (SPEC \xA712/\xA716 M3); model-agnostic (D10/D11/D13)";
2649
- function platformSkillPath(platform) {
3644
+ var SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
3645
+ function isSafeSeedId(id) {
3646
+ return SAFE_ID.test(id) && !id.includes("..");
3647
+ }
3648
+ function postsDir(workspace) {
3649
+ return join8(workspace, "posts");
3650
+ }
3651
+ function seedDir(workspace, id) {
3652
+ if (!isSafeSeedId(id)) throw new Error(`unsafe seed id ${JSON.stringify(id)}`);
3653
+ return join8(postsDir(workspace), id);
3654
+ }
3655
+ function seedPath(workspace, id) {
3656
+ return join8(seedDir(workspace, id), "seed.md");
3657
+ }
3658
+ function ledgerPath(workspace, id) {
3659
+ return join8(seedDir(workspace, id), "post.yaml");
3660
+ }
3661
+ function variantPath(workspace, id, channel, variant) {
3662
+ if (!SAFE_ID.test(variant) || variant.includes("..")) {
3663
+ throw new Error(`unsafe variant id ${JSON.stringify(variant)}`);
3664
+ }
3665
+ return join8(seedDir(workspace, id), channel, `${variant}.md`);
3666
+ }
3667
+ function hashText(text) {
3668
+ return createHash3("sha256").update(text).digest("hex");
3669
+ }
3670
+ function defaultLedger(id, source) {
3671
+ return {
3672
+ id,
3673
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3674
+ source,
3675
+ seed: "seed.md",
3676
+ status: "draft",
3677
+ channels: {}
3678
+ };
3679
+ }
3680
+ function readLedger(workspace, id) {
3681
+ const path = ledgerPath(workspace, id);
3682
+ if (!existsSync7(path)) throw new Error(`no post ledger at ${path}`);
3683
+ const doc = parseDocument2(readFileSync6(path, "utf8"));
3684
+ if (doc.errors.length > 0) {
3685
+ throw new Error(`${path}: invalid YAML \u2014 ${doc.errors[0]?.message ?? "parse error"}`);
3686
+ }
3687
+ const raw = doc.toJS({ maxAliasCount: 100 });
3688
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
3689
+ throw new Error(`${path} must contain a mapping`);
3690
+ }
3691
+ const obj = raw;
3692
+ const channelsRaw = obj.channels;
3693
+ const channels = channelsRaw !== null && typeof channelsRaw === "object" && !Array.isArray(channelsRaw) ? channelsRaw : {};
3694
+ return {
3695
+ id: typeof obj.id === "string" ? obj.id : id,
3696
+ createdAt: typeof obj.createdAt === "string" ? obj.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
3697
+ source: obj.source === "imported" ? "imported" : "authored",
3698
+ seed: typeof obj.seed === "string" ? obj.seed : "seed.md",
3699
+ status: obj.status === "published" || obj.status === "archived" ? obj.status : "draft",
3700
+ channels
3701
+ };
3702
+ }
3703
+ function writeLedger(workspace, ledger) {
3704
+ const dir = seedDir(workspace, ledger.id);
3705
+ mkdirSync3(dir, { recursive: true });
3706
+ writeFileSync3(ledgerPath(workspace, ledger.id), stringify(ledger));
3707
+ }
3708
+ function seedText(workspace, id) {
3709
+ const path = seedPath(workspace, id);
3710
+ if (!existsSync7(path)) throw new Error(`no seed at ${path}`);
3711
+ return readFileSync6(path, "utf8");
3712
+ }
3713
+ function createSeed(workspace, id, text, source = "authored") {
3714
+ if (!isSafeSeedId(id)) throw new Error(`unsafe seed id ${JSON.stringify(id)}`);
3715
+ const dir = seedDir(workspace, id);
3716
+ mkdirSync3(dir, { recursive: true });
3717
+ const seed = seedPath(workspace, id);
3718
+ if (!existsSync7(seed)) writeFileSync3(seed, text);
3719
+ const ledgerFile = ledgerPath(workspace, id);
3720
+ if (existsSync7(ledgerFile)) return readLedger(workspace, id);
3721
+ const ledger = defaultLedger(id, source);
3722
+ writeLedger(workspace, ledger);
3723
+ return ledger;
3724
+ }
3725
+ function listSeedIds(workspace) {
3726
+ const dir = postsDir(workspace);
3727
+ if (!existsSync7(dir)) return [];
3728
+ return readdirSync3(dir).filter((name) => {
3729
+ try {
3730
+ return statSync3(join8(dir, name)).isDirectory() && isSafeSeedId(name);
3731
+ } catch {
3732
+ return false;
3733
+ }
3734
+ }).sort();
3735
+ }
3736
+ function chooseVariant(workspace, id, channel, variant, publishedText) {
3737
+ const path = variantPath(workspace, id, channel, variant);
3738
+ if (!existsSync7(path)) throw new Error(`no variant body at ${path}`);
3739
+ const text = readFileSync6(path, "utf8");
3740
+ if (text.trim() === "") throw new Error(`variant body is empty at ${path}`);
3741
+ const ledger = readLedger(workspace, id);
3742
+ const templateHash = hashText(text);
3743
+ const contentHash = hashText(publishedText ?? text);
3744
+ const chosenAt = (/* @__PURE__ */ new Date()).toISOString();
3745
+ ledger.channels[channel] = {
3746
+ ...ledger.channels[channel] ?? {},
3747
+ chosen: variant,
3748
+ chosenAt,
3749
+ contentHash,
3750
+ templateHash,
3751
+ sync: "local-only",
3752
+ certification: "unverifiable"
3753
+ };
3754
+ ledger.status = "draft";
3755
+ writeLedger(workspace, ledger);
3756
+ return { ledger, path, contentHash, templateHash };
3757
+ }
3758
+ function chosenVariant(workspace, ledger, channel) {
3759
+ const chosen = ledger.channels[channel]?.chosen;
3760
+ if (chosen === void 0) return void 0;
3761
+ const path = variantPath(workspace, ledger.id, channel, chosen);
3762
+ if (!existsSync7(path)) return void 0;
3763
+ const text = readFileSync6(path, "utf8");
3764
+ return { variant: chosen, text, path };
3765
+ }
3766
+
3767
+ // src/skill-path.ts
3768
+ import { existsSync as existsSync8, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
3769
+ import { dirname as dirname3, join as join9 } from "path";
3770
+ import { fileURLToPath } from "url";
3771
+ function skillsDir() {
3772
+ const override = process.env.PASCO_SKILLS_DIR;
3773
+ if (override !== void 0 && override !== "" && isDirectory(override)) return override;
2650
3774
  const here = dirname3(fileURLToPath(import.meta.url));
2651
- const candidates = [
2652
- join6(here, "..", "..", "..", "..", "skills", platform, "SKILL.md")
2653
- // repo dev
2654
- ];
2655
- return candidates.find((p) => existsSync5(p));
3775
+ const pkgDir = join9(here, "..");
3776
+ const shipped = join9(pkgDir, "skills");
3777
+ if (isDirectory(shipped)) return shipped;
3778
+ const repoRoot = join9(pkgDir, "..", "..");
3779
+ if (existsSync8(join9(repoRoot, "pnpm-workspace.yaml")) && isDirectory(join9(repoRoot, "skills"))) {
3780
+ return join9(repoRoot, "skills");
3781
+ }
3782
+ return void 0;
3783
+ }
3784
+ function isDirectory(path) {
3785
+ return existsSync8(path) && statSync4(path).isDirectory();
3786
+ }
3787
+ function listChannels(root) {
3788
+ return readdirSync4(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
3789
+ }
3790
+ function listSkills(root, file = "SKILL.md") {
3791
+ return listChannels(root).filter((channel) => existsSync8(join9(root, channel, file)));
3792
+ }
3793
+ function skillFile(channel, file) {
3794
+ const root = skillsDir();
3795
+ if (root === void 0) return void 0;
3796
+ const path = join9(root, channel, file);
3797
+ return existsSync8(path) ? path : void 0;
3798
+ }
3799
+ var NOT_SHIPPED_MESSAGE = "skills not shipped with this package \u2014 this build carries no skills/; report it at https://github.com/ChristianLuciani/profile-as-code/issues";
3800
+ function lookupSkill(channel, file = "SKILL.md") {
3801
+ const root = skillsDir();
3802
+ if (root === void 0) return { state: "not-shipped" };
3803
+ const path = join9(root, channel, file);
3804
+ if (existsSync8(path)) return { state: "found", path, root };
3805
+ return { state: "no-skill", channel, root, shipped: listSkills(root, file) };
3806
+ }
3807
+ function skillLine(lookup) {
3808
+ if (lookup.state === "found") return lookup.path;
3809
+ if (lookup.state === "not-shipped") return NOT_SHIPPED_MESSAGE;
3810
+ const shipped = lookup.shipped.length > 0 ? `this package ships: ${lookup.shipped.join(", ")}` : "this package ships no platform skills";
3811
+ return `no skill for ${lookup.channel} yet \u2014 ${shipped}; fall back to the deterministic renderer output (the platform's limits are not encoded yet)`;
3812
+ }
3813
+
3814
+ // src/commands/post.ts
3815
+ function postSkillPath(channel) {
3816
+ return skillFile(channel, "post.md");
3817
+ }
3818
+ function postingPath(channel) {
3819
+ return skillFile(channel, "posting.yaml");
3820
+ }
3821
+ function readPosting(channel) {
3822
+ const path = postingPath(channel);
3823
+ if (path === void 0) {
3824
+ throw new UsageError(
3825
+ skillsDir() === void 0 ? NOT_SHIPPED_MESSAGE : `no post skill for "${channel}" \u2014 expected skills/${channel}/posting.yaml + post.md`
3826
+ );
3827
+ }
3828
+ const doc = parseDocument3(readFileSync7(path, "utf8"));
3829
+ if (doc.errors.length > 0) {
3830
+ throw new UsageError(`${path}: invalid YAML \u2014 ${doc.errors[0]?.message ?? "parse error"}`);
3831
+ }
3832
+ const raw = doc.toJS({ maxAliasCount: 100 });
3833
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
3834
+ throw new UsageError(`${path} must contain a mapping`);
3835
+ }
3836
+ const rules = raw;
3837
+ if (rules.platform !== void 0 && rules.platform !== channel) {
3838
+ throw new UsageError(
3839
+ `${path}: platform "${rules.platform}" does not match channel "${channel}"`
3840
+ );
3841
+ }
3842
+ const type = rules.settings?.__type;
3843
+ if (type !== void 0 && String(type) !== channel) {
3844
+ throw new UsageError(
3845
+ `${path}: settings.__type "${String(type)}" does not match channel "${channel}"`
3846
+ );
3847
+ }
3848
+ return rules;
2656
3849
  }
2657
3850
  function loadProfile(workspace) {
2658
3851
  try {
2659
3852
  return load(workspace);
2660
3853
  } catch (err) {
2661
3854
  if (err instanceof SchemaValidationError) {
2662
- return report("refine", false, EXIT.FAIL, {
3855
+ return report("post", false, EXIT.FAIL, {
2663
3856
  error: {
2664
3857
  kind: "schema",
2665
3858
  message: `schema validation failed (D12 fail-on-schema) \u2014 ${err.issues.length} violation(s)`,
@@ -2668,156 +3861,1175 @@ function loadProfile(workspace) {
2668
3861
  });
2669
3862
  }
2670
3863
  if (err instanceof LoadError) {
2671
- return report("refine", false, EXIT.FAIL, { error: { kind: "load", message: err.message } });
3864
+ return report("post", false, EXIT.FAIL, { error: { kind: "load", message: err.message } });
2672
3865
  }
2673
3866
  throw err;
2674
3867
  }
2675
3868
  }
2676
- function recordVariant(workspace, platform, lang, variantId) {
2677
- const activePath = join6(workspace, "content", "active.yaml");
2678
- let active = {};
2679
- if (existsSync5(activePath)) {
2680
- const doc = parseDocument2(readFileSync4(activePath, "utf8"));
2681
- if (doc.errors.length > 0) {
2682
- throw new Error(
2683
- `content/active.yaml: invalid YAML \u2014 ${doc.errors[0]?.message ?? "parse error"}`
2684
- );
2685
- }
2686
- const node = doc.toJS({ maxAliasCount: 100 });
2687
- if (node !== null && node !== void 0) {
2688
- if (typeof node !== "object" || Array.isArray(node)) {
2689
- throw new Error("content/active.yaml must contain a mapping (platform \u2192 variant entry)");
3869
+ function splitChannels(value) {
3870
+ if (value === void 0 || value.trim() === "") {
3871
+ throw new UsageError("--channel is required (comma-separated, e.g. linkedin,x)");
3872
+ }
3873
+ const seen = /* @__PURE__ */ new Set();
3874
+ const out = [];
3875
+ for (const raw of value.split(",")) {
3876
+ const name = raw.trim();
3877
+ if (name === "" || seen.has(name)) continue;
3878
+ seen.add(name);
3879
+ out.push(parsePlatform(name));
3880
+ }
3881
+ if (out.length === 0) throw new UsageError("--channel is required (comma-separated)");
3882
+ return out;
3883
+ }
3884
+ function resolveFact(profile, pointer) {
3885
+ const segments = pointer.split(".");
3886
+ let current = profile;
3887
+ for (const segment of segments) {
3888
+ const match = /^([A-Za-z0-9_]+)(?:\[([^\]]+)\])?$/.exec(segment);
3889
+ if (match === null) return void 0;
3890
+ const key = match[1] ?? "";
3891
+ const selector = match[2];
3892
+ if (current === null || typeof current !== "object") return void 0;
3893
+ current = current[key];
3894
+ if (selector !== void 0) {
3895
+ if (Array.isArray(current)) {
3896
+ const index = Number(selector);
3897
+ current = Number.isInteger(index) ? current[index] : current.find(
3898
+ (item) => item !== null && typeof item === "object" && item.id === selector
3899
+ );
3900
+ } else if (current !== null && typeof current === "object") {
3901
+ current = current[selector];
3902
+ } else {
3903
+ return void 0;
2690
3904
  }
2691
- active = node;
2692
3905
  }
2693
3906
  }
2694
- const chosenAt = (/* @__PURE__ */ new Date()).toISOString();
2695
- active[platform] = { lang, variantId, chosenAt };
2696
- writeFileSync3(activePath, stringify(active));
2697
- return { chosenAt };
3907
+ if (typeof current === "string") return current;
3908
+ if (typeof current === "number" || typeof current === "boolean") return String(current);
3909
+ return void 0;
2698
3910
  }
2699
- function runRefine(args) {
2700
- if (args.platformArg === void 0) {
2701
- throw new UsageError(
2702
- "usage: pasco refine <platform> [--lang <l>] [--variants N] [--choose <variantId>]"
2703
- );
3911
+ function renderFacts(text, profile) {
3912
+ const dangling = [];
3913
+ const rendered = text.replace(/\{\{fact:([^}]+)\}\}/g, (_whole, pointer) => {
3914
+ const value = resolveFact(profile, pointer.trim());
3915
+ if (value === void 0) {
3916
+ dangling.push(pointer.trim());
3917
+ return `{{fact:${pointer}}}`;
3918
+ }
3919
+ return value;
3920
+ });
3921
+ return { text: rendered, dangling };
3922
+ }
3923
+ function allowedUrls(profile) {
3924
+ const urls = /* @__PURE__ */ new Set();
3925
+ const add = (value) => {
3926
+ if (value !== void 0 && value !== "") urls.add(value);
3927
+ };
3928
+ for (const link of profile.identity.links ?? []) add(link.url);
3929
+ for (const project of profile.projects) for (const url of project.links ?? []) add(url);
3930
+ for (const publication of profile.publications) add(publication.url);
3931
+ for (const press of profile.press ?? []) add(press.url);
3932
+ for (const credential of profile.credentials ?? []) add(credential.verifyUrl);
3933
+ for (const artifact of profile.artifacts ?? []) add(artifact.locator?.url);
3934
+ return urls;
3935
+ }
3936
+ function factLines(profile) {
3937
+ const lines2 = [];
3938
+ lines2.push(`identity.name = ${profile.identity.name}`);
3939
+ if (profile.identity.location !== void 0) {
3940
+ lines2.push(`identity.location = ${profile.identity.location}`);
3941
+ }
3942
+ (profile.identity.links ?? []).forEach((link, index) => {
3943
+ lines2.push(`identity.links[${index}].url = ${link.url}`);
3944
+ });
3945
+ profile.projects.forEach((project, index) => {
3946
+ const selector = project.id ?? String(index);
3947
+ lines2.push(`projects[${selector}].name = ${project.name}`);
3948
+ if (project.status !== void 0) {
3949
+ lines2.push(`projects[${selector}].status = ${project.status}`);
3950
+ }
3951
+ (project.links ?? []).forEach((url, linkIndex) => {
3952
+ lines2.push(`projects[${selector}].links[${linkIndex}] = ${url}`);
3953
+ });
3954
+ });
3955
+ profile.experience.forEach((item, index) => {
3956
+ const selector = item.id ?? String(index);
3957
+ lines2.push(`experience[${selector}].role = ${item.role}`);
3958
+ lines2.push(`experience[${selector}].org = ${item.org}`);
3959
+ lines2.push(`experience[${selector}].start = ${item.start}`);
3960
+ if (item.end !== void 0) lines2.push(`experience[${selector}].end = ${item.end}`);
3961
+ });
3962
+ profile.publications.forEach((publication, index) => {
3963
+ const selector = publication.id ?? String(index);
3964
+ lines2.push(`publications[${selector}].title = ${publication.title}`);
3965
+ if (publication.doi !== void 0) {
3966
+ lines2.push(`publications[${selector}].doi = ${publication.doi}`);
3967
+ }
3968
+ if (publication.url !== void 0) {
3969
+ lines2.push(`publications[${selector}].url = ${publication.url}`);
3970
+ }
3971
+ });
3972
+ (profile.press ?? []).forEach((item, index) => {
3973
+ if (item.url !== void 0) lines2.push(`press[${index}].url = ${item.url}`);
3974
+ });
3975
+ for (const credential of profile.credentials ?? []) {
3976
+ if (credential.id === void 0) continue;
3977
+ lines2.push(`credentials[${credential.id}].title = ${credential.title}`);
3978
+ if (credential.verifyUrl !== void 0) {
3979
+ lines2.push(`credentials[${credential.id}].verifyUrl = ${credential.verifyUrl}`);
3980
+ }
2704
3981
  }
2705
- const platform = parsePlatform(args.platformArg);
2706
- const lang = parseLang(defaultsLang(args.pascorc, args.langArg));
2707
- const variants = parseVariants(args.variantsArg, args.pascorc.defaults?.variants ?? 3);
2708
- const loaded = loadProfile(args.workspace);
2709
- if (typeof loaded === "number") return loaded;
2710
- const profile = loaded;
2711
- if (args.choose !== void 0) {
2712
- let chosenAt;
2713
- try {
2714
- chosenAt = recordVariant(args.workspace, platform, lang, args.choose).chosenAt;
2715
- } catch (err) {
2716
- return report("refine", false, EXIT.FAIL, {
2717
- error: { kind: "load", message: err.message }
3982
+ for (const artifact of profile.artifacts ?? []) {
3983
+ if (artifact.id === void 0) continue;
3984
+ lines2.push(`artifacts[${artifact.id}].title = ${artifact.title}`);
3985
+ if (artifact.locator?.url !== void 0) {
3986
+ lines2.push(`artifacts[${artifact.id}].locator.url = ${artifact.locator.url}`);
3987
+ }
3988
+ }
3989
+ return lines2;
3990
+ }
3991
+ function urlCandidates(text) {
3992
+ const out = /* @__PURE__ */ new Set();
3993
+ const pattern = /\b(?:https?:\/\/|www\.)[^\s)\]]+|\b[a-z0-9-]+(?:\.[a-z0-9-]+)+\/[^\s)\]]*/gi;
3994
+ for (const match of text.matchAll(pattern)) {
3995
+ let raw = (match[0] ?? "").replace(/[.,;:!?]+$/, "");
3996
+ if (raw === "") continue;
3997
+ if (!/^https?:\/\//i.test(raw)) raw = `https://${raw}`;
3998
+ out.add(raw.replace(/^https?:\/\//i, (scheme) => scheme.toLowerCase()));
3999
+ }
4000
+ return [...out];
4001
+ }
4002
+ function checkText(text, rules, profile) {
4003
+ const findings = [];
4004
+ const content = text.trim();
4005
+ const max = rules.limits?.maxChars;
4006
+ const min = rules.limits?.minChars;
4007
+ if (max !== void 0 && content.length > max) {
4008
+ findings.push({
4009
+ code: "max-chars",
4010
+ severity: "error",
4011
+ message: `post is ${content.length} chars; ${rules.platform ?? "channel"} allows ${max}`
4012
+ });
4013
+ }
4014
+ if (min !== void 0 && content.length < min) {
4015
+ findings.push({
4016
+ code: "min-chars",
4017
+ severity: "error",
4018
+ message: `post is ${content.length} chars; ${rules.platform ?? "channel"} requires at least ${min}`
4019
+ });
4020
+ }
4021
+ const allowed = allowedUrls(profile);
4022
+ for (const url of urlCandidates(content)) {
4023
+ if (!allowed.has(url)) {
4024
+ findings.push({
4025
+ code: "url-not-in-ssot",
4026
+ severity: "error",
4027
+ message: `${url} is not in the profile URL SSOT (identity.links / projects / publications / credentials / artifacts)`
2718
4028
  });
2719
4029
  }
2720
- if (isJsonMode()) {
2721
- return report("refine", true, EXIT.OK, {
2722
- data: {
2723
- mode: "recorded",
2724
- platform,
2725
- lang,
2726
- variantId: args.choose,
2727
- chosenAt,
2728
- workspace: args.workspace
2729
- }
4030
+ }
4031
+ const maxTags = rules.hashtags?.max;
4032
+ if (maxTags !== void 0) {
4033
+ const tags = content.match(/(^|\s)#[A-Za-z0-9_]+/g) ?? [];
4034
+ if (tags.length > maxTags) {
4035
+ findings.push({
4036
+ code: "hashtags",
4037
+ severity: "warn",
4038
+ message: `${tags.length} hashtags; the ${rules.platform ?? "channel"} skill recommends at most ${maxTags}`
2730
4039
  });
2731
4040
  }
2732
- line(
2733
- `recorded active[${platform}] = { lang: ${lang}, variantId: ${args.choose}, chosenAt: ${chosenAt} } (content/active.yaml)`
2734
- );
2735
- return EXIT.OK;
2736
4041
  }
2737
- const slice = compile(profile, platform, lang);
2738
- const identity = slice.identity;
2739
- const links = (identity.links ?? []).map((l) => `${l.platform ?? "?"}:${l.url}`).join(", ");
2740
- const experience = slice.experience.map((e) => `${e.role} @ ${e.org} (${e.start}\u2013${e.end ?? "present"})`).join(" | ");
2741
- const projects = slice.projects.map((p) => p.name).join(", ");
2742
- const publications = slice.publications.map((p) => p.title).join(" | ");
2743
- const anchors = slice.keywords.geo.associations?.join(", ") ?? "";
2744
- const skillPath = platformSkillPath(platform);
2745
- const template = [
2746
- `# pasco refine \u2014 agent task (agent-driven: D10/D11/D13; no LLM call, no hub)`,
2747
- `Write ${variants} variant(s) of the ${platform} profile in ${lang} \u2014 in PROSE, per the platform skill.`,
4042
+ for (const rawLine of content.split("\n")) {
4043
+ const lineText = rawLine.trim();
4044
+ if (lineText === "" || lineText.includes("{{fact:") || lineText.includes("evidence:")) continue;
4045
+ if (/\d/.test(lineText) && /[%$]|\b\d{2,}\b/.test(lineText)) {
4046
+ findings.push({
4047
+ code: "unsourced-claim",
4048
+ severity: "warn",
4049
+ message: `numeric claim without a {{fact:...}} or evidence: marker \u2014 "${lineText.slice(0, 80)}"`
4050
+ });
4051
+ }
4052
+ }
4053
+ return findings;
4054
+ }
4055
+ function buildTask(args) {
4056
+ const { seedId, channel, seed, profile, rules, skill, variants, lang } = args;
4057
+ const facts = factLines(profile).join("\n ");
4058
+ const structure = (rules.structure ?? []).join(" \u2192 ") || "\u2014";
4059
+ const sources = (rules.sources ?? []).map((source) => `${source.url ?? "\u2014"} (${source.date ?? "\u2014"})`).join("; ");
4060
+ return [
4061
+ `# pasco post task \u2014 ${seedId} / ${channel}`,
2748
4062
  ``,
2749
- `Platform: ${platform}`,
2750
- `Language: ${lang}`,
2751
- `Skill (communicative strategy \u2014 READ IT FIRST): ${skillPath ?? "(not installed \u2014 fall back to the deterministic renderer output)"}`,
2752
- `Research (dated sources): ${skillPath === void 0 ? "\u2014" : skillPath.replace(/SKILL\.md$/, "research/SOTA-2026.md")}`,
2753
- `Variant id convention: ${platform.replace(/[^a-z]/g, "")}-${lang}-<n> (e.g. gh-en-1, gh-en-2, gh-en-3).`,
4063
+ `Write ${variants} variant(s) of a ${channel} POST in ${lang}. You are the agent:`,
4064
+ `the CLI never calls an LLM. Read the skill FIRST:`,
4065
+ ` ${skill ?? `(missing skills/${channel}/post.md)`}`,
2754
4066
  ``,
2755
- `Facts (language-independent \u2014 MUST stay byte-identical across variants and platforms):`,
2756
- ` name: ${identity.name}`,
2757
- ` location: ${identity.location ?? "\u2014"}`,
2758
- ` links: ${links || "\u2014"}`,
2759
- ` experience: ${experience || "\u2014"}`,
2760
- ` projects: ${projects || "\u2014"}`,
2761
- ` publications: ${publications || "\u2014"}`,
4067
+ `Seed (canonical \u2014 do not rewrite its meaning):`,
4068
+ seed.trim(),
2762
4069
  ``,
2763
- `Existing ${platform} texts (base to vary):`,
2764
- ` bios: ${slice.bios.map((b) => `[${b.length}] ${b.text}`).join(" | ") || "\u2014"}`,
2765
- ` headlines: ${slice.headlines.map((h) => h.text).join(" | ") || "\u2014"}`,
2766
- ` active variant: ${slice.active?.variantId ?? "\u2014"}`,
4070
+ `Facts (byte-identical; cite with {{fact:<pointer>}}):`,
4071
+ ` ${facts}`,
2767
4072
  ``,
2768
- `GEO anchors to reinforce (semantic associations): ${anchors || "\u2014"}`,
4073
+ `Channel rules (from skills/${channel}/posting.yaml):`,
4074
+ ` maxChars: ${rules.limits?.maxChars ?? "\u2014"}`,
4075
+ ` settings: ${JSON.stringify(rules.settings ?? {})}`,
4076
+ ` links: ${rules.links?.policy ?? "identity-ssot-only"}`,
4077
+ ` hashtags (max): ${rules.hashtags?.max ?? "\u2014"}`,
4078
+ ` structure: ${structure}`,
4079
+ ` sources: ${sources || "\u2014"}`,
2769
4080
  ``,
2770
- `Constraints:`,
2771
- ` - Facts never change (D4) \u2014 only storytelling/angles vary.`,
2772
- ` - Every text langMap must include 'en' (contract requirement) and the requested lang.`,
2773
- ` - Never invent URLs, DOIs or numbers \u2014 evidence claims stay untouched.`,
2774
- ` - Platform-specific limits come from the platform skill (SPEC \xA716) + checks.yaml.`,
4081
+ `Output: save each variant as ONE text block at`,
4082
+ ` posts/${seedId}/${channel}/${channel}-<n>.md`,
4083
+ `Then record the choice:`,
4084
+ ` pasco post choose ${seedId} --channel ${channel} --variant ${channel}-<n>`,
2775
4085
  ``,
2776
- `Workflow: read the skill \u2192 produce the ${variants} variant(s) \u2192 the user picks one \u2192 record it:`,
2777
- ` pasco refine ${platform} --lang ${lang} --choose <variantId>`,
4086
+ `Constraints:`,
4087
+ ` - Facts never change (D4). Quote them as {{fact:<pointer>}}; do not paraphrase a number, date or URL.`,
4088
+ ` - Links only from identity.links / projects[].links / publications[].url /`,
4089
+ ` credentials[].verifyUrl / artifacts[].locator.url \u2014 the SSOT is the only URL source.`,
4090
+ ` - Never invent URLs, DOIs or numbers.`,
4091
+ ` - Keep the channel's native shape; the skill is the strategy, not the template.`,
2778
4092
  ``
2779
4093
  ].join("\n");
4094
+ }
4095
+ function readPostizConfig(workspace) {
4096
+ const path = join10(workspace, "postiz.yaml");
4097
+ if (!existsSync9(path)) return {};
4098
+ const doc = parseDocument3(readFileSync7(path, "utf8"));
4099
+ if (doc.errors.length > 0) {
4100
+ throw new UsageError(`${path}: invalid YAML \u2014 ${doc.errors[0]?.message ?? "parse error"}`);
4101
+ }
4102
+ const raw = doc.toJS({ maxAliasCount: 100 });
4103
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
4104
+ const channels = raw.channels;
4105
+ if (channels === null || typeof channels !== "object" || Array.isArray(channels)) return {};
4106
+ const out = {};
4107
+ for (const [key, value] of Object.entries(channels)) {
4108
+ if (typeof value === "string" && value !== "") out[key] = value;
4109
+ }
4110
+ return out;
4111
+ }
4112
+ function buildPostizPayload(args) {
4113
+ const integrationIds = readPostizConfig(args.workspace);
4114
+ const posts = [];
4115
+ for (const [channel, state] of Object.entries(args.ledger.channels)) {
4116
+ if (state.chosen === void 0) continue;
4117
+ const chosen = chosenVariant(args.workspace, args.ledger, channel);
4118
+ if (chosen === void 0) {
4119
+ throw new Error(
4120
+ `channel ${channel}: chosen variant "${state.chosen}" has no body at ${variantPath(
4121
+ args.workspace,
4122
+ args.ledger.id,
4123
+ channel,
4124
+ state.chosen
4125
+ )}`
4126
+ );
4127
+ }
4128
+ const rendered = renderFacts(chosen.text, args.profile);
4129
+ if (rendered.dangling.length > 0) {
4130
+ throw new Error(`channel ${channel}: dangling fact ref(s): ${rendered.dangling.join(", ")}`);
4131
+ }
4132
+ const rules = readPosting(channel);
4133
+ const settings = {
4134
+ ...rules.settings ?? {},
4135
+ __type: rules.settings?.__type ?? channel
4136
+ };
4137
+ posts.push({
4138
+ integration: { id: integrationIds[channel] ?? "REPLACE_ME" },
4139
+ value: [{ content: rendered.text.trim(), image: [] }],
4140
+ settings
4141
+ });
4142
+ }
4143
+ return {
4144
+ type: args.type,
4145
+ date: args.date,
4146
+ shortLink: false,
4147
+ tags: [],
4148
+ posts
4149
+ };
4150
+ }
4151
+ function requireSeed(args) {
4152
+ if (args.seedArg === void 0 || args.seedArg === "") {
4153
+ throw new UsageError("a <seed-id> is required");
4154
+ }
4155
+ if (!isSafeSeedId(args.seedArg)) {
4156
+ throw new UsageError(
4157
+ "seed id must be a safe path segment (letters, digits, dot, underscore, dash)"
4158
+ );
4159
+ }
4160
+ return args.seedArg;
4161
+ }
4162
+ function handleSeed(args) {
4163
+ const seedId = requireSeed(args);
4164
+ if (args.textArg === void 0) {
4165
+ throw new UsageError("usage: pasco post seed <seed-id> --text <file>");
4166
+ }
4167
+ if (!existsSync9(args.textArg)) {
4168
+ throw new UsageError(`seed file not found: ${args.textArg}`);
4169
+ }
4170
+ const text = readFileSync7(args.textArg, "utf8");
4171
+ if (text.trim() === "") throw new UsageError("seed text is empty");
4172
+ if (existsSync9(seedPath(args.workspace, seedId))) {
4173
+ throw new UsageError(
4174
+ `seed ${seedId} already exists at ${seedPath(args.workspace, seedId)}; edit it directly`
4175
+ );
4176
+ }
4177
+ const ledger = createSeed(args.workspace, seedId, text);
2780
4178
  if (isJsonMode()) {
2781
- return report("refine", true, EXIT.OK, {
2782
- data: { mode: "template", platform, lang, variants, template, ownedBy: REFINE_OWNER }
4179
+ return report("post", true, EXIT.OK, {
4180
+ data: { mode: "seed", seedId, seedPath: seedPath(args.workspace, seedId), ledger }
2783
4181
  });
2784
4182
  }
2785
- process.stdout.write(template);
4183
+ line(`seeded ${seedId} at ${seedPath(args.workspace, seedId)}`);
2786
4184
  return EXIT.OK;
2787
4185
  }
2788
-
2789
- // src/commands/skills-install.ts
2790
- var SKILLS_OWNER = "M3 SOTA research per platform \u2192 skills v1 authored in skills-studio, projected via AGOS bin/project.sh";
2791
- var AGENTS = ["pi", "claude", "aionui", "opencode"];
2792
- function runSkillsInstall(args) {
2793
- if (args.agent === void 0) {
2794
- throw new UsageError(`usage: pasco skills:install <agent> (${AGENTS.join(" | ")})`);
4186
+ function handleTask(args) {
4187
+ const seedId = requireSeed(args);
4188
+ const channels = splitChannels(args.channelArg);
4189
+ const variants = parseVariants(args.variantsArg, args.pascorc.defaults?.variants ?? 3);
4190
+ const lang = parseLang(defaultsLang(args.pascorc));
4191
+ const seed = seedText(args.workspace, seedId);
4192
+ const loaded = loadProfile(args.workspace);
4193
+ if (typeof loaded === "number") return loaded;
4194
+ const profile = loaded;
4195
+ if (!existsSync9(ledgerPath(args.workspace, seedId))) {
4196
+ createSeed(args.workspace, seedId, seed);
4197
+ }
4198
+ const results = [];
4199
+ for (const channel of channels) {
4200
+ const rules = readPosting(channel);
4201
+ const skill = postSkillPath(channel);
4202
+ const task = buildTask({
4203
+ seedId,
4204
+ channel,
4205
+ seed,
4206
+ profile,
4207
+ rules,
4208
+ skill,
4209
+ variants,
4210
+ lang
4211
+ });
4212
+ const taskPath = join10(seedDir(args.workspace, seedId), channel, "task.md");
4213
+ mkdirSync4(dirname4(taskPath), { recursive: true });
4214
+ writeFileSync4(taskPath, task);
4215
+ results.push({ channel, taskPath, skill, maxChars: rules.limits?.maxChars });
4216
+ line(`task: ${taskPath}`);
4217
+ line(` skill: ${skill ?? "(missing)"}`);
4218
+ line(` maxChars: ${rules.limits?.maxChars ?? "\u2014"}`);
4219
+ }
4220
+ if (isJsonMode()) {
4221
+ return report("post", true, EXIT.OK, {
4222
+ data: { mode: "task", seedId, lang, variants, channels: results }
4223
+ });
4224
+ }
4225
+ return EXIT.OK;
4226
+ }
4227
+ function handleChoose(args) {
4228
+ const seedId = requireSeed(args);
4229
+ const channel = parsePlatform(args.channelArg ?? "");
4230
+ if (args.variantArg === void 0 || args.variantArg === "") {
4231
+ throw new UsageError("--variant is required");
4232
+ }
4233
+ readPosting(channel);
4234
+ if (args.textArg !== void 0) {
4235
+ if (!existsSync9(args.textArg)) throw new UsageError(`variant file not found: ${args.textArg}`);
4236
+ const target = variantPath(args.workspace, seedId, channel, args.variantArg);
4237
+ mkdirSync4(dirname4(target), { recursive: true });
4238
+ writeFileSync4(target, readFileSync7(args.textArg, "utf8"));
4239
+ }
4240
+ const loaded = loadProfile(args.workspace);
4241
+ if (typeof loaded === "number") return loaded;
4242
+ const variantFile = variantPath(args.workspace, seedId, channel, args.variantArg);
4243
+ if (!existsSync9(variantFile)) {
4244
+ throw new UsageError(`no variant body at ${variantFile}`);
2795
4245
  }
2796
- if (!AGENTS.includes(args.agent)) {
2797
- throw new UsageError(`unknown agent "${args.agent}" (allowed: ${AGENTS.join(", ")})`);
4246
+ const rendered = renderFacts(readFileSync7(variantFile, "utf8"), loaded);
4247
+ if (rendered.dangling.length > 0) {
4248
+ throw new UsageError(`dangling fact ref(s): ${rendered.dangling.join(", ")}`);
2798
4249
  }
2799
- const projection = `cd <skills-studio> && bin/project.sh ${args.agent}`;
4250
+ const result = chooseVariant(args.workspace, seedId, channel, args.variantArg, rendered.text);
2800
4251
  if (isJsonMode()) {
2801
- return report("skills:install", true, EXIT.OK, {
2802
- data: { agent: args.agent, implemented: false, projection, ownedBy: SKILLS_OWNER }
4252
+ return report("post", true, EXIT.OK, {
4253
+ data: {
4254
+ mode: "choose",
4255
+ seedId,
4256
+ channel,
4257
+ variant: args.variantArg,
4258
+ contentHash: result.contentHash,
4259
+ path: result.path
4260
+ }
2803
4261
  });
2804
4262
  }
2805
4263
  line(
2806
- "pasco skills:install: partial \u2014 prints the projection command; projecting is the skills-studio job (AGOS), not the CLI"
2807
- );
2808
- line(` run: ${projection}`);
2809
- line(
2810
- " profile-as-code platform skills (per-platform living knowledge, D5/D6) land with M3 research."
4264
+ `chose ${seedId}/${channel} = ${args.variantArg} (sha256 ${result.contentHash.slice(0, 12)}\u2026)`
2811
4265
  );
2812
4266
  return EXIT.OK;
2813
4267
  }
2814
-
2815
- // src/commands/status.ts
2816
- import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2817
- import { join as join7 } from "path";
2818
- import { parseDocument as parseDocument3, stringify as stringify2 } from "yaml";
2819
- var CHECKLIST_HEADER = `# CHECKLIST \u2014 deploy status
2820
-
4268
+ function handleCheck(args) {
4269
+ const seedId = requireSeed(args);
4270
+ const loaded = loadProfile(args.workspace);
4271
+ if (typeof loaded === "number") return loaded;
4272
+ const profile = loaded;
4273
+ const ledger = readLedger(args.workspace, seedId);
4274
+ const findingsByChannel = [];
4275
+ let errors = 0;
4276
+ let warnings = 0;
4277
+ for (const channel of Object.keys(ledger.channels)) {
4278
+ const state = ledger.channels[channel];
4279
+ if (state?.chosen === void 0) continue;
4280
+ const chosen = chosenVariant(args.workspace, ledger, channel);
4281
+ if (chosen === void 0) {
4282
+ errors += 1;
4283
+ findingsByChannel.push({
4284
+ channel,
4285
+ findings: [
4286
+ {
4287
+ code: "missing-body",
4288
+ severity: "error",
4289
+ message: `chosen variant "${state.chosen}" has no body at ${variantPath(
4290
+ args.workspace,
4291
+ ledger.id,
4292
+ channel,
4293
+ state.chosen
4294
+ )}`
4295
+ }
4296
+ ]
4297
+ });
4298
+ continue;
4299
+ }
4300
+ const rendered = renderFacts(chosen.text, profile);
4301
+ const findings = rendered.dangling.map((pointer) => ({
4302
+ code: "dangling-ref",
4303
+ severity: "error",
4304
+ message: `{{fact:${pointer}}} does not resolve in the profile`
4305
+ }));
4306
+ let rules;
4307
+ try {
4308
+ rules = readPosting(channel);
4309
+ } catch (err) {
4310
+ findings.push({
4311
+ code: "unsupported-channel",
4312
+ severity: "error",
4313
+ message: err.message
4314
+ });
4315
+ }
4316
+ if (rules !== void 0) findings.push(...checkText(rendered.text, rules, profile));
4317
+ for (const finding2 of findings) {
4318
+ if (finding2.severity === "error") errors += 1;
4319
+ else warnings += 1;
4320
+ }
4321
+ findingsByChannel.push({ channel, findings });
4322
+ }
4323
+ if (findingsByChannel.length === 0) {
4324
+ return report("post", false, EXIT.FAIL, {
4325
+ error: { kind: "empty", message: `${seedId} has no chosen variant` }
4326
+ });
4327
+ }
4328
+ if (isJsonMode()) {
4329
+ return report("post", errors === 0, errors === 0 ? EXIT.OK : EXIT.FAIL, {
4330
+ data: { mode: "check", seedId, errors, warnings, channels: findingsByChannel }
4331
+ });
4332
+ }
4333
+ section(`post check \u2014 ${seedId}`);
4334
+ for (const entry of findingsByChannel) {
4335
+ line(`${entry.channel}:`);
4336
+ if (entry.findings.length === 0) line(" ok");
4337
+ for (const finding2 of entry.findings) {
4338
+ const prefix = finding2.severity === "error" ? " error" : " warn";
4339
+ line(`${prefix} [${finding2.code}] ${finding2.message}`);
4340
+ }
4341
+ }
4342
+ line(`
4343
+ ${errors} error(s), ${warnings} warning(s)`);
4344
+ return errors === 0 ? EXIT.OK : EXIT.FAIL;
4345
+ }
4346
+ function handleExport(args) {
4347
+ const seedId = requireSeed(args);
4348
+ const target = args.targetArg ?? "postiz";
4349
+ if (target !== "postiz") {
4350
+ throw new UsageError(`unsupported target "${target}" (only "postiz" today)`);
4351
+ }
4352
+ const type = args.typeArg ?? "draft";
4353
+ if (!["draft", "schedule", "now"].includes(type)) {
4354
+ throw new UsageError(`--type must be draft|schedule|now (got "${type}")`);
4355
+ }
4356
+ const date = args.dateArg ?? (/* @__PURE__ */ new Date()).toISOString();
4357
+ if (Number.isNaN(Date.parse(date))) {
4358
+ throw new UsageError(`--date must be an ISO date (got "${date}")`);
4359
+ }
4360
+ if (type === "schedule" && args.dateArg === void 0) {
4361
+ throw new UsageError("--type schedule requires --date");
4362
+ }
4363
+ const loaded = loadProfile(args.workspace);
4364
+ if (typeof loaded === "number") return loaded;
4365
+ const profile = loaded;
4366
+ const ledger = readLedger(args.workspace, seedId);
4367
+ const payload = buildPostizPayload({
4368
+ workspace: args.workspace,
4369
+ ledger,
4370
+ profile,
4371
+ type,
4372
+ date
4373
+ });
4374
+ if (payload.posts.length === 0) {
4375
+ return report("post", false, EXIT.FAIL, {
4376
+ error: { kind: "empty", message: `${seedId} has no chosen variant to export` }
4377
+ });
4378
+ }
4379
+ const out = args.outArg ?? join10(seedDir(args.workspace, seedId), "postiz.json");
4380
+ mkdirSync4(dirname4(out), { recursive: true });
4381
+ writeFileSync4(out, `${JSON.stringify(payload, null, 2)}
4382
+ `);
4383
+ if (isJsonMode()) {
4384
+ return report("post", true, EXIT.OK, { data: { mode: "export", seedId, out, payload } });
4385
+ }
4386
+ line(`wrote ${out}`);
4387
+ line(
4388
+ ` ${payload.posts.length} channel(s); integration IDs: ${payload.posts.map((post) => post.integration.id).join(", ")}`
4389
+ );
4390
+ if (payload.posts.some((post) => post.integration.id === "REPLACE_ME")) {
4391
+ warn2(
4392
+ "some integration IDs are REPLACE_ME \u2014 add postiz.yaml (channels: {linkedin: <id>, x: <id>})"
4393
+ );
4394
+ }
4395
+ line(
4396
+ ` POST it: curl -X POST "$POSTIZ_API_URL/posts" -H "Authorization: $POSTIZ_API_KEY" -H "Content-Type: application/json" -d @${out}`
4397
+ );
4398
+ return EXIT.OK;
4399
+ }
4400
+ function slugify(value) {
4401
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
4402
+ }
4403
+ function handleImport(args) {
4404
+ if (args.seedArg === void 0) {
4405
+ throw new UsageError("usage: pasco post import <dir> --channel <channel>");
4406
+ }
4407
+ const sourceDir = args.seedArg;
4408
+ if (!existsSync9(sourceDir) || !statSync5(sourceDir).isDirectory()) {
4409
+ throw new UsageError(`import dir not found: ${sourceDir}`);
4410
+ }
4411
+ const channel = parsePlatform(args.channelArg ?? "");
4412
+ readPosting(channel);
4413
+ const files = readdirSync5(sourceDir).filter((name) => name.endsWith(".md")).sort();
4414
+ if (files.length === 0) throw new UsageError(`no .md files in ${sourceDir}`);
4415
+ const started = Date.now();
4416
+ const imported = [];
4417
+ for (const file of files) {
4418
+ const text = readFileSync7(join10(sourceDir, file), "utf8");
4419
+ if (text.trim() === "") continue;
4420
+ const base = slugify(file.replace(/\.md$/, "")) || "post";
4421
+ let id = `imported-${base}`;
4422
+ let n = 2;
4423
+ while (existsSync9(seedDir(args.workspace, id))) id = `imported-${base}-${n++}`;
4424
+ createSeed(args.workspace, id, text, "imported");
4425
+ const target = variantPath(args.workspace, id, channel, "imported");
4426
+ mkdirSync4(dirname4(target), { recursive: true });
4427
+ writeFileSync4(target, text);
4428
+ const ledger = readLedger(args.workspace, id);
4429
+ ledger.channels[channel] = {
4430
+ chosen: "imported",
4431
+ chosenAt: (/* @__PURE__ */ new Date()).toISOString(),
4432
+ contentHash: hashText(text),
4433
+ publishedAt: null,
4434
+ url: null,
4435
+ sync: "published",
4436
+ certification: "unverifiable"
4437
+ };
4438
+ ledger.status = "published";
4439
+ writeLedger(args.workspace, ledger);
4440
+ imported.push(id);
4441
+ }
4442
+ const elapsed = Date.now() - started;
4443
+ if (isJsonMode()) {
4444
+ return report("post", true, EXIT.OK, {
4445
+ data: { mode: "import", channel, imported, elapsedMs: elapsed }
4446
+ });
4447
+ }
4448
+ line(`imported ${imported.length} post(s) as ${channel} seeds in ${elapsed} ms`);
4449
+ for (const id of imported) line(` ${id}`);
4450
+ return EXIT.OK;
4451
+ }
4452
+ function handleList(args) {
4453
+ const ids = listSeedIds(args.workspace);
4454
+ const rows = ids.map((id) => {
4455
+ try {
4456
+ const ledger = readLedger(args.workspace, id);
4457
+ const channels = Object.entries(ledger.channels).map(([channel, state]) => `${channel}:${state.chosen ?? "\u2014"}`).join(" ");
4458
+ return { id, source: ledger.source, status: ledger.status, channels };
4459
+ } catch {
4460
+ return { id, source: "?", status: "?", channels: "?" };
4461
+ }
4462
+ });
4463
+ if (isJsonMode()) {
4464
+ return report("post", true, EXIT.OK, { data: { mode: "list", posts: rows } });
4465
+ }
4466
+ if (rows.length === 0) {
4467
+ line(`no posts under ${postsDir(args.workspace)}`);
4468
+ return EXIT.OK;
4469
+ }
4470
+ for (const row of rows) {
4471
+ line(`${row.id} [${row.source}/${row.status}] ${row.channels}`);
4472
+ }
4473
+ return EXIT.OK;
4474
+ }
4475
+ function runPost(args) {
4476
+ switch (args.subcommand) {
4477
+ case "seed":
4478
+ return handleSeed(args);
4479
+ case "task":
4480
+ return handleTask(args);
4481
+ case "choose":
4482
+ return handleChoose(args);
4483
+ case "check":
4484
+ return handleCheck(args);
4485
+ case "export":
4486
+ return handleExport(args);
4487
+ case "import":
4488
+ return handleImport(args);
4489
+ case "list":
4490
+ return handleList(args);
4491
+ default:
4492
+ throw new UsageError(
4493
+ "usage: pasco post <seed|task|choose|check|export|import|list> \u2026 (run pasco post --help)"
4494
+ );
4495
+ }
4496
+ }
4497
+
4498
+ // src/commands/refine.ts
4499
+ import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
4500
+ import { join as join11 } from "path";
4501
+ import { parseDocument as parseDocument4, stringify as stringify2 } from "yaml";
4502
+ var REFINE_OWNER = "M3 SOTA research per platform \u2192 skills v1 + the full refine flow (SPEC \xA712/\xA716 M3); model-agnostic (D10/D11/D13)";
4503
+ function loadProfile2(workspace) {
4504
+ try {
4505
+ return load(workspace);
4506
+ } catch (err) {
4507
+ if (err instanceof SchemaValidationError) {
4508
+ return report("refine", false, EXIT.FAIL, {
4509
+ error: {
4510
+ kind: "schema",
4511
+ message: `schema validation failed (D12 fail-on-schema) \u2014 ${err.issues.length} violation(s)`,
4512
+ violations: [...err.issues]
4513
+ }
4514
+ });
4515
+ }
4516
+ if (err instanceof LoadError) {
4517
+ return report("refine", false, EXIT.FAIL, { error: { kind: "load", message: err.message } });
4518
+ }
4519
+ throw err;
4520
+ }
4521
+ }
4522
+ function recordVariant(workspace, platform, lang, variantId) {
4523
+ const activePath = join11(workspace, "content", "active.yaml");
4524
+ let active = {};
4525
+ if (existsSync10(activePath)) {
4526
+ const doc = parseDocument4(readFileSync8(activePath, "utf8"));
4527
+ if (doc.errors.length > 0) {
4528
+ throw new Error(
4529
+ `content/active.yaml: invalid YAML \u2014 ${doc.errors[0]?.message ?? "parse error"}`
4530
+ );
4531
+ }
4532
+ const node = doc.toJS({ maxAliasCount: 100 });
4533
+ if (node !== null && node !== void 0) {
4534
+ if (typeof node !== "object" || Array.isArray(node)) {
4535
+ throw new Error("content/active.yaml must contain a mapping (platform \u2192 variant entry)");
4536
+ }
4537
+ active = node;
4538
+ }
4539
+ }
4540
+ const chosenAt = (/* @__PURE__ */ new Date()).toISOString();
4541
+ active[platform] = { lang, variantId, chosenAt };
4542
+ writeFileSync5(activePath, stringify2(active));
4543
+ return { chosenAt };
4544
+ }
4545
+ function runRefine(args) {
4546
+ if (args.platformArg === void 0) {
4547
+ throw new UsageError(
4548
+ "usage: pasco refine <platform> [--lang <l>] [--variants N] [--choose <variantId>]"
4549
+ );
4550
+ }
4551
+ const platform = parsePlatform(args.platformArg);
4552
+ const lang = parseLang(defaultsLang(args.pascorc, args.langArg));
4553
+ const variants = parseVariants(args.variantsArg, args.pascorc.defaults?.variants ?? 3);
4554
+ if (args.choose !== void 0 && !isSafeVariantId(args.choose)) {
4555
+ throw new UsageError(
4556
+ "--choose <variantId> must be a safe id (letters, digits, dot, underscore, dash; no slash or '..')"
4557
+ );
4558
+ }
4559
+ const loaded = loadProfile2(args.workspace);
4560
+ if (typeof loaded === "number") return loaded;
4561
+ const profile = loaded;
4562
+ if (args.choose !== void 0) {
4563
+ let chosenAt;
4564
+ try {
4565
+ chosenAt = recordVariant(args.workspace, platform, lang, args.choose).chosenAt;
4566
+ } catch (err) {
4567
+ return report("refine", false, EXIT.FAIL, {
4568
+ error: { kind: "load", message: err.message }
4569
+ });
4570
+ }
4571
+ const variantFile = variantBodyPath(args.workspace, platform, args.choose);
4572
+ const hasBody = hasVariantBody(args.workspace, platform, args.choose);
4573
+ if (isJsonMode()) {
4574
+ return report("refine", true, EXIT.OK, {
4575
+ data: {
4576
+ mode: "recorded",
4577
+ platform,
4578
+ lang,
4579
+ variantId: args.choose,
4580
+ chosenAt,
4581
+ workspace: args.workspace,
4582
+ variantFile,
4583
+ hasBody
4584
+ }
4585
+ });
4586
+ }
4587
+ line(
4588
+ `recorded active[${platform}] = { lang: ${lang}, variantId: ${args.choose}, chosenAt: ${chosenAt} } (content/active.yaml)`
4589
+ );
4590
+ if (!hasBody) {
4591
+ line(
4592
+ ` warning: no variant body at ${variantFile} \u2014 pasco build will render the deterministic text until it exists`
4593
+ );
4594
+ }
4595
+ return EXIT.OK;
4596
+ }
4597
+ const slice = compile(profile, platform, lang);
4598
+ const identity = slice.identity;
4599
+ const links = (identity.links ?? []).map((l) => `${l.platform ?? "?"}:${l.url}`).join(", ");
4600
+ const experience = slice.experience.map((e) => `${e.role} @ ${e.org} (${e.start}\u2013${e.end ?? "present"})`).join(" | ");
4601
+ const projects = slice.projects.map((p) => p.name).join(", ");
4602
+ const publications = slice.publications.map((p) => p.title).join(" | ");
4603
+ const anchors = slice.keywords.geo.associations?.join(", ") ?? "";
4604
+ const skill = lookupSkill(platform, "SKILL.md");
4605
+ const skillPath = skill.state === "found" ? skill.path : void 0;
4606
+ const researchCandidate = skillPath === void 0 ? void 0 : skillPath.replace(/SKILL\.md$/, "research/SOTA-2026.md");
4607
+ const researchPath = researchCandidate !== void 0 && existsSync10(researchCandidate) ? researchCandidate : "\u2014";
4608
+ const template = [
4609
+ `# pasco refine \u2014 agent task (agent-driven: D10/D11/D13; no LLM call, no hub)`,
4610
+ `Write ${variants} variant(s) of the ${platform} profile in ${lang} \u2014 in PROSE, per the platform skill.`,
4611
+ ``,
4612
+ `Platform: ${platform}`,
4613
+ `Language: ${lang}`,
4614
+ `Skill (communicative strategy \u2014 READ IT FIRST): ${skillLine(skill)}`,
4615
+ `Research (dated sources): ${researchPath}`,
4616
+ `Variant id convention: ${platform.replace(/[^a-z]/g, "")}-${lang}-<n> (e.g. gh-en-1, gh-en-2, gh-en-3).`,
4617
+ ``,
4618
+ `Facts (language-independent \u2014 MUST stay byte-identical across variants and platforms):`,
4619
+ ` name: ${identity.name}`,
4620
+ ` location: ${identity.location ?? "\u2014"}`,
4621
+ ` links: ${links || "\u2014"}`,
4622
+ ` experience: ${experience || "\u2014"}`,
4623
+ ` projects: ${projects || "\u2014"}`,
4624
+ ` publications: ${publications || "\u2014"}`,
4625
+ ``,
4626
+ `Existing ${platform} texts (base to vary):`,
4627
+ ` bios: ${slice.bios.map((b) => `[${b.length}] ${b.text}`).join(" | ") || "\u2014"}`,
4628
+ ` headlines: ${slice.headlines.map((h) => h.text).join(" | ") || "\u2014"}`,
4629
+ ` active variant: ${slice.active?.variantId ?? "\u2014"}`,
4630
+ ``,
4631
+ `GEO anchors to reinforce (semantic associations): ${anchors || "\u2014"}`,
4632
+ ``,
4633
+ `Constraints:`,
4634
+ ` - Facts never change (D4) \u2014 only storytelling/angles vary.`,
4635
+ ` - Every text langMap must include 'en' (contract requirement) and the requested lang.`,
4636
+ ` - Never invent URLs, DOIs or numbers \u2014 evidence claims stay untouched.`,
4637
+ ` - Platform-specific limits come from the platform skill (SPEC \xA716) + checks.yaml.`,
4638
+ ``,
4639
+ `Workflow: read the skill \u2192 produce the ${variants} variant(s) \u2192 the user picks one \u2192 save its body to`,
4640
+ ` profiles/${platform}/variants/<variantId>.md (one text block), then record it:`,
4641
+ ` pasco refine ${platform} --lang ${lang} --choose <variantId>`,
4642
+ ``
4643
+ ].join("\n");
4644
+ if (isJsonMode()) {
4645
+ return report("refine", true, EXIT.OK, {
4646
+ data: { mode: "template", platform, lang, variants, template, skill, ownedBy: REFINE_OWNER }
4647
+ });
4648
+ }
4649
+ process.stdout.write(template);
4650
+ return EXIT.OK;
4651
+ }
4652
+
4653
+ // src/commands/skills-export.ts
4654
+ import { cpSync, existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync6, statSync as statSync6 } from "fs";
4655
+ import { join as join12, resolve as resolve4, sep } from "path";
4656
+ function countFiles(dir) {
4657
+ let total = 0;
4658
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
4659
+ if (entry.isDirectory()) total += countFiles(join12(dir, entry.name));
4660
+ else total += 1;
4661
+ }
4662
+ return total;
4663
+ }
4664
+ function runSkillsExport(args) {
4665
+ if (!args.list && args.dir === void 0) {
4666
+ throw new UsageError("usage: pasco skills:export <dir> [--force] | pasco skills:export --list");
4667
+ }
4668
+ if (args.list && args.dir !== void 0) {
4669
+ throw new UsageError("--list takes no destination \u2014 run them separately");
4670
+ }
4671
+ const root = skillsDir();
4672
+ if (root === void 0) {
4673
+ return report("skills:export", false, EXIT.FAIL, {
4674
+ error: { kind: "skills", message: NOT_SHIPPED_MESSAGE }
4675
+ });
4676
+ }
4677
+ const platforms = listChannels(root);
4678
+ const strategySkills = listSkills(root);
4679
+ if (args.list) {
4680
+ if (isJsonMode()) {
4681
+ return report("skills:export", true, EXIT.OK, {
4682
+ data: { root, platforms, strategySkills }
4683
+ });
4684
+ }
4685
+ line(
4686
+ `pasco skills:export: ${platforms.length} skill channels shipped with this package (${strategySkills.length} with SKILL.md)`
4687
+ );
4688
+ line(` ${root}`);
4689
+ for (const platform of platforms) {
4690
+ line(` - ${platform}${strategySkills.includes(platform) ? "" : " (post-only)"}`);
4691
+ }
4692
+ line(" run: pasco skills:export <dir> # copy them into a directory you name");
4693
+ return EXIT.OK;
4694
+ }
4695
+ const destination = resolve4(args.dir);
4696
+ if (destination === resolve4(root) || destination.startsWith(`${resolve4(root)}${sep}`)) {
4697
+ return report("skills:export", false, EXIT.FAIL, {
4698
+ error: {
4699
+ kind: "destination",
4700
+ message: "the destination cannot be inside the shipped skills tree"
4701
+ }
4702
+ });
4703
+ }
4704
+ if (existsSync11(destination) && !statSync6(destination).isDirectory()) {
4705
+ return report("skills:export", false, EXIT.FAIL, {
4706
+ error: { kind: "destination", message: `${destination} exists and is not a directory` }
4707
+ });
4708
+ }
4709
+ if (existsSync11(destination) && readdirSync6(destination).length > 0 && !args.force) {
4710
+ return report("skills:export", false, EXIT.FAIL, {
4711
+ error: {
4712
+ kind: "destination",
4713
+ message: `${destination} is not empty \u2014 pass --force to overlay the shipped skills`
4714
+ }
4715
+ });
4716
+ }
4717
+ mkdirSync5(destination, { recursive: true });
4718
+ cpSync(root, destination, {
4719
+ recursive: true,
4720
+ force: true,
4721
+ filter: (source) => !source.endsWith(".DS_Store")
4722
+ });
4723
+ const files = countFiles(root);
4724
+ if (isJsonMode()) {
4725
+ return report("skills:export", true, EXIT.OK, {
4726
+ data: { root, destination, platforms, strategySkills, files }
4727
+ });
4728
+ }
4729
+ line(
4730
+ `pasco skills:export: copied ${platforms.length} skill channels (${files} files) to ${destination}`
4731
+ );
4732
+ return EXIT.OK;
4733
+ }
4734
+
4735
+ // src/commands/status.ts
4736
+ import { writeFileSync as writeFileSync7 } from "fs";
4737
+ import { join as join14 } from "path";
4738
+
4739
+ // src/report-shape.ts
4740
+ function summaryOf(data) {
4741
+ return {
4742
+ total: data.units.length,
4743
+ inSync: data.units.filter((u) => u.state === "in-sync").length,
4744
+ stale: data.units.filter((u) => u.state === "stale").length,
4745
+ never: data.units.filter((u) => u.state === "never").length
4746
+ };
4747
+ }
4748
+ function reportDataOf(data, args, publication) {
4749
+ return {
4750
+ workspace: args.workspace,
4751
+ platform: args.platformArg ?? null,
4752
+ ...summaryOf(data),
4753
+ ...publication === void 0 ? {} : { publication },
4754
+ units: data.units,
4755
+ untracked: data.untracked,
4756
+ removed: data.removed
4757
+ };
4758
+ }
4759
+ function certificationMap(profile) {
4760
+ const map = {};
4761
+ for (const unit of deployStatus(profile).units) map[unit.key] = unit.certification;
4762
+ return map;
4763
+ }
4764
+ function filterReport(reportData, platform) {
4765
+ if (platform === void 0) return reportData;
4766
+ return {
4767
+ ...reportData,
4768
+ units: reportData.units.map((u) => {
4769
+ const onSurface = u.surfaces[platform];
4770
+ const state = onSurface === "in-sync" || onSurface === "stale" ? onSurface : "never";
4771
+ const surfaceCert = u.certification?.surfaces[platform];
4772
+ if (surfaceCert === void 0) return { ...u, surfaces: {}, state, certification: null };
4773
+ const certification = {
4774
+ state: surfaceCert.state,
4775
+ surfaces: { [platform]: surfaceCert },
4776
+ ...surfaceCert.reason !== void 0 ? { reason: surfaceCert.reason } : {}
4777
+ };
4778
+ return {
4779
+ ...u,
4780
+ surfaces: onSurface === void 0 ? {} : { [platform]: onSurface },
4781
+ state,
4782
+ certification
4783
+ };
4784
+ })
4785
+ };
4786
+ }
4787
+
4788
+ // src/verify.ts
4789
+ import { createHash as createHash4 } from "crypto";
4790
+ import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
4791
+ import { join as join13 } from "path";
4792
+ import { parseDocument as parseDocument5, stringify as stringify3 } from "yaml";
4793
+
4794
+ // src/capabilities.ts
4795
+ function capabilitiesOf(profile) {
4796
+ return {
4797
+ "github-readme": {
4798
+ extractor: extractArtifact2,
4799
+ fetchHosts: fetchHosts2,
4800
+ artifactUrl: artifactUrl2(profile)
4801
+ },
4802
+ ...profile.flavor === "brand" ? {
4803
+ "github-org": {
4804
+ extractor: extractArtifact,
4805
+ fetchHosts,
4806
+ artifactUrl: artifactUrl(profile)
4807
+ }
4808
+ } : {}
4809
+ };
4810
+ }
4811
+
4812
+ // src/verify.ts
4813
+ var depsOf = (args, profile) => ({
4814
+ capabilities: args.deps?.capabilities ?? capabilitiesOf(profile),
4815
+ fetchText: args.deps?.fetchText ?? (async () => ({ status: 0 }))
4816
+ });
4817
+ function ledgerPathOf(workspace) {
4818
+ return join13(workspace, "content", "deployments.yaml");
4819
+ }
4820
+ function readLedger2(args, kind) {
4821
+ const ledgerPath2 = ledgerPathOf(args.workspace);
4822
+ let ledger = {};
4823
+ if (existsSync12(ledgerPath2)) {
4824
+ const doc = parseDocument5(readFileSync9(ledgerPath2, "utf8"));
4825
+ if (doc.errors.length > 0) {
4826
+ return {
4827
+ fail: report("status", false, EXIT.FAIL, {
4828
+ error: {
4829
+ kind,
4830
+ message: `content/deployments.yaml: invalid YAML \u2014 ${doc.errors[0]?.message ?? "parse error"}`
4831
+ }
4832
+ })
4833
+ };
4834
+ }
4835
+ const node = doc.toJS({ maxAliasCount: 100 });
4836
+ if (node !== null && typeof node === "object")
4837
+ ledger = node;
4838
+ }
4839
+ return { ledger };
4840
+ }
4841
+ function persistLedger(args, ledger) {
4842
+ writeFileSync6(ledgerPathOf(args.workspace), stringify3(ledger), "utf8");
4843
+ }
4844
+ function digestOf(artifact) {
4845
+ return createHash4("sha256").update(artifact, "utf8").digest("hex");
4846
+ }
4847
+ async function verifyStatus(args, profile) {
4848
+ const deps = depsOf(args, profile);
4849
+ if (args.platformArg !== void 0 && !PLATFORMS.includes(args.platformArg)) {
4850
+ return report("status", false, EXIT.USAGE, {
4851
+ error: {
4852
+ kind: "usage",
4853
+ message: `surface "${args.platformArg}" is not in the platform set (D16)`
4854
+ }
4855
+ });
4856
+ }
4857
+ const ledger = profile.deployments;
4858
+ const subjects = [];
4859
+ for (const unit of trackableUnits(profile)) {
4860
+ const surfacesOfUnit = ledger[unit.key];
4861
+ if (surfacesOfUnit === void 0) continue;
4862
+ for (const surface of Object.keys(surfacesOfUnit).sort()) {
4863
+ if (args.platformArg !== void 0 && surface !== args.platformArg) continue;
4864
+ subjects.push({
4865
+ key: unit.key,
4866
+ surface,
4867
+ existing: surfacesOfUnit[surface]
4868
+ });
4869
+ }
4870
+ }
4871
+ const verdicts = [];
4872
+ let requests = 0;
4873
+ for (const subject of subjects) {
4874
+ const verdict = await verifySurface(
4875
+ subject.surface,
4876
+ subject.existing,
4877
+ deps.capabilities[subject.surface],
4878
+ deps.fetchText
4879
+ );
4880
+ if (verdict.fetched) requests += 1;
4881
+ if (verdict.write) {
4882
+ const surfacesOfUnit = ledger[subject.key];
4883
+ if (surfacesOfUnit !== void 0) {
4884
+ surfacesOfUnit[subject.surface] = {
4885
+ ...subject.existing,
4886
+ certification: verdict.certification
4887
+ };
4888
+ }
4889
+ }
4890
+ verdicts.push({ key: subject.key, verdict });
4891
+ }
4892
+ if (verdicts.some((v) => v.verdict.write)) persistLedger(args, ledger);
4893
+ return finishVerify(args, profile, verdicts, requests);
4894
+ }
4895
+ function finishVerify(args, profile, verdicts, requests) {
4896
+ const filtered = filterReport(deployStatus(profile), args.platformArg);
4897
+ const derived = /* @__PURE__ */ new Map();
4898
+ for (const { key, verdict } of verdicts) {
4899
+ if (verdict.write) continue;
4900
+ const surfaces = derived.get(key) ?? /* @__PURE__ */ new Map();
4901
+ surfaces.set(verdict.surface, verdict.certification);
4902
+ derived.set(key, surfaces);
4903
+ }
4904
+ const withDerived = {
4905
+ ...filtered,
4906
+ units: filtered.units.map((unit) => {
4907
+ const overlay = derived.get(unit.key);
4908
+ if (overlay === void 0) return unit;
4909
+ const surfaces = {
4910
+ ...unit.certification?.surfaces ?? {},
4911
+ ...Object.fromEntries(overlay)
4912
+ };
4913
+ const rollup = certificationRollup(surfaces);
4914
+ return {
4915
+ ...unit,
4916
+ certification: {
4917
+ state: rollup.state,
4918
+ surfaces,
4919
+ ...rollup.reason !== void 0 ? { reason: rollup.reason } : {}
4920
+ }
4921
+ };
4922
+ })
4923
+ };
4924
+ const publication = publicationOfCaller(args, profile, withDerived, DEFAULT_CHECKS);
4925
+ if (isJsonMode()) {
4926
+ return report("status", true, EXIT.OK, {
4927
+ data: reportDataOf(withDerived, args, publication)
4928
+ });
4929
+ }
4930
+ section(
4931
+ `verified \u2014 ${requests} fetch(es) for ${verdicts.length} record(s)${args.platformArg !== void 0 ? ` (surface: ${args.platformArg})` : ""}`
4932
+ );
4933
+ for (const { key, verdict } of verdicts) {
4934
+ line(` ${key} \u2192 ${verdict.surface}: ${certToken(verdict.certification)}`);
4935
+ }
4936
+ if (publication.length > 0) {
4937
+ section("publication truth \u2014 derived, never stored (SPEC \xA720.1)");
4938
+ for (const truth of publication) {
4939
+ line(` ${publicationLine(truth)}`);
4940
+ for (const finding2 of truth.findings) {
4941
+ line(` warn: [${finding2.code}] ${finding2.message} (${finding2.path})`);
4942
+ }
4943
+ }
4944
+ line(` coverage: ${publicationCoverageLine(publication)}`);
4945
+ }
4946
+ if (verdicts.length === 0) {
4947
+ line(" nothing to verify \u2014 no ledger record on the requested surface(s)");
4948
+ }
4949
+ line(
4950
+ " certification compares the LIVE digest against the stored anchor; a record with no anchor is unverifiable: unattributed"
4951
+ );
4952
+ line(
4953
+ " the ledger file was written \u2014 committing content/deployments.yaml is the operator's (D2)"
4954
+ );
4955
+ return EXIT.OK;
4956
+ }
4957
+ async function verifySurface(surface, existing, capability, fetchText) {
4958
+ const stored = existing.certification;
4959
+ const declared = declaredReason(capability);
4960
+ if (declared !== void 0) {
4961
+ return {
4962
+ surface,
4963
+ certification: { state: "unverifiable", reason: declared },
4964
+ fetched: false,
4965
+ write: false
4966
+ };
4967
+ }
4968
+ const extractor = capability?.extractor;
4969
+ const url = capability?.artifactUrl;
4970
+ let host = "";
4971
+ try {
4972
+ host = new URL(url).host;
4973
+ } catch {
4974
+ return {
4975
+ surface,
4976
+ certification: { state: "unverifiable", reason: "no-artifact-url" },
4977
+ fetched: false,
4978
+ write: false
4979
+ };
4980
+ }
4981
+ if (!(capability?.fetchHosts ?? []).includes(host)) {
4982
+ return {
4983
+ surface,
4984
+ certification: { state: "unverifiable", reason: "host-not-allowed" },
4985
+ fetched: false,
4986
+ write: false
4987
+ };
4988
+ }
4989
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
4990
+ const response = await fetchText(url);
4991
+ const evidence = {
4992
+ ...stored?.artifactHash !== void 0 ? { artifactHash: stored.artifactHash } : {},
4993
+ fetchedAt,
4994
+ evidenceUrl: url
4995
+ };
4996
+ if (response.status < 200 || response.status >= 300) {
4997
+ return {
4998
+ surface,
4999
+ certification: attemptRecord(
5000
+ { state: "unverifiable", reason: fetchFailureReason(response.status) },
5001
+ evidence
5002
+ ),
5003
+ fetched: true,
5004
+ write: true
5005
+ };
5006
+ }
5007
+ let digest;
5008
+ if (response.text !== void 0) {
5009
+ try {
5010
+ digest = digestOf(extractor(response.text));
5011
+ } catch {
5012
+ digest = void 0;
5013
+ }
5014
+ }
5015
+ const verdict = certificationVerdict(
5016
+ { certification: stored },
5017
+ { status: response.status, ...digest !== void 0 ? { digest } : {} }
5018
+ );
5019
+ return {
5020
+ surface,
5021
+ certification: attemptRecord(verdict, {
5022
+ ...evidence,
5023
+ ...digest !== void 0 ? { liveHash: digest } : {}
5024
+ }),
5025
+ fetched: true,
5026
+ write: true
5027
+ };
5028
+ }
5029
+
5030
+ // src/commands/status.ts
5031
+ var CHECKLIST_HEADER = `# CHECKLIST \u2014 deploy status
5032
+
2821
5033
  <!-- AUTO-GENERATED by \`pasco status --write-checklist\` (D18, SPEC \xA719) \u2014 do not edit by hand.
2822
5034
  Regenerate after every deploy confirm: pasco status --mark <ledgerKey> <surface>.
2823
5035
  Source of truth: content/deployments.yaml (only \`pasco\` mutates it). -->`;
@@ -2839,6 +5051,26 @@ function loadOrReport(args) {
2839
5051
  throw err;
2840
5052
  }
2841
5053
  }
5054
+ function artifactAnchor(args, profile, surface, capability) {
5055
+ const declared = declaredReason(capability);
5056
+ if (declared !== void 0) return { reason: declared };
5057
+ const extractor = capability?.extractor;
5058
+ if (extractor === void 0) return { reason: "no-extractor" };
5059
+ try {
5060
+ const resolver = createArtifactResolver({
5061
+ workspace: args.workspace,
5062
+ profile,
5063
+ completenessPolicy: args.completenessPolicy,
5064
+ selectionPolicy: args.selectionPolicy
5065
+ });
5066
+ const artifact = resolver.built(surface) ?? resolver.render(surface, args.lang ?? "en");
5067
+ if (artifact === void 0) return { reason: "no-renderer" };
5068
+ if (artifact === "") return { reason: "empty-render" };
5069
+ return { anchor: digestOf(extractor(artifact)) };
5070
+ } catch (err) {
5071
+ return { reason: `render-failed (${err instanceof Error ? err.message : String(err)})` };
5072
+ }
5073
+ }
2842
5074
  function markDeployed(args, profile) {
2843
5075
  const key = args.markKey;
2844
5076
  const surface = args.platformArg;
@@ -2879,39 +5111,52 @@ function markDeployed(args, profile) {
2879
5111
  }
2880
5112
  deployedAt = at.toISOString();
2881
5113
  }
2882
- const ledgerPath = join7(args.workspace, "content", "deployments.yaml");
2883
- let ledger = {};
2884
- if (existsSync6(ledgerPath)) {
2885
- const doc = parseDocument3(readFileSync5(ledgerPath, "utf8"));
2886
- if (doc.errors.length > 0) {
2887
- return report("status", false, EXIT.FAIL, {
2888
- error: {
2889
- kind: "mark",
2890
- message: `content/deployments.yaml: invalid YAML \u2014 ${doc.errors[0]?.message ?? "parse error"}`
2891
- }
2892
- });
2893
- }
2894
- const node = doc.toJS({ maxAliasCount: 100 });
2895
- if (node !== null && typeof node === "object")
2896
- ledger = node;
2897
- }
2898
- const record = { deployedAt, contentHash: unit.hash, source: "manual" };
5114
+ const read = readLedger2(args, "mark");
5115
+ if ("fail" in read) return read.fail;
5116
+ const { ledger } = read;
5117
+ const capability = depsOf(args, profile).capabilities[surface];
5118
+ const { anchor, reason: anchorReason } = artifactAnchor(args, profile, surface, capability);
5119
+ const certification = attestedRecord(anchor);
5120
+ const record = { deployedAt, contentHash: unit.hash, source: "manual", certification };
2899
5121
  ledger[key] = { ...ledger[key] ?? {}, [surface]: record };
2900
- writeFileSync4(ledgerPath, stringify2(ledger), "utf8");
5122
+ persistLedger(args, ledger);
2901
5123
  if (isJsonMode()) {
2902
5124
  return report("status", true, EXIT.OK, {
2903
- data: { mode: "marked", key, surface, ...record, workspace: args.workspace }
5125
+ data: {
5126
+ mode: "marked",
5127
+ key,
5128
+ surface,
5129
+ ...record,
5130
+ ...anchorReason !== void 0 ? { anchorReason } : {},
5131
+ workspace: args.workspace
5132
+ }
2904
5133
  });
2905
5134
  }
2906
5135
  section("marked");
2907
5136
  line(` ${key} \u2192 ${surface}: deployedAt ${deployedAt} \xB7 contentHash ${unit.hash.slice(0, 12)}\u2026`);
5137
+ line(
5138
+ certification.artifactHash !== void 0 ? ` certification: attested (anchor ${certification.artifactHash.slice(0, 12)}\u2026 \u2014 --verify can judge this record)` : ` certification: attested (no anchor \u2014 ${anchorReason}; --verify will observe this record but cannot judge it)`
5139
+ );
2908
5140
  line(
2909
5141
  " confirm the paste is live on the surface before running this (the ledger records the claim)"
2910
5142
  );
2911
5143
  return EXIT.OK;
2912
5144
  }
2913
- function checklistBody(reportData, completion) {
2914
- const fmt = (surfaces) => Object.entries(surfaces).sort().map(([s, state]) => `${s} (${state})`).join(", ");
5145
+ function certToken(certification) {
5146
+ return certification.reason === void 0 ? certification.state : `${certification.state}: ${certification.reason}`;
5147
+ }
5148
+ function certOf(certification, surface) {
5149
+ const surfaceCert = certification?.surfaces[surface];
5150
+ return surfaceCert === void 0 ? "\u2014" : certToken(surfaceCert);
5151
+ }
5152
+ function formatSurfacePairs(surfaces, certification) {
5153
+ const keys = Object.keys(surfaces).sort();
5154
+ if (keys.length === 0) return "\u2014";
5155
+ return keys.map((surface) => `${surface} (${surfaces[surface]}/${certOf(certification, surface)})`).join(", ");
5156
+ }
5157
+ function checklistBody(reportData, completion, profile, publication) {
5158
+ const certification = certificationMap(profile);
5159
+ const fmt = (u) => formatSurfacePairs(u.surfaces, certification[u.key] ?? null);
2915
5160
  const stale = reportData.units.filter((u) => u.state === "stale");
2916
5161
  const inSync = reportData.units.filter((u) => u.state === "in-sync");
2917
5162
  const never = reportData.units.filter((u) => u.state === "never");
@@ -2921,17 +5166,20 @@ function checklistBody(reportData, completion) {
2921
5166
  // Issue #102: the completeness number rides the SAME generated view — it is
2922
5167
  // derived state (never stored) and the checklist is where the user looks.
2923
5168
  `Profile completeness: ${completenessLine(completion)}`,
5169
+ // #242: the derived publication truth rides the SAME generated view — it is
5170
+ // coverage of publication, never a new score (SPEC §20).
5171
+ `Publication truth: ${publicationCoverageOrNone(publication)}`,
2924
5172
  ""
2925
5173
  ];
2926
5174
  lines2.push("## Pending \u2014 stale (SSOT changed after the last deploy)");
2927
5175
  if (stale.length === 0) lines2.push("_None._");
2928
- for (const u of stale) lines2.push(`- [ ] \`${u.key}\` (${u.label}) \u2014 ${fmt(u.surfaces)}`);
5176
+ for (const u of stale) lines2.push(`- [ ] \`${u.key}\` (${u.label}) \u2014 ${fmt(u)}`);
2929
5177
  lines2.push("", "## Never deployed");
2930
5178
  if (never.length === 0) lines2.push("_None._");
2931
5179
  for (const u of never) lines2.push(`- [ ] \`${u.key}\` (${u.label})`);
2932
5180
  lines2.push("", "## In sync");
2933
5181
  if (inSync.length === 0) lines2.push("_None._");
2934
- for (const u of inSync) lines2.push(`- [x] \`${u.key}\` (${u.label}) \u2014 ${fmt(u.surfaces)}`);
5182
+ for (const u of inSync) lines2.push(`- [x] \`${u.key}\` (${u.label}) \u2014 ${fmt(u)}`);
2935
5183
  lines2.push("", "## Untracked (add an `id` to track)");
2936
5184
  if (reportData.untracked.length === 0) lines2.push("_None._");
2937
5185
  for (const u of reportData.untracked) lines2.push(`- ${u.block} \u2014 ${u.label}`);
@@ -2943,11 +5191,40 @@ function checklistBody(reportData, completion) {
2943
5191
  lines2.push("");
2944
5192
  return lines2.join("\n");
2945
5193
  }
5194
+ function certificationLineOf(reportData) {
5195
+ const withRecord = reportData.units.filter((u) => u.certification !== null);
5196
+ const certified = withRecord.filter(
5197
+ (u) => u.certification?.state === "verified" || u.certification?.state === "attested"
5198
+ );
5199
+ return `${certified.length}/${withRecord.length} recorded unit(s) certified`;
5200
+ }
2946
5201
  function completenessOf(args, profile) {
2947
5202
  return completeness(profile, sanitizeCompletenessPolicy(args.completenessPolicy));
2948
5203
  }
2949
- function runCompleteness(args, profile) {
5204
+ function publicationOf(args, profile, reportData) {
5205
+ return publicationOfCaller(args, profile, reportData, DEFAULT_CHECKS);
5206
+ }
5207
+ function publicationAllOf(args, profile, reportData) {
5208
+ return publicationOfCaller(
5209
+ { ...args, platformArg: void 0 },
5210
+ profile,
5211
+ reportData,
5212
+ DEFAULT_CHECKS
5213
+ );
5214
+ }
5215
+ function artifactDetail(reason) {
5216
+ switch (reason) {
5217
+ case "local-mismatch":
5218
+ return "the artifact on disk differs from the SSOT render \u2014 run pasco build";
5219
+ case "live-drift":
5220
+ return "\xA719.5 recorded out-of-band drift: the live surface no longer matches what was deployed \u2014 re-publish, then re-confirm";
5221
+ case "no-artifact":
5222
+ return "no artifact on disk for this surface \u2014 run pasco build";
5223
+ }
5224
+ }
5225
+ function runCompleteness(args, profile, reportData) {
2950
5226
  const result = completenessOf(args, profile);
5227
+ const publication = publicationAllOf(args, profile, reportData);
2951
5228
  if (isJsonMode()) {
2952
5229
  return report("status", true, EXIT.OK, {
2953
5230
  data: {
@@ -2957,7 +5234,8 @@ function runCompleteness(args, profile) {
2957
5234
  satisfied: result.satisfied,
2958
5235
  total: result.total,
2959
5236
  target: result.target,
2960
- items: result.items
5237
+ items: result.items,
5238
+ publication
2961
5239
  }
2962
5240
  });
2963
5241
  }
@@ -2965,10 +5243,42 @@ function runCompleteness(args, profile) {
2965
5243
  for (const item of result.items) {
2966
5244
  line(` ${item.satisfied ? "[x]" : "[ ]"} ${item.label} \u2014 ${item.detail}`);
2967
5245
  }
5246
+ line(` publication coverage \u2014 ${publicationCoverageOrNone(publication)}`);
2968
5247
  line(" \u2192 pasco status --write-checklist folds the number into CHECKLIST.md");
2969
5248
  return EXIT.OK;
2970
5249
  }
2971
- function runStatus(args) {
5250
+ function runSelection(args, profile) {
5251
+ const policy = sanitizeSelectionPolicy(args.selectionPolicy);
5252
+ const viewName = args.viewArg ?? args.platformArg;
5253
+ const rule = viewName === void 0 ? void 0 : policy?.views?.[viewName] ?? policy?.default;
5254
+ const platform = args.platformArg !== void 0 ? parsePlatform(args.platformArg) : rule?.platform !== void 0 ? parsePlatform(rule.platform) : void 0;
5255
+ const platforms = platform !== void 0 ? [platform] : [...PLATFORMS];
5256
+ const lang = args.lang ?? "en";
5257
+ const reports = platforms.map(
5258
+ (p) => selectFacts(profile, viewName ?? p, p, lang, policy).report
5259
+ );
5260
+ const withOmissions = reports.filter((r) => r.omitted.length > 0);
5261
+ if (isJsonMode()) {
5262
+ return report("status", true, EXIT.OK, {
5263
+ data: { mode: "selection", workspace: args.workspace, view: viewName ?? null, reports }
5264
+ });
5265
+ }
5266
+ section(
5267
+ `fact selection (D22/\xA721) \u2014 ${viewName ?? "implicit platform view"}${withOmissions.length === 0 ? " \xB7 nothing omitted" : ""}`
5268
+ );
5269
+ for (const r of reports) {
5270
+ const emitted = Object.entries(r.selected).filter(([, n]) => n > 0).map(([block, n]) => `${block} ${n}`).join(" \xB7 ");
5271
+ line(
5272
+ `
5273
+ [${r.platform}]${r.mode === "tags" ? " (mode: tags)" : ""} \u2014 ${emitted || "no entries"}`
5274
+ );
5275
+ for (const o of r.omitted) line(` omitted ${o.block}:${o.label} \u2014 ${o.reason}`);
5276
+ for (const f of r.findings) line(` ${f.severity}: [${f.code}] ${f.message}`);
5277
+ }
5278
+ if (withOmissions.length === 0) line(" subset-only by design (D22): nothing was removed");
5279
+ return EXIT.OK;
5280
+ }
5281
+ async function runStatus(args) {
2972
5282
  if (args.markKey !== void 0 && args.writeChecklist === true) {
2973
5283
  return report("status", false, EXIT.USAGE, {
2974
5284
  error: {
@@ -2977,6 +5287,30 @@ function runStatus(args) {
2977
5287
  }
2978
5288
  });
2979
5289
  }
5290
+ if (args.verify === true && args.markKey !== void 0) {
5291
+ return report("status", false, EXIT.USAGE, {
5292
+ error: {
5293
+ kind: "usage",
5294
+ message: "usage: --verify and --mark are both ledger write paths \u2014 run them separately (--mark confirms a paste; --verify checks a live surface)"
5295
+ }
5296
+ });
5297
+ }
5298
+ if (args.verify === true && args.writeChecklist === true) {
5299
+ return report("status", false, EXIT.USAGE, {
5300
+ error: {
5301
+ kind: "usage",
5302
+ message: "usage: --verify writes the ledger and --write-checklist writes CHECKLIST.md \u2014 run them separately (regenerate the checklist after verifying)"
5303
+ }
5304
+ });
5305
+ }
5306
+ if (args.verify === true && args.completeness === true) {
5307
+ return report("status", false, EXIT.USAGE, {
5308
+ error: {
5309
+ kind: "usage",
5310
+ message: "usage: --verify is a live check and --completeness is a read-only report \u2014 run them separately"
5311
+ }
5312
+ });
5313
+ }
2980
5314
  if (args.completeness === true && args.markKey !== void 0) {
2981
5315
  return report("status", false, EXIT.USAGE, {
2982
5316
  error: {
@@ -2985,6 +5319,14 @@ function runStatus(args) {
2985
5319
  }
2986
5320
  });
2987
5321
  }
5322
+ if (args.selection === true && (args.markKey !== void 0 || args.verify === true || args.writeChecklist === true)) {
5323
+ return report("status", false, EXIT.USAGE, {
5324
+ error: {
5325
+ kind: "usage",
5326
+ message: "usage: --selection is a read-only report \u2014 run it separately from the ledger write paths (--mark / --verify / --write-checklist)"
5327
+ }
5328
+ });
5329
+ }
2988
5330
  if (args.completeness === true && args.writeChecklist === true) {
2989
5331
  return report("status", false, EXIT.USAGE, {
2990
5332
  error: {
@@ -2997,13 +5339,17 @@ function runStatus(args) {
2997
5339
  if ("fail" in loaded) return loaded.fail;
2998
5340
  const profile = loaded.profile;
2999
5341
  if (args.markKey !== void 0) return markDeployed(args, profile);
5342
+ if (args.verify === true) return await verifyStatus(args, profile);
3000
5343
  const reportData = deployStatus(profile);
3001
5344
  const completion = completenessOf(args, profile);
3002
- if (args.completeness === true) return runCompleteness(args, profile);
5345
+ if (args.completeness === true) return runCompleteness(args, profile, reportData);
5346
+ if (args.selection === true) return runSelection(args, profile);
5347
+ const publicationAll = publicationAllOf(args, profile, reportData);
5348
+ const publication = publicationOf(args, profile, reportData);
3003
5349
  if (args.writeChecklist === true) {
3004
- writeFileSync4(
3005
- join7(args.workspace, "CHECKLIST.md"),
3006
- checklistBody(reportData, completion),
5350
+ writeFileSync7(
5351
+ join14(args.workspace, "CHECKLIST.md"),
5352
+ checklistBody(reportData, completion, profile, publicationAll),
3007
5353
  "utf8"
3008
5354
  );
3009
5355
  if (isJsonMode()) {
@@ -3016,33 +5362,17 @@ function runStatus(args) {
3016
5362
  percent: completion.percent,
3017
5363
  satisfied: completion.satisfied,
3018
5364
  total: completion.total
3019
- }
5365
+ },
5366
+ publication: publicationAll
3020
5367
  }
3021
5368
  });
3022
5369
  }
3023
5370
  line("CHECKLIST.md regenerated from content/deployments.yaml (AUTO-GENERATED view)");
3024
5371
  line(` Profile completeness: ${completenessLine(completion)}`);
5372
+ line(` Certification: ${certificationLineOf(reportData)}`);
3025
5373
  }
3026
- const filtered = args.platformArg !== void 0 ? {
3027
- ...reportData,
3028
- units: reportData.units.map((u) => {
3029
- const onSurface = u.surfaces[args.platformArg];
3030
- const state = onSurface === "in-sync" || onSurface === "stale" ? onSurface : "never";
3031
- return {
3032
- ...u,
3033
- surfaces: onSurface === void 0 ? {} : { [args.platformArg]: onSurface },
3034
- state
3035
- };
3036
- })
3037
- } : reportData;
3038
- const data = {
3039
- workspace: args.workspace,
3040
- platform: args.platformArg ?? null,
3041
- ...summaryOf(filtered),
3042
- units: filtered.units,
3043
- untracked: filtered.untracked,
3044
- removed: filtered.removed
3045
- };
5374
+ const filtered = filterReport(reportData, args.platformArg);
5375
+ const data = reportDataOf(filtered, args, publication);
3046
5376
  if (isJsonMode()) return report("status", true, EXIT.OK, { data });
3047
5377
  const s = summaryOf(filtered);
3048
5378
  section(
@@ -3053,7 +5383,7 @@ function runStatus(args) {
3053
5383
  return rank[a.state] - rank[b.state] || a.key.localeCompare(b.key);
3054
5384
  });
3055
5385
  for (const u of ordered) {
3056
- const surfaces = Object.keys(u.surfaces).length === 0 ? "\u2014" : Object.entries(u.surfaces).sort().map(([sf, st]) => `${sf} (${st})`).join(", ");
5386
+ const surfaces = formatSurfacePairs(u.surfaces, u.certification);
3057
5387
  const origin = u.origin === "template" ? " [template]" : "";
3058
5388
  line(` ${u.state.padEnd(8)} ${u.key.padEnd(36)} ${surfaces}${origin}`);
3059
5389
  }
@@ -3066,18 +5396,30 @@ function runStatus(args) {
3066
5396
  for (const r of filtered.removed) line(` ${r.key} \u2014 was on: ${r.surfaces.join(", ")}`);
3067
5397
  }
3068
5398
  line(
3069
- " \u2192 pasco status --mark <ledgerKey> <surface> confirms a paste; --write-checklist regenerates CHECKLIST.md"
5399
+ ` certification: ${certificationLineOf(filtered)} (surfaces carry staleness/certification)`
5400
+ );
5401
+ if (publication.length === 0) {
5402
+ line(
5403
+ publicationAll.length === 0 ? " no surface published yet \u2014 run pasco build, or confirm a paste with --mark" : ` no surface published on ${args.platformArg} \u2014 ${publicationAll.length} surface(s) elsewhere; drop the platform filter to see them`
5404
+ );
5405
+ } else {
5406
+ section("publication truth \u2014 derived, never stored (SPEC \xA720.1)");
5407
+ for (const truth of publication) {
5408
+ line(` ${publicationLine(truth)}`);
5409
+ if (truth.artifactReason !== void 0) {
5410
+ line(` artifact: ${artifactDetail(truth.artifactReason)}`);
5411
+ }
5412
+ for (const finding2 of truth.findings) {
5413
+ line(` warn: [${finding2.code}] ${finding2.message} (${finding2.path})`);
5414
+ }
5415
+ }
5416
+ line(` coverage: ${publicationCoverageLine(publication)}`);
5417
+ }
5418
+ line(
5419
+ " \u2192 pasco status --mark <ledgerKey> <surface> confirms a paste; --verify [platform] runs the opt-in C1 live check; --write-checklist regenerates CHECKLIST.md"
3070
5420
  );
3071
5421
  return EXIT.OK;
3072
5422
  }
3073
- function summaryOf(data) {
3074
- return {
3075
- total: data.units.length,
3076
- inSync: data.units.filter((u) => u.state === "in-sync").length,
3077
- stale: data.units.filter((u) => u.state === "stale").length,
3078
- never: data.units.filter((u) => u.state === "never").length
3079
- };
3080
- }
3081
5423
 
3082
5424
  // src/commands/validate.ts
3083
5425
  function runValidate(args) {
@@ -3101,7 +5443,14 @@ function runValidate(args) {
3101
5443
  }
3102
5444
  throw err;
3103
5445
  }
3104
- const drift = driftCheck(profile);
5446
+ const drift = driftCheck(profile).filter((issue) => {
5447
+ if (issue.code !== "drift/active-variant-missing") return true;
5448
+ const platform = issue.surfaces?.[0];
5449
+ if (platform === void 0) return true;
5450
+ const active = profile.active?.[platform];
5451
+ if (active === void 0) return true;
5452
+ return !hasVariantBody(args.workspace, platform, active.variantId);
5453
+ });
3105
5454
  const geo = geoCheck(profile);
3106
5455
  if (isJsonMode()) {
3107
5456
  return report("validate", true, EXIT.OK, {
@@ -3129,10 +5478,29 @@ function runValidate(args) {
3129
5478
  return EXIT.OK;
3130
5479
  }
3131
5480
 
5481
+ // src/fetch-http.ts
5482
+ var httpFetchText = async (url) => {
5483
+ try {
5484
+ const response = await fetch(url, {
5485
+ // F7: following a redirect would let the FINAL hop leave the declared
5486
+ // allowlist the caller validated BEFORE this request — `raw.githubusercontent`
5487
+ // serving a redirect is also a signal something is off. A 3xx then lands in
5488
+ // the pass's honest `http-<code>` bucket like any other non-2xx.
5489
+ redirect: "error",
5490
+ headers: {
5491
+ "user-agent": "pasco-verify (+https://github.com/ChristianLuciani/profile-as-code)"
5492
+ }
5493
+ });
5494
+ return { status: response.status, text: await response.text() };
5495
+ } catch {
5496
+ return { status: 0 };
5497
+ }
5498
+ };
5499
+
3132
5500
  // src/index.ts
3133
5501
  var VERSION = (() => {
3134
5502
  try {
3135
- const pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
5503
+ const pkg = JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8"));
3136
5504
  return pkg.version;
3137
5505
  } catch {
3138
5506
  return "0.0.0";
@@ -3150,11 +5518,16 @@ Commands:
3150
5518
  full variant flow M3)
3151
5519
  check [platform] Executes offline rules: brand handle matrix (M6/\xA717,
3152
5520
  brand flavor), registry checks (M7/\xA718.8) and
3153
- media-transport rules (M8/\xA718.6); content/checks.yaml
3154
- rows are listed as not executed (M3/M4)
3155
- status [platform] [--mark \u2026] Per-entry deploy report \u2014 origin \xB7 surfaces \xB7 in-sync/stale/never (D18)
3156
- skills:install <agent> Prints the skills-studio projection command (AGOS);
3157
- projecting stays the studio's job, not the CLI's
5521
+ media-transport rules (M8/\xA718.6) and max-chars/min-chars
5522
+ over headlines/bios (offline, #240); remaining rows M3/M4
5523
+ post <subcommand> MVP (week-1 experiment): seed \u2192 task bundle \u2192 choose \u2192
5524
+ check \u2192 optional Postiz draft payload. No LLM call.
5525
+ Subcommands: seed \xB7 task \xB7 choose \xB7 check \xB7 export \xB7 import \xB7 list
5526
+ status [platform] [--verify] Per-entry deploy report \u2014 origin \xB7 surfaces \xB7 in-sync/stale/never
5527
+ + certification (D18/\xA719.5); --verify runs the opt-in C1
5528
+ live check (the engine's ONE network path, \xA719.5)
5529
+ skills:export <dir> Copy the skill channels this package ships into <dir>
5530
+ [--force] [--list] (--force overlays; --list shows them); no harness paths
3158
5531
 
3159
5532
  Common flags (after the command):
3160
5533
  --workspace <path> Workspace path (resolution: --workspace > .pascorc > cwd, D14)
@@ -3199,9 +5572,13 @@ README.md, .pascorc. Minimal but pasco validate-clean. Refuses to overwrite.
3199
5572
  validate: `pasco validate [--json] [--workspace <path>]
3200
5573
  Load + validate the workspace (D12): schema violations FAIL (exit 1); drift and
3201
5574
  GEO are warnings (exit 0). See schema/README.md "Validation semantics".`,
3202
- build: `pasco build [platform] [--lang <l>] [--json] [--workspace <path>]
5575
+ build: `pasco build [platform] [--view <name>] [--lang <l>] [--json] [--workspace <path>]
3203
5576
  Deterministic base texts per platform+lang (compile() slices). No platform =
3204
- all platforms. lang defaults to .pascorc defaults.lang or 'en'.`,
5577
+ all platforms. lang defaults to .pascorc defaults.lang or 'en'.
5578
+ --view resolves a declared view (D22/\xA721): its rule may carry the platform, the
5579
+ lang and the fact selection, so the surface shows a SUBSET of the SSOT. Every
5580
+ omission is reported as a warning; an addressed key that resolves to nothing is
5581
+ an error. No selection policy = byte-identical output.`,
3205
5582
  diff: `pasco diff [--json] [--workspace <path>]
3206
5583
  Git-based: what changed since the last tag matching release-* (.pascorc
3207
5584
  diff.tagPattern) \u2014 commits + changed content files \u2192 CHANGELOG hint.`,
@@ -3212,27 +5589,67 @@ content/active.yaml. No LLM call, no hub. Full variant flow: M3.`,
3212
5589
  check: `pasco check [platform] [--json] [--workspace <path>]
3213
5590
  Executes the offline rules that exist today: the brand handle matrix (brand
3214
5591
  flavor, D15/\xA717), registry checks (M7/\xA718.8) and media-transport rules
3215
- (M8/\xA718.6). Rows declared in content/checks.yaml (schema-validated catalog)
3216
- are listed and reported as not executed \u2014 rule execution for those ships with
3217
- the skills checks.yaml union (M3) and the full pasco check (M4). Prints its
3218
- owner line (src/commands/check.ts).`,
3219
- "skills:install": `pasco skills:install <agent>
3220
- Prints the skills-studio projection command for <agent> (pi|claude|aionui|opencode).
3221
- Projection is the skills-studio job (AGOS), not the CLI's.`,
3222
- status: `pasco status [platform] [--mark <ledgerKey> <surface>] [--at <iso8601>] [--write-checklist] [--completeness] [--json] [--workspace <path>]
5592
+ (M8/\xA718.6), plus max-chars/min-chars over the headline/bio rows that exist
5593
+ (offline, #240; optional surface selector). Remaining rows in
5594
+ content/checks.yaml ship with the skills checks.yaml union (M3) and the full
5595
+ pasco check (M4). Prints its owner line (src/commands/check.ts).`,
5596
+ "skills:export": `pasco skills:export <dir> [--force] [--json]
5597
+ pasco skills:export --list [--json]
5598
+ Copies the skill channels this package ships (SPEC \xA716) into a directory you
5599
+ name. --list shows them; an existing non-empty directory needs --force (files are
5600
+ overlaid, nothing is deleted). No harness-specific destination is invented.`,
5601
+ post: `pasco post <seed|task|choose|check|export|import|list> [...] [--json] [--workspace <path>]
5602
+ MVP (week-1 experiment): a post is a PROJECTION of a seed, not an identity fact.
5603
+ seed <id> --text <file> create posts/<id>/seed.md + post.yaml
5604
+ task <id> --channel <c1,c2> [--variants N]
5605
+ emit posts/<id>/<channel>/task.md (agent task:
5606
+ seed + facts + skill + channel rules); no LLM call
5607
+ choose <id> --channel <c> --variant <v> [--text <file>]
5608
+ record the chosen variant in post.yaml
5609
+ check <id> max-chars, URLs only from the SSOT, {{fact:...}}
5610
+ refs resolve; unsourced numeric claims warn
5611
+ export <id> [--target postiz] [--out <file>] [--type draft|schedule|now] [--date <iso>]
5612
+ build a Postiz POST /posts payload (integration
5613
+ IDs from postiz.yaml; REPLACE_ME when missing)
5614
+ import <dir> --channel <c> import historical .md posts as seeds
5615
+ list list seeds and their chosen variants`,
5616
+ status: `pasco status [platform] [--mark <ledgerKey> <surface>] [--at <iso8601>] [--verify] [--write-checklist] [--completeness] [--json] [--workspace <path>]
3223
5617
  Per-entry deploy-status report (D18, \xA719): ledger key \xB7 origin (template/user) \xB7
3224
- surfaces published \xB7 sync state (in-sync / stale / never) \u2014 plus untracked rows
3225
- (add an id to track) and removed keys (ledger keeps the audit trail).
5618
+ surfaces published \xB7 sync state (in-sync / stale / never) \xB7 certification
5619
+ (verified / attested / unverifiable / out-of-band-drift, \xA719.5) \u2014 plus untracked
5620
+ rows (add an id to track) and removed keys (ledger keeps the audit trail).
5621
+ It ends with the DERIVED publication truth per surface (SPEC \xA720.1, #242):
5622
+ <surface>: <sync> \xB7 artifact: <matches-SSOT|diverges|absent> \xB7 limits: <ok|over(cap)>
5623
+ \u2014 the artifact on disk vs what the SSOT renders TODAY (offline; offline cap
5624
+ warnings carry their source). Derived, never stored: nothing here writes.
3226
5625
  --mark <ledgerKey> <surface> confirm a manual paste: records deployedAt + the
3227
5626
  CURRENT contentHash in content/deployments.yaml (only pasco writes it,
3228
- D14). --at overrides the timestamp.
5627
+ D14) plus certification.state attested \u2014 and the artifactHash ANCHOR when
5628
+ the surface can produce one. --at overrides the timestamp. The anchor is
5629
+ the digest of profiles/<surface>/README.md (what \`pasco build\` wrote, so
5630
+ \`--lang\` is honoured); with no build artifact on disk it renders at
5631
+ .pascorc defaults.lang instead.
5632
+ --verify [platform] \xA719.5 C1: the opt-in live check \u2014 the engine's ONE
5633
+ network-touching path. Resolves each recorded surface's artifact URL from
5634
+ the URL SSOT, validates its host against the surface's declared allowlist
5635
+ BEFORE any request, fetches sequentially without retries (one request per
5636
+ surface), and updates certification in place. v1: GitHub raw only; an
5637
+ unavailable check is unverifiable WITH the honest reason, never an error.
5638
+ A record with no anchor reads unverifiable: unattributed (anchors are
5639
+ forward-only in v1 \u2014 re-confirm with --mark to record one).
3229
5640
  --write-checklist regenerate CHECKLIST.md from the ledger + the
3230
- completeness line (AUTO-GENERATED view \u2014 do not edit by hand).
5641
+ completeness line (AUTO-GENERATED view \u2014 do not edit by hand). Each
5642
+ surface carries its certification token.
3231
5643
  --completeness profile-completeness checklist + percentage
3232
5644
  (issue #102). Policy (target/weights/langs) from .pascorc "completeness";
3233
- the percentage is DERIVED \u2014 only the policy is ever stored.`
5645
+ the percentage is DERIVED \u2014 only the policy is ever stored. Carries the
5646
+ publication-coverage line (SPEC \xA720.1): coverage, never a new score.
5647
+ --selection [--view <name>] the resolved VIEW (D22/\xA721): what the surface
5648
+ emits, every entry it omits and WHY. Read-only. Policy from .pascorc
5649
+ "selection". Subset-only by design \u2014 the report exists so an omission is
5650
+ never silent.`
3234
5651
  };
3235
- function runMain() {
5652
+ async function runMain() {
3236
5653
  const argv = process.argv.slice(2);
3237
5654
  if (argv.length === 0) {
3238
5655
  process.stderr.write(`${GLOBAL_HELP}
@@ -3284,20 +5701,24 @@ function runMain() {
3284
5701
  return runValidate({ workspace });
3285
5702
  }
3286
5703
  case "build": {
3287
- const { flags, positionals } = parseCommandFlags(rest, { lang: { type: "string" } });
5704
+ const { flags, positionals } = parseCommandFlags(rest, {
5705
+ lang: { type: "string" },
5706
+ view: { type: "string" }
5707
+ });
3288
5708
  if (flags.help === true) {
3289
5709
  process.stdout.write(`${COMMAND_HELP.build}
3290
5710
  `);
3291
5711
  return EXIT.OK;
3292
5712
  }
3293
5713
  if (positionals.length > 1)
3294
- throw new UsageError("usage: pasco build [platform] [--lang <l>] [--json]");
5714
+ throw new UsageError("usage: pasco build [platform] [--view <name>] [--lang <l>] [--json]");
3295
5715
  setJsonMode(flags.json === true);
3296
5716
  const workspace = resolveWorkspace(process.cwd(), flags.workspace);
3297
5717
  const pascorc = readWorkspacePascorc(workspace);
3298
5718
  return runBuild({
3299
5719
  workspace,
3300
5720
  platformArg: positionals[0],
5721
+ viewArg: flags.view,
3301
5722
  langArg: flags.lang,
3302
5723
  pascorc
3303
5724
  });
@@ -3356,12 +5777,54 @@ function runMain() {
3356
5777
  const workspace = resolveWorkspace(process.cwd(), flags.workspace);
3357
5778
  return runCheck({ workspace, platformArg: positionals[0] });
3358
5779
  }
5780
+ case "post": {
5781
+ const { flags, positionals } = parseCommandFlags(rest, {
5782
+ channel: { type: "string" },
5783
+ variants: { type: "string" },
5784
+ variant: { type: "string" },
5785
+ text: { type: "string" },
5786
+ target: { type: "string" },
5787
+ out: { type: "string" },
5788
+ type: { type: "string" },
5789
+ date: { type: "string" }
5790
+ });
5791
+ if (flags.help === true) {
5792
+ process.stdout.write(`${COMMAND_HELP.post}
5793
+ `);
5794
+ return EXIT.OK;
5795
+ }
5796
+ if (positionals.length < 1 || positionals.length > 2) {
5797
+ throw new UsageError(
5798
+ "usage: pasco post <seed|task|choose|check|export|import|list> [<seed-id|dir>] [flags]"
5799
+ );
5800
+ }
5801
+ setJsonMode(flags.json === true);
5802
+ const workspace = resolveWorkspace(process.cwd(), flags.workspace);
5803
+ const pascorc = readWorkspacePascorc(workspace);
5804
+ return runPost({
5805
+ workspace,
5806
+ subcommand: positionals[0],
5807
+ ...positionals[1] !== void 0 ? { seedArg: positionals[1] } : {},
5808
+ channelArg: flags.channel,
5809
+ variantsArg: flags.variants,
5810
+ variantArg: flags.variant,
5811
+ textArg: flags.text,
5812
+ targetArg: flags.target,
5813
+ outArg: flags.out,
5814
+ typeArg: flags.type,
5815
+ dateArg: flags.date,
5816
+ pascorc
5817
+ });
5818
+ }
3359
5819
  case "status": {
3360
5820
  const { flags, positionals } = parseCommandFlags(rest, {
3361
5821
  mark: { type: "string" },
3362
5822
  at: { type: "string" },
3363
5823
  "write-checklist": { type: "boolean" },
3364
- completeness: { type: "boolean" }
5824
+ completeness: { type: "boolean" },
5825
+ selection: { type: "boolean" },
5826
+ view: { type: "string" },
5827
+ verify: { type: "boolean" }
3365
5828
  });
3366
5829
  if (flags.help === true) {
3367
5830
  process.stdout.write(`${COMMAND_HELP.status}
@@ -3376,28 +5839,67 @@ function runMain() {
3376
5839
  "usage: pasco status [platform] [--write-checklist | --completeness] [--json]"
3377
5840
  );
3378
5841
  }
5842
+ if (flags.verify === true && flags.mark !== void 0) {
5843
+ throw new UsageError(
5844
+ "usage: --verify and --mark are both ledger write paths \u2014 run them separately"
5845
+ );
5846
+ }
5847
+ if (flags.verify === true && flags["write-checklist"] === true) {
5848
+ throw new UsageError(
5849
+ "usage: --verify writes the ledger and --write-checklist writes CHECKLIST.md \u2014 run them separately"
5850
+ );
5851
+ }
5852
+ if (flags.verify === true && flags.completeness === true) {
5853
+ throw new UsageError(
5854
+ "usage: --verify is a live check and --completeness is a read-only report \u2014 run them separately"
5855
+ );
5856
+ }
3379
5857
  setJsonMode(flags.json === true);
3380
5858
  const workspace = resolveWorkspace(process.cwd(), flags.workspace);
3381
- return runStatus({
5859
+ const pascorc = readWorkspacePascorc(workspace);
5860
+ return await runStatus({
3382
5861
  workspace,
3383
5862
  platformArg: positionals[0],
3384
5863
  markKey: flags.mark,
3385
5864
  markAt: flags.at,
3386
5865
  writeChecklist: flags["write-checklist"],
3387
5866
  completeness: flags.completeness,
3388
- completenessPolicy: readWorkspacePascorc(workspace).completeness
5867
+ selection: flags.selection,
5868
+ viewArg: flags.view,
5869
+ verify: flags.verify,
5870
+ completenessPolicy: pascorc.completeness,
5871
+ selectionPolicy: pascorc.selection,
5872
+ // §19.5: the `--mark` anchor renders at the same lang `pasco build` wrote,
5873
+ // so the digest is of the pasted artifact, not an approximation.
5874
+ lang: defaultsLang(pascorc),
5875
+ // §19.5: the HTTP driver is the production dep; the surface capabilities
5876
+ // are resolved PER WORKSPACE from the loaded profile (`verify.ts`), not
5877
+ // passed here — `artifactUrl` reads this workspace's URL SSOT. Tests
5878
+ // inject both through `deps`.
5879
+ deps: { fetchText: httpFetchText }
3389
5880
  });
3390
5881
  }
3391
- case "skills:install": {
3392
- const { flags, positionals } = parseCommandFlags(rest);
5882
+ case "skills:export": {
5883
+ const { flags, positionals } = parseCommandFlags(rest, {
5884
+ list: { type: "boolean" },
5885
+ force: { type: "boolean" }
5886
+ });
3393
5887
  if (flags.help === true) {
3394
- process.stdout.write(`${COMMAND_HELP["skills:install"]}
5888
+ process.stdout.write(`${COMMAND_HELP["skills:export"]}
3395
5889
  `);
3396
5890
  return EXIT.OK;
3397
5891
  }
3398
- if (positionals.length > 1) throw new UsageError("usage: pasco skills:install <agent>");
5892
+ if (positionals.length > 1) {
5893
+ throw new UsageError(
5894
+ "usage: pasco skills:export <dir> [--force] | pasco skills:export --list"
5895
+ );
5896
+ }
3399
5897
  setJsonMode(flags.json === true);
3400
- return runSkillsInstall({ agent: positionals[0] });
5898
+ return runSkillsExport({
5899
+ dir: positionals[0],
5900
+ list: flags.list === true,
5901
+ force: flags.force === true
5902
+ });
3401
5903
  }
3402
5904
  default:
3403
5905
  throw new UsageError(
@@ -3407,7 +5909,7 @@ function runMain() {
3407
5909
  }
3408
5910
  var exitCode;
3409
5911
  try {
3410
- exitCode = runMain();
5912
+ exitCode = await runMain();
3411
5913
  } catch (err) {
3412
5914
  if (err instanceof UsageError) {
3413
5915
  process.stderr.write(`usage: ${err.message}