@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/README.md +71 -20
- package/dist/index.js +2791 -289
- package/package.json +12 -7
- package/skills/README.md +62 -0
- package/skills/github-org/SKILL.md +92 -0
- package/skills/github-org/checks.yaml +40 -0
- package/skills/github-org/playbook.md +30 -0
- package/skills/github-org/research/SOTA-2026.md +39 -0
- package/skills/github-readme/SKILL.md +114 -0
- package/skills/github-readme/checks.yaml +32 -0
- package/skills/github-readme/playbook.md +36 -0
- package/skills/github-readme/research/SOTA-2026.md +105 -0
- package/skills/hotmart/SKILL.md +150 -0
- package/skills/hotmart/checks.yaml +43 -0
- package/skills/hotmart/playbook.md +53 -0
- package/skills/hotmart/research/SOTA-2026.md +142 -0
- package/skills/kofi/SKILL.md +127 -0
- package/skills/kofi/checks.yaml +42 -0
- package/skills/kofi/playbook.md +47 -0
- package/skills/kofi/research/SOTA-2026.md +160 -0
- package/skills/linkedin/post.md +50 -0
- package/skills/linkedin/posting.yaml +23 -0
- package/skills/x/post.md +46 -0
- package/skills/x/posting.yaml +24 -0
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
|
|
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
|
|
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:
|
|
1011
|
-
experience:
|
|
1012
|
-
projects:
|
|
1013
|
-
publications:
|
|
1014
|
-
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:
|
|
1033
|
-
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(
|
|
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,
|
|
1354
|
+
function groupBy(items, keyOf2) {
|
|
1094
1355
|
const groups = /* @__PURE__ */ new Map();
|
|
1095
1356
|
items.forEach((item, index) => {
|
|
1096
|
-
const key =
|
|
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
|
-
|
|
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
|
|
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
|
|
2031
|
-
import { dirname, join as
|
|
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 =
|
|
2725
|
+
const pascorcPath = join4(dir, ".pascorc");
|
|
2053
2726
|
if (!existsSync3(pascorcPath)) {
|
|
2054
|
-
const legacy =
|
|
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 =
|
|
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
|
|
2117
|
-
const
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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: {
|
|
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
|
-
"
|
|
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
|
|
2407
|
-
return
|
|
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
|
|
2463
|
-
import { join as
|
|
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
|
-
-
|
|
2554
|
-
|
|
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
|
-
-
|
|
2577
|
-
|
|
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 =
|
|
3598
|
+
const contentDir = join7(dir, "content");
|
|
2608
3599
|
const brand = args.flavor === "brand";
|
|
2609
|
-
if (
|
|
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:
|
|
2617
|
-
{ path:
|
|
2618
|
-
{ path:
|
|
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 (
|
|
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/
|
|
2644
|
-
import { existsSync as
|
|
2645
|
-
import { dirname as
|
|
2646
|
-
import {
|
|
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
|
|
2649
|
-
function
|
|
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
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
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("
|
|
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("
|
|
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
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
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
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
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
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
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
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
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
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
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
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
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
|
-
`
|
|
2750
|
-
`
|
|
2751
|
-
`
|
|
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
|
-
`
|
|
2756
|
-
|
|
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
|
-
`
|
|
2764
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
2771
|
-
`
|
|
2772
|
-
`
|
|
2773
|
-
`
|
|
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
|
-
`
|
|
2777
|
-
`
|
|
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("
|
|
2782
|
-
data: { mode: "
|
|
4179
|
+
return report("post", true, EXIT.OK, {
|
|
4180
|
+
data: { mode: "seed", seedId, seedPath: seedPath(args.workspace, seedId), ledger }
|
|
2783
4181
|
});
|
|
2784
4182
|
}
|
|
2785
|
-
|
|
4183
|
+
line(`seeded ${seedId} at ${seedPath(args.workspace, seedId)}`);
|
|
2786
4184
|
return EXIT.OK;
|
|
2787
4185
|
}
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
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
|
-
|
|
2797
|
-
|
|
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
|
|
4250
|
+
const result = chooseVariant(args.workspace, seedId, channel, args.variantArg, rendered.text);
|
|
2800
4251
|
if (isJsonMode()) {
|
|
2801
|
-
return report("
|
|
2802
|
-
data: {
|
|
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
|
-
|
|
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
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
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
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
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
|
-
|
|
5122
|
+
persistLedger(args, ledger);
|
|
2901
5123
|
if (isJsonMode()) {
|
|
2902
5124
|
return report("status", true, EXIT.OK, {
|
|
2903
|
-
data: {
|
|
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
|
|
2914
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3005
|
-
|
|
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
|
|
3027
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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)
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
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)
|
|
3216
|
-
|
|
3217
|
-
the skills checks.yaml union (M3) and the full
|
|
3218
|
-
owner line (src/commands/check.ts).`,
|
|
3219
|
-
"skills:
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
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) \
|
|
3225
|
-
(
|
|
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).
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
5888
|
+
process.stdout.write(`${COMMAND_HELP["skills:export"]}
|
|
3395
5889
|
`);
|
|
3396
5890
|
return EXIT.OK;
|
|
3397
5891
|
}
|
|
3398
|
-
if (positionals.length > 1)
|
|
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
|
|
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}
|