@profile-as-code/cli 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,298 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync16 } from "fs";
5
4
  import { parseArgs } from "util";
6
5
 
6
+ // ../core/dist/registry.js
7
+ function isPublicEntry(entry) {
8
+ return (entry.visibility ?? "public") === "public";
9
+ }
10
+ function credentialLdEntries(credentials) {
11
+ return credentials.filter(isPublicEntry).map((c) => ({
12
+ "@type": "EducationalOccupationalCredential",
13
+ ...c.title ? { name: c.title } : {},
14
+ ...c.verifyUrl !== void 0 ? { url: c.verifyUrl } : {},
15
+ ...c.issuer ? { awardedBy: { "@type": "Organization", name: c.issuer } } : {},
16
+ ...c.issued !== void 0 ? { validFrom: c.issued } : {},
17
+ ...c.expires !== void 0 ? { validThrough: c.expires } : {}
18
+ }));
19
+ }
20
+ function integrity(path, message) {
21
+ return { path, message };
22
+ }
23
+ function idsOf(entries) {
24
+ const map = /* @__PURE__ */ new Map();
25
+ for (const [index, entry] of (entries ?? []).entries()) {
26
+ if (entry.id === void 0)
27
+ continue;
28
+ map.set(entry.id, index);
29
+ }
30
+ return map;
31
+ }
32
+ function duplicateIssues(block, entries) {
33
+ const issues = [];
34
+ const seen = /* @__PURE__ */ new Map();
35
+ for (const [index, entry] of (entries ?? []).entries()) {
36
+ if (entry.id === void 0)
37
+ continue;
38
+ const first = seen.get(entry.id);
39
+ if (first !== void 0) {
40
+ issues.push(integrity(`${block}[${index}].id`, `duplicate registry id "${entry.id}" (first used at ${block}[${first}]) \u2014 registry ids are unique per block (load fails, D12 fail class)`));
41
+ } else {
42
+ seen.set(entry.id, index);
43
+ }
44
+ }
45
+ return issues;
46
+ }
47
+ function locatorIssues(entries) {
48
+ const issues = [];
49
+ for (const [index, entry] of (entries ?? []).entries()) {
50
+ const hasUrl = entry.locator?.url !== void 0;
51
+ const hasPath = entry.locator?.path !== void 0;
52
+ if (hasUrl === hasPath) {
53
+ issues.push(integrity(`artifacts[${index}].locator`, `locator must have exactly one of "url" | "path" (\xA718.3 capability matrix) \u2014 got ${hasUrl ? "both" : "neither"}`));
54
+ }
55
+ }
56
+ return issues;
57
+ }
58
+ function refIssues(profile) {
59
+ const credentialIds = idsOf(profile.credentials);
60
+ const artifactIds = idsOf(profile.artifacts);
61
+ const issues = [];
62
+ for (const [pIndex, project] of (profile.projects ?? []).entries()) {
63
+ for (const [eIndex, evidence] of (project.evidence ?? []).entries()) {
64
+ if (evidence.credentialId !== void 0 && !credentialIds.has(evidence.credentialId)) {
65
+ issues.push(integrity(`projects[${pIndex}].evidence[${eIndex}].credentialId`, `references credentials[].id "${evidence.credentialId}" which does not exist (dangling cross-ref, load fails)`));
66
+ }
67
+ if (evidence.artifactId !== void 0 && !artifactIds.has(evidence.artifactId)) {
68
+ issues.push(integrity(`projects[${pIndex}].evidence[${eIndex}].artifactId`, `references artifacts[].id "${evidence.artifactId}" which does not exist (dangling cross-ref, load fails)`));
69
+ }
70
+ }
71
+ for (const [rIndex, ref] of (project.artifactRefs ?? []).entries()) {
72
+ if (!artifactIds.has(ref)) {
73
+ issues.push(integrity(`projects[${pIndex}].artifactRefs[${rIndex}]`, `references artifacts[].id "${ref}" which does not exist (dangling cross-ref, load fails)`));
74
+ }
75
+ }
76
+ }
77
+ for (const [pIndex, pub] of (profile.publications ?? []).entries()) {
78
+ for (const [rIndex, ref] of (pub.artifactRefs ?? []).entries()) {
79
+ if (!artifactIds.has(ref)) {
80
+ issues.push(integrity(`publications[${pIndex}].artifactRefs[${rIndex}]`, `references artifacts[].id "${ref}" which does not exist (dangling cross-ref, load fails)`));
81
+ }
82
+ }
83
+ }
84
+ for (const [pIndex, press] of (profile.press ?? []).entries()) {
85
+ for (const [rIndex, ref] of (press.artifactRefs ?? []).entries()) {
86
+ if (!artifactIds.has(ref)) {
87
+ issues.push(integrity(`press[${pIndex}].artifactRefs[${rIndex}]`, `references artifacts[].id "${ref}" which does not exist (dangling cross-ref, load fails)`));
88
+ }
89
+ }
90
+ }
91
+ return issues;
92
+ }
93
+ function checkRegistryIntegrity(profile) {
94
+ return [
95
+ ...duplicateIssues("credentials", profile.credentials),
96
+ ...duplicateIssues("artifacts", profile.artifacts),
97
+ ...locatorIssues(profile.artifacts),
98
+ ...refIssues(profile)
99
+ ];
100
+ }
101
+ function todayIso() {
102
+ const now = /* @__PURE__ */ new Date();
103
+ const mm = String(now.getMonth() + 1).padStart(2, "0");
104
+ const dd = String(now.getDate()).padStart(2, "0");
105
+ return `${now.getFullYear()}-${mm}-${dd}`;
106
+ }
107
+ function registryCheck(profile, options = {}) {
108
+ const findings = [];
109
+ const credentials = profile.credentials ?? [];
110
+ const artifacts = profile.artifacts ?? [];
111
+ const today = options.today ?? todayIso();
112
+ for (const [index, credential] of credentials.entries()) {
113
+ if (credential.expires !== void 0 && credential.expires < today) {
114
+ findings.push({
115
+ code: "check/credential-expired",
116
+ severity: "warn",
117
+ message: `credential "${credential.title}" (${credential.id}) expired ${credential.expires} \u2014 renew or update the workspace`,
118
+ path: `credentials[${index}]`
119
+ });
120
+ }
121
+ }
122
+ const nonPublic = (id, kind, list) => {
123
+ if (id === void 0)
124
+ return void 0;
125
+ const entry = list.find((e) => e.id === id);
126
+ if (entry === void 0 || entry.visibility === void 0 || isPublicEntry(entry))
127
+ return void 0;
128
+ return { blockPath: `${kind}[${list.indexOf(entry)}]`, visibility: entry.visibility, id };
129
+ };
130
+ const flagLeak = (leak, refPath, where) => {
131
+ if (leak === void 0)
132
+ return;
133
+ findings.push({
134
+ code: "check/private-in-public-output",
135
+ severity: "error",
136
+ message: `${where} references registry entry "${leak.id}" with visibility "${leak.visibility}" \u2014 public output renders only public entries (\xA718.5); make it public or drop the reference`,
137
+ path: refPath
138
+ });
139
+ };
140
+ for (const [pIndex, project] of (profile.projects ?? []).entries()) {
141
+ for (const [eIndex, evidence] of (project.evidence ?? []).entries()) {
142
+ flagLeak(nonPublic(evidence.credentialId, "credentials", credentials), `projects[${pIndex}].evidence[${eIndex}].credentialId`, "evidence claim attests a credential");
143
+ flagLeak(nonPublic(evidence.artifactId, "artifacts", artifacts), `projects[${pIndex}].evidence[${eIndex}].artifactId`, "evidence claim proves an artifact");
144
+ }
145
+ for (const [rIndex, ref] of (project.artifactRefs ?? []).entries()) {
146
+ flagLeak(nonPublic(ref, "artifacts", artifacts), `projects[${pIndex}].artifactRefs[${rIndex}]`, "project row");
147
+ }
148
+ }
149
+ for (const [pIndex, pub] of (profile.publications ?? []).entries()) {
150
+ for (const [rIndex, ref] of (pub.artifactRefs ?? []).entries()) {
151
+ flagLeak(nonPublic(ref, "artifacts", artifacts), `publications[${pIndex}].artifactRefs[${rIndex}]`, "publication");
152
+ }
153
+ }
154
+ for (const [pIndex, press] of (profile.press ?? []).entries()) {
155
+ for (const [rIndex, ref] of (press.artifactRefs ?? []).entries()) {
156
+ flagLeak(nonPublic(ref, "artifacts", artifacts), `press[${pIndex}].artifactRefs[${rIndex}]`, "press mention");
157
+ }
158
+ }
159
+ return findings;
160
+ }
161
+
162
+ // ../core/dist/achievements.js
163
+ var DEFAULT_ACHIEVEMENT_DIRECTION = "increase";
164
+ function directionMet(value, target, direction) {
165
+ switch (direction) {
166
+ case "increase":
167
+ return value >= target;
168
+ case "decrease":
169
+ return value <= target;
170
+ case "maintain":
171
+ return value === target;
172
+ }
173
+ }
174
+ function achievementStatus(achievement) {
175
+ if (achievement.kind !== "metric")
176
+ return void 0;
177
+ const metric = achievement.metric;
178
+ if (metric === void 0)
179
+ return void 0;
180
+ const { value, target } = metric;
181
+ if (target !== void 0 && value === void 0)
182
+ return "planned";
183
+ if (value !== void 0 && target === void 0)
184
+ return "measured";
185
+ if (value === void 0 || target === void 0)
186
+ return void 0;
187
+ const direction = metric.direction ?? DEFAULT_ACHIEVEMENT_DIRECTION;
188
+ return directionMet(value, target, direction) ? "achieved" : "partial";
189
+ }
190
+ function achievementScope(achievements) {
191
+ const scope = {
192
+ total: achievements.length,
193
+ planned: 0,
194
+ measured: 0,
195
+ achieved: 0,
196
+ partial: 0
197
+ };
198
+ for (const achievement of achievements) {
199
+ const status = achievementStatus(achievement);
200
+ if (status !== void 0)
201
+ scope[status] += 1;
202
+ }
203
+ return scope;
204
+ }
205
+ var ACHIEVEMENT_REF_BLOCKS = [
206
+ "experience",
207
+ "projects",
208
+ "publications",
209
+ "education",
210
+ "press",
211
+ "credentials",
212
+ "artifacts"
213
+ ];
214
+ function declaredIds(profile) {
215
+ const rows = {
216
+ experience: profile.experience,
217
+ projects: profile.projects,
218
+ publications: profile.publications,
219
+ education: profile.education,
220
+ press: profile.press,
221
+ credentials: profile.credentials,
222
+ artifacts: profile.artifacts
223
+ };
224
+ const map = /* @__PURE__ */ new Map();
225
+ for (const block of ACHIEVEMENT_REF_BLOCKS) {
226
+ const ids = /* @__PURE__ */ new Set();
227
+ for (const row of rows[block] ?? [])
228
+ if (row.id !== void 0)
229
+ ids.add(row.id);
230
+ map.set(block, ids);
231
+ }
232
+ return map;
233
+ }
234
+ function issue(path, message) {
235
+ return { path, message };
236
+ }
237
+ function checkAchievementIntegrity(profile) {
238
+ const issues = [];
239
+ const achievements = profile.achievements ?? [];
240
+ const ids = declaredIds(profile);
241
+ const credentials = profile.credentials ?? [];
242
+ const artifacts = profile.artifacts ?? [];
243
+ const seen = /* @__PURE__ */ new Map();
244
+ for (const [index, achievement] of achievements.entries()) {
245
+ const first = seen.get(achievement.id);
246
+ if (first !== void 0) {
247
+ issues.push(issue(`achievements[${index}].id`, `duplicate achievement id "${achievement.id}" (first used at achievements[${first}]) \u2014 achievement ids are unique per block (load fails, D12 fail class)`));
248
+ } else {
249
+ seen.set(achievement.id, index);
250
+ }
251
+ if (achievement.kind === "metric" && achievement.metric === void 0) {
252
+ issues.push(issue(`achievements[${index}].metric`, `kind: metric requires a "metric" block (unit is required; value/target/baseline are the numbers) \u2014 without it there is no status to derive`));
253
+ }
254
+ if (achievement.kind === "milestone" && achievement.metric !== void 0) {
255
+ issues.push(issue(`achievements[${index}].metric`, `kind: milestone must NOT carry a "metric" block \u2014 a point event has no progress to derive; use kind: metric, or move the numbers into the statement`));
256
+ }
257
+ for (const [refIndex, ref] of (achievement.refs ?? []).entries()) {
258
+ const separator = ref.indexOf(":");
259
+ const block = ref.slice(0, separator);
260
+ const id = ref.slice(separator + 1);
261
+ if (!ids.get(block)?.has(id)) {
262
+ issues.push(issue(`achievements[${index}].refs[${refIndex}]`, `references ${block}[].id "${id}" which does not exist (dangling cross-ref, load fails)`));
263
+ }
264
+ }
265
+ if (achievement.credentialId !== void 0) {
266
+ const credential = credentials.find((c) => c.id === achievement.credentialId);
267
+ if (credential === void 0) {
268
+ issues.push(issue(`achievements[${index}].credentialId`, `references credentials[].id "${achievement.credentialId}" which does not exist (dangling cross-ref, load fails)`));
269
+ } else if (achievement.visibility === "public" && !isPublicEntry(credential)) {
270
+ issues.push(issue(`achievements[${index}].credentialId`, `a public achievement attests credential "${credential.id}" with visibility "${credential.visibility}" \u2014 public output renders only public entries (\xA718.5); make it public, or make the achievement non-public`));
271
+ }
272
+ }
273
+ if (achievement.artifactId !== void 0) {
274
+ const artifact = artifacts.find((a) => a.id === achievement.artifactId);
275
+ if (artifact === void 0) {
276
+ issues.push(issue(`achievements[${index}].artifactId`, `references artifacts[].id "${achievement.artifactId}" which does not exist (dangling cross-ref, load fails)`));
277
+ } else if (achievement.visibility === "public" && !isPublicEntry(artifact)) {
278
+ issues.push(issue(`achievements[${index}].artifactId`, `a public achievement points at artifact "${artifact.id}" with visibility "${artifact.visibility}" \u2014 public output renders only public entries (\xA718.5); make it public, or make the achievement non-public`));
279
+ }
280
+ }
281
+ if (achievement.visibility === "public") {
282
+ for (const [refIndex, ref] of (achievement.refs ?? []).entries()) {
283
+ const separator = ref.indexOf(":");
284
+ const block = ref.slice(0, separator);
285
+ const id = ref.slice(separator + 1);
286
+ const target = block === "credentials" ? credentials.find((c) => c.id === id) : block === "artifacts" ? artifacts.find((a) => a.id === id) : void 0;
287
+ if (target !== void 0 && !isPublicEntry(target)) {
288
+ issues.push(issue(`achievements[${index}].refs[${refIndex}]`, `a public achievement references ${block}[].id "${id}" with visibility "${target.visibility}" \u2014 public output renders only public entries (\xA718.5)`));
289
+ }
290
+ }
291
+ }
292
+ }
293
+ return issues;
294
+ }
295
+
7
296
  // ../core/dist/byte-order.js
8
297
  function byteOrder(a, b) {
9
298
  return a < b ? -1 : a > b ? 1 : 0;
@@ -242,10 +531,17 @@ var HANDLE_HOSTS = {
242
531
  };
243
532
 
244
533
  // ../core/dist/types.js
245
- function textOf(v) {
534
+ function textOf(v, lang) {
246
535
  if (v == null)
247
536
  return "";
248
- return typeof v === "string" ? v : v.en ?? "";
537
+ if (typeof v === "string")
538
+ return v;
539
+ if (lang !== void 0) {
540
+ const direct = v[lang];
541
+ if (direct)
542
+ return direct;
543
+ }
544
+ return v.en ?? "";
249
545
  }
250
546
  var PLATFORMS = PLATFORM_SLUGS;
251
547
  var LANGS = ["en", "es", "fr"];
@@ -309,7 +605,7 @@ function driftCheck(profile) {
309
605
  const issues = [];
310
606
  const identity = profile.identity;
311
607
  const identityUrls = new Set((identity.links ?? []).map((link) => normalizeUrl(link.url)));
312
- const push = (issue) => issues.push(issue);
608
+ const push = (issue2) => issues.push(issue2);
313
609
  const links = identity.links ?? [];
314
610
  const linkGroups = groupBy(links, (link) => normalizeUrl(link.url));
315
611
  for (const [url, indices] of linkGroups) {
@@ -446,162 +742,6 @@ function driftCheck(profile) {
446
742
  return issues;
447
743
  }
448
744
 
449
- // ../core/dist/registry.js
450
- function isPublicEntry(entry) {
451
- return (entry.visibility ?? "public") === "public";
452
- }
453
- function credentialLdEntries(credentials) {
454
- return credentials.filter(isPublicEntry).map((c) => ({
455
- "@type": "EducationalOccupationalCredential",
456
- ...c.title ? { name: c.title } : {},
457
- ...c.verifyUrl !== void 0 ? { url: c.verifyUrl } : {},
458
- ...c.issuer ? { awardedBy: { "@type": "Organization", name: c.issuer } } : {},
459
- ...c.issued !== void 0 ? { validFrom: c.issued } : {},
460
- ...c.expires !== void 0 ? { validThrough: c.expires } : {}
461
- }));
462
- }
463
- function integrity(path, message) {
464
- return { path, message };
465
- }
466
- function idsOf(entries) {
467
- const map = /* @__PURE__ */ new Map();
468
- for (const [index, entry] of (entries ?? []).entries()) {
469
- if (entry.id === void 0)
470
- continue;
471
- map.set(entry.id, index);
472
- }
473
- return map;
474
- }
475
- function duplicateIssues(block, entries) {
476
- const issues = [];
477
- const seen = /* @__PURE__ */ new Map();
478
- for (const [index, entry] of (entries ?? []).entries()) {
479
- if (entry.id === void 0)
480
- continue;
481
- const first = seen.get(entry.id);
482
- if (first !== void 0) {
483
- issues.push(integrity(`${block}[${index}].id`, `duplicate registry id "${entry.id}" (first used at ${block}[${first}]) \u2014 registry ids are unique per block (load fails, D12 fail class)`));
484
- } else {
485
- seen.set(entry.id, index);
486
- }
487
- }
488
- return issues;
489
- }
490
- function locatorIssues(entries) {
491
- const issues = [];
492
- for (const [index, entry] of (entries ?? []).entries()) {
493
- const hasUrl = entry.locator?.url !== void 0;
494
- const hasPath = entry.locator?.path !== void 0;
495
- if (hasUrl === hasPath) {
496
- issues.push(integrity(`artifacts[${index}].locator`, `locator must have exactly one of "url" | "path" (\xA718.3 capability matrix) \u2014 got ${hasUrl ? "both" : "neither"}`));
497
- }
498
- }
499
- return issues;
500
- }
501
- function refIssues(profile) {
502
- const credentialIds = idsOf(profile.credentials);
503
- const artifactIds = idsOf(profile.artifacts);
504
- const issues = [];
505
- for (const [pIndex, project] of (profile.projects ?? []).entries()) {
506
- for (const [eIndex, evidence] of (project.evidence ?? []).entries()) {
507
- if (evidence.credentialId !== void 0 && !credentialIds.has(evidence.credentialId)) {
508
- issues.push(integrity(`projects[${pIndex}].evidence[${eIndex}].credentialId`, `references credentials[].id "${evidence.credentialId}" which does not exist (dangling cross-ref, load fails)`));
509
- }
510
- if (evidence.artifactId !== void 0 && !artifactIds.has(evidence.artifactId)) {
511
- issues.push(integrity(`projects[${pIndex}].evidence[${eIndex}].artifactId`, `references artifacts[].id "${evidence.artifactId}" which does not exist (dangling cross-ref, load fails)`));
512
- }
513
- }
514
- for (const [rIndex, ref] of (project.artifactRefs ?? []).entries()) {
515
- if (!artifactIds.has(ref)) {
516
- issues.push(integrity(`projects[${pIndex}].artifactRefs[${rIndex}]`, `references artifacts[].id "${ref}" which does not exist (dangling cross-ref, load fails)`));
517
- }
518
- }
519
- }
520
- for (const [pIndex, pub] of (profile.publications ?? []).entries()) {
521
- for (const [rIndex, ref] of (pub.artifactRefs ?? []).entries()) {
522
- if (!artifactIds.has(ref)) {
523
- issues.push(integrity(`publications[${pIndex}].artifactRefs[${rIndex}]`, `references artifacts[].id "${ref}" which does not exist (dangling cross-ref, load fails)`));
524
- }
525
- }
526
- }
527
- for (const [pIndex, press] of (profile.press ?? []).entries()) {
528
- for (const [rIndex, ref] of (press.artifactRefs ?? []).entries()) {
529
- if (!artifactIds.has(ref)) {
530
- issues.push(integrity(`press[${pIndex}].artifactRefs[${rIndex}]`, `references artifacts[].id "${ref}" which does not exist (dangling cross-ref, load fails)`));
531
- }
532
- }
533
- }
534
- return issues;
535
- }
536
- function checkRegistryIntegrity(profile) {
537
- return [
538
- ...duplicateIssues("credentials", profile.credentials),
539
- ...duplicateIssues("artifacts", profile.artifacts),
540
- ...locatorIssues(profile.artifacts),
541
- ...refIssues(profile)
542
- ];
543
- }
544
- function todayIso() {
545
- const now = /* @__PURE__ */ new Date();
546
- const mm = String(now.getMonth() + 1).padStart(2, "0");
547
- const dd = String(now.getDate()).padStart(2, "0");
548
- return `${now.getFullYear()}-${mm}-${dd}`;
549
- }
550
- function registryCheck(profile, options = {}) {
551
- const findings = [];
552
- const credentials = profile.credentials ?? [];
553
- const artifacts = profile.artifacts ?? [];
554
- const today = options.today ?? todayIso();
555
- for (const [index, credential] of credentials.entries()) {
556
- if (credential.expires !== void 0 && credential.expires < today) {
557
- findings.push({
558
- code: "check/credential-expired",
559
- severity: "warn",
560
- message: `credential "${credential.title}" (${credential.id}) expired ${credential.expires} \u2014 renew or update the workspace`,
561
- path: `credentials[${index}]`
562
- });
563
- }
564
- }
565
- const nonPublic = (id, kind, list) => {
566
- if (id === void 0)
567
- return void 0;
568
- const entry = list.find((e) => e.id === id);
569
- if (entry === void 0 || entry.visibility === void 0 || isPublicEntry(entry))
570
- return void 0;
571
- return { blockPath: `${kind}[${list.indexOf(entry)}]`, visibility: entry.visibility, id };
572
- };
573
- const flagLeak = (leak, refPath, where) => {
574
- if (leak === void 0)
575
- return;
576
- findings.push({
577
- code: "check/private-in-public-output",
578
- severity: "error",
579
- message: `${where} references registry entry "${leak.id}" with visibility "${leak.visibility}" \u2014 public output renders only public entries (\xA718.5); make it public or drop the reference`,
580
- path: refPath
581
- });
582
- };
583
- for (const [pIndex, project] of (profile.projects ?? []).entries()) {
584
- for (const [eIndex, evidence] of (project.evidence ?? []).entries()) {
585
- flagLeak(nonPublic(evidence.credentialId, "credentials", credentials), `projects[${pIndex}].evidence[${eIndex}].credentialId`, "evidence claim attests a credential");
586
- flagLeak(nonPublic(evidence.artifactId, "artifacts", artifacts), `projects[${pIndex}].evidence[${eIndex}].artifactId`, "evidence claim proves an artifact");
587
- }
588
- for (const [rIndex, ref] of (project.artifactRefs ?? []).entries()) {
589
- flagLeak(nonPublic(ref, "artifacts", artifacts), `projects[${pIndex}].artifactRefs[${rIndex}]`, "project row");
590
- }
591
- }
592
- for (const [pIndex, pub] of (profile.publications ?? []).entries()) {
593
- for (const [rIndex, ref] of (pub.artifactRefs ?? []).entries()) {
594
- flagLeak(nonPublic(ref, "artifacts", artifacts), `publications[${pIndex}].artifactRefs[${rIndex}]`, "publication");
595
- }
596
- }
597
- for (const [pIndex, press] of (profile.press ?? []).entries()) {
598
- for (const [rIndex, ref] of (press.artifactRefs ?? []).entries()) {
599
- flagLeak(nonPublic(ref, "artifacts", artifacts), `press[${pIndex}].artifactRefs[${rIndex}]`, "press mention");
600
- }
601
- }
602
- return findings;
603
- }
604
-
605
745
  // ../core/dist/structured-data.js
606
746
  var DEFAULT_LINK_ROLE = "realization";
607
747
  function linkRole(link) {
@@ -770,6 +910,7 @@ function structuredData(profile, lang = "en") {
770
910
  }
771
911
 
772
912
  // ../core/dist/geo.js
913
+ var GEO_BASIS_VERSION = "geo-heuristic-v2";
773
914
  var CONFLICT_CODES = /* @__PURE__ */ new Set([
774
915
  "drift/date-range-inverted",
775
916
  "drift/duplicate-experience",
@@ -867,22 +1008,26 @@ function definitionEntries(profile) {
867
1008
  }
868
1009
  function freshnessFacts(profile) {
869
1010
  const facts = {};
1011
+ const instant = (when) => ({ when, precision: "instant" });
870
1012
  for (const [key, rows] of Object.entries(profile.deployments ?? {})) {
871
1013
  for (const [surface, rec] of Object.entries(rows ?? {})) {
872
- facts[`${key}\u2192${surface}`] = rec.deployedAt;
1014
+ facts[`${key}\u2192${surface}`] = instant(rec.deployedAt);
873
1015
  }
874
1016
  }
875
1017
  for (const p of profile.press ?? []) {
876
1018
  if (p.date)
877
- facts[`press:${(p.outlet ?? p.url ?? "?").toString().slice(0, 40)}`] = p.date;
1019
+ facts[`press:${(p.outlet ?? p.url ?? "?").toString().slice(0, 40)}`] = instant(p.date);
878
1020
  }
879
1021
  for (const c of profile.credentials ?? []) {
880
1022
  if (c.issued)
881
- facts[`credential:${c.title}`] = c.issued;
1023
+ facts[`credential:${c.title}`] = instant(c.issued);
882
1024
  }
883
1025
  if ((profile.publications ?? []).length > 0) {
884
1026
  const newestYear = Math.max(...(profile.publications ?? []).map((p) => p.year));
885
- facts["publications:newest-year"] = `${newestYear}-12-31`;
1027
+ facts["publications:newest-year"] = {
1028
+ when: `${newestYear}-12-31`,
1029
+ precision: "year"
1030
+ };
886
1031
  }
887
1032
  return facts;
888
1033
  }
@@ -891,20 +1036,30 @@ function freshnessWindowStart(now = Date.now()) {
891
1036
  windowStart.setUTCMonth(windowStart.getUTCMonth() - 12);
892
1037
  return windowStart.getTime();
893
1038
  }
1039
+ function isFutureDated(fact, now) {
1040
+ if (fact.precision === "year") {
1041
+ return Number(fact.when.slice(0, 4)) > new Date(now).getUTCFullYear();
1042
+ }
1043
+ return Date.parse(fact.when) > now;
1044
+ }
1045
+ function freshnessLabel(fact) {
1046
+ return fact.precision === "year" ? fact.when.slice(0, 4) : fact.when;
1047
+ }
894
1048
  function readFreshness(profile, now = Date.now()) {
895
1049
  const windowStart = freshnessWindowStart(now);
896
1050
  const reading = { future: [] };
897
- for (const [key, when] of Object.entries(freshnessFacts(profile))) {
898
- const t = Date.parse(when);
899
- if (Number.isNaN(t))
1051
+ for (const [key, fact] of Object.entries(freshnessFacts(profile))) {
1052
+ const at2 = Date.parse(fact.when);
1053
+ if (Number.isNaN(at2))
900
1054
  continue;
901
- if (t > now)
902
- reading.future.push({ key, when });
903
- else if (t >= windowStart) {
904
- if (!reading.fresh || t > Date.parse(reading.fresh.when))
905
- reading.fresh = { key, when };
906
- } else if (!reading.stale || t > Date.parse(reading.stale.when))
907
- reading.stale = { key, when };
1055
+ const hit = { key, when: freshnessLabel(fact), at: at2 };
1056
+ if (isFutureDated(fact, now))
1057
+ reading.future.push(hit);
1058
+ else if (at2 >= windowStart) {
1059
+ if (!reading.fresh || at2 > reading.fresh.at)
1060
+ reading.fresh = hit;
1061
+ } else if (!reading.stale || at2 > reading.stale.at)
1062
+ reading.stale = hit;
908
1063
  }
909
1064
  return reading;
910
1065
  }
@@ -946,9 +1101,21 @@ function nameCollisionObservation(profile) {
946
1101
  }
947
1102
  return { kind: "unqualified" };
948
1103
  }
1104
+ function geoSection(label, detail, subset) {
1105
+ const applicable = subset.filter((check) => check.applicable !== false);
1106
+ const weight = applicable.reduce((sum, check) => sum + check.weight, 0);
1107
+ const score = weight === 0 ? 0 : Math.round(applicable.reduce((sum, check) => sum + check.score * check.weight, 0) / weight * 100);
1108
+ return {
1109
+ label,
1110
+ detail,
1111
+ checks: subset,
1112
+ score,
1113
+ ...weight === 0 ? { applicable: false } : {}
1114
+ };
1115
+ }
949
1116
  function geoCheck(profile, lang = "en") {
950
1117
  const identity = profile.identity;
951
- const conflicts = driftCheck(profile).filter((issue) => CONFLICT_CODES.has(issue.code));
1118
+ const conflicts = driftCheck(profile).filter((issue2) => CONFLICT_CODES.has(issue2.code));
952
1119
  const targets = targetedPlatforms(profile);
953
1120
  const targetsWithBio = [...targets].filter((platform) => (profile.bios ?? []).some((b) => b.platform === platform)).length;
954
1121
  const targetsWithHeadline = [...targets].filter((platform) => (profile.headlines ?? []).some((h) => h.platform === platform)).length;
@@ -959,6 +1126,7 @@ function geoCheck(profile, lang = "en") {
959
1126
  const citablePublications = publications.filter((p) => p.doi || p.orcidId).length;
960
1127
  const projects = profile.projects ?? [];
961
1128
  const projectsWithEvidence = projects.filter((p) => (p.evidence ?? []).length > 0).length;
1129
+ const hasAnyEvidence = projectsWithEvidence > 0;
962
1130
  const evidenceWithVerified = projects.flatMap((p) => p.evidence ?? []).filter((e) => Boolean(e.verified)).length;
963
1131
  const totalEvidence = projects.flatMap((p) => p.evidence ?? []).length;
964
1132
  const freshness = readFreshness(profile);
@@ -1063,8 +1231,12 @@ function geoCheck(profile, lang = "en") {
1063
1231
  code: "geo/evidence-claims",
1064
1232
  label: "Projects carry verifiable evidence[]",
1065
1233
  weight: 0.08,
1066
- score: projects.length === 0 ? 1 : projectsWithEvidence / projects.length,
1067
- detail: projects.length === 0 ? "no projects" : `${projectsWithEvidence}/${projects.length} projects with evidence; ${evidenceWithVerified}/${totalEvidence} evidence claims have a verified date`
1234
+ // §8/#399 (option C): N/A while no project declares evidence. The declared
1235
+ // coverage signal lives in §20's completeness `evidence` item, so absence is
1236
+ // not subtracted twice. Once evidence is declared, the ratio is scored.
1237
+ applicable: hasAnyEvidence,
1238
+ score: hasAnyEvidence ? projectsWithEvidence / projects.length : 1,
1239
+ detail: !hasAnyEvidence ? "not applicable \u2014 no project declares evidence[]; evidence coverage is scored by profile completeness (SPEC \xA720)" : `${projectsWithEvidence}/${projects.length} projects with evidence; ${evidenceWithVerified}/${totalEvidence} evidence claims have a verified date`
1068
1240
  },
1069
1241
  // ── Citability v2 (v1.10, issue #289) — the five codes §8's table declares.
1070
1242
  // The weights below take the 0.20 freed by the ×0.8 dilution of the nine
@@ -1137,7 +1309,9 @@ function geoCheck(profile, lang = "en") {
1137
1309
  // absence of freshness degrades citability without changing the pass story.
1138
1310
  severity: "info",
1139
1311
  // Calendar 12-month window; future-dated facts cannot evidence freshness
1140
- // (fresh-pass F4: a typo'd 2099 deploy used to score 1).
1312
+ // (fresh-pass F4: a 2099 deploy used to score 1). A year-precision datum
1313
+ // is read at year granularity — a current-year publication is not future
1314
+ // (#438).
1141
1315
  score: freshness.fresh !== void 0 ? 1 : 0,
1142
1316
  // The anomaly is reported whenever future-dated facts are OBSERVED (r2
1143
1317
  // fresh-pass F1 of #321) — fresh, failing or both — never only in the
@@ -1212,17 +1386,13 @@ function geoCheck(profile, lang = "en") {
1212
1386
  })()
1213
1387
  }
1214
1388
  ];
1215
- const totalWeight = checks.reduce((sum, check) => sum + check.weight, 0);
1216
- const score = Math.round(checks.reduce((sum, check) => sum + check.score * check.weight, 0) / totalWeight * 100);
1217
- const section2 = (label, detail, subset) => {
1218
- const weight = subset.reduce((sum, check) => sum + check.weight, 0);
1219
- const sectionScore = Math.round(subset.reduce((sum, check) => sum + check.score * check.weight, 0) / weight * 100);
1220
- return { label, detail, checks: subset, score: sectionScore };
1221
- };
1389
+ const scored = checks.filter((check) => check.applicable !== false);
1390
+ const totalWeight = scored.reduce((sum, check) => sum + check.weight, 0);
1391
+ const score = Math.round(scored.reduce((sum, check) => sum + check.score * check.weight, 0) / totalWeight * 100);
1222
1392
  const sections = {
1223
- factConsistency: section2("Fact consistency", "An LLM triangulating platforms must find the same dates/roles/papers.", checks.filter((c) => c.code.startsWith("geo/conflicts") || c.code.startsWith("geo/platform-"))),
1393
+ factConsistency: geoSection("Fact consistency", "An LLM triangulating platforms must find the same dates/roles/papers.", checks.filter((c) => c.code.startsWith("geo/conflicts") || c.code.startsWith("geo/platform-"))),
1224
1394
  structuredData: {
1225
- ...section2(
1395
+ ...geoSection(
1226
1396
  "Structured-data readiness",
1227
1397
  // Mirrors `ready = Boolean(name)` exactly (structured-data.ts). The old string still
1228
1398
  // claimed "need name + ≥1 sameAs link" — the predicate it no longer uses, i.e. the
@@ -1241,10 +1411,10 @@ function geoCheck(profile, lang = "en") {
1241
1411
  readiness: sd
1242
1412
  },
1243
1413
  semanticAssociations: {
1244
- ...section2("Semantic associations", "The 3\u20135 anchors an LLM should associate with the person, reinforced across surfaces.", checks.filter((c) => c.code === "geo/semantic-anchors")),
1414
+ ...geoSection("Semantic associations", "The 3\u20135 anchors an LLM should associate with the person, reinforced across surfaces.", checks.filter((c) => c.code === "geo/semantic-anchors")),
1245
1415
  anchors
1246
1416
  },
1247
- citability: section2(
1417
+ citability: geoSection(
1248
1418
  "Citability",
1249
1419
  // The v1.10 framing (#289): citability is a DIFFERENT property from
1250
1420
  // consistency, and it is the one a buyer pays for — resolvable claims,
@@ -1253,7 +1423,7 @@ function geoCheck(profile, lang = "en") {
1253
1423
  checks.filter((c) => c.code.startsWith("geo/citable") || c.code.startsWith("geo/evidence") || c.code.startsWith("geo/canonical") || c.code.startsWith("geo/faq") || c.code.startsWith("geo/freshness") || c.code.startsWith("geo/sameas-coverage") || c.code.startsWith("geo/name-collision"))
1254
1424
  )
1255
1425
  };
1256
- return { score, sections, checks };
1426
+ return { score, sections, checks, basis: GEO_BASIS_VERSION, measured: false };
1257
1427
  }
1258
1428
 
1259
1429
  // ../core/dist/normalize.js
@@ -1320,14 +1490,14 @@ function findTerm(folded, term) {
1320
1490
  const out = [];
1321
1491
  let from = 0;
1322
1492
  for (; ; ) {
1323
- const at = folded.text.indexOf(needle, from);
1324
- if (at === -1)
1493
+ const at2 = folded.text.indexOf(needle, from);
1494
+ if (at2 === -1)
1325
1495
  break;
1326
- const end = at + needle.length;
1327
- if (isWordBounded(folded.text, at, end)) {
1328
- out.push({ start: folded.starts[at] ?? 0, end: folded.ends[end - 1] ?? 0 });
1496
+ const end = at2 + needle.length;
1497
+ if (isWordBounded(folded.text, at2, end)) {
1498
+ out.push({ start: folded.starts[at2] ?? 0, end: folded.ends[end - 1] ?? 0 });
1329
1499
  }
1330
- from = at + 1;
1500
+ from = at2 + 1;
1331
1501
  }
1332
1502
  return out;
1333
1503
  }
@@ -1336,12 +1506,12 @@ function termOccursIn(folded, term, from, to) {
1336
1506
  if (needle === "")
1337
1507
  return false;
1338
1508
  const haystack = folded.text.slice(from, to);
1339
- let at = haystack.indexOf(needle);
1340
- while (at !== -1) {
1341
- const end = at + needle.length;
1342
- if (isWordBounded(haystack, at, end))
1509
+ let at2 = haystack.indexOf(needle);
1510
+ while (at2 !== -1) {
1511
+ const end = at2 + needle.length;
1512
+ if (isWordBounded(haystack, at2, end))
1343
1513
  return true;
1344
- at = haystack.indexOf(needle, at + 1);
1514
+ at2 = haystack.indexOf(needle, at2 + 1);
1345
1515
  }
1346
1516
  return false;
1347
1517
  }
@@ -1946,7 +2116,9 @@ function checkLedgerIntegrity(profile) {
1946
2116
  // ../core/dist/completeness.js
1947
2117
  var COMPLETENESS_CATALOG = [
1948
2118
  { id: "presence", label: "Required blocks present", block: "profile", weight: 0.3 },
1949
- { id: "langCoverage", label: "Language coverage (en/es/fr)", block: "langMaps", weight: 0.15 },
2119
+ // No language list in the catalog label: the policy names the langs it
2120
+ // measures, so the report appends them (`Language coverage (en/es)`).
2121
+ { id: "langCoverage", label: "Language coverage", block: "langMaps", weight: 0.15 },
1950
2122
  { id: "geoAnchors", label: "GEO semantic anchors", block: "positioning.geo", weight: 0.15 },
1951
2123
  { id: "evidence", label: "Claims carry evidence", block: "projects.evidence", weight: 0.15 },
1952
2124
  { id: "deployment", label: "Deployed to a surface", block: "deployments", weight: 0.25 }
@@ -2016,6 +2188,9 @@ function langMaps(profile) {
2016
2188
  for (const [i, a] of (profile.artifacts ?? []).entries()) {
2017
2189
  push(`artifacts[${i}].description`, a.description);
2018
2190
  }
2191
+ for (const [i, achievement] of (profile.achievements ?? []).entries()) {
2192
+ push(`achievements[${i}].statement`, achievement.statement);
2193
+ }
2019
2194
  push("newsletter.name", profile.newsletter?.name);
2020
2195
  push("newsletter.description", profile.newsletter?.description);
2021
2196
  push("podcast.title", profile.podcast?.title);
@@ -2115,6 +2290,11 @@ function deploymentScore(profile) {
2115
2290
  const deployed = units.filter((u) => Object.keys(profile.deployments[u.key] ?? {}).length > 0).length;
2116
2291
  return { score: deployed / units.length, detail: `${deployed}/${units.length} units deployed` };
2117
2292
  }
2293
+ function itemLabel(entry, langs) {
2294
+ if (entry.id !== "langCoverage" || langs.length === 0)
2295
+ return entry.label;
2296
+ return `${entry.label} (${langs.join("/")})`;
2297
+ }
2118
2298
  function completeness(profile, policy = DEFAULT_COMPLETENESS_POLICY) {
2119
2299
  const target = policy.target ?? DEFAULT_COMPLETENESS_POLICY.target ?? "";
2120
2300
  const langs = policy.langs ?? DEFAULT_COMPLETENESS_POLICY.langs ?? ["en"];
@@ -2135,7 +2315,7 @@ function completeness(profile, policy = DEFAULT_COMPLETENESS_POLICY) {
2135
2315
  const clamped = Math.max(0, Math.min(1, score));
2136
2316
  items.push({
2137
2317
  id: entry.id,
2138
- label: entry.label,
2318
+ label: itemLabel(entry, langs),
2139
2319
  block: entry.block,
2140
2320
  score: clamped,
2141
2321
  weight,
@@ -2167,8 +2347,8 @@ import { parseDocument } from "yaml";
2167
2347
 
2168
2348
  // ../core/dist/generated/schema.zod.js
2169
2349
  import { z } from "zod";
2170
- 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(), "definition": langMapKeys(z.object({ "en": z.string().min(1).max(160) }).catchall(z.string().min(1).max(160))).describe("v1.10 (issue #288): the canonical definition per language \u2014 one sentence of the form 'X is a \u2026' an answer engine can quote verbatim. \u2264 160 chars per value (what an answer engine quotes); MUST contain the entity name \u2014 JSON Schema cannot express it: the check is `geo/canonical-definition` in geoCheck v2 (#289). EN required (langMap rule, D8/\xA711); the propertyNames restriction shares the langMap precedent (issue #86, F3 \u2014 gen-zod reconstructs it).").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(), "nationality": z.string().regex(new RegExp("^[A-Z]{2}$")).describe("v1.13 (issue #334): ISO 3166-1 alpha-2 nationality code (uppercase, e.g. `VE`), a fact distinct from `location` (residence). Maps to schema.org `Person.nationality` as a `Country` node with `identifier` on the JSON-LD. Person-only by the standard's model: a `flavor: brand` workspace may declare it (identity is shared) but the Organization branch emits no `nationality` node \u2014 the value is not silently rewritten into a brand fact. The pattern asserts ISO 3166-1 alpha-2 FORM only \u2014 `ve`, `Venezuela` and `VEN` fail loudly (ECMA-262 pattern, no flags; D12 fail class); membership in the published code list is not enforced. Optional: outside the structured-data `ready` gate and completeness ratio, so absence changes nothing.").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) \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(), "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.12 (D17/\xA718.1, issue #333): references `artifacts[].id` \u2014 the work behind this row. projects keep their domain (no duplication); target resolved by core (load fails on a dangling ref). Published-vs-in-development is DERIVED from the targets (`visibility` + `locator.url`), never stored (\xA720.1).").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), "kind": z.enum(["journal-article", "preprint", "conference-abstract"]).describe("v1.14 (issue #349): the output's structured classification \u2014 journal-article (peer-reviewed, arbitrated), preprint (not yet peer-reviewed), conference-abstract (extended abstract, typically not full peer review). Closed enum, additive and extensible in a future minor. `peerReviewed` is DERIVED from `kind == journal-article` and never stored \u2014 the same derive-never-store discipline as published-vs-in-development (\xA720.1): a boolean exception needs a documented decision first. Optional: a row without `kind` validates and renders byte-identically to a pre-v1.14 workspace, so existing workspaces stay valid. Consumers that need counts MUST require the field explicitly instead of inferring it from `venue` \u2014 venue stays a free string, and a new venue must never fail a check.").optional(), "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("Citable research output \u2014 a peer-reviewed article, a preprint or a conference abstract, classified by the optional `kind` (v1.14); `peerReviewed` is derived from `kind == journal-article`, never stored. 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/projects are NOT registry kinds, they reference registry ids).").optional(), "faq": 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."), "q": langMapKeys(z.object({ "en": z.string().min(1).max(90) }).catchall(z.string().min(1).max(90))).describe("v1.10 (#288): the question, \u2264 90 chars per value. Unique per workspace \u2014 JSON Schema cannot express it: it is a geoCheck v2 rule (#289, `geo/faq-coverage`); duplicates collapse into one answer surface."), "a": langMapKeys(z.object({ "en": z.string().min(1).max(320) }).catchall(z.string().min(1).max(320))).describe("v1.10 (#288): the answer, \u2264 320 chars per value \u2014 beyond that it is prose, not an answer. The owner's assertion (D11/D14), never invented copy."), "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.").optional() }).strict().describe("v1.10 (issue #288): ONE citable Q&A pair \u2014 the fact pair the `FAQPage` JSON-LD node projects (#316); the answer is the owner's assertion, rendered deterministically (same curation rule as `bios[]`).")).min(3).max(20).describe("v1.10 (issue #288): the citable answer surface \u2014 the questions THIS entity answers, as fact pairs (never generated copy, D11/D14). 3\u201320 entries: below 3 there is no surface; above 20 it is a page, not a profile. The `structuredData()` projection (a `FAQPage` node ONLY when present, never an empty FAQPage) ships in #316; the citability rules beyond JSON Schema are geoCheck v2 (#289).").optional(), "releases": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9-]*$")).describe("Stable release id (`v0-3-0`, `2026-10-launch`). The CLI addresses a release by it; it never changes."), "version": z.string().regex(new RegExp("^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$")).describe("The published semver version this event describes (the tag's version, not the schema version)."), "date": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01)."), "summary": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "changedUnits": z.array(z.string()).describe("The \xA719.2 ledger keys this release changed (`identity`, `projects:foo`, \u2026). The fan-out compares each declared surface against these units' ledger records.").optional(), "surfaces": z.array(z.string().min(1)).min(1).describe("The presence surfaces this release must reach, as platforms/registry.yaml slugs of any `kind` (profile \xB7 repo-meta \xB7 distribution). Order here is declaration order; `pasco release plan` sorts by kind (repo-meta \u2192 profile \u2192 distribution), then slug.") }).strict().describe("v1.15 (D23, #262): one release event. `changedUnits[]` + `surfaces[]` are the fan-out; the state per surface is DERIVED from the \xA719 ledger, never stored here.")).describe("v1.15 (D23, #262): the release event and its fan-out. A release is the unit that has surfaces and a changed set; `pasco release plan` derives the ordered fan-out from it, reusing the \xA719 ledger semantics (in-sync/stale/never) instead of a second state model. Optional and additive: a workspace without releases[] validates and every earlier command reads exactly as before.").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). New in v1.10 (issue #288): the answer-engine surface \u2014 `identity.definition` (the canonical per-language definition: \u2264160 chars per value, must contain the entity name \u2014 the length cap is schema-enforced, the name check is a geoCheck rule, #289) and root `faq[]` (3\u201320 citable Q&A pairs: `q` \u226490 per value and unique per workspace, `a` \u2264320, optional `$defs/url` `source`; below 3 there is no surface, above 20 it is a page, not a profile). The `structuredData()` projection of these fields (Person `description` + a separate `FAQPage` JSON-LD node, only when the fields exist) ships in #316. The same minor completes the version label of the \xA719.5 certification additions (issue #132 \u2014 shipped in `d172dffc` under the v1.9 URI). New in v1.12 (issue #333): `projects[]` gains `artifactRefs` \u2014 the third reference site (publications and press were the first two) \u2014 so a project names the work behind it, and published-vs-in-development is DERIVED from those artifacts (`visibility: public` AND `locator.url`), never stored (\xA720.1's derived-never-stored rule). New in v1.13 (issue #334): `identity.nationality` \u2014 the ISO 3166-1 alpha-2 nationality code as a citable fact (uppercase, e.g. `VE`); schema.org `Person.nationality` \u2192 `Country` with `identifier` on the JSON-LD. Single-valued in v1.13 (dual nationality would need an array \u2014 a future additive change). Optional and additive: a workspace that omits it validates and scores as before (it is outside the structured-data `ready` gate and completeness ratio). New in v1.14 (issue #349): `publication.kind` \u2014 the closed output classification (`journal-article` | `preprint` | `conference-abstract`) that replaces per-consumer venue-string heuristics; `peerReviewed` is DERIVED from `kind == journal-article`, never stored. Optional and additive: a row without `kind` validates and renders exactly as before. New in v1.15 (D23, #262): `releases[]` \u2014 the release EVENT and its fan-out. Each row names the version, the date, an optional summary, the ledger units it changed (`changedUnits`, \xA719.2 keys) and the presence surfaces it must reach (`surfaces`, registry slugs of any `kind`). Additive: a workspace without `releases[]` validates unchanged and every pre-v1.15 command reads exactly the same profile.");
2171
- var PROFILE_SCHEMA_VERSION = "1.15";
2350
+ 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(), "definition": langMapKeys(z.object({ "en": z.string().min(1).max(160) }).catchall(z.string().min(1).max(160))).describe("v1.10 (issue #288): the canonical definition per language \u2014 one sentence of the form 'X is a \u2026' an answer engine can quote verbatim. \u2264 160 chars per value (what an answer engine quotes); MUST contain the entity name \u2014 JSON Schema cannot express it: the check is `geo/canonical-definition` in geoCheck v2 (#289). EN required (langMap rule, D8/\xA711); the propertyNames restriction shares the langMap precedent (issue #86, F3 \u2014 gen-zod reconstructs it).").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(), "nationality": z.string().regex(new RegExp("^[A-Z]{2}$")).describe("v1.13 (issue #334): ISO 3166-1 alpha-2 nationality code (uppercase, e.g. `VE`), a fact distinct from `location` (residence). Maps to schema.org `Person.nationality` as a `Country` node with `identifier` on the JSON-LD. Person-only by the standard's model: a `flavor: brand` workspace may declare it (identity is shared) but the Organization branch emits no `nationality` node \u2014 the value is not silently rewritten into a brand fact. The pattern asserts ISO 3166-1 alpha-2 FORM only \u2014 `ve`, `Venezuela` and `VEN` fail loudly (ECMA-262 pattern, no flags; D12 fail class); membership in the published code list is not enforced. Optional: outside the structured-data `ready` gate and completeness ratio, so absence changes nothing.").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) \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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. 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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. 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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. 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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. 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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. 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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. 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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. 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(), "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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. Optional: rows without id are listed by `pasco status` as untracked.")).describe("v1.12 (D17/\xA718.1, issue #333): references `artifacts[].id` \u2014 the work behind this row. projects keep their domain (no duplication); target resolved by core (load fails on a dangling ref). Published-vs-in-development is DERIVED from the targets (`visibility` + `locator.url`), never stored (\xA720.1).").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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. 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), "kind": z.enum(["journal-article", "preprint", "conference-abstract"]).describe("v1.14 (issue #349): the output's structured classification \u2014 journal-article (peer-reviewed, arbitrated), preprint (not yet peer-reviewed), conference-abstract (extended abstract, typically not full peer review). Closed enum, additive and extensible in a future minor. `peerReviewed` is DERIVED from `kind == journal-article` and never stored \u2014 the same derive-never-store discipline as published-vs-in-development (\xA720.1): a boolean exception needs a documented decision first. Optional: a row without `kind` validates and renders byte-identically to a pre-v1.14 workspace, so existing workspaces stay valid. Consumers that need counts MUST require the field explicitly instead of inferring it from `venue` \u2014 venue stays a free string, and a new venue must never fail a check.").optional(), "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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. 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("Citable research output \u2014 a peer-reviewed article, a preprint or a conference abstract, classified by the optional `kind` (v1.14); `peerReviewed` is derived from `kind == journal-article`, never stored. 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; the union decides which premises run (`fact-present`), while char caps come from the workspace catalog when it declares checks (whole catalog, never merged row by row) or the caps shipped with pasco \u2014 no skill `max-chars` row reaches the limit executor (SPEC \xA77). 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. The union with the platform skill's checks.yaml decides which premises run (`fact-present`); when this catalog declares checks it is the whole cap catalog (never merged row by row with the package defaults), and no skill `max-chars` row reaches the limit executor (SPEC \xA77).").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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. 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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. 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/projects are NOT registry kinds, they reference registry ids).").optional(), "faq": 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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. Optional: rows without id are listed by `pasco status` as untracked."), "q": langMapKeys(z.object({ "en": z.string().min(1).max(90) }).catchall(z.string().min(1).max(90))).describe("v1.10 (#288): the question, \u2264 90 chars per value. Unique per workspace \u2014 JSON Schema cannot express it: it is a geoCheck v2 rule (#289, `geo/faq-coverage`); duplicates collapse into one answer surface."), "a": langMapKeys(z.object({ "en": z.string().min(1).max(320) }).catchall(z.string().min(1).max(320))).describe("v1.10 (#288): the answer, \u2264 320 chars per value \u2014 beyond that it is prose, not an answer. The owner's assertion (D11/D14), never invented copy."), "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.").optional() }).strict().describe("v1.10 (issue #288): ONE citable Q&A pair \u2014 the fact pair the `FAQPage` JSON-LD node projects (#316); the answer is the owner's assertion, rendered deterministically (same curation rule as `bios[]`).")).min(3).max(20).describe("v1.10 (issue #288): the citable answer surface \u2014 the questions THIS entity answers, as fact pairs (never generated copy, D11/D14). 3\u201320 entries: below 3 there is no surface; above 20 it is a page, not a profile. The `structuredData()` projection (a `FAQPage` node ONLY when present, never an empty FAQPage) ships in #316; the citability rules beyond JSON Schema are geoCheck v2 (#289).").optional(), "releases": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9-]*$")).describe("Stable release id (`v0-3-0`, `2026-10-launch`). The CLI addresses a release by it; it never changes."), "version": z.string().regex(new RegExp("^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$")).describe("The published semver version this event describes (the tag's version, not the schema version)."), "date": z.string().date().describe("YYYY-MM-DD. Facts are language-independent (GEO anchor). Month/year precision allowed (YYYY-MM via YYYY-MM-01)."), "summary": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())).optional(), "changedUnits": z.array(z.string()).describe("The \xA719.2 ledger keys this release changed (`identity`, `projects:foo`, \u2026). The fan-out compares each declared surface against these units' ledger records.").optional(), "surfaces": z.array(z.string().min(1)).min(1).describe("The presence surfaces this release must reach, as platforms/registry.yaml slugs of any `kind` (profile \xB7 repo-meta \xB7 distribution). Order here is declaration order; `pasco release plan` sorts by kind (repo-meta \u2192 profile \u2192 distribution), then slug.") }).strict().describe("v1.15 (D23, #262): one release event. `changedUnits[]` + `surfaces[]` are the fan-out; the state per surface is DERIVED from the \xA719 ledger, never stored here.")).describe("v1.15 (D23, #262): the release event and its fan-out. A release is the unit that has surfaces and a changed set; `pasco release plan` derives the ordered fan-out from it, reusing the \xA719 ledger semantics (in-sync/stale/never) instead of a second state model. Optional and additive: a workspace without releases[] validates and every earlier command reads exactly as before.").optional(), "achievements": 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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. Optional: rows without id are listed by `pasco status` as untracked."), "statement": langMapKeys(z.object({ "en": z.string() }).catchall(z.string())), "kind": z.enum(["metric", "milestone"]).describe("`metric` requires `metric`; `milestone` requires it ABSENT. An award belongs in `credentials[]` \u2014 the achievement references it."), "metric": z.object({ "value": z.number().describe("The measured number. Absent with `target` present = a PLANNED goal (status derived, never stored).").optional(), "target": z.number().describe("The goal number. Absent with `value` present = a measurement with no goal (status `measured`).").optional(), "baseline": z.number().describe("Where the result started, when it is a delta (`72 \u2192 4`). Context only: status compares `value` against `target`, never against the baseline.").optional(), "unit": z.string().min(1).describe("What the numbers count (`tutors`, `hours`, `%`). Required: a number with no unit is not a metric."), "direction": z.enum(["increase", "decrease", "maintain"]).describe("Which way is better. Absent = `increase` (the declared default in core `achievementStatus()`; never inferred from the baseline).").optional() }).strict().describe("v1.16 (D24, #446): the numeric shape of a `metric` achievement. Structured \u2014 never a free-text string \u2014 so the status can be derived and the CLI can render `72 h \u2192 4 h` without parsing prose.").optional(), "refs": z.array(z.string().regex(new RegExp("^(experience|projects|publications|education|press|credentials|artifacts):[a-z0-9][a-z0-9._:-]*$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("The units this result belongs to, as `block:id` strings. The refs are resolved by core (a dangling ref fails `load`, same class as `artifactRefs`), and they are what the CLI groups the log by.").optional(), "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.").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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. Optional: rows without id are listed by `pasco status` as untracked.").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 for the ledger row blocks (`experience`, `projects`, `publications`, `education`, `press`): core-checked, fails validation; JSON Schema cannot express uniqueness. `faq[]` is root and not ledger-tracked, so its ids are enforced where they are addressed \u2014 `pasco edit` refuses an ambiguous id \u2014 not by the schema. Optional: rows without id are listed by `pasco status` as untracked.").optional(), "basis": z.enum(["measured", "estimated", "reported", "attested"]).describe("HOW the result is known \u2014 the honesty field, not a gate (capture first, prove later): `measured` instrumented/documented \xB7 `estimated` derived from partial data \xB7 `reported` the person's own account (a first-class value: honest, not blocked) \xB7 `attested` a third party confirms."), "date": 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).") }).strict().describe("v1.16 (D24, #446): one row of the career impact log \u2014 a result, the units it belongs to, and how it is known. The status is DERIVED by core `achievementStatus()`, never stored here.")).describe("v1.16 (D24, #446): the career impact log \u2014 what CHANGED, as opposed to `experience[].highlights` (what was done) and `projects[].evidence[]` (what can be proven). One ROOT log, never per-row `outcomes[]`: `refs[]` points at the units a result belongs to, so one result is not duplicated across rows. `target` without `value` is a planned goal and the status is DERIVED (planned | measured | achieved | partial), never stored. Optional and additive: a workspace without achievements[] validates and renders byte-identically. No `minItems`: `load()` normalizes an undeclared log to `[]` and `pasco export profile` emits it (the loaded-shape rule every list root follows), so an empty array must stay schema-valid: the engine's own export payload materializes it, and `minItems: 1` would reject it.").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). New in v1.10 (issue #288): the answer-engine surface \u2014 `identity.definition` (the canonical per-language definition: \u2264160 chars per value, must contain the entity name \u2014 the length cap is schema-enforced, the name check is a geoCheck rule, #289) and root `faq[]` (3\u201320 citable Q&A pairs: `q` \u226490 per value and unique per workspace, `a` \u2264320, optional `$defs/url` `source`; below 3 there is no surface, above 20 it is a page, not a profile). The `structuredData()` projection of these fields (Person `description` + a separate `FAQPage` JSON-LD node, only when the fields exist) ships in #316. The same minor completes the version label of the \xA719.5 certification additions (issue #132 \u2014 shipped in `d172dffc` under the v1.9 URI). New in v1.12 (issue #333): `projects[]` gains `artifactRefs` \u2014 the third reference site (publications and press were the first two) \u2014 so a project names the work behind it, and published-vs-in-development is DERIVED from those artifacts (`visibility: public` AND `locator.url`), never stored (\xA720.1's derived-never-stored rule). New in v1.13 (issue #334): `identity.nationality` \u2014 the ISO 3166-1 alpha-2 nationality code as a citable fact (uppercase, e.g. `VE`); schema.org `Person.nationality` \u2192 `Country` with `identifier` on the JSON-LD. Single-valued in v1.13 (dual nationality would need an array \u2014 a future additive change). Optional and additive: a workspace that omits it validates and scores as before (it is outside the structured-data `ready` gate and completeness ratio). New in v1.14 (issue #349): `publication.kind` \u2014 the closed output classification (`journal-article` | `preprint` | `conference-abstract`) that replaces per-consumer venue-string heuristics; `peerReviewed` is DERIVED from `kind == journal-article`, never stored. Optional and additive: a row without `kind` validates and renders exactly as before. New in v1.15 (D23, #262): `releases[]` \u2014 the release EVENT and its fan-out. Each row names the version, the date, an optional summary, the ledger units it changed (`changedUnits`, \xA719.2 keys) and the presence surfaces it must reach (`surfaces`, registry slugs of any `kind`). Additive: a workspace without `releases[]` validates unchanged and every pre-v1.15 command reads exactly the same profile. New in v1.16 (D24, #446): `achievements[]` \u2014 the career impact log. Each row states a RESULT (`statement`), classifies it (`kind`: `metric` | `milestone`), attaches the numeric shape (`metric`: value/target/baseline/unit/direction), points at the units it belongs to (`refs`, `block:id`) and declares HOW the result is known (`basis`: measured | estimated | reported | attested) plus a required `visibility`. `target` without `value` is a PLANNED goal and the status is DERIVED at read time (`planned` | `measured` | `achieved` | `partial`), never stored. Optional and additive: a workspace without `achievements[]` validates and renders byte-identically.");
2351
+ var PROFILE_SCHEMA_VERSION = "1.16";
2172
2352
  var LANG_KEYS = ["en", "es", "fr"];
2173
2353
  function langMapKeys(schema) {
2174
2354
  return schema.superRefine((value, ctx) => {
@@ -2205,9 +2385,9 @@ var SchemaValidationError = class extends LoadError {
2205
2385
  function validateProfile(value) {
2206
2386
  const result = profileSchema.safeParse(value);
2207
2387
  if (!result.success) {
2208
- const issues = result.error.issues.map((issue) => ({
2209
- path: issue.path.length ? issue.path.join(".") : "<root>",
2210
- message: issue.message
2388
+ const issues = result.error.issues.map((issue2) => ({
2389
+ path: issue2.path.length ? issue2.path.join(".") : "<root>",
2390
+ message: issue2.message
2211
2391
  }));
2212
2392
  throw new SchemaValidationError(issues);
2213
2393
  }
@@ -2239,7 +2419,10 @@ function normalizeProfile(profile) {
2239
2419
  faq: profile.faq ?? [],
2240
2420
  // v1.15 (D23): the release events a workspace declares. Optional and additive
2241
2421
  // — an old workspace without it reads the same, now with an empty list.
2242
- releases: profile.releases ?? []
2422
+ releases: profile.releases ?? [],
2423
+ // v1.16 (D24, #446): the career impact log. Same rule — the loaded shape never
2424
+ // depends on whether the workspace declares results.
2425
+ achievements: profile.achievements ?? []
2243
2426
  };
2244
2427
  }
2245
2428
  function readContentFiles(workspace) {
@@ -2277,6 +2460,10 @@ function load(workspace) {
2277
2460
  if (registryIssues.length > 0) {
2278
2461
  throw new SchemaValidationError(registryIssues);
2279
2462
  }
2463
+ const achievementIssues = checkAchievementIntegrity(profile);
2464
+ if (achievementIssues.length > 0) {
2465
+ throw new SchemaValidationError(achievementIssues);
2466
+ }
2280
2467
  return normalizeProfile(profile);
2281
2468
  }
2282
2469
 
@@ -2707,14 +2894,15 @@ function kindOf(surface) {
2707
2894
  }
2708
2895
  function surfaceState(profile, release, surface, unitHashes) {
2709
2896
  const missingUnits = [];
2897
+ const unknownUnits = [];
2710
2898
  const changed = release.changedUnits ?? [];
2711
2899
  if (changed.length === 0)
2712
- return { state: "never", missingUnits };
2900
+ return { state: "never", missingUnits, unknownUnits };
2713
2901
  let stale = false;
2714
2902
  for (const key of changed) {
2715
2903
  const hash = unitHashes.get(key);
2716
2904
  if (hash === void 0) {
2717
- missingUnits.push(key);
2905
+ unknownUnits.push(key);
2718
2906
  continue;
2719
2907
  }
2720
2908
  const record = profile.deployments[key]?.[surface];
@@ -2725,9 +2913,10 @@ function surfaceState(profile, release, surface, unitHashes) {
2725
2913
  if (record.contentHash !== hash)
2726
2914
  stale = true;
2727
2915
  }
2728
- if (missingUnits.length > 0)
2729
- return { state: "never", missingUnits };
2730
- return { state: stale ? "stale" : "in-sync", missingUnits };
2916
+ if (missingUnits.length > 0 || unknownUnits.length > 0) {
2917
+ return { state: "never", missingUnits, unknownUnits };
2918
+ }
2919
+ return { state: stale ? "stale" : "in-sync", missingUnits, unknownUnits };
2731
2920
  }
2732
2921
  function unitHashMap(profile) {
2733
2922
  const map = /* @__PURE__ */ new Map();
@@ -2780,7 +2969,7 @@ function releasePlan(profile, id) {
2780
2969
  kind,
2781
2970
  ...surfaceState(profile, release, surface, unitHashes)
2782
2971
  };
2783
- }).sort(compareSurfaces);
2972
+ }).sort(compareSurfaces).map((row, index) => ({ order: index + 1, ...row }));
2784
2973
  const summary = release.summary?.en ?? release.id;
2785
2974
  const kinds = [];
2786
2975
  for (const row of surfaces)
@@ -2835,9 +3024,200 @@ function presenceBoard(profile) {
2835
3024
  };
2836
3025
  }
2837
3026
 
3027
+ // src/exit.ts
3028
+ var EXIT = {
3029
+ OK: 0,
3030
+ FAIL: 1,
3031
+ USAGE: 2
3032
+ };
3033
+
3034
+ // src/output.ts
3035
+ var jsonMode = false;
3036
+ function humanCommand(command) {
3037
+ return `pasco ${command}`;
3038
+ }
3039
+ function setJsonMode(enabled) {
3040
+ jsonMode = enabled;
3041
+ }
3042
+ function isJsonMode() {
3043
+ return jsonMode;
3044
+ }
3045
+ function report(command, ok, exitCode2, payload) {
3046
+ if (jsonMode) {
3047
+ const envelope = {
3048
+ command,
3049
+ ok,
3050
+ exitCode: exitCode2,
3051
+ ...payload.data !== void 0 ? { data: payload.data } : {},
3052
+ ...payload.error !== void 0 ? { error: payload.error } : {}
3053
+ };
3054
+ process.stdout.write(`${JSON.stringify(envelope, null, 2)}
3055
+ `);
3056
+ return exitCode2;
3057
+ }
3058
+ if (payload.error !== void 0 && !ok) {
3059
+ process.stderr.write(`${humanCommand(command)}: ${payload.error.message}
3060
+ `);
3061
+ for (const v of payload.error.violations ?? []) {
3062
+ process.stderr.write(` ${v.path || "<root>"}: ${v.message}
3063
+ `);
3064
+ }
3065
+ if (payload.error.hint !== void 0) process.stderr.write(`${payload.error.hint}
3066
+ `);
3067
+ }
3068
+ return exitCode2;
3069
+ }
3070
+ function section(title) {
3071
+ if (jsonMode) return;
3072
+ process.stdout.write(`
3073
+ ${title}
3074
+ `);
3075
+ }
3076
+ function line(text) {
3077
+ if (jsonMode) return;
3078
+ process.stdout.write(`${text}
3079
+ `);
3080
+ }
3081
+ function warn2(text) {
3082
+ if (jsonMode) return;
3083
+ process.stderr.write(`warning: ${text}
3084
+ `);
3085
+ }
3086
+
3087
+ // src/commands/achievements.ts
3088
+ var ACHIEVEMENTS_SCHEMA_VERSION = "1";
3089
+ function formatMetric(metric) {
3090
+ const unit = metric.unit;
3091
+ const suffix = metric.target !== void 0 && metric.value !== void 0 ? ` (target ${metric.target} ${unit})` : "";
3092
+ const direction = metric.direction !== void 0 && metric.direction !== "increase" ? ` \xB7 ${metric.direction}` : "";
3093
+ if (metric.baseline !== void 0 && metric.value !== void 0) {
3094
+ return `${metric.baseline} ${unit} \u2192 ${metric.value} ${unit}${suffix}${direction}`;
3095
+ }
3096
+ if (metric.value !== void 0) return `${metric.value} ${unit}${suffix}${direction}`;
3097
+ if (metric.target !== void 0) return `target ${metric.target} ${unit}${direction}`;
3098
+ return `\u2014 ${unit}${direction}`;
3099
+ }
3100
+ function achievementPointer(achievement) {
3101
+ if (achievement.source !== void 0) return achievement.source;
3102
+ if (achievement.artifactId !== void 0) return `artifacts:${achievement.artifactId}`;
3103
+ if (achievement.credentialId !== void 0) return `credentials:${achievement.credentialId}`;
3104
+ return "(no pointer)";
3105
+ }
3106
+ function rowLine(achievement) {
3107
+ const status = achievementStatus(achievement);
3108
+ const label = status ?? achievement.kind;
3109
+ const metric = achievement.kind === "metric" && achievement.metric !== void 0 ? ` \u2014 ${formatMetric(achievement.metric)}` : "";
3110
+ return `[${label}] ${textOf(achievement.statement)}${metric} \xB7 basis: ${achievement.basis} \xB7 ${achievementPointer(achievement)}`;
3111
+ }
3112
+ function groupByRef(achievements) {
3113
+ const groups = /* @__PURE__ */ new Map();
3114
+ for (const achievement of achievements) {
3115
+ const key = achievement.refs?.[0] ?? "Unattached";
3116
+ const bucket = groups.get(key);
3117
+ if (bucket === void 0) groups.set(key, [achievement]);
3118
+ else bucket.push(achievement);
3119
+ }
3120
+ return new Map(
3121
+ [...groups.entries()].sort(
3122
+ ([a], [b]) => a === "Unattached" ? 1 : b === "Unattached" ? -1 : a < b ? -1 : a > b ? 1 : 0
3123
+ )
3124
+ );
3125
+ }
3126
+ function loadOrReport(command, workspace) {
3127
+ try {
3128
+ return { profile: load(workspace) };
3129
+ } catch (err) {
3130
+ if (err instanceof SchemaValidationError || err instanceof LoadError) {
3131
+ return {
3132
+ fail: report(command, false, EXIT.FAIL, {
3133
+ error: {
3134
+ kind: "load",
3135
+ message: err.message,
3136
+ ...err instanceof SchemaValidationError ? { violations: [...err.issues] } : {}
3137
+ }
3138
+ })
3139
+ };
3140
+ }
3141
+ throw err;
3142
+ }
3143
+ }
3144
+ function targetRows(profile) {
3145
+ return profile.achievements.filter((a) => achievementStatus(a) === "planned");
3146
+ }
3147
+ function achievementsPayload(achievements) {
3148
+ return {
3149
+ schemaVersion: ACHIEVEMENTS_SCHEMA_VERSION,
3150
+ // `scope` counts the rows THIS view returned: an empty scope must read as
3151
+ // "nothing declared/measured here", never as a fabricated zero (§7.1 shape).
3152
+ scope: achievementScope(achievements),
3153
+ achievements: achievements.map((a) => ({ ...a, status: achievementStatus(a) ?? null }))
3154
+ };
3155
+ }
3156
+ function runAchievements(args) {
3157
+ const loaded = loadOrReport("achievements", args.workspace);
3158
+ if ("fail" in loaded) return loaded.fail;
3159
+ const rows = args.targets === true ? targetRows(loaded.profile) : loaded.profile.achievements;
3160
+ if (isJsonMode()) {
3161
+ return report("achievements", true, EXIT.OK, { data: achievementsPayload(rows) });
3162
+ }
3163
+ if (rows.length === 0) {
3164
+ section(
3165
+ args.targets === true ? "Achievements \u2014 no planned targets" : "Achievements \u2014 the log is empty"
3166
+ );
3167
+ line(
3168
+ args.targets === true ? "No row declares a `target` without a `value` yet \u2014 the goal view is empty." : "Add content/achievements.yaml: one row per result, with `basis` and `visibility` (D24)."
3169
+ );
3170
+ return EXIT.OK;
3171
+ }
3172
+ const scope = achievementScope(rows);
3173
+ section(
3174
+ args.targets === true ? `Achievements \u2014 ${scope.total} planned target(s)` : `Achievements \u2014 ${scope.total} row(s) \xB7 planned ${scope.planned} \xB7 measured ${scope.measured} \xB7 achieved ${scope.achieved} \xB7 partial ${scope.partial}`
3175
+ );
3176
+ for (const [group, groupRows] of groupByRef(rows)) {
3177
+ line("");
3178
+ line(group === "Unattached" ? "Unattached" : group);
3179
+ for (const achievement of groupRows) {
3180
+ const refs = achievement.refs ?? [];
3181
+ const extra = refs.length > 1 ? ` [also: ${refs.slice(1).join(", ")}]` : "";
3182
+ line(` ${rowLine(achievement)}${extra}`);
3183
+ }
3184
+ }
3185
+ line("");
3186
+ line(
3187
+ "Status is DERIVED from the numbers (planned \xB7 measured \xB7 achieved \xB7 partial) \u2014 never stored."
3188
+ );
3189
+ return EXIT.OK;
3190
+ }
3191
+
2838
3192
  // src/claims.ts
3193
+ var NUMBER = String.raw`\d{1,18}(?:[.,]\d{1,18}){0,5}`;
3194
+ var CURRENCY_OR_PERCENT = new RegExp(
3195
+ String.raw`(?:[$€£¥₹¥₽]|%)\s?${NUMBER}|${NUMBER}\s?(?:%|%)`,
3196
+ "u"
3197
+ );
3198
+ var PLUS_OR_MAGNITUDE = new RegExp(String.raw`${NUMBER}\s?(?:\+|(?:bn|tn|[kmb])(?!\p{L}))`, "iu");
3199
+ var NUMBER_THEN_WORD = new RegExp(String.raw`(?<![\w.])(${NUMBER})(\s?)(\p{L}+)`, "gu");
3200
+ var MONTH_OR_PREPOSITION_BEFORE = /\b(?:(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\s+(?:\d{1,2},?\s*)?|(?:in|on|since|by|until|during|from)\s+)$/i;
3201
+ function isCalendarYear(token) {
3202
+ if (!/^\d{4}$/.test(token)) return false;
3203
+ const year = Number(token);
3204
+ return year >= 1900 && year <= 2099;
3205
+ }
2839
3206
  function isQuantitativeClaim(text) {
2840
- return /\d/.test(text) && /[%$]|\b\d{2,}\b/.test(text);
3207
+ if (!/\d/.test(text)) return false;
3208
+ if (CURRENCY_OR_PERCENT.test(text)) return true;
3209
+ if (PLUS_OR_MAGNITUDE.test(text)) return true;
3210
+ for (const match of text.matchAll(NUMBER_THEN_WORD)) {
3211
+ const token = match[1] ?? "";
3212
+ const separator = match[2] ?? "";
3213
+ if (separator === "" && token.length === 1) continue;
3214
+ if (isCalendarYear(token)) {
3215
+ const before = text.slice(0, match.index);
3216
+ if (MONTH_OR_PREPOSITION_BEFORE.test(before)) continue;
3217
+ }
3218
+ return true;
3219
+ }
3220
+ return false;
2841
3221
  }
2842
3222
 
2843
3223
  // src/variant-store.ts
@@ -2893,9 +3273,9 @@ function resolveVariantBody(workspace, slice) {
2893
3273
 
2894
3274
  // src/drift-reconcile.ts
2895
3275
  function reconciledDrift(workspace, profile) {
2896
- return driftCheck(profile).filter((issue) => {
2897
- if (issue.code !== "drift/active-variant-missing") return true;
2898
- const platform = issue.surfaces?.[0];
3276
+ return driftCheck(profile).filter((issue2) => {
3277
+ if (issue2.code !== "drift/active-variant-missing") return true;
3278
+ const platform = issue2.surfaces?.[0];
2899
3279
  if (platform === void 0) return true;
2900
3280
  const active = profile.active?.[platform];
2901
3281
  if (active === void 0) return true;
@@ -2903,13 +3283,6 @@ function reconciledDrift(workspace, profile) {
2903
3283
  });
2904
3284
  }
2905
3285
 
2906
- // src/exit.ts
2907
- var EXIT = {
2908
- OK: 0,
2909
- FAIL: 1,
2910
- USAGE: 2
2911
- };
2912
-
2913
3286
  // src/fact-present.ts
2914
3287
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
2915
3288
  import { relative as relative2 } from "path";
@@ -2974,6 +3347,17 @@ function pasteSource(p) {
2974
3347
  return "";
2975
3348
  }
2976
3349
 
3350
+ // src/check-catalog.ts
3351
+ function capCatalog(profile, defaults) {
3352
+ return profile.checks.length > 0 ? profile.checks : defaults;
3353
+ }
3354
+ function catalogSource(profile) {
3355
+ return profile.checks.length > 0 ? "workspace" : "defaults";
3356
+ }
3357
+ function catalogLabel(source) {
3358
+ return source === "workspace" ? "workspace checks (declared in content/, schema-validated catalog)" : "workspace checks: none declared \u2014 the conservative caps shipped with pasco apply (#240)";
3359
+ }
3360
+
2977
3361
  // ../renderers/buymeacoffee/dist/index.js
2978
3362
  function render(p) {
2979
3363
  return { text: pasteSource(p) };
@@ -3174,7 +3558,6 @@ var extractArtifact = createArtifactExtractor("github-org");
3174
3558
 
3175
3559
  // ../renderers/github-org/dist/index.js
3176
3560
  var BADGE = {
3177
- github: { color: "181717", logo: "github", label: "GitHub" },
3178
3561
  "github-org": { color: "181717", logo: "github", label: "GitHub" },
3179
3562
  x: { color: "000000", logo: "X", label: "X" },
3180
3563
  bluesky: { color: "0285FF", logo: "bluesky", label: "Bluesky" },
@@ -3185,8 +3568,7 @@ var BADGE = {
3185
3568
  spotify: { color: "1DB954", logo: "spotify", label: "Spotify" },
3186
3569
  reddit: { color: "FF4500", logo: "reddit", label: "Reddit" },
3187
3570
  mastodon: { color: "6364FF", logo: "mastodon", label: "Mastodon" },
3188
- soundcloud: { color: "FF3300", logo: "soundcloud", label: "SoundCloud" },
3189
- website: { color: "111111", logo: "internetarchive", label: "Website" }
3571
+ soundcloud: { color: "FF3300", logo: "soundcloud", label: "SoundCloud" }
3190
3572
  };
3191
3573
  function hostnamePath(url) {
3192
3574
  try {
@@ -3523,6 +3905,24 @@ var RENDERERS = {
3523
3905
  };
3524
3906
 
3525
3907
  // src/limits.ts
3908
+ var CONTRACT_CAPS = {
3909
+ definition: {
3910
+ cap: 160,
3911
+ source: "[OFICIAL] the v1.10 contract itself (schema maxLength for identity.definition, #288, 2026-10-24) \u2014 over the cap the schema refuses the write (D12)"
3912
+ },
3913
+ "faq.q": {
3914
+ cap: 90,
3915
+ source: "[OFICIAL] the v1.10 contract itself (schema maxLength for faq[].q, #288, 2026-10-24) \u2014 over the cap the schema refuses the write (D12)"
3916
+ },
3917
+ "faq.a": {
3918
+ cap: 320,
3919
+ source: "[OFICIAL] the v1.10 contract itself (schema maxLength for faq[].a, #288, 2026-10-24) \u2014 over the cap the schema refuses the write (D12)"
3920
+ }
3921
+ };
3922
+ function contractLimit(field) {
3923
+ const row = CONTRACT_CAPS[field];
3924
+ return { cap: row.cap, source: row.source, tier: "OFICIAL" };
3925
+ }
3526
3926
  function limitRows(profile) {
3527
3927
  return [
3528
3928
  ...(profile.headlines ?? []).map((h) => ({
@@ -3838,7 +4238,7 @@ function publicationTruth(args) {
3838
4238
  });
3839
4239
  }
3840
4240
  function publicationOfCaller(args, profile, reportData, defaultChecks) {
3841
- const checks = profile.checks.length > 0 ? profile.checks : defaultChecks;
4241
+ const checks = capCatalog(profile, defaultChecks);
3842
4242
  return publicationTruth({
3843
4243
  workspace: args.workspace,
3844
4244
  profile,
@@ -4120,54 +4520,6 @@ function factPresentFindings(args) {
4120
4520
  return { catalog: catalogState(), declared, checked, notRun, findings };
4121
4521
  }
4122
4522
 
4123
- // src/output.ts
4124
- var jsonMode = false;
4125
- function setJsonMode(enabled) {
4126
- jsonMode = enabled;
4127
- }
4128
- function isJsonMode() {
4129
- return jsonMode;
4130
- }
4131
- function report(command, ok, exitCode2, payload) {
4132
- if (jsonMode) {
4133
- const envelope = {
4134
- command,
4135
- ok,
4136
- exitCode: exitCode2,
4137
- ...payload.data !== void 0 ? { data: payload.data } : {},
4138
- ...payload.error !== void 0 ? { error: payload.error } : {}
4139
- };
4140
- process.stdout.write(`${JSON.stringify(envelope, null, 2)}
4141
- `);
4142
- return exitCode2;
4143
- }
4144
- if (payload.error !== void 0 && !ok) {
4145
- process.stderr.write(`${command}: ${payload.error.message}
4146
- `);
4147
- for (const v of payload.error.violations ?? []) {
4148
- process.stderr.write(` ${v.path || "<root>"}: ${v.message}
4149
- `);
4150
- }
4151
- }
4152
- return exitCode2;
4153
- }
4154
- function section(title) {
4155
- if (jsonMode) return;
4156
- process.stdout.write(`
4157
- ${title}
4158
- `);
4159
- }
4160
- function line(text) {
4161
- if (jsonMode) return;
4162
- process.stdout.write(`${text}
4163
- `);
4164
- }
4165
- function warn2(text) {
4166
- if (jsonMode) return;
4167
- process.stderr.write(`warning: ${text}
4168
- `);
4169
- }
4170
-
4171
4523
  // src/commands/audit.ts
4172
4524
  var AUDIT_SCHEMA_VERSION = "1";
4173
4525
  var AUDIT_SEVERITY = {
@@ -4248,6 +4600,20 @@ function ssotFindings(profile, pascorc) {
4248
4600
  );
4249
4601
  }
4250
4602
  }
4603
+ for (const [index, achievement] of (profile.achievements ?? []).entries()) {
4604
+ if (achievement.visibility !== "public") continue;
4605
+ if (achievement.basis !== "measured" && achievement.basis !== "attested") continue;
4606
+ if (achievement.source !== void 0 || achievement.artifactId !== void 0 || achievement.credentialId !== void 0) {
4607
+ continue;
4608
+ }
4609
+ findings.push(
4610
+ finding2(
4611
+ "unsourced-claim",
4612
+ `achievements[${index}]`,
4613
+ `the ${achievement.basis} claim carries no pointer (\`source\`, \`artifactId\` or \`credentialId\`) \u2014 a claim with no evidence behind it`
4614
+ )
4615
+ );
4616
+ }
4251
4617
  const faqDeficient = geoCheck(profile).checks.some(
4252
4618
  (check) => check.code === "geo/faq-coverage" && check.score === 0
4253
4619
  );
@@ -4297,10 +4663,10 @@ function runAudit(args) {
4297
4663
  }
4298
4664
  throw err;
4299
4665
  }
4300
- const drift = reconciledDrift(args.workspace, profile).flatMap((issue) => {
4301
- const mapped = Object.hasOwn(DRIFT_CLASS, issue.code) ? DRIFT_CLASS[issue.code] : "contradiction";
4666
+ const drift = reconciledDrift(args.workspace, profile).flatMap((issue2) => {
4667
+ const mapped = Object.hasOwn(DRIFT_CLASS, issue2.code) ? DRIFT_CLASS[issue2.code] : "contradiction";
4302
4668
  if (mapped === null || mapped === void 0) return [];
4303
- return [finding2(mapped, factRef(profile, issue.path), `${issue.code} \u2014 ${issue.message}`)];
4669
+ return [finding2(mapped, factRef(profile, issue2.path), `${issue2.code} \u2014 ${issue2.message}`)];
4304
4670
  });
4305
4671
  const facts = factPresentFindings({
4306
4672
  workspace: args.workspace,
@@ -4328,14 +4694,25 @@ function runAudit(args) {
4328
4694
  return report("audit", failures === 0, exitCode2, { data: reportData });
4329
4695
  }
4330
4696
  section("audit \u2014 findings only (no score, no prioritisation)");
4331
- const compared = scope.checked.length === 0 ? "no surface measured" : `${scope.checked.length} surface(s) compared (${scope.checked.join(", ")})`;
4697
+ const compared = scope.checked.length === 0 ? "0 surfaces measured" : `${scope.checked.length} surface(s) compared (${scope.checked.join(", ")})`;
4332
4698
  const missing = scope.notRun.length === 0 ? "" : ` \xB7 not run: ${scope.notRun.join(", ")}`;
4333
4699
  const declared = scope.catalog === "not-shipped" ? " \xB7 skills tree NOT SHIPPED with this package (the catalog cannot declare the premise)" : scope.declared.length === 0 ? " \xB7 premise not declared by any catalog" : "";
4334
4700
  line(` scope: ${compared}${missing}${declared}`);
4335
4701
  if (scope.checked.length === 0) {
4336
- line(
4337
- " an empty scope is not a clean audit \u2014 no artifact to compare (run pasco build), or no catalog declares fact-present"
4338
- );
4702
+ if (scope.catalog === "not-shipped") {
4703
+ line(
4704
+ " an empty scope is not a clean audit \u2014 the built-in check catalog is not shipped with this package, so no rule could declare what to compare"
4705
+ );
4706
+ } else if (scope.declared.length === 0) {
4707
+ line(
4708
+ " an empty scope is not a clean audit \u2014 no check catalog declares the surface-facts rule for this workspace's surfaces (add a content/checks.yaml row to declare it)"
4709
+ );
4710
+ } else {
4711
+ const surfaces = scope.notRun.length > 0 ? ` for ${scope.notRun.join(", ")}` : "";
4712
+ line(
4713
+ ` an empty scope is not a clean audit \u2014 no built artifact to compare${surfaces} (run pasco build; a flavor with no renderer cannot be compared)`
4714
+ );
4715
+ }
4339
4716
  }
4340
4717
  if (findings.length === 0) {
4341
4718
  line(" no findings");
@@ -4413,8 +4790,11 @@ function parseFlavor(value) {
4413
4790
  if (FLAVORS.includes(value)) return value;
4414
4791
  throw new UsageError(`unknown flavor "${value}" (allowed: ${FLAVORS.join(", ")})`);
4415
4792
  }
4793
+ var DEFAULT_VARIANTS = 3;
4416
4794
  function parseVariants(value, fallback) {
4417
- if (value === void 0) return fallback;
4795
+ if (value === void 0) {
4796
+ return Number.isInteger(fallback) && fallback >= 1 ? fallback : DEFAULT_VARIANTS;
4797
+ }
4418
4798
  const n = Number(value);
4419
4799
  if (!Number.isInteger(n) || n < 1)
4420
4800
  throw new UsageError(`--variants must be a positive integer (got "${value}")`);
@@ -4427,14 +4807,51 @@ function headlineAddress(platform, lang) {
4427
4807
  function bioAddress(platform, lang, length) {
4428
4808
  return `bios[platform=${platform},lang=${lang},length=${length}].text`;
4429
4809
  }
4810
+ function definitionAddress(lang) {
4811
+ return `identity.definition.${lang}`;
4812
+ }
4813
+ function faqAddress(id, field, lang) {
4814
+ if (field === "source") return `faq[id=${id}].source`;
4815
+ return `faq[id=${id},lang=${lang}].${field}`;
4816
+ }
4817
+ var ENTRY_ID = /^[a-z0-9][a-z0-9._:-]*$/;
4818
+ function parseEntryId(block, value) {
4819
+ if (!ENTRY_ID.test(value)) {
4820
+ throw new UsageError(
4821
+ `${block}: id ${JSON.stringify(value)} is not a valid entry id (the schema's $defs/entryId: ^[a-z0-9][a-z0-9._:-]*$) \u2014 valid addresses:
4822
+ ${editAddressTable()}`
4823
+ );
4824
+ }
4825
+ return value;
4826
+ }
4430
4827
  function linkAddress(platform, label) {
4431
4828
  return label === void 0 ? `identity.links[platform=${platform}].url` : `identity.links[platform=${platform},label=${label}].url`;
4432
4829
  }
4830
+ function statementAddress(lang) {
4831
+ return `positioning.statement.${lang}`;
4832
+ }
4833
+ var EDIT_LIST_BLOCKS = [
4834
+ "positioning",
4835
+ "headlines",
4836
+ "bios",
4837
+ "identity.definition",
4838
+ "identity.links",
4839
+ "faq"
4840
+ ];
4841
+ function parseEditListBlock(value) {
4842
+ if (EDIT_LIST_BLOCKS.includes(value)) return value;
4843
+ throw new UsageError(
4844
+ `unknown block "${value}" \u2014 --list scopes to one of: ${EDIT_LIST_BLOCKS.join(" | ")}`
4845
+ );
4846
+ }
4433
4847
  function editAddressTable() {
4434
4848
  return [
4435
4849
  ` positioning.statement.<lang> lang: ${LANGS2.join("|")}`,
4436
4850
  ` headlines[platform=<p>,lang=<l>].text platform: <schema platform id, \xA76> \xB7 lang: ${LANGS2.join("|")}`,
4437
4851
  ` bios[platform=<p>,lang=<l>,length=<len>].text length: ${BIO_LENGTHS.join("|")}`,
4852
+ ` identity.definition.<lang> lang: ${LANGS2.join("|")} (the canonical per-language sentence)`,
4853
+ ` faq[id=<slug>,lang=<l>].q | .a lang: ${LANGS2.join("|")} (q/a are langMaps, so lang is required)`,
4854
+ ` faq[id=<slug>].source the optional http(s) pointer backing the answer`,
4438
4855
  ` identity.links[platform=<p>,label=<l>].url (read-only; label optional \u2014 disambiguates two links on one platform)`
4439
4856
  ].join("\n");
4440
4857
  }
@@ -4497,7 +4914,30 @@ ${editAddressTable()}`);
4497
4914
  const statement = /^positioning\.statement\.([A-Za-z0-9_-]+)$/.exec(text);
4498
4915
  if (statement !== null) {
4499
4916
  const lang = parseLang(statement[1]);
4500
- return { kind: "statement", lang, address: `positioning.statement.${lang}` };
4917
+ return { kind: "statement", lang, address: statementAddress(lang) };
4918
+ }
4919
+ const definition = /^identity\.definition\.([A-Za-z0-9_-]+)$/.exec(text);
4920
+ if (definition !== null) {
4921
+ const lang = parseLang(definition[1]);
4922
+ return { kind: "definition", lang, address: definitionAddress(lang) };
4923
+ }
4924
+ const faq = /^faq\[([^\]]*)\]\.([A-Za-z]+)$/.exec(text);
4925
+ if (faq !== null) {
4926
+ const field = faq[2];
4927
+ if (field !== "q" && field !== "a" && field !== "source") {
4928
+ throw new UsageError(
4929
+ `faq[..].${field} is not addressable \u2014 the fields are faq[id=<slug>,lang=<l>].q | .a and faq[id=<slug>].source`
4930
+ );
4931
+ }
4932
+ if (field === "source") {
4933
+ const s2 = parseSelectors("faq", faq[1], ["id"]);
4934
+ const id2 = parseEntryId("faq", s2.id);
4935
+ return { kind: "faq", id: id2, field, address: faqAddress(id2, "source") };
4936
+ }
4937
+ const s = parseSelectors("faq", faq[1], ["id", "lang"]);
4938
+ const id = parseEntryId("faq", s.id);
4939
+ const lang = parseLang(s.lang);
4940
+ return { kind: "faq", id, field, lang, address: faqAddress(id, field, lang) };
4501
4941
  }
4502
4942
  const addressed = /^(headlines|bios|identity\.links)\[([^\]]*)\]\.([A-Za-z]+)$/.exec(text);
4503
4943
  if (addressed !== null) {
@@ -4740,8 +5180,12 @@ function runBuild(args) {
4740
5180
  );
4741
5181
  for (const b of s.bios) line(` bio (${b.length}): ${b.text}`);
4742
5182
  for (const h of s.headlines) line(` headline: ${h.text}`);
4743
- if (s.bios.length === 0 && s.headlines.length === 0)
4744
- line(" (no texts for this platform+lang)");
5183
+ if (s.bios.length === 0 && s.headlines.length === 0) {
5184
+ const wroteArtifact = written.some((w) => w.platform === s.platform);
5185
+ line(
5186
+ wroteArtifact ? " (0 base texts \u2014 bios/headlines \u2014 for this platform+lang; the artifact is still written from the declared facts)" : " (0 base texts \u2014 bios/headlines \u2014 and no artifact for this platform+lang)"
5187
+ );
5188
+ }
4745
5189
  if (s.fallbacks.length > 0) line(` (${s.fallbacks.length} text(s) fell back to en)`);
4746
5190
  }
4747
5191
  if (written.length > 0) {
@@ -4812,7 +5256,7 @@ var HANDLE_CODES = /* @__PURE__ */ new Set([
4812
5256
  ]);
4813
5257
  function buildCheckReport(args) {
4814
5258
  const { workspace, profile, platform } = args;
4815
- const baseChecks = profile.checks.length > 0 ? profile.checks : DEFAULT_CHECKS;
5259
+ const baseChecks = capCatalog(profile, DEFAULT_CHECKS);
4816
5260
  const checks = baseChecks.filter(
4817
5261
  (c) => platform === void 0 || c.platform === "all" || c.platform === platform
4818
5262
  );
@@ -4905,7 +5349,7 @@ function runCheck(args) {
4905
5349
  });
4906
5350
  }
4907
5351
  section(
4908
- `evidence registry (D17/\xA718.8)${platform !== void 0 ? ` for ${platform}` : ""}: ${registry.length} finding(s)`
5352
+ `evidence registry${platform !== void 0 ? ` for ${platform}` : ""}: ${registry.length} finding(s)`
4909
5353
  );
4910
5354
  for (const finding3 of registry) {
4911
5355
  line(` ${finding3.severity}: [${finding3.code}] ${finding3.message} (${finding3.path})`);
@@ -4914,11 +5358,11 @@ function runCheck(args) {
4914
5358
  line(" credential-expiry / no-private-in-public-output: OK");
4915
5359
  } else if (registry.filter((f) => f.severity === "error").length > 0) {
4916
5360
  line(
4917
- ` ${registry.filter((f) => f.severity === "error").length} leak error(s) \u2014 FAIL (\xA718.5: public output renders only public entries)`
5361
+ ` ${registry.filter((f) => f.severity === "error").length} leak error(s) \u2014 FAIL (public output renders only public entries)`
4918
5362
  );
4919
5363
  }
4920
5364
  section(
4921
- `media pipeline (M8/\xA718.6)${platform !== void 0 ? ` for ${platform}` : ""}: ${media.length} finding(s)`
5365
+ `media transport${platform !== void 0 ? ` for ${platform}` : ""}: ${media.length} finding(s)`
4922
5366
  );
4923
5367
  for (const finding3 of media) {
4924
5368
  line(` ${finding3.severity}: [${finding3.code}] ${finding3.message} (${finding3.path})`);
@@ -4936,7 +5380,7 @@ function runCheck(args) {
4936
5380
  line(" text limits: OK (no findings)");
4937
5381
  }
4938
5382
  section(
4939
- `fact-present (surface facts vs the SSOT render, #283): ${facts.findings.length} finding(s)`
5383
+ `surface facts vs the rendered profile${platform !== void 0 ? ` for ${platform}` : ""}: ${facts.findings.length} finding(s)`
4940
5384
  );
4941
5385
  for (const finding3 of facts.findings) {
4942
5386
  line(` warn: [${finding3.code}] ${finding3.message} (${finding3.path})`);
@@ -4961,7 +5405,7 @@ function runCheck(args) {
4961
5405
  );
4962
5406
  }
4963
5407
  if (profile.flavor === "brand") {
4964
- section("handle matrix (brand, \xA717)");
5408
+ section("handle matrix (brand)");
4965
5409
  if (profile.socialHandles.length === 0) {
4966
5410
  line(" (no socialHandles rows)");
4967
5411
  } else {
@@ -4974,8 +5418,8 @@ function runCheck(args) {
4974
5418
  if (handleIssues.length === 0) {
4975
5419
  line(" handle-claimed / handle-consistent: OK (no offline violations)");
4976
5420
  } else {
4977
- for (const issue of handleIssues) {
4978
- line(` warn: [${issue.code}] ${issue.message} (${issue.path})`);
5421
+ for (const issue2 of handleIssues) {
5422
+ line(` warn: [${issue2.code}] ${issue2.message} (${issue2.path})`);
4979
5423
  }
4980
5424
  line(
4981
5425
  ` ${handleIssues.length} handle warning(s) \u2014 fix before deploy (live check: github-org skill url-live)`
@@ -4983,10 +5427,10 @@ function runCheck(args) {
4983
5427
  }
4984
5428
  }
4985
5429
  line(
4986
- "pasco check: rule engine for the remaining catalog ships M3/M4 (registry + handle rules already executed above)"
5430
+ "pasco check: the offline rules above ran \u2014 evidence registry, media transport, text limits, surface facts, and (for brand) the handle matrix."
4987
5431
  );
4988
5432
  section(
4989
- `user checks (content/checks.yaml, schema-validated catalog)${platform !== void 0 ? ` for ${platform}` : ""}: ${checks.length}`
5433
+ `${catalogLabel(catalogSource(profile))}${platform !== void 0 ? ` for ${platform}` : ""}: ${checks.length}`
4990
5434
  );
4991
5435
  for (const c of checks) {
4992
5436
  const note = c.note !== void 0 ? ` note="${c.note}"` : "";
@@ -4996,9 +5440,8 @@ function runCheck(args) {
4996
5440
  );
4997
5441
  }
4998
5442
  line(
4999
- " max-chars/min-chars executed above; the remaining rules ship with the skills checks.yaml union (M3) and pasco check (M4)"
5443
+ " checks whose rule is max-chars or min-chars ran above; the other rule names are listed for the record and do not run in this release."
5000
5444
  );
5001
- line(` owner: ${CHECK_OWNER}`);
5002
5445
  return report_.ok ? EXIT.OK : EXIT.FAIL;
5003
5446
  }
5004
5447
 
@@ -5159,11 +5602,11 @@ function runCiteCheck(args) {
5159
5602
  const parsed = parseAnswers(source.text);
5160
5603
  if (parsed.issue !== void 0) {
5161
5604
  const { line: badLine, field, message } = parsed.issue;
5162
- const at = field === void 0 ? `line ${badLine}` : `line ${badLine} (${field})`;
5605
+ const at2 = field === void 0 ? `line ${badLine}` : `line ${badLine} (${field})`;
5163
5606
  return report("cite-check", false, EXIT.USAGE, {
5164
5607
  error: {
5165
5608
  kind: "input",
5166
- message: `malformed NDJSON at ${at} \u2014 ${message}`,
5609
+ message: `malformed NDJSON at ${at2} \u2014 ${message}`,
5167
5610
  violations: [{ path: `line ${badLine}`, message }]
5168
5611
  }
5169
5612
  });
@@ -5355,6 +5798,18 @@ function asDate(value) {
5355
5798
  function slug(value) {
5356
5799
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
5357
5800
  }
5801
+ function digest(value) {
5802
+ let hash = 2166136261;
5803
+ for (const byte of Buffer.from(value, "utf8")) {
5804
+ hash = Math.imul(hash ^ byte, 16777619) >>> 0;
5805
+ }
5806
+ return hash.toString(36);
5807
+ }
5808
+ function experienceId(surface, org) {
5809
+ const raw = `${surface}-${org}`.normalize("NFC").toLowerCase().replace(/\s+/gu, " ");
5810
+ const base = slug(raw);
5811
+ return /^[\x20-\x7e]*$/u.test(raw) ? base : `${base}-${digest(raw)}`;
5812
+ }
5358
5813
  function httpUrl(value) {
5359
5814
  try {
5360
5815
  const url = new URL(value);
@@ -5417,7 +5872,7 @@ function provisionalProfile(observations) {
5417
5872
  if (role === void 0) continue;
5418
5873
  for (const row of worksForRows(node)) {
5419
5874
  if (row.start === void 0) continue;
5420
- const id = slug(`${observation.surface}-${row.org}`);
5875
+ const id = experienceId(observation.surface, row.org);
5421
5876
  if (id === "" || experience.some((item) => item.id === id)) continue;
5422
5877
  experience.push({
5423
5878
  id,
@@ -5431,7 +5886,7 @@ function provisionalProfile(observations) {
5431
5886
  }
5432
5887
  if (name === void 0) {
5433
5888
  throw new DiscoveryError(
5434
- "no surface declared an entity name (schema.org JSON-LD `Person`/`Organization`) \u2014 a provisional profile needs at least `identity.name`; nothing was invented"
5889
+ "no surface declared an entity name: discovery reads only schema.org `Person`/`Organization` JSON-LD, which a GitHub profile (and a product/page node such as `SoftwareApplication`) does not carry \u2014 point it at a personal site or page that declares a `Person` or `Organization` node; a provisional profile needs at least `identity.name`, and nothing was invented"
5435
5890
  );
5436
5891
  }
5437
5892
  if (nameSource !== void 0) record("identity.name", nameSource);
@@ -5451,6 +5906,117 @@ function provisionalProfile(observations) {
5451
5906
  absent: [...absent].sort()
5452
5907
  };
5453
5908
  }
5909
+ var NAVIGATION_SEGMENTS = /* @__PURE__ */ new Set([
5910
+ "about",
5911
+ "articles",
5912
+ "blog",
5913
+ "careers",
5914
+ "categories",
5915
+ "category",
5916
+ "contact",
5917
+ "cv",
5918
+ "docs",
5919
+ "download",
5920
+ "downloads",
5921
+ "events",
5922
+ "faq",
5923
+ "feed",
5924
+ "help",
5925
+ "home",
5926
+ "index",
5927
+ "jobs",
5928
+ "login",
5929
+ "me",
5930
+ "news",
5931
+ "page",
5932
+ "pages",
5933
+ "portfolio",
5934
+ "posts",
5935
+ "pricing",
5936
+ "privacy",
5937
+ "profile",
5938
+ "projects",
5939
+ "resume",
5940
+ "rss",
5941
+ "search",
5942
+ "series",
5943
+ "signup",
5944
+ "site",
5945
+ "sitemap",
5946
+ "subscribe",
5947
+ "support",
5948
+ "t",
5949
+ "tag",
5950
+ "tags",
5951
+ "team",
5952
+ "terms",
5953
+ "topic",
5954
+ "topics",
5955
+ "work",
5956
+ "writing"
5957
+ ]);
5958
+ var NAME_PARTICLES = /* @__PURE__ */ new Set([
5959
+ "bin",
5960
+ "das",
5961
+ "del",
5962
+ "den",
5963
+ "der",
5964
+ "des",
5965
+ "dos",
5966
+ "ibn",
5967
+ "las",
5968
+ "los",
5969
+ "ter",
5970
+ "van",
5971
+ "von"
5972
+ ]);
5973
+ function subjectTokens(provisional, observations) {
5974
+ const name = provisional.profile.identity.name.trim();
5975
+ const tokens = /* @__PURE__ */ new Set([name]);
5976
+ for (const word of name.split(/\s+/).filter((item) => item !== "")) {
5977
+ if (word === word.toLowerCase() && (word.length < 3 || NAME_PARTICLES.has(word))) {
5978
+ continue;
5979
+ }
5980
+ tokens.add(word);
5981
+ }
5982
+ for (const url of observations.map((item) => item.url)) {
5983
+ let segment;
5984
+ try {
5985
+ segment = new URL(url).pathname.split("/").filter((item) => item !== "").pop();
5986
+ } catch {
5987
+ continue;
5988
+ }
5989
+ if (segment === void 0 || /^\d+$/u.test(segment)) continue;
5990
+ let decoded;
5991
+ try {
5992
+ decoded = decodeURIComponent(segment);
5993
+ } catch {
5994
+ continue;
5995
+ }
5996
+ if (NAVIGATION_SEGMENTS.has(decoded.toLowerCase())) continue;
5997
+ tokens.add(decoded);
5998
+ if (decoded === decoded.toLowerCase() && decoded.length > 1) {
5999
+ tokens.add(decoded.charAt(0).toUpperCase() + decoded.slice(1));
6000
+ }
6001
+ }
6002
+ return [...tokens];
6003
+ }
6004
+ var TOKEN_START = "(?<![\\p{L}\\p{N}_])";
6005
+ var TOKEN_END = "(?![\\p{L}\\p{N}_])";
6006
+ var FIRST_PERSON_PRONOUN = new RegExp(`${TOKEN_START}I${TOKEN_END}`, "u");
6007
+ var FIRST_PERSON_CONTRACTION = new RegExp(`${TOKEN_START}[Ii]['\u2019](?:m|ve|d|ll)${TOKEN_END}`, "u");
6008
+ var FIRST_PERSON_POSSESSIVE = new RegExp(`${TOKEN_START}(?:my|me|mine|myself)${TOKEN_END}`, "iu");
6009
+ function isFirstPerson(text) {
6010
+ return FIRST_PERSON_PRONOUN.test(text) || FIRST_PERSON_CONTRACTION.test(text) || FIRST_PERSON_POSSESSIVE.test(text);
6011
+ }
6012
+ function isAboutEntity(text, tokens) {
6013
+ if (isFirstPerson(text)) return true;
6014
+ return tokens.some((token) => {
6015
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6016
+ const flags = token.length <= 4 ? "u" : "iu";
6017
+ return new RegExp(`${TOKEN_START}${escaped}${TOKEN_END}`, flags).test(text);
6018
+ });
6019
+ }
5454
6020
  function experienceKey(row) {
5455
6021
  const role = typeof row.role === "string" ? row.role : row.role.en;
5456
6022
  return `${role}/${row.org}`.toLowerCase();
@@ -5464,6 +6030,10 @@ function conflictRows(profile, path) {
5464
6030
  const key = experienceKey(row);
5465
6031
  return profile.experience.filter((item) => experienceKey(item) === key);
5466
6032
  }
6033
+ function declaredName(observation) {
6034
+ const node = flattenJsonLd(observation.jsonLd)[0];
6035
+ return node === void 0 ? void 0 : asString(node.name);
6036
+ }
5467
6037
  function discoveryFindings(provisional, observations, unresolved = []) {
5468
6038
  const { profile, provenance } = provisional;
5469
6039
  const findings = [];
@@ -5471,13 +6041,30 @@ function discoveryFindings(provisional, observations, unresolved = []) {
5471
6041
  const record = provenance[ref];
5472
6042
  return record === void 0 ? ref : `${record.surface} (${record.url})`;
5473
6043
  };
5474
- for (const issue of driftCheck(profile)) {
5475
- const mapped = Object.hasOwn(DRIFT_CLASS, issue.code) ? DRIFT_CLASS[issue.code] : "contradiction";
6044
+ for (const issue2 of driftCheck(profile)) {
6045
+ const mapped = Object.hasOwn(DRIFT_CLASS, issue2.code) ? DRIFT_CLASS[issue2.code] : "contradiction";
5476
6046
  if (mapped === null || mapped === void 0) continue;
5477
- const ref = factRef(profile, issue.path);
5478
- const cited = issue.code === "drift/duplicate-experience" ? conflictRows(profile, issue.path).map((row) => provenanceOf(`experience[${row.id}]`)).join(" \xB7 ") : provenanceOf(ref);
6047
+ const ref = factRef(profile, issue2.path);
6048
+ const cited = issue2.code === "drift/duplicate-experience" ? conflictRows(profile, issue2.path).map((row) => provenanceOf(`experience[${row.id}]`)).join(" \xB7 ") : provenanceOf(ref);
5479
6049
  const suffix = cited === ref || cited === "" ? "" : ` \u2014 asserted by ${cited}`;
5480
- findings.push(finding2(mapped, ref, `${issue.code} \u2014 ${issue.message}${suffix}`));
6050
+ findings.push(finding2(mapped, ref, `${issue2.code} \u2014 ${issue2.message}${suffix}`));
6051
+ }
6052
+ const declared = observations.flatMap((observation) => {
6053
+ const name = declaredName(observation);
6054
+ return name === void 0 ? [] : [{ name, observation }];
6055
+ });
6056
+ const normalizedNames = new Set(
6057
+ declared.map((item) => item.name.normalize("NFC").toLowerCase().replace(/\s+/gu, " "))
6058
+ );
6059
+ if (normalizedNames.size > 1) {
6060
+ const where = new Set(declared.map((item) => item.observation.surface)).size > 1 ? "across surfaces" : "on one surface";
6061
+ findings.push(
6062
+ finding2(
6063
+ "contradiction",
6064
+ "identity.name",
6065
+ `identity.name is declared differently ${where}: ${declared.map(({ name, observation }) => `"${name}" \u2014 ${observation.surface} (${observation.url})`).join(" \xB7 ")}`
6066
+ )
6067
+ );
5481
6068
  }
5482
6069
  for (const link of unresolved) {
5483
6070
  findings.push(
@@ -5488,11 +6075,17 @@ function discoveryFindings(provisional, observations, unresolved = []) {
5488
6075
  )
5489
6076
  );
5490
6077
  }
6078
+ const subjects = subjectTokens(provisional, observations);
6079
+ const seenSentences = /* @__PURE__ */ new Set();
5491
6080
  for (const observation of observations) {
5492
6081
  for (const [index, sentence] of observation.text.split(/(?<=[.!?])\s+|\n/).entries()) {
5493
6082
  const text = sentence.trim();
5494
6083
  if (!isQuantitativeClaim(text)) continue;
5495
6084
  if (/source:|evidence:|https?:\/\//i.test(text)) continue;
6085
+ if (!isAboutEntity(text, subjects)) continue;
6086
+ const key = text.toLowerCase();
6087
+ if (seenSentences.has(key)) continue;
6088
+ seenSentences.add(key);
5496
6089
  findings.push(
5497
6090
  finding2(
5498
6091
  "unsourced-claim",
@@ -5524,8 +6117,195 @@ var httpFetchText = async (url) => {
5524
6117
  }
5525
6118
  };
5526
6119
 
6120
+ // src/observations.ts
6121
+ import { readFileSync as readFileSync9 } from "fs";
6122
+ import { parse as parse3 } from "yaml";
6123
+ var OBSERVATION_FIELDS = ["name", "link", "experience"];
6124
+ var ROW_KEYS = ["surface", "field", "value", "url", "retrieved_at"];
6125
+ var EXPERIENCE_KEYS = ["role", "org", "start", "end"];
6126
+ var HTTP_URL = /^https?:\/\//u;
6127
+ var DATE = /^\d{4}(?:-\d{2}(?:-\d{2})?)?$/u;
6128
+ var RETRIEVED_AT = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2}))?$/u;
6129
+ function at(index, reason) {
6130
+ return new DiscoveryError(`observations: row ${index + 1}: ${reason}`);
6131
+ }
6132
+ function isRecord2(value) {
6133
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6134
+ }
6135
+ function nonEmptyString(value, index, key) {
6136
+ if (typeof value !== "string" || value.trim() === "") {
6137
+ throw at(index, `\`${key}\` must be a non-empty string`);
6138
+ }
6139
+ return value.trim();
6140
+ }
6141
+ function daysInMonth(year, month) {
6142
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
6143
+ }
6144
+ function calendarDate(text) {
6145
+ const match = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/u.exec(text);
6146
+ if (match === null) return false;
6147
+ const year = Number(match[1]);
6148
+ const month = match[2] === void 0 ? void 0 : Number(match[2]);
6149
+ const day = match[3] === void 0 ? void 0 : Number(match[3]);
6150
+ if (month !== void 0 && (month < 1 || month > 12)) return false;
6151
+ if (day !== void 0 && (day < 1 || day > daysInMonth(year, month))) return false;
6152
+ return true;
6153
+ }
6154
+ function dateValue(value, index, key) {
6155
+ const text = typeof value === "number" && Number.isInteger(value) ? String(value) : value;
6156
+ if (typeof text !== "string" || !DATE.test(text.trim()) || !calendarDate(text.trim())) {
6157
+ throw at(index, `\`${key}\` must be a real date: YYYY, YYYY-MM or YYYY-MM-DD`);
6158
+ }
6159
+ return text.trim();
6160
+ }
6161
+ function endValue(value, index, key) {
6162
+ if (typeof value === "string" && value.trim().toLowerCase() === "present") return "present";
6163
+ return dateValue(value, index, key);
6164
+ }
6165
+ function httpUrl2(value, index, key) {
6166
+ const text = nonEmptyString(value, index, key);
6167
+ if (!HTTP_URL.test(text)) throw at(index, `\`${key}\` must be an absolute http(s) URL`);
6168
+ try {
6169
+ new URL(text);
6170
+ } catch {
6171
+ throw at(index, `\`${key}\` is not a parseable URL: ${text}`);
6172
+ }
6173
+ return text;
6174
+ }
6175
+ function retrievedAt(value, index) {
6176
+ if (typeof value !== "string" || !RETRIEVED_AT.test(value.trim())) {
6177
+ throw at(
6178
+ index,
6179
+ "`retrieved_at` must be YYYY-MM-DD or an ISO 8601 date-time with an explicit zone (quote it in YAML)"
6180
+ );
6181
+ }
6182
+ const text = value.trim();
6183
+ if (!calendarDate(text.slice(0, 10))) {
6184
+ throw at(index, `\`retrieved_at\` is not a real calendar date: ${text}`);
6185
+ }
6186
+ if (text.length === 10) return `${text}T00:00:00.000Z`;
6187
+ if (Number.isNaN(Date.parse(text)))
6188
+ throw at(index, `\`retrieved_at\` is not a valid date-time: ${text}`);
6189
+ return text;
6190
+ }
6191
+ var keysOf = (record) => Object.keys(record).sort();
6192
+ function sameKeys(record, allowed) {
6193
+ return keysOf(record).join(",") === [...allowed].sort().join(",");
6194
+ }
6195
+ function nodeFor(field, value, index) {
6196
+ if (field === "name") return { "@type": "Person", name: nonEmptyString(value, index, "value") };
6197
+ if (field === "link") return { "@type": "Person", sameAs: [httpUrl2(value, index, "value")] };
6198
+ if (!isRecord2(value)) {
6199
+ throw at(index, "`value` for field `experience` must be a map with role, org and start");
6200
+ }
6201
+ const unknown = keysOf(value).filter(
6202
+ (key) => !EXPERIENCE_KEYS.includes(key)
6203
+ );
6204
+ if (unknown.length > 0) {
6205
+ throw at(
6206
+ index,
6207
+ `unknown experience key(s): ${unknown.join(", ")} (allowed: ${EXPERIENCE_KEYS.join(", ")})`
6208
+ );
6209
+ }
6210
+ const missing = EXPERIENCE_KEYS.filter((key) => key !== "end" && !Object.hasOwn(value, key));
6211
+ if (missing.length > 0) {
6212
+ throw at(
6213
+ index,
6214
+ `\`value\` for field \`experience\` requires role, org and start (missing: ${missing.join(", ")})`
6215
+ );
6216
+ }
6217
+ const end = value.end === void 0 ? void 0 : endValue(value.end, index, "value.end");
6218
+ return {
6219
+ "@type": "Person",
6220
+ jobTitle: nonEmptyString(value.role, index, "value.role"),
6221
+ worksFor: {
6222
+ "@type": "Organization",
6223
+ name: nonEmptyString(value.org, index, "value.org"),
6224
+ startDate: dateValue(value.start, index, "value.start"),
6225
+ ...end !== void 0 && end !== "present" ? { endDate: end } : {}
6226
+ }
6227
+ };
6228
+ }
6229
+ function rowFrom(raw, index) {
6230
+ if (!isRecord2(raw)) throw at(index, "each row must be a map (- surface: \u2026, field: \u2026, value: \u2026)");
6231
+ if (!sameKeys(raw, ROW_KEYS)) {
6232
+ const unknown = keysOf(raw).filter((key) => !ROW_KEYS.includes(key));
6233
+ const missing = ROW_KEYS.filter((key) => !Object.hasOwn(raw, key));
6234
+ throw at(
6235
+ index,
6236
+ unknown.length > 0 ? `unknown key(s): ${unknown.join(", ")} (allowed: ${ROW_KEYS.join(", ")})` : `missing key(s): ${missing.join(", ")} (required: ${ROW_KEYS.join(", ")})`
6237
+ );
6238
+ }
6239
+ const surface = nonEmptyString(raw.surface, index, "surface");
6240
+ const field = raw.field;
6241
+ if (typeof field !== "string" || !OBSERVATION_FIELDS.includes(field)) {
6242
+ throw at(
6243
+ index,
6244
+ `\`field\` must be one of ${OBSERVATION_FIELDS.join(", ")} (got ${JSON.stringify(raw.field)})`
6245
+ );
6246
+ }
6247
+ const url = httpUrl2(raw.url, index, "url");
6248
+ const retrieved = retrievedAt(raw.retrieved_at, index);
6249
+ return {
6250
+ surface,
6251
+ url,
6252
+ retrieved,
6253
+ jsonLd: [nodeFor(field, raw.value, index)],
6254
+ text: "",
6255
+ links: []
6256
+ };
6257
+ }
6258
+ function parseObservations(text) {
6259
+ let document;
6260
+ try {
6261
+ document = parse3(text);
6262
+ } catch (err) {
6263
+ const message = err instanceof Error ? err.message : String(err);
6264
+ throw new DiscoveryError(`observations: not valid YAML \u2014 ${message}`);
6265
+ }
6266
+ if (document === null || document === void 0) {
6267
+ throw new DiscoveryError("observations: the file is empty \u2014 expected a YAML sequence of rows");
6268
+ }
6269
+ if (!Array.isArray(document)) {
6270
+ throw new DiscoveryError(
6271
+ "observations: the file must be a YAML sequence of rows (- surface: \u2026, field: \u2026, value: \u2026)"
6272
+ );
6273
+ }
6274
+ if (document.length === 0) {
6275
+ throw new DiscoveryError("observations: the sequence declares no rows");
6276
+ }
6277
+ const observations = document.map((row, index) => rowFrom(row, index));
6278
+ const employment = /* @__PURE__ */ new Map();
6279
+ for (const [index, observation] of observations.entries()) {
6280
+ const node = observation.jsonLd[0];
6281
+ const worksFor = node?.worksFor;
6282
+ if (worksFor === void 0) continue;
6283
+ const org = String(worksFor.name);
6284
+ const key = experienceId(observation.surface, org);
6285
+ const first = employment.get(key);
6286
+ if (first !== void 0) {
6287
+ throw at(
6288
+ index,
6289
+ `experience for ${observation.surface} / ${org} is already declared by row ${first.index + 1} (${first.org}) \u2014 the provisional profile holds ONE employment row per surface and org, so a second role at the same org cannot be represented; keep one row per (surface, org)`
6290
+ );
6291
+ }
6292
+ employment.set(key, { index, org });
6293
+ }
6294
+ return observations;
6295
+ }
6296
+ function loadObservations(path) {
6297
+ let text;
6298
+ try {
6299
+ text = readFileSync9(path, "utf8");
6300
+ } catch (err) {
6301
+ const message = err instanceof Error ? err.message : String(err);
6302
+ throw new DiscoveryError(`observations: cannot read ${path} \u2014 ${message}`);
6303
+ }
6304
+ return parseObservations(text);
6305
+ }
6306
+
5527
6307
  // src/commands/discover.ts
5528
- var DISCOVER_SCHEMA_VERSION = "1";
6308
+ var DISCOVER_SCHEMA_VERSION = "2";
5529
6309
  function hostOf3(url) {
5530
6310
  try {
5531
6311
  return new URL(url).hostname.toLowerCase().replace(/^www\./, "");
@@ -5533,7 +6313,7 @@ function hostOf3(url) {
5533
6313
  return void 0;
5534
6314
  }
5535
6315
  }
5536
- function httpUrl2(raw, base) {
6316
+ function httpUrl3(raw, base) {
5537
6317
  try {
5538
6318
  const url = new URL(raw, base);
5539
6319
  return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : void 0;
@@ -5558,9 +6338,13 @@ function jsonLdOf(html) {
5558
6338
  return nodes;
5559
6339
  }
5560
6340
  function textOf2(html) {
5561
- return decodeEntities(
5562
- html.replace(/<script\b[\s\S]*?<\/script>/gi, " ").replace(/<style\b[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ")
5563
- ).replace(/\s+/g, " ").trim();
6341
+ const collapsed = html.replace(/<script\b[\s\S]*?<\/script>/gi, " ").replace(/<style\b[\s\S]*?<\/style>/gi, " ").replace(/\s+/g, " ");
6342
+ const withBreaks = collapsed.replace(
6343
+ /<\/(?:p|div|li|h[1-6]|title|header|footer|section|article|tr|td|th|main|nav)\s*>/gi,
6344
+ "\n"
6345
+ ).replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, " ");
6346
+ const lines2 = decodeEntities(withBreaks).split("\n").map((line2) => line2.replace(/\s+/g, " ").trim()).filter((line2) => line2 !== "");
6347
+ return lines2.join("\n");
5564
6348
  }
5565
6349
  function linksOf(html, base) {
5566
6350
  const links = [];
@@ -5569,7 +6353,7 @@ function linksOf(html, base) {
5569
6353
  for (const match of html.matchAll(pattern)) {
5570
6354
  const raw = match[1];
5571
6355
  if (raw === void 0) continue;
5572
- const resolved = httpUrl2(decodeEntities(raw), base);
6356
+ const resolved = httpUrl3(decodeEntities(raw), base);
5573
6357
  if (resolved === void 0 || seen.has(resolved)) continue;
5574
6358
  seen.add(resolved);
5575
6359
  links.push(resolved);
@@ -5594,10 +6378,23 @@ async function discover(options) {
5594
6378
  )
5595
6379
  );
5596
6380
  const surfaces = [];
5597
- const observations = [];
6381
+ const manual = options.manualObservations ?? [];
6382
+ const observations = [...manual];
6383
+ const seenManual = /* @__PURE__ */ new Set();
6384
+ for (const observation of manual) {
6385
+ const key = `${observation.surface}\0${observation.url}`;
6386
+ if (seenManual.has(key)) continue;
6387
+ seenManual.add(key);
6388
+ surfaces.push({
6389
+ url: observation.url,
6390
+ surface: observation.surface,
6391
+ status: null,
6392
+ via: "manual"
6393
+ });
6394
+ }
5598
6395
  const unchecked = [];
5599
6396
  for (const raw of options.urls) {
5600
- const url = httpUrl2(raw);
6397
+ const url = httpUrl3(raw);
5601
6398
  const host = url === void 0 ? void 0 : hostOf3(url);
5602
6399
  const surface = host ?? raw;
5603
6400
  if (url === void 0 || host === void 0) {
@@ -5618,7 +6415,7 @@ async function discover(options) {
5618
6415
  continue;
5619
6416
  }
5620
6417
  const html = response.text ?? "";
5621
- surfaces.push({ url, surface, status: response.status });
6418
+ surfaces.push({ url, surface, status: response.status, via: "fetch" });
5622
6419
  observations.push({
5623
6420
  surface,
5624
6421
  url,
@@ -5650,6 +6447,11 @@ async function discover(options) {
5650
6447
  });
5651
6448
  }
5652
6449
  }
6450
+ if (manual.length > 0 && !observations.some((item) => declaredName(item) !== void 0)) {
6451
+ throw new DiscoveryError(
6452
+ "observations: no row declares `name` (field: name) and no machine-read surface declared one either \u2014 a provisional profile needs identity.name, and nothing is invented; add a row with field: name"
6453
+ );
6454
+ }
5653
6455
  const provisional = provisionalProfile(observations);
5654
6456
  return {
5655
6457
  schemaVersion: DISCOVER_SCHEMA_VERSION,
@@ -5663,7 +6465,14 @@ async function discover(options) {
5663
6465
  async function runDiscover(args) {
5664
6466
  let result;
5665
6467
  try {
5666
- result = await discover(args);
6468
+ const manualObservations = args.observationsPath === void 0 ? void 0 : loadObservations(args.observationsPath);
6469
+ result = await discover({
6470
+ urls: args.urls,
6471
+ ...args.allowHosts !== void 0 ? { allowHosts: args.allowHosts } : {},
6472
+ ...args.retrieved !== void 0 ? { retrieved: args.retrieved } : {},
6473
+ ...args.fetchText !== void 0 ? { fetchText: args.fetchText } : {},
6474
+ ...manualObservations !== void 0 ? { manualObservations } : {}
6475
+ });
5667
6476
  } catch (err) {
5668
6477
  if (err instanceof DiscoveryError) {
5669
6478
  return report("discover", false, EXIT.FAIL, {
@@ -5678,7 +6487,11 @@ async function runDiscover(args) {
5678
6487
  return report("discover", failures === 0, exitCode2, { data: result });
5679
6488
  }
5680
6489
  section("discover \u2014 a provisional profile (nothing written, every fact unattested)");
5681
- for (const surface of result.surfaces) line(` read ${surface.surface} \u2014 ${surface.url}`);
6490
+ for (const surface of result.surfaces) {
6491
+ line(
6492
+ surface.via === "manual" ? ` observed ${surface.surface} \u2014 ${surface.url} (manual, no request)` : ` read ${surface.surface} \u2014 ${surface.url}`
6493
+ );
6494
+ }
5682
6495
  for (const item of result.unchecked) line(` NOT read ${item.surface} \u2014 ${item.reason}`);
5683
6496
  line(` facts: ${Object.keys(result.provisional.provenance).length} with provenance`);
5684
6497
  line(` absent (not discovered, never defaulted): ${result.provisional.absent.join(", ")}`);
@@ -5717,6 +6530,12 @@ function compareEngineVersions(a, b) {
5717
6530
  if (left === null || right === null) return null;
5718
6531
  return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
5719
6532
  }
6533
+ function engineFloorFor(installed) {
6534
+ const trimmed = installed.trim();
6535
+ const version = parseInstalledVersion(trimmed);
6536
+ if (version === null || /-/.test(trimmed)) return null;
6537
+ return `${version.major}.${version.minor}.${version.patch}`;
6538
+ }
5720
6539
  var ENGINE_SYNTAX = 'MAJOR.MINOR.PATCH (e.g. "0.4.0")';
5721
6540
  function evaluateEngineRequirement(pascorc, installed) {
5722
6541
  const declared = pascorc.engine;
@@ -5801,9 +6620,11 @@ function loadWorkspace(workspace) {
5801
6620
  }
5802
6621
  function requirementOf(sources, engineVersion) {
5803
6622
  const requirement = evaluateEngineRequirements(sources, engineVersion);
6623
+ const state = requirement.problem !== void 0 ? "INVALID" : requirement.declared === null ? "absent" : requirement.met ? "met" : "NOT MET";
5804
6624
  return {
5805
6625
  declared: requirement.declared,
5806
6626
  met: requirement.met,
6627
+ state,
5807
6628
  ...requirement.problem === void 0 ? {} : { problem: requirement.problem },
5808
6629
  ...requirement.source === void 0 ? {} : { source: requirement.source }
5809
6630
  };
@@ -5813,14 +6634,19 @@ function describe(reportData) {
5813
6634
  const requirement = reportData.requirement;
5814
6635
  const from = requirement.source === void 0 ? "" : `, declared in ${requirement.source}`;
5815
6636
  let requirementText;
5816
- if (requirement.problem !== void 0) {
5817
- requirementText = `engine requirement INVALID: ${requirement.problem}${from}`;
5818
- } else if (requirement.declared === null) {
5819
- requirementText = "no engine requirement";
5820
- } else if (requirement.met) {
5821
- requirementText = `requires engine >= ${requirement.declared} (met${from})`;
5822
- } else {
5823
- requirementText = `requires engine >= ${requirement.declared} (NOT MET \u2014 upgrade: ${ENGINE_UPGRADE_COMMAND}${from})`;
6637
+ switch (requirement.state) {
6638
+ case "INVALID":
6639
+ requirementText = `engine requirement INVALID: ${requirement.problem ?? "malformed declaration"}${from}`;
6640
+ break;
6641
+ case "absent":
6642
+ requirementText = "engine requirement absent \u2014 no .pascorc declares one";
6643
+ break;
6644
+ case "met":
6645
+ requirementText = `requires engine >= ${requirement.declared} (met${from})`;
6646
+ break;
6647
+ case "NOT MET":
6648
+ requirementText = `requires engine >= ${requirement.declared} (NOT MET \u2014 upgrade: ${ENGINE_UPGRADE_COMMAND}${from})`;
6649
+ break;
5824
6650
  }
5825
6651
  return `engine ${reportData.engine.version} \xB7 schema v${reportData.schema.version} \xB7 workspace ${reportData.workspace.path} (${workspaceState}) \xB7 ${requirementText}`;
5826
6652
  }
@@ -5845,9 +6671,60 @@ function runDoctor(args) {
5845
6671
  return healthy ? EXIT.OK : EXIT.FAIL;
5846
6672
  }
5847
6673
 
5848
- // src/commands/edit.ts
5849
- import { readdirSync as readdirSync4 } from "fs";
5850
- import { join as join10 } from "path";
6674
+ // src/edit-seed.ts
6675
+ var SEEDABLE_ROOTS = ["faq"];
6676
+ var SEED_FAQ_ENTRIES = [
6677
+ {
6678
+ id: "question-1",
6679
+ q: "<the first question this entity answers>",
6680
+ a: "<the answer \u2014 a fact the owner can defend, not marketing copy>"
6681
+ },
6682
+ {
6683
+ id: "question-2",
6684
+ q: "<the second question this entity answers>",
6685
+ a: "<the answer \u2014 a fact the owner can defend, not marketing copy>"
6686
+ },
6687
+ {
6688
+ id: "question-3",
6689
+ q: "<the third question this entity answers>",
6690
+ a: "<the answer \u2014 a fact the owner can defend, not marketing copy>"
6691
+ }
6692
+ ];
6693
+ var FAQ_SEED_HEADER = `# faq[] \u2014 the answer-engine surface (contract v1.10, #288): 3\u201320 citable
6694
+ # Q&A pairs. Replace every <placeholder>: the answer is the owner's own
6695
+ # assertion, never invented copy; \`en\` is required on q and a; a quantitative
6696
+ # answer should carry a \`source\`.`;
6697
+ function seedTemplate(root) {
6698
+ if (root === "faq") {
6699
+ const rows = SEED_FAQ_ENTRIES.map(
6700
+ (entry) => `- id: ${entry.id}
6701
+ q:
6702
+ en: ${entry.q}
6703
+ a:
6704
+ en: ${entry.a}`
6705
+ );
6706
+ return `${FAQ_SEED_HEADER}
6707
+ ${rows.join("\n")}
6708
+ `;
6709
+ }
6710
+ throw new UsageError(`no seed template for "${root}"`);
6711
+ }
6712
+ function parseCreateRoot(raw) {
6713
+ const text = raw.trim();
6714
+ const match = /^(?:content\/)?([A-Za-z][A-Za-z0-9]*)(?:\.ya?ml)?$/i.exec(text);
6715
+ const root = match?.[1];
6716
+ if (root === void 0) {
6717
+ throw new UsageError(
6718
+ `--create takes a root name or its content file (faq \xB7 content/faq.yaml) \u2014 got ${JSON.stringify(raw)}`
6719
+ );
6720
+ }
6721
+ if (!SEEDABLE_ROOTS.includes(root)) {
6722
+ throw new UsageError(
6723
+ `--create has no seed template for "${root}" \u2014 seedable roots: ${SEEDABLE_ROOTS.join(", ")}`
6724
+ );
6725
+ }
6726
+ return root;
6727
+ }
5851
6728
 
5852
6729
  // src/edit-write.ts
5853
6730
  import { randomUUID } from "crypto";
@@ -5859,7 +6736,8 @@ import {
5859
6736
  cpSync,
5860
6737
  mkdtempSync,
5861
6738
  openSync,
5862
- readFileSync as readFileSync9,
6739
+ readdirSync as readdirSync4,
6740
+ readFileSync as readFileSync10,
5863
6741
  realpathSync as realpathSync3,
5864
6742
  renameSync,
5865
6743
  rmSync as rmSync2,
@@ -5869,6 +6747,16 @@ import {
5869
6747
  import { tmpdir } from "os";
5870
6748
  import { dirname as dirname4, join as join9 } from "path";
5871
6749
  import { isAlias, isMap, isScalar, isSeq, parseDocument as parseDocument2, stringify } from "yaml";
6750
+ function owningFile(workspace, root) {
6751
+ let entries;
6752
+ try {
6753
+ entries = readdirSync4(join9(workspace, "content"));
6754
+ } catch {
6755
+ return void 0;
6756
+ }
6757
+ const match = entries.filter((name) => new RegExp(`^${root}\\.ya?ml$`, "i").test(name)).sort()[0];
6758
+ return match === void 0 ? void 0 : `content/${match}`;
6759
+ }
5872
6760
  var LOCK_NAME = ".pasco-edit.lock";
5873
6761
  var LOCK_STALE_MS = 3e4;
5874
6762
  function sleep(ms) {
@@ -5968,7 +6856,7 @@ function assertSpliceable(node, original, file, path) {
5968
6856
  }
5969
6857
  }
5970
6858
  function inspect(args, filePath) {
5971
- const original = readFileSync9(filePath, "utf8");
6859
+ const original = readFileSync10(filePath, "utf8");
5972
6860
  const document = parseDocument2(original);
5973
6861
  if (document.errors.length > 0) {
5974
6862
  throw new LoadError(
@@ -6054,7 +6942,7 @@ function writeEditLocked(args, deps) {
6054
6942
  }
6055
6943
  let current;
6056
6944
  try {
6057
- current = readFileSync9(filePath, "utf8");
6945
+ current = readFileSync10(filePath, "utf8");
6058
6946
  } catch (err) {
6059
6947
  throw new StaleEditError(
6060
6948
  `${args.target.file}: the file disappeared while the edit was being validated \u2014 nothing was written; re-run (${err.code ?? "error"})`
@@ -6094,38 +6982,102 @@ function writeEditLocked(args, deps) {
6094
6982
  }
6095
6983
  return { file: args.target.file, before, noop: false };
6096
6984
  }
6985
+ function previewCreate(args, deps = {}) {
6986
+ const existing = owningFile(args.workspace, args.root);
6987
+ if (existing !== void 0) {
6988
+ throw new WriteRefusedError(
6989
+ `${existing} already exists \u2014 --create seeds a root the workspace does not declare yet and never overwrites one (nothing was written); edit it with \`pasco edit\` or an editor`
6990
+ );
6991
+ }
6992
+ validateSeed(args, deps);
6993
+ return { file: `content/${args.root}.yaml` };
6994
+ }
6995
+ function createRoot(args, deps = {}) {
6996
+ return withWriteLock(args.workspace, deps.lockWaitMs ?? 2e3, () => {
6997
+ const { file } = previewCreate(args, deps);
6998
+ const filePath = join9(args.workspace, file);
6999
+ if (owningFile(args.workspace, args.root) !== void 0) {
7000
+ throw new WriteRefusedError(
7001
+ `${file} appeared while the seed was being validated \u2014 nothing was written; re-run`
7002
+ );
7003
+ }
7004
+ try {
7005
+ writeFileSync2(filePath, args.text, { mode: 420, flag: "wx" });
7006
+ } catch (err) {
7007
+ if (err instanceof WriteRefusedError) throw err;
7008
+ const code = err.code ?? "error";
7009
+ if (code === "EEXIST") {
7010
+ throw new WriteRefusedError(
7011
+ `${file} appeared before the seed could be written \u2014 nothing was written; re-run`
7012
+ );
7013
+ }
7014
+ throw new LoadError(`${file}: could not write the seed (${code}) \u2014 nothing was written`);
7015
+ }
7016
+ return { file, created: true };
7017
+ });
7018
+ }
7019
+ function validateSeed(args, deps) {
7020
+ const validate = deps.validate ?? ((workspace) => void load(workspace));
7021
+ const tempWorkspace = mkdtempSync(join9(tmpdir(), "pasco-edit-create-"));
7022
+ try {
7023
+ cpSync(join9(args.workspace, "content"), join9(tempWorkspace, "content"), { recursive: true });
7024
+ writeFileSync2(join9(tempWorkspace, "content", `${args.root}.yaml`), args.text, { mode: 384 });
7025
+ validate(tempWorkspace);
7026
+ } catch (err) {
7027
+ if (err instanceof LoadError) throw err;
7028
+ const code = err.code ?? "error";
7029
+ throw new LoadError(
7030
+ `content/${args.root}.yaml: the seed could not be validated (${code}) \u2014 nothing was written`
7031
+ );
7032
+ } finally {
7033
+ rmSync2(tempWorkspace, { recursive: true, force: true });
7034
+ }
7035
+ }
6097
7036
 
6098
7037
  // src/commands/edit.ts
6099
7038
  var SELECTOR_UNSAFE = /[,\]=]|"/;
6100
7039
  function rowLinkAddress(row) {
6101
- const platform = row.platform ?? "<no platform>";
6102
- const addressable = row.label !== void 0 && !SELECTOR_UNSAFE.test(row.label);
6103
- return linkAddress(platform, addressable ? row.label : void 0);
7040
+ if (row.platform === void 0) return "<no platform \u2014 not addressable>";
7041
+ if (!selectorExpressible(row.platform)) {
7042
+ return `<not addressable: platform ${JSON.stringify(row.platform)} cannot be expressed>`;
7043
+ }
7044
+ const label = row.label !== void 0 && selectorExpressible(row.label) ? row.label : void 0;
7045
+ return linkAddress(row.platform, label);
6104
7046
  }
6105
7047
  function linkNote(row) {
6106
7048
  const parts = [];
6107
7049
  if (row.platform === void 0) parts.push("no platform \u2014 not addressable");
6108
- else if (SELECTOR_UNSAFE.test(row.platform)) {
7050
+ else if (!selectorExpressible(row.platform)) {
6109
7051
  parts.push(
6110
- `platform ${JSON.stringify(row.platform)} contains a character the address grammar cannot express`
7052
+ `platform ${JSON.stringify(row.platform)} cannot be expressed as a selector (empty, padded, control character or \`, ] = "\`)`
6111
7053
  );
6112
7054
  }
6113
- if (row.label !== void 0 && SELECTOR_UNSAFE.test(row.label)) {
7055
+ if (row.label !== void 0 && !selectorExpressible(row.label)) {
6114
7056
  parts.push(
6115
- `label ${JSON.stringify(row.label)} contains a character the address grammar cannot express`
7057
+ `label ${JSON.stringify(row.label)} cannot be expressed as a selector (empty, padded, control character or \`, ] = "\`)`
6116
7058
  );
6117
7059
  }
6118
7060
  return parts.length === 0 ? void 0 : parts.join(" \xB7 ");
6119
7061
  }
6120
- function owningFile(workspace, root) {
6121
- let entries;
6122
- try {
6123
- entries = readdirSync4(join10(workspace, "content"));
6124
- } catch {
6125
- return void 0;
6126
- }
6127
- const match = entries.filter((name) => new RegExp(`^${root}\\.ya?ml$`, "i").test(name)).sort()[0];
6128
- return match === void 0 ? void 0 : `content/${match}`;
7062
+ function headlineHits(profile, platform, lang) {
7063
+ return profile.headlines.map((row, index) => ({ row, index })).filter(({ row }) => row.platform === platform && row.lang === lang);
7064
+ }
7065
+ function bioHits(profile, platform, lang, length) {
7066
+ return profile.bios.map((row, index) => ({ row, index })).filter(({ row }) => row.platform === platform && row.lang === lang && row.length === length);
7067
+ }
7068
+ function linkHits(links, platform, label) {
7069
+ return (links ?? []).map((row, index) => ({ row, index })).filter(
7070
+ ({ row }) => row.platform === platform && (label === void 0 || (row.label ?? "") === label)
7071
+ );
7072
+ }
7073
+ function faqHits(rows, id) {
7074
+ return rows.map((row, index) => ({ row, index })).filter(({ row }) => row.id === id);
7075
+ }
7076
+ function faqValue(row, field, lang) {
7077
+ if (field === "source")
7078
+ return row.source === void 0 || row.source === "" ? void 0 : row.source;
7079
+ const value = row[field]?.[lang];
7080
+ return typeof value === "string" && value !== "" ? value : void 0;
6129
7081
  }
6130
7082
  function resolveEditTarget(profile, address, workspace) {
6131
7083
  if (address.kind === "statement") {
@@ -6134,7 +7086,7 @@ function resolveEditTarget(profile, address, workspace) {
6134
7086
  const present = typeof value === "string" && value !== "";
6135
7087
  const available = LANGS2.filter(
6136
7088
  (lang) => typeof statement[lang] === "string" && statement[lang] !== ""
6137
- ).map((lang) => `positioning.statement.${lang}`);
7089
+ ).map((lang) => statementAddress(lang));
6138
7090
  const file = owningFile(workspace, "positioning");
6139
7091
  return {
6140
7092
  address,
@@ -6152,7 +7104,7 @@ function resolveEditTarget(profile, address, workspace) {
6152
7104
  };
6153
7105
  }
6154
7106
  if (address.kind === "headline") {
6155
- const hits2 = profile.headlines.map((row, index) => ({ row, index })).filter(({ row }) => row.platform === address.platform && row.lang === address.lang);
7107
+ const hits2 = headlineHits(profile, address.platform, address.lang);
6156
7108
  const file = owningFile(workspace, "headlines");
6157
7109
  return {
6158
7110
  address,
@@ -6175,9 +7127,7 @@ function resolveEditTarget(profile, address, workspace) {
6175
7127
  };
6176
7128
  }
6177
7129
  if (address.kind === "bio") {
6178
- const hits2 = profile.bios.map((row, index) => ({ row, index })).filter(
6179
- ({ row }) => row.platform === address.platform && row.lang === address.lang && row.length === address.length
6180
- );
7130
+ const hits2 = bioHits(profile, address.platform, address.lang, address.length);
6181
7131
  const file = owningFile(workspace, "bios");
6182
7132
  return {
6183
7133
  address,
@@ -6199,10 +7149,76 @@ function resolveEditTarget(profile, address, workspace) {
6199
7149
  ]
6200
7150
  };
6201
7151
  }
7152
+ if (address.kind === "definition") {
7153
+ const definition = profile.identity.definition ?? {};
7154
+ const value = definition[address.lang];
7155
+ const present = typeof value === "string" && value !== "";
7156
+ const available = LANGS2.filter(
7157
+ (lang) => typeof definition[lang] === "string" && definition[lang] !== ""
7158
+ ).map((lang) => definitionAddress(lang));
7159
+ const file = owningFile(workspace, "identity");
7160
+ return {
7161
+ address,
7162
+ ...present && file !== void 0 ? {
7163
+ locate: {
7164
+ file,
7165
+ path: ["definition", address.lang],
7166
+ profilePath: ["identity", "definition", address.lang]
7167
+ }
7168
+ } : {},
7169
+ ...present ? { current: value } : {},
7170
+ matches: present ? 1 : 0,
7171
+ rows: present ? [{ address: address.address, value }] : [],
7172
+ available
7173
+ };
7174
+ }
7175
+ if (address.kind === "faq") {
7176
+ const rows = profile.faq ?? [];
7177
+ const hits2 = faqHits(rows, address.id);
7178
+ const file = owningFile(workspace, "faq");
7179
+ const first = hits2[0];
7180
+ const value = hits2.length === 1 && first !== void 0 ? faqValue(first.row, address.field, address.lang) : void 0;
7181
+ const matches = hits2.length > 1 ? hits2.length : value === void 0 ? 0 : 1;
7182
+ const available = [
7183
+ ...new Set(
7184
+ rows.flatMap((row) => {
7185
+ const out = [];
7186
+ for (const lang of LANGS2) {
7187
+ if (faqValue(row, "q", lang) !== void 0) {
7188
+ out.push(faqAddress(row.id, "q", lang));
7189
+ }
7190
+ if (faqValue(row, "a", lang) !== void 0) {
7191
+ out.push(faqAddress(row.id, "a", lang));
7192
+ }
7193
+ }
7194
+ if (faqValue(row, "source", void 0) !== void 0) {
7195
+ out.push(faqAddress(row.id, "source"));
7196
+ }
7197
+ return out;
7198
+ })
7199
+ )
7200
+ ];
7201
+ const locateIndex = first?.index;
7202
+ return {
7203
+ address,
7204
+ ...matches === 1 && file !== void 0 && locateIndex !== void 0 ? {
7205
+ locate: {
7206
+ file,
7207
+ path: address.field === "source" ? [locateIndex, "source"] : [locateIndex, address.field, address.lang],
7208
+ profilePath: address.field === "source" ? ["faq", locateIndex, "source"] : ["faq", locateIndex, address.field, address.lang]
7209
+ }
7210
+ } : {},
7211
+ ...matches === 1 ? { current: value } : {},
7212
+ matches,
7213
+ rows: hits2.length > 1 ? hits2.map(({ row }) => ({
7214
+ address: faqAddress(row.id, address.field, address.lang),
7215
+ value: faqValue(row, address.field, address.lang) ?? ""
7216
+ })) : matches === 1 ? [{ address: address.address, value }] : [],
7217
+ available
7218
+ };
7219
+ }
6202
7220
  const links = profile.identity.links ?? [];
6203
- const hits = links.map((row, index) => ({ row, index })).filter(
6204
- ({ row }) => row.platform === address.platform && (address.label === void 0 || (row.label ?? "") === address.label)
6205
- );
7221
+ const hits = linkHits(links, address.platform, address.label);
6206
7222
  return {
6207
7223
  address,
6208
7224
  // Read-only address: no write handle is emitted (S2 must not grow a second
@@ -6221,6 +7237,119 @@ function resolveEditTarget(profile, address, workspace) {
6221
7237
  ]
6222
7238
  };
6223
7239
  }
7240
+ function hasControlChar(value) {
7241
+ for (const char of value) {
7242
+ const code = char.codePointAt(0);
7243
+ if (code < 32 || code >= 127 && code <= 159) return true;
7244
+ if (code === 8232 || code === 8233) return true;
7245
+ }
7246
+ return false;
7247
+ }
7248
+ function selectorExpressible(value) {
7249
+ return value !== "" && value === value.trim() && !hasControlChar(value) && !SELECTOR_UNSAFE.test(value);
7250
+ }
7251
+ function listEditAddresses(profile, block) {
7252
+ const addresses = [];
7253
+ const warnings = [];
7254
+ const seen = /* @__PURE__ */ new Set();
7255
+ const scoped = (candidate) => block === void 0 || block === candidate;
7256
+ const warnOnce = (message) => {
7257
+ if (!warnings.includes(message)) warnings.push(message);
7258
+ };
7259
+ const add = (address, kind, matches, readOnly = false) => {
7260
+ if (seen.has(address)) return;
7261
+ seen.add(address);
7262
+ if (matches !== 1) {
7263
+ warnOnce(
7264
+ `${address} resolves to ${matches} rows \u2014 several rows share it and edit refuses to choose; not listed`
7265
+ );
7266
+ return;
7267
+ }
7268
+ addresses.push({ address, kind, readOnly });
7269
+ };
7270
+ if (scoped("positioning")) {
7271
+ const statement = profile.positioning?.statement ?? {};
7272
+ for (const lang of LANGS2) {
7273
+ if (typeof statement[lang] === "string" && statement[lang] !== "") {
7274
+ add(statementAddress(lang), "statement", 1);
7275
+ }
7276
+ }
7277
+ }
7278
+ if (scoped("headlines")) {
7279
+ const counts = /* @__PURE__ */ new Map();
7280
+ for (const row of profile.headlines) {
7281
+ const address = headlineAddress(row.platform, row.lang);
7282
+ counts.set(address, (counts.get(address) ?? 0) + 1);
7283
+ }
7284
+ for (const row of profile.headlines) {
7285
+ const address = headlineAddress(row.platform, row.lang);
7286
+ add(address, "headline", counts.get(address) ?? 0);
7287
+ }
7288
+ }
7289
+ if (scoped("bios")) {
7290
+ const counts = /* @__PURE__ */ new Map();
7291
+ for (const row of profile.bios) {
7292
+ const address = bioAddress(row.platform, row.lang, row.length);
7293
+ counts.set(address, (counts.get(address) ?? 0) + 1);
7294
+ }
7295
+ for (const row of profile.bios) {
7296
+ const address = bioAddress(row.platform, row.lang, row.length);
7297
+ add(address, "bio", counts.get(address) ?? 0);
7298
+ }
7299
+ }
7300
+ if (scoped("identity.definition")) {
7301
+ const definition = profile.identity.definition ?? {};
7302
+ for (const lang of LANGS2) {
7303
+ if (typeof definition[lang] === "string" && definition[lang] !== "") {
7304
+ add(definitionAddress(lang), "definition", 1);
7305
+ }
7306
+ }
7307
+ }
7308
+ if (scoped("identity.links")) {
7309
+ const links = profile.identity.links ?? [];
7310
+ links.forEach((row, index) => {
7311
+ const where = `identity.links[${index}]`;
7312
+ if (row.platform === void 0) {
7313
+ warnOnce(`${where} has no platform \u2014 not addressable, not listed`);
7314
+ return;
7315
+ }
7316
+ if (!selectorExpressible(row.platform)) {
7317
+ warnOnce(
7318
+ `${where}: platform ${JSON.stringify(row.platform)} cannot be expressed as a selector (empty, padded, control character, or carrying a comma, bracket, equals sign or quote) \u2014 not listed`
7319
+ );
7320
+ return;
7321
+ }
7322
+ const label = row.label !== void 0 && selectorExpressible(row.label) ? row.label : void 0;
7323
+ const matches = linkHits(links, row.platform, label).length;
7324
+ add(linkAddress(row.platform, label), "link", matches, true);
7325
+ if (row.label !== void 0 && label === void 0 && matches === 1) {
7326
+ warnOnce(
7327
+ `${where}: label ${JSON.stringify(row.label)} cannot be expressed as a selector (empty, padded, or carrying a comma, bracket, equals sign, quote or control character) \u2014 listed by its platform only`
7328
+ );
7329
+ }
7330
+ });
7331
+ }
7332
+ if (scoped("faq")) {
7333
+ const rows = profile.faq ?? [];
7334
+ const counts = /* @__PURE__ */ new Map();
7335
+ for (const row of rows) counts.set(row.id, (counts.get(row.id) ?? 0) + 1);
7336
+ for (const row of rows) {
7337
+ const matches = counts.get(row.id) ?? 0;
7338
+ for (const lang of LANGS2) {
7339
+ if (faqValue(row, "q", lang) !== void 0) {
7340
+ add(faqAddress(row.id, "q", lang), "faq", matches);
7341
+ }
7342
+ if (faqValue(row, "a", lang) !== void 0) {
7343
+ add(faqAddress(row.id, "a", lang), "faq", matches);
7344
+ }
7345
+ }
7346
+ if (faqValue(row, "source", void 0) !== void 0) {
7347
+ add(faqAddress(row.id, "source"), "faq", matches);
7348
+ }
7349
+ }
7350
+ }
7351
+ return { addresses, warnings };
7352
+ }
6224
7353
  function applyEdit(profile, target, value) {
6225
7354
  const copy = structuredClone(profile);
6226
7355
  const path = target.locate.profilePath;
@@ -6270,11 +7399,30 @@ function explainMiss(artifact, address, platform) {
6270
7399
  }
6271
7400
  return `the ${platform} renderer does not emit this headline`;
6272
7401
  }
7402
+ if (address.kind === "definition") {
7403
+ return `the ${platform} artifact embeds no entity JSON-LD \u2014 only a renderer that emits it publishes identity.definition`;
7404
+ }
6273
7405
  return "no rendered artifact carries this statement \u2014 only renderers that emit it can publish it";
6274
7406
  }
7407
+ function jsonText(value) {
7408
+ return value === void 0 ? "" : JSON.stringify(value, null, 2).replace(/</g, "\\u003c");
7409
+ }
6275
7410
  function publishedByEdit(before, after, address, workspace, pascorc) {
7411
+ if (address.kind === "faq") {
7412
+ const lang = address.lang ?? "en";
7413
+ const oldNode = jsonText(structuredData(before, lang).faqJsonLd);
7414
+ const newNode = jsonText(structuredData(after, lang).faqJsonLd);
7415
+ const region = changedRegion(oldNode, newNode);
7416
+ if (region !== void 0) {
7417
+ return { changes: [{ platform: "entity", ...region }] };
7418
+ }
7419
+ return {
7420
+ changes: [],
7421
+ reason: "faq[] reaches the FAQPage node of `pasco entity --jsonld` (#360), and this edit does not change that node's bytes"
7422
+ };
7423
+ }
6276
7424
  const media = collectMedia(before, workspace);
6277
- const platforms = address.kind === "statement" ? [...PLATFORMS] : [address.platform];
7425
+ const platforms = address.kind === "statement" || address.kind === "definition" ? [...PLATFORMS] : [address.platform];
6278
7426
  const beforeSlices = /* @__PURE__ */ new Map();
6279
7427
  for (const platform of PLATFORMS) {
6280
7428
  beforeSlices.set(platform, compileSlice(before, platform, address.lang, pascorc));
@@ -6315,6 +7463,8 @@ function failLoad(err) {
6315
7463
  }
6316
7464
  function ledgerKeyOf(address) {
6317
7465
  if (address.kind === "statement") return "positioning";
7466
+ if (address.kind === "definition") return "identity";
7467
+ if (address.kind === "faq") return void 0;
6318
7468
  if (address.kind === "bio") {
6319
7469
  return `bios:${address.platform}:${address.lang}:${address.length}`;
6320
7470
  }
@@ -6322,15 +7472,31 @@ function ledgerKeyOf(address) {
6322
7472
  }
6323
7473
  function ledgerImpact(profile, address) {
6324
7474
  const key = ledgerKeyOf(address);
7475
+ if (key === void 0) return void 0;
6325
7476
  const unit = deployStatus(profile).units.find((candidate) => candidate.key === key);
6326
7477
  return { key, state: unit?.state ?? "never", surfaces: unit?.surfaces ?? {} };
6327
7478
  }
7479
+ function limitScope(address) {
7480
+ if (address.kind === "link") return "link";
7481
+ if (address.kind === "definition") return "definition";
7482
+ if (address.kind === "faq")
7483
+ return address.field === "source" ? "faq source" : `faq ${address.field}`;
7484
+ return address.kind === "bio" ? "bio" : "headline";
7485
+ }
6328
7486
  function limitPreflight(profile, address, text) {
6329
- if (address.kind === "statement") return void 0;
7487
+ if (address.kind === "statement" || address.kind === "faq" && address.field === "source") {
7488
+ return void 0;
7489
+ }
7490
+ const length = [...text].length;
7491
+ if (address.kind === "definition" || address.kind === "faq") {
7492
+ const limit2 = contractLimit(
7493
+ address.kind === "definition" ? "definition" : address.field === "q" ? "faq.q" : "faq.a"
7494
+ );
7495
+ return { limit: limit2, length, over: length > limit2.cap, enforced: true };
7496
+ }
6330
7497
  const surface = address.kind === "bio" ? "bio" : "headline";
6331
- const checks = profile.checks.length > 0 ? profile.checks : DEFAULT_CHECKS;
7498
+ const checks = capCatalog(profile, DEFAULT_CHECKS);
6332
7499
  const limit = applicableLimit(checks, address.platform, surface);
6333
- const length = [...text].length;
6334
7500
  return {
6335
7501
  ...limit === void 0 ? {} : { limit },
6336
7502
  length,
@@ -6361,11 +7527,11 @@ function warningsForEdit(published, preflight, profile, address, workspace, pasc
6361
7527
  }
6362
7528
  if (preflight?.limit !== void 0 && preflight.over) {
6363
7529
  out.push(
6364
- `${preflight.length} chars is over the published cap ${preflight.limit.cap} (${preflight.limit.source})`
7530
+ preflight.enforced === true ? `${preflight.length} chars is over the contract cap ${preflight.limit.cap} \u2014 the schema refuses the write (D12): shorten it first (${preflight.limit.source})` : `${preflight.length} chars is over the published cap ${preflight.limit.cap} (${preflight.limit.source})`
6365
7531
  );
6366
7532
  }
6367
7533
  if (address.kind === "headline" || address.kind === "bio") {
6368
- const checks = profile.checks.length > 0 ? profile.checks : DEFAULT_CHECKS;
7534
+ const checks = capCatalog(profile, DEFAULT_CHECKS);
6369
7535
  for (const finding3 of limitFindings(profile, checks, address.platform)) {
6370
7536
  if (finding3.message.includes("non-numeric")) out.push(finding3.message);
6371
7537
  }
@@ -6374,6 +7540,8 @@ function warningsForEdit(published, preflight, profile, address, workspace, pasc
6374
7540
  }
6375
7541
  function scopeLabel(address) {
6376
7542
  if (address.kind === "statement") return "every rendered platform";
7543
+ if (address.kind === "definition") return "every artifact that embeds the entity JSON-LD";
7544
+ if (address.kind === "faq") return "the entity FAQPage (`pasco entity --jsonld`, #360)";
6377
7545
  if (address.kind === "link") return "verbatim \u2014 the URL SSOT (\xA73)";
6378
7546
  return `${address.platform}/${address.lang}`;
6379
7547
  }
@@ -6384,6 +7552,18 @@ function runEdit(args) {
6384
7552
  } catch (err) {
6385
7553
  return failLoad(err);
6386
7554
  }
7555
+ if (args.createRoot !== void 0) return runCreate(args);
7556
+ if (args.list === true) {
7557
+ const listed = listEditAddresses(profile, args.listBlock);
7558
+ line("addresses:");
7559
+ for (const entry of listed.addresses) {
7560
+ line(`${entry.address}${entry.readOnly ? " (read-only)" : ""}`);
7561
+ }
7562
+ for (const warning of listed.warnings) warn2(warning);
7563
+ return report("edit", true, EXIT.OK, {
7564
+ data: { addresses: listed.addresses, warnings: listed.warnings }
7565
+ });
7566
+ }
6387
7567
  if (args.addressArg === void 0) {
6388
7568
  throw new UsageError("usage: pasco edit <contract-path> [--set <text> --check]");
6389
7569
  }
@@ -6540,14 +7720,16 @@ function runEdit(args) {
6540
7720
  line("resolves to 0 rows \u2014 no current value");
6541
7721
  if (proposed !== void 0) {
6542
7722
  line(`proposed: ${proposed}`);
6543
- line("(edit does not create rows: the field must already exist in the schema and the file)");
7723
+ line(
7724
+ "(edit does not create rows: the field must already exist in the schema and the file; --create seeds a root the workspace does not declare yet)"
7725
+ );
6544
7726
  }
6545
7727
  if (target.available.length > 0) {
6546
7728
  line("available:");
6547
7729
  for (const candidate of target.available) line(` ${candidate}`);
6548
7730
  }
6549
7731
  } else {
6550
- const kind = address.kind === "bio" ? "a duplicate row is drift/duplicate-bio" : address.kind === "link" ? "several links share this platform \u2014 add label=<\u2026> (not a drift finding: drift flags the same URL twice)" : "several rows share this address";
7732
+ const kind = address.kind === "bio" ? "a duplicate row is drift/duplicate-bio" : address.kind === "link" ? "several links share this platform \u2014 add label=<\u2026> (not a drift finding: drift flags the same URL twice)" : address.kind === "faq" ? "several faq rows share this id \u2014 fix the duplicate id first" : "several rows share this address";
6551
7733
  line(`resolves to ${target.matches} rows \u2014 ${kind}; edit refuses to choose`);
6552
7734
  target.rows.forEach((row, index) => {
6553
7735
  line(
@@ -6556,7 +7738,7 @@ function runEdit(args) {
6556
7738
  });
6557
7739
  }
6558
7740
  if (preflight !== void 0) {
6559
- const scope = `${address.kind === "bio" ? "bio" : "headline"}`;
7741
+ const scope = limitScope(address);
6560
7742
  if (preflight.limit === void 0) {
6561
7743
  line(
6562
7744
  `limits: ${preflight.length} chars \u2014 cap unknown (no sourced max-chars for this ${scope} in the catalog: the workspace's content/checks.yaml or the caps shipped with pasco)`
@@ -6568,7 +7750,7 @@ function runEdit(args) {
6568
7750
  );
6569
7751
  if (preflight.over) {
6570
7752
  warn2(
6571
- `${scope} is ${preflight.length} chars, over the published cap ${preflight.limit.cap} \u2014 the platform may truncate it (${preflight.limit.source})`
7753
+ preflight.enforced === true ? `${scope} is ${preflight.length} chars, over the contract cap ${preflight.limit.cap} \u2014 the schema refuses the write (D12); shorten it (${preflight.limit.source})` : `${scope} is ${preflight.length} chars, over the published cap ${preflight.limit.cap} \u2014 the platform may truncate it (${preflight.limit.source})`
6572
7754
  );
6573
7755
  }
6574
7756
  }
@@ -6624,6 +7806,96 @@ function runEdit(args) {
6624
7806
  }
6625
7807
  });
6626
7808
  }
7809
+ function seedLimits(root) {
7810
+ if (root !== "faq") return [];
7811
+ const limits = [];
7812
+ for (const field of ["q", "a"]) {
7813
+ const limit = contractLimit(field === "q" ? "faq.q" : "faq.a");
7814
+ const length = Math.max(...SEED_FAQ_ENTRIES.map((entry) => [...entry[field]].length));
7815
+ limits.push({
7816
+ field,
7817
+ cap: limit.cap,
7818
+ length,
7819
+ over: length > limit.cap,
7820
+ source: limit.source
7821
+ });
7822
+ }
7823
+ return limits;
7824
+ }
7825
+ function runCreate(args) {
7826
+ const root = args.createRoot;
7827
+ const text = seedTemplate(root);
7828
+ const limits = seedLimits(root);
7829
+ const preview = args.check;
7830
+ const seedLabel = root === "faq" ? `${SEED_FAQ_ENTRIES.length} pairs (the schema minimum) \u2014 replace every <placeholder>` : "template";
7831
+ let file;
7832
+ try {
7833
+ file = preview ? previewCreate({ workspace: args.workspace, root, text }).file : createRoot({ workspace: args.workspace, root, text }).file;
7834
+ } catch (err) {
7835
+ if (err instanceof WriteRefusedError || err instanceof StaleEditError) {
7836
+ return report("edit", false, EXIT.FAIL, {
7837
+ error: {
7838
+ kind: err instanceof StaleEditError ? "stale" : "refused",
7839
+ message: err.message
7840
+ }
7841
+ });
7842
+ }
7843
+ if (err instanceof SchemaValidationError || err instanceof LoadError) {
7844
+ return report("edit", false, EXIT.FAIL, {
7845
+ error: {
7846
+ kind: err instanceof SchemaValidationError ? "schema" : "load",
7847
+ message: err.message,
7848
+ ...err instanceof SchemaValidationError ? { violations: [...err.issues] } : {}
7849
+ }
7850
+ });
7851
+ }
7852
+ throw err;
7853
+ }
7854
+ line(`root: ${root}`);
7855
+ line(`file: ${file}`);
7856
+ line(`seed: ${seedLabel}`);
7857
+ if (limits.length > 0) {
7858
+ for (const limit of limits) {
7859
+ line(
7860
+ `limits: ${limit.field} ${limit.length}/${limit.cap}${limit.over ? " \u2014 OVER" : " \u2014 ok"} \u2014 ${limit.source}`
7861
+ );
7862
+ if (limit.over) {
7863
+ warn2(
7864
+ `${limit.field} is ${limit.length} chars, over the contract cap ${limit.cap} \u2014 the schema refuses the write (D12); shorten it (${limit.source})`
7865
+ );
7866
+ }
7867
+ }
7868
+ }
7869
+ line(
7870
+ preview ? "would create: yes (--check: validated, nothing was written)" : "written: yes (validated by load()+schema over a temp copy, then written)"
7871
+ );
7872
+ if (preview) {
7873
+ line("--- seed (nothing was written) ---");
7874
+ for (const seedLine of text.trimEnd().split("\n")) line(` ${seedLine}`);
7875
+ }
7876
+ line("validates: yes (schema)");
7877
+ return report("edit", true, EXIT.OK, {
7878
+ data: {
7879
+ mode: "create",
7880
+ root,
7881
+ file,
7882
+ created: !preview,
7883
+ preview,
7884
+ seed: seedLabel,
7885
+ // The machine contract carries the template too: a label is not a preview
7886
+ // (#451 fresh pass 2, F2).
7887
+ ...preview ? { template: text } : {},
7888
+ limits: limits.map((limit) => ({
7889
+ field: limit.field,
7890
+ cap: limit.cap,
7891
+ length: limit.length,
7892
+ over: limit.over,
7893
+ source: limit.source
7894
+ })),
7895
+ warnings: []
7896
+ }
7897
+ });
7898
+ }
6627
7899
 
6628
7900
  // src/commands/entity.ts
6629
7901
  import { existsSync as existsSync9, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
@@ -6775,12 +8047,12 @@ function runEntity(args) {
6775
8047
  import {
6776
8048
  existsSync as existsSync10,
6777
8049
  mkdirSync as mkdirSync2,
6778
- readFileSync as readFileSync10,
8050
+ readFileSync as readFileSync11,
6779
8051
  renameSync as renameSync3,
6780
8052
  unlinkSync as unlinkSync2,
6781
8053
  writeFileSync as writeFileSync4
6782
8054
  } from "fs";
6783
- import { join as join11, resolve as resolve3 } from "path";
8055
+ import { join as join10, resolve as resolve3 } from "path";
6784
8056
  var AI_POLICY_SCHEMA_VERSION = "1";
6785
8057
  var AI_POLICY_MAX_AGE_DAYS = 180;
6786
8058
  var AI_POLICY_FILES = ["robots.txt", "llms.txt", "AI-POLICY.md"];
@@ -7100,10 +8372,10 @@ function aiPolicyArtifacts(input) {
7100
8372
  };
7101
8373
  }
7102
8374
  function writeArtifacts(dir, artifacts, force) {
7103
- const targets = AI_POLICY_FILES.map((name) => ({ name, path: join11(dir, name) }));
8375
+ const targets = AI_POLICY_FILES.map((name) => ({ name, path: join10(dir, name) }));
7104
8376
  for (const target of targets) {
7105
8377
  if (!force && existsSync10(target.path)) {
7106
- const current = readFileSync10(target.path, "utf8");
8378
+ const current = readFileSync11(target.path, "utf8");
7107
8379
  if (!current.includes(AI_POLICY_MARKER)) {
7108
8380
  return {
7109
8381
  error: `refusing to overwrite "${target.path}" \u2014 it was not generated by pasco (pass --force to replace it)`
@@ -7350,7 +8622,20 @@ function runExportProfile(args) {
7350
8622
 
7351
8623
  // src/commands/init.ts
7352
8624
  import { existsSync as existsSync11, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
7353
- import { join as join12, resolve as resolve4 } from "path";
8625
+ import { join as join11, resolve as resolve4 } from "path";
8626
+
8627
+ // src/version.ts
8628
+ import { readFileSync as readFileSync12 } from "fs";
8629
+ var CLI_VERSION = (() => {
8630
+ try {
8631
+ const pkg = JSON.parse(readFileSync12(new URL("../package.json", import.meta.url), "utf8"));
8632
+ return pkg.version;
8633
+ } catch {
8634
+ return "0.0.0";
8635
+ }
8636
+ })();
8637
+
8638
+ // src/commands/init.ts
7354
8639
  var FLAVOR_BRAND_TEMPLATE = `# ${WORKSPACE_NAME} brand workspace \u2014 content/ flavor (D15/\xA717)
7355
8640
  # The file value IS the root value: a bare scalar (file name = root property).
7356
8641
  brand
@@ -7438,18 +8723,36 @@ Your professional facts live here as text-first YAML (SPEC \xA72.1); the engine
7438
8723
  checks drift and renders per-platform texts. Facts are your own \u2014 this workspace is
7439
8724
  private by default and never leaves your control (D14: integration by configured path).
7440
8725
 
8726
+ ## The cycle
8727
+
8728
+ \`\`\`sh
8729
+ pasco init <dir> scaffold a workspace (content/, README.md, .pascorc)
8730
+ edit content/*.yaml the single source of truth \u2014 edit these, not profiles/
8731
+ pasco validate schema + drift + geo (warnings)
8732
+ pasco build [platform] write the per-platform artifacts
8733
+ \`\`\`
8734
+
7441
8735
  ## Files
7442
8736
 
7443
- Three top-level directories, three grains \u2014 nothing nests one inside another:
8737
+ Three grains, one per area \u2014 nothing nests one inside another:
7444
8738
  \`content/\` is identity, \`profiles/\` is generated output (its \`variants/\` subdir is input),
7445
8739
  \`posts/\` is the stream.
7446
8740
 
7447
- - \`content/*.yaml\` \u2014 identity facts, the SSOT (file name = root property, see schema/README.md)
8741
+ - \`content/*.yaml\` \u2014 identity facts, the SSOT (file name = root property; the contract is described in the [schema documentation](https://github.com/ChristianLuciani/profile-as-code/blob/main/schema/README.md))
7448
8742
  - \`profiles/\` \u2014 generated outputs (created by pasco build; committed: they ARE the deploy artifacts)
7449
8743
  - \`profiles/<platform>/variants/\` \u2014 chosen variant bodies (INPUT, #238; preserved by build)
7450
8744
  - \`posts/\` \u2014 the stream, created by \`pasco post\` (#264): seeds + per-channel projections; never identity facts
7451
8745
  - \`.pascorc\` \u2014 workspace config (defaults)
7452
8746
 
8747
+ ## Skills
8748
+
8749
+ The platform playbooks ship with \`pasco\`, outside this workspace. Run this from
8750
+ the workspace root when you need to know what to write for a channel:
8751
+ \`pasco skills:export .pasco-skills\` copies them in; each channel lands under
8752
+ \`.pasco-skills/<platform>/\`, where you can read its guidance.
8753
+ The copy is disposable: add it to .gitignore if you do not want it committed
8754
+ (re-running needs \`--force\`).
8755
+
7453
8756
  ## Workflow
7454
8757
 
7455
8758
  \`\`\`sh
@@ -7458,7 +8761,7 @@ pasco build github-readme --lang en # deterministic base texts
7458
8761
  pasco refine github-readme --variants 3 # agent-driven variants (template + --choose; full flow M3)
7459
8762
  \`\`\`
7460
8763
 
7461
- See packages/cli/README.md for the full command reference.
8764
+ See the [CLI reference](https://github.com/ChristianLuciani/profile-as-code/tree/main/packages/cli#readme) for the full command reference, including what \`check\` compares against \`content/\`, the order \`build\` chooses an artifact, and where the per-platform character caps come from.
7462
8765
  `;
7463
8766
  var BRAND_README_TEMPLATE = `# ${WORKSPACE_NAME}
7464
8767
 
@@ -7467,18 +8770,36 @@ Brand workspace for [profile-as-code](https://github.com/ChristianLuciani/profil
7467
8770
  podcast, press, audiences \u2014 live here as text-first YAML; the engine validates,
7468
8771
  checks drift and renders brand surfaces. Private by default (D14: configured path).
7469
8772
 
8773
+ ## The cycle
8774
+
8775
+ \`\`\`sh
8776
+ pasco init <dir> scaffold a workspace (content/, README.md, .pascorc)
8777
+ edit content/*.yaml the single source of truth \u2014 edit these, not profiles/
8778
+ pasco validate schema + drift + geo (warnings)
8779
+ pasco build [platform] write the per-platform artifacts
8780
+ \`\`\`
8781
+
7470
8782
  ## Files
7471
8783
 
7472
- Three top-level directories, three grains \u2014 nothing nests one inside another:
8784
+ Three grains, one per area \u2014 nothing nests one inside another:
7473
8785
  \`content/\` is identity, \`profiles/\` is generated output (its \`variants/\` subdir is input),
7474
8786
  \`posts/\` is the stream.
7475
8787
 
7476
- - \`content/*.yaml\` \u2014 the brand profile, identity facts (file name = root property, see schema/README.md)
8788
+ - \`content/*.yaml\` \u2014 the brand profile, identity facts (file name = root property; the contract is described in the [schema documentation](https://github.com/ChristianLuciani/profile-as-code/blob/main/schema/README.md))
7477
8789
  - \`profiles/\` \u2014 generated outputs (github-org README is the first deploy artifact; pasco build also writes manual-paste files)
7478
8790
  - \`profiles/<platform>/variants/\` \u2014 chosen variant bodies (INPUT, #238; preserved by build)
7479
8791
  - \`posts/\` \u2014 the stream, created by \`pasco post\` (#264): seeds + per-channel projections; never identity facts
7480
8792
  - \`.pascorc\` \u2014 workspace config (defaults)
7481
8793
 
8794
+ ## Skills
8795
+
8796
+ The platform playbooks ship with \`pasco\`, outside this workspace. Run this from
8797
+ the workspace root when you need to know what to write for a channel:
8798
+ \`pasco skills:export .pasco-skills\` copies them in; each channel lands under
8799
+ \`.pasco-skills/<platform>/\`, where you can read its guidance.
8800
+ The copy is disposable: add it to .gitignore if you do not want it committed
8801
+ (re-running needs \`--force\`).
8802
+
7482
8803
  ## Workflow
7483
8804
 
7484
8805
  \`\`\`sh
@@ -7487,14 +8808,19 @@ pasco check # handle-matrix rules (handle-claimed / handle-con
7487
8808
  pasco build github-org --lang en # org README (git deploy)
7488
8809
  \`\`\`
7489
8810
 
7490
- See packages/cli/README.md for the full command reference.
8811
+ See the [CLI reference](https://github.com/ChristianLuciani/profile-as-code/tree/main/packages/cli#readme) for the full command reference, including what \`check\` compares against \`content/\`, the order \`build\` chooses an artifact, and where the per-platform character caps come from.
8812
+ `;
8813
+ function pascorcTemplate(engineVersion) {
8814
+ const floor = engineFloorFor(engineVersion);
8815
+ const engine = floor === null ? "" : ` "engine": "${floor}",
7491
8816
  `;
7492
- var PASCORC_TEMPLATE = `{
8817
+ return `{
7493
8818
  "workspace": ".",
7494
- "defaults": { "lang": "en", "variants": 3 },
8819
+ ${engine} "defaults": { "lang": "en", "variants": 3 },
7495
8820
  "diff": { "tagPattern": "release-*" }
7496
8821
  }
7497
8822
  `;
8823
+ }
7498
8824
  var BRAND_FILES = {
7499
8825
  "identity.yaml": BRAND_IDENTITY_TEMPLATE,
7500
8826
  "socialHandles.yaml": BRAND_SOCIAL_HANDLES_TEMPLATE,
@@ -7506,7 +8832,7 @@ var BRAND_FILES = {
7506
8832
  };
7507
8833
  function runInit(args) {
7508
8834
  const dir = resolve4(args.cwd, args.dir ?? ".");
7509
- const contentDir = join12(dir, "content");
8835
+ const contentDir = join11(dir, "content");
7510
8836
  const brand = args.flavor === "brand";
7511
8837
  if (existsSync11(contentDir)) {
7512
8838
  return report("init", false, EXIT.FAIL, {
@@ -7515,9 +8841,9 @@ function runInit(args) {
7515
8841
  }
7516
8842
  const files = brand ? { ...BRAND_FILES, "checks.yaml": DEFAULT_CHECKS_YAML } : { "identity.yaml": IDENTITY_TEMPLATE, "checks.yaml": DEFAULT_CHECKS_YAML };
7517
8843
  const targets = [
7518
- ...Object.entries(files).map(([name, content]) => ({ path: join12(contentDir, name), content })),
7519
- { path: join12(dir, "README.md"), content: brand ? BRAND_README_TEMPLATE : README_TEMPLATE },
7520
- { path: join12(dir, ".pascorc"), content: PASCORC_TEMPLATE }
8844
+ ...Object.entries(files).map(([name, content]) => ({ path: join11(contentDir, name), content })),
8845
+ { path: join11(dir, "README.md"), content: brand ? BRAND_README_TEMPLATE : README_TEMPLATE },
8846
+ { path: join11(dir, ".pascorc"), content: pascorcTemplate(CLI_VERSION) }
7521
8847
  ];
7522
8848
  for (const t of targets) {
7523
8849
  if (existsSync11(t.path)) {
@@ -7543,37 +8869,37 @@ function runInit(args) {
7543
8869
  }
7544
8870
 
7545
8871
  // src/commands/post.ts
7546
- import { existsSync as existsSync13, mkdirSync as mkdirSync5, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync6, writeFileSync as writeFileSync7 } from "fs";
7547
- import { dirname as dirname5, join as join14 } from "path";
8872
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6, writeFileSync as writeFileSync7 } from "fs";
8873
+ import { dirname as dirname5, join as join13 } from "path";
7548
8874
  import { parseDocument as parseDocument4 } from "yaml";
7549
8875
 
7550
8876
  // src/post-store.ts
7551
8877
  import { createHash as createHash3 } from "crypto";
7552
- import { existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync5, readFileSync as readFileSync11, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
7553
- import { join as join13 } from "path";
8878
+ import { existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
8879
+ import { join as join12 } from "path";
7554
8880
  import { parseDocument as parseDocument3, stringify as stringify2 } from "yaml";
7555
8881
  var SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
7556
8882
  function isSafeSeedId(id) {
7557
8883
  return SAFE_ID.test(id) && !id.includes("..");
7558
8884
  }
7559
8885
  function postsDir(workspace) {
7560
- return join13(workspace, "posts");
8886
+ return join12(workspace, "posts");
7561
8887
  }
7562
8888
  function seedDir(workspace, id) {
7563
8889
  if (!isSafeSeedId(id)) throw new Error(`unsafe seed id ${JSON.stringify(id)}`);
7564
- return join13(postsDir(workspace), id);
8890
+ return join12(postsDir(workspace), id);
7565
8891
  }
7566
8892
  function seedPath(workspace, id) {
7567
- return join13(seedDir(workspace, id), "seed.md");
8893
+ return join12(seedDir(workspace, id), "seed.md");
7568
8894
  }
7569
8895
  function ledgerPath(workspace, id) {
7570
- return join13(seedDir(workspace, id), "post.yaml");
8896
+ return join12(seedDir(workspace, id), "post.yaml");
7571
8897
  }
7572
8898
  function variantPath(workspace, id, channel, variant) {
7573
8899
  if (!SAFE_ID.test(variant) || variant.includes("..")) {
7574
8900
  throw new Error(`unsafe variant id ${JSON.stringify(variant)}`);
7575
8901
  }
7576
- return join13(seedDir(workspace, id), channel, `${variant}.md`);
8902
+ return join12(seedDir(workspace, id), channel, `${variant}.md`);
7577
8903
  }
7578
8904
  function hashText(text) {
7579
8905
  return createHash3("sha256").update(text).digest("hex");
@@ -7595,7 +8921,7 @@ function defaultLedger(id, source, options = {}) {
7595
8921
  function readLedger(workspace, id) {
7596
8922
  const path = ledgerPath(workspace, id);
7597
8923
  if (!existsSync12(path)) throw new Error(`no post ledger at ${path}`);
7598
- const doc = parseDocument3(readFileSync11(path, "utf8"));
8924
+ const doc = parseDocument3(readFileSync13(path, "utf8"));
7599
8925
  if (doc.errors.length > 0) {
7600
8926
  throw new Error(`${path}: invalid YAML \u2014 ${doc.errors[0]?.message ?? "parse error"}`);
7601
8927
  }
@@ -7631,7 +8957,7 @@ function seedText(workspace, id, seen = /* @__PURE__ */ new Set()) {
7631
8957
  }
7632
8958
  const path = seedPath(workspace, id);
7633
8959
  if (!existsSync12(path)) throw new Error(`no seed at ${path}`);
7634
- return readFileSync11(path, "utf8");
8960
+ return readFileSync13(path, "utf8");
7635
8961
  }
7636
8962
  function createSeed(workspace, id, text, source = "authored", options = {}) {
7637
8963
  if (!isSafeSeedId(id)) throw new Error(`unsafe seed id ${JSON.stringify(id)}`);
@@ -7655,7 +8981,7 @@ function listSeedIds(workspace) {
7655
8981
  if (!existsSync12(dir)) return [];
7656
8982
  return readdirSync5(dir).filter((name) => {
7657
8983
  try {
7658
- return statSync5(join13(dir, name)).isDirectory() && isSafeSeedId(name);
8984
+ return statSync5(join12(dir, name)).isDirectory() && isSafeSeedId(name);
7659
8985
  } catch {
7660
8986
  return false;
7661
8987
  }
@@ -7664,7 +8990,7 @@ function listSeedIds(workspace) {
7664
8990
  function chooseVariant(workspace, id, channel, variant, publishedText) {
7665
8991
  const path = variantPath(workspace, id, channel, variant);
7666
8992
  if (!existsSync12(path)) throw new Error(`no variant body at ${path}`);
7667
- const text = readFileSync11(path, "utf8");
8993
+ const text = readFileSync13(path, "utf8");
7668
8994
  if (text.trim() === "") throw new Error(`variant body is empty at ${path}`);
7669
8995
  const ledger = readLedger(workspace, id);
7670
8996
  const templateHash = hashText(text);
@@ -7688,7 +9014,7 @@ function chosenVariant(workspace, ledger, channel) {
7688
9014
  if (chosen === void 0) return void 0;
7689
9015
  const path = variantPath(workspace, ledger.id, channel, chosen);
7690
9016
  if (!existsSync12(path)) return void 0;
7691
- const text = readFileSync11(path, "utf8");
9017
+ const text = readFileSync13(path, "utf8");
7692
9018
  return { variant: chosen, text, path };
7693
9019
  }
7694
9020
 
@@ -7711,7 +9037,7 @@ function readRules(channel, format) {
7711
9037
  skillsDir() === void 0 ? NOT_SHIPPED_MESSAGE : `no ${format} post skill for "${channel}" \u2014 expected skills/${channel}/${file}`
7712
9038
  );
7713
9039
  }
7714
- const doc = parseDocument4(readFileSync12(path, "utf8"));
9040
+ const doc = parseDocument4(readFileSync14(path, "utf8"));
7715
9041
  if (doc.errors.length > 0) {
7716
9042
  throw new UsageError(`${path}: invalid YAML \u2014 ${doc.errors[0]?.message ?? "parse error"}`);
7717
9043
  }
@@ -8155,9 +9481,9 @@ function buildLongformTask(args) {
8155
9481
  ].join("\n");
8156
9482
  }
8157
9483
  function readPostizConfig(workspace) {
8158
- const path = join14(workspace, "postiz.yaml");
9484
+ const path = join13(workspace, "postiz.yaml");
8159
9485
  if (!existsSync13(path)) return {};
8160
- const doc = parseDocument4(readFileSync12(path, "utf8"));
9486
+ const doc = parseDocument4(readFileSync14(path, "utf8"));
8161
9487
  if (doc.errors.length > 0) {
8162
9488
  throw new UsageError(`${path}: invalid YAML \u2014 ${doc.errors[0]?.message ?? "parse error"}`);
8163
9489
  }
@@ -8226,7 +9552,7 @@ function handleSeed(args) {
8226
9552
  if (!existsSync13(args.textArg)) {
8227
9553
  throw new UsageError(`seed file not found: ${args.textArg}`);
8228
9554
  }
8229
- const text = readFileSync12(args.textArg, "utf8");
9555
+ const text = readFileSync14(args.textArg, "utf8");
8230
9556
  if (text.trim() === "") throw new UsageError("seed text is empty");
8231
9557
  if (existsSync13(seedPath(args.workspace, seedId))) {
8232
9558
  throw new UsageError(
@@ -8266,7 +9592,7 @@ function handleTask(args) {
8266
9592
  const channels = splitChannels(args.channelArg);
8267
9593
  const variants = parseVariants(
8268
9594
  args.variantsArg,
8269
- format === "longform" ? 1 : args.pascorc.defaults?.variants ?? 3
9595
+ format === "longform" ? 1 : args.pascorc.defaults?.variants ?? DEFAULT_VARIANTS
8270
9596
  );
8271
9597
  const lang = parseLang(defaultsLang(args.pascorc));
8272
9598
  const existing = existsSync13(ledgerPath(args.workspace, seedId)) ? readLedger(args.workspace, seedId) : void 0;
@@ -8326,7 +9652,7 @@ function handleTask(args) {
8326
9652
  lang,
8327
9653
  ...from !== void 0 ? { derivedFrom: from } : {}
8328
9654
  });
8329
- const taskPath = join14(seedDir(args.workspace, seedId), channel, "task.md");
9655
+ const taskPath = join13(seedDir(args.workspace, seedId), channel, "task.md");
8330
9656
  mkdirSync5(dirname5(taskPath), { recursive: true });
8331
9657
  writeFileSync7(taskPath, task);
8332
9658
  results.push({
@@ -8374,7 +9700,7 @@ function handleChoose(args) {
8374
9700
  if (!existsSync13(args.textArg)) throw new UsageError(`variant file not found: ${args.textArg}`);
8375
9701
  const target = variantPath(args.workspace, seedId, channel, args.variantArg);
8376
9702
  mkdirSync5(dirname5(target), { recursive: true });
8377
- writeFileSync7(target, readFileSync12(args.textArg, "utf8"));
9703
+ writeFileSync7(target, readFileSync14(args.textArg, "utf8"));
8378
9704
  }
8379
9705
  const loaded = loadProfile3(args.workspace);
8380
9706
  if (typeof loaded === "number") return loaded;
@@ -8382,7 +9708,7 @@ function handleChoose(args) {
8382
9708
  if (!existsSync13(variantFile)) {
8383
9709
  throw new UsageError(`no variant body at ${variantFile}`);
8384
9710
  }
8385
- const rendered = renderFacts(readFileSync12(variantFile, "utf8"), loaded);
9711
+ const rendered = renderFacts(readFileSync14(variantFile, "utf8"), loaded);
8386
9712
  if (rendered.dangling.length > 0) {
8387
9713
  throw new UsageError(`dangling fact ref(s): ${rendered.dangling.join(", ")}`);
8388
9714
  }
@@ -8638,7 +9964,7 @@ function handleExport(args) {
8638
9964
  posts.push(item.post);
8639
9965
  continue;
8640
9966
  }
8641
- const path = join14(seedDir(args.workspace, ledger.id), item.channel, "paste-checklist.md");
9967
+ const path = join13(seedDir(args.workspace, ledger.id), item.channel, "paste-checklist.md");
8642
9968
  mkdirSync5(dirname5(path), { recursive: true });
8643
9969
  writeFileSync7(
8644
9970
  path,
@@ -8658,7 +9984,7 @@ function handleExport(args) {
8658
9984
  });
8659
9985
  }
8660
9986
  const payload = { type, date, shortLink: false, tags: [], posts };
8661
- const out = args.outArg ?? join14(seedDir(args.workspace, seedId), "postiz.json");
9987
+ const out = args.outArg ?? join13(seedDir(args.workspace, seedId), "postiz.json");
8662
9988
  if (posts.length > 0) {
8663
9989
  mkdirSync5(dirname5(out), { recursive: true });
8664
9990
  writeFileSync7(out, `${JSON.stringify(payload, null, 2)}
@@ -8716,7 +10042,7 @@ function handleImport(args) {
8716
10042
  const started = Date.now();
8717
10043
  const imported = [];
8718
10044
  for (const file of files) {
8719
- const text = readFileSync12(join14(sourceDir, file), "utf8");
10045
+ const text = readFileSync14(join13(sourceDir, file), "utf8");
8720
10046
  if (text.trim() === "") continue;
8721
10047
  const base = slugify(file.replace(/\.md$/, "")) || "post";
8722
10048
  let id = `imported-${base}`;
@@ -8813,7 +10139,7 @@ function runPost(args) {
8813
10139
 
8814
10140
  // src/commands/presence.ts
8815
10141
  var PRESENCE_SCHEMA_VERSION = "1";
8816
- function loadOrReport(command, workspace) {
10142
+ function loadOrReport2(command, workspace) {
8817
10143
  try {
8818
10144
  return { profile: load(workspace) };
8819
10145
  } catch (err) {
@@ -8856,7 +10182,7 @@ function releasePlanPayload(plan) {
8856
10182
  }
8857
10183
  var KIND_ORDER = ["repo-meta", "profile", "distribution"];
8858
10184
  function runPresence(args) {
8859
- const loaded = loadOrReport("presence", args.workspace);
10185
+ const loaded = loadOrReport2("presence", args.workspace);
8860
10186
  if ("fail" in loaded) return loaded.fail;
8861
10187
  let board;
8862
10188
  try {
@@ -8885,7 +10211,7 @@ function runPresence(args) {
8885
10211
  return EXIT.OK;
8886
10212
  }
8887
10213
  function runReleasePlan(args) {
8888
- const loaded = loadOrReport("release plan", args.workspace);
10214
+ const loaded = loadOrReport2("release plan", args.workspace);
8889
10215
  if ("fail" in loaded) return loaded.fail;
8890
10216
  let plan;
8891
10217
  try {
@@ -8900,11 +10226,16 @@ function runReleasePlan(args) {
8900
10226
  `Release plan v${plan.release.version} (${plan.release.date}) \xB7 ${plan.release.id} \u2014 ${plan.release.summary}`
8901
10227
  );
8902
10228
  line("ORDER KIND SURFACE STATE");
8903
- plan.surfaces.forEach((row, index) => {
8904
- const order = String(index + 1).padStart(5);
10229
+ plan.surfaces.forEach((row) => {
10230
+ const order = String(row.order).padStart(5);
8905
10231
  line(`${order} ${row.kind.padEnd(15)} ${row.surface.padEnd(21)} ${row.state}`);
10232
+ for (const unknown of row.unknownUnits) {
10233
+ line(` \u21B3 changedUnit "${unknown}" is not an SSOT unit \u2014 fix the release row`);
10234
+ }
8906
10235
  for (const missing of row.missingUnits) {
8907
- line(` \u21B3 changedUnit "${missing}" is not an SSOT unit \u2014 fix the release row`);
10236
+ line(
10237
+ ` \u21B3 changedUnit "${missing}" has no deploy record on this surface yet \u2014 pending fan-out`
10238
+ );
8908
10239
  }
8909
10240
  });
8910
10241
  section("Base text (factual spine; the copy per kind lives in the shipped skills)");
@@ -8921,13 +10252,13 @@ import {
8921
10252
  existsSync as existsSync15,
8922
10253
  mkdirSync as mkdirSync6,
8923
10254
  readdirSync as readdirSync7,
8924
- readFileSync as readFileSync13,
10255
+ readFileSync as readFileSync15,
8925
10256
  renameSync as renameSync4,
8926
10257
  rmSync as rmSync3,
8927
10258
  statSync as statSync7,
8928
10259
  writeFileSync as writeFileSync8
8929
10260
  } from "fs";
8930
- import { basename as basename2, join as join16, dirname as parentDir, relative as relative4, resolve as resolve6 } from "path";
10261
+ import { basename as basename2, join as join15, dirname as parentDir, relative as relative4, resolve as resolve6 } from "path";
8931
10262
 
8932
10263
  // src/preview/document.ts
8933
10264
  import { Marked } from "marked";
@@ -9031,7 +10362,7 @@ function renderSection(section2, assets) {
9031
10362
  ${notes}
9032
10363
  </section>`;
9033
10364
  }
9034
- function renderChecks(checks, limits) {
10365
+ function renderChecks(checks, limits, limitsSource) {
9035
10366
  const errors = checks.filter((c) => c.severity === "error").length;
9036
10367
  const warns = checks.length - errors;
9037
10368
  const rows = checks.length === 0 ? `<p class="empty">no finding \u2014 the offline rules that ran did not complain</p>` : `<ul class="findings">${checks.map(
@@ -9043,7 +10374,7 @@ function renderChecks(checks, limits) {
9043
10374
  <span class="tag">${errors} error(s) \xB7 ${warns} warning(s)</span>
9044
10375
  <span class="path">the same offline rules pasco check runs</span>
9045
10376
  </header>
9046
- <div class="body">${renderLimits(limits)}${rows}</div>
10377
+ <div class="body">${renderLimits(limits, limitsSource)}${rows}</div>
9047
10378
  </section>`;
9048
10379
  }
9049
10380
  function renderUnpublished(items) {
@@ -9056,9 +10387,10 @@ function renderUnpublished(items) {
9056
10387
  <div class="body"><ul class="findings">${items.map((item) => `<li><code>${escapeHtml(item.id)}</code> \u2014 ${escapeHtml(item.reason)}</li>`).join("")}</ul></div>
9057
10388
  </section>`;
9058
10389
  }
9059
- function renderLimits(limits) {
10390
+ function renderLimits(limits, source) {
9060
10391
  if (limits.length === 0) return "";
9061
- return `<table class="limits"><caption>declared limits (content/checks.yaml)</caption><thead><tr><th>platform</th><th>rule</th><th>value</th></tr></thead><tbody>${limits.map(
10392
+ const caption = source === "workspace" ? "declared limits (content/)" : "declared limits (caps shipped with pasco, #240)";
10393
+ return `<table class="limits"><caption>${caption}</caption><thead><tr><th>platform</th><th>rule</th><th>value</th></tr></thead><tbody>${limits.map(
9062
10394
  (l) => `<tr><td><code>${escapeHtml(l.platform)}</code></td><td>${escapeHtml(l.rule)}</td><td><code>${escapeHtml(l.value)}</code></td></tr>`
9063
10395
  ).join("")}</tbody></table>`;
9064
10396
  }
@@ -9090,7 +10422,7 @@ function renderPreviewDocument(doc) {
9090
10422
  <main>
9091
10423
  ${doc.sections.map((s) => renderSection(s, assets)).join("\n")}
9092
10424
  ${renderUnpublished(doc.unpublished)}
9093
- ${renderChecks(doc.checks, doc.limits)}
10425
+ ${renderChecks(doc.checks, doc.limits, doc.limitsSource)}
9094
10426
  ${warnings}
9095
10427
  ${omitted === "" ? "" : `<section class="surface" id="assets" aria-labelledby="assets-title">
9096
10428
  <header><h2 id="assets-title">assets</h2><span class="tag">${doc.assets.length} embedded \xB7 ${doc.omitted.length} left out</span></header>
@@ -9108,7 +10440,7 @@ ${omitted === "" ? "" : `<section class="surface" id="assets" aria-labelledby="a
9108
10440
 
9109
10441
  // src/preview/privacy.ts
9110
10442
  import { existsSync as existsSync14, realpathSync as realpathSync4 } from "fs";
9111
- import { basename, join as join15, resolve as resolve5, sep } from "path";
10443
+ import { basename, join as join14, resolve as resolve5, sep } from "path";
9112
10444
  var ABSOLUTE_PATHS = [
9113
10445
  {
9114
10446
  name: "posix-home-or-system",
@@ -9317,13 +10649,13 @@ function countFacts(value) {
9317
10649
  return 0;
9318
10650
  }
9319
10651
  function contentSources(workspace) {
9320
- const root = join16(workspace, "content");
10652
+ const root = join15(workspace, "content");
9321
10653
  if (!existsSync15(root)) return [];
9322
10654
  const out = [];
9323
10655
  const walk = (dir) => {
9324
10656
  for (const name of readdirSync7(dir).sort()) {
9325
10657
  if (name.startsWith(".")) continue;
9326
- const path = join16(dir, name);
10658
+ const path = join15(dir, name);
9327
10659
  if (statSync7(path).isDirectory()) {
9328
10660
  walk(path);
9329
10661
  continue;
@@ -9374,7 +10706,7 @@ function inlineAssets(bundle) {
9374
10706
  omitted.push({ target: asset.target, reason: "source file is gone" });
9375
10707
  continue;
9376
10708
  }
9377
- const bytes = readFileSync13(asset.source);
10709
+ const bytes = readFileSync15(asset.source);
9378
10710
  total += bytes.byteLength;
9379
10711
  assets.push({
9380
10712
  target: asset.target,
@@ -9502,11 +10834,11 @@ function runPreview(args) {
9502
10834
  `fact-present not run for ${finding3}: no artifact on disk or no renderer for the flavor`
9503
10835
  );
9504
10836
  }
9505
- const outDir = args.outArg === void 0 ? join16(args.workspace, ".pasco", "preview") : resolve6(process.cwd(), args.outArg);
9506
- const file = join16(outDir, "index.html");
10837
+ const outDir = args.outArg === void 0 ? join15(args.workspace, ".pasco", "preview") : resolve6(process.cwd(), args.outArg);
10838
+ const file = join15(outDir, "index.html");
9507
10839
  if (outIsUnignoredInRepo(outDir, file)) {
9508
10840
  warnings.push(
9509
- `--out lands inside a git work tree and is not ignored \u2014 commit only if you mean to publish it (add "${relative4(resolve6(args.workspace), file)}" to .gitignore)`
10841
+ `preview output (${relative4(resolve6(args.workspace), file)}) lands inside a git work tree and is not ignored \u2014 commit only if you mean to publish it (add it to .gitignore)`
9510
10842
  );
9511
10843
  }
9512
10844
  const facts = countFacts(profile);
@@ -9527,11 +10859,12 @@ function runPreview(args) {
9527
10859
  rule: row.rule,
9528
10860
  value: JSON.stringify(row.value)
9529
10861
  })),
10862
+ limitsSource: catalogSource(profile),
9530
10863
  warnings
9531
10864
  });
9532
10865
  const sources = [
9533
10866
  ...contentSources(args.workspace),
9534
- ...existsSync15(join16(args.workspace, ".pascorc")) ? [{ role: "config", path: join16(args.workspace, ".pascorc") }] : [],
10867
+ ...existsSync15(join15(args.workspace, ".pascorc")) ? [{ role: "config", path: join15(args.workspace, ".pascorc") }] : [],
9535
10868
  ...inlined.sources
9536
10869
  ];
9537
10870
  const privacy = privacyFindings({
@@ -9627,8 +10960,8 @@ function runPreview(args) {
9627
10960
  }
9628
10961
 
9629
10962
  // src/commands/refine.ts
9630
- import { existsSync as existsSync16, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
9631
- import { join as join17 } from "path";
10963
+ import { existsSync as existsSync16, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
10964
+ import { join as join16 } from "path";
9632
10965
  import { parseDocument as parseDocument5, stringify as stringify3 } from "yaml";
9633
10966
  var REFINE_OWNER = "M3 SOTA research per platform \u2192 skills v1 + the full refine flow (SPEC \xA712/\xA716 M3); model-agnostic (D10/D11/D13)";
9634
10967
  function loadProfile4(workspace) {
@@ -9651,10 +10984,10 @@ function loadProfile4(workspace) {
9651
10984
  }
9652
10985
  }
9653
10986
  function recordVariant(workspace, platform, lang, variantId) {
9654
- const activePath = join17(workspace, "content", "active.yaml");
10987
+ const activePath = join16(workspace, "content", "active.yaml");
9655
10988
  let active = {};
9656
10989
  if (existsSync16(activePath)) {
9657
- const doc = parseDocument5(readFileSync14(activePath, "utf8"));
10990
+ const doc = parseDocument5(readFileSync16(activePath, "utf8"));
9658
10991
  if (doc.errors.length > 0) {
9659
10992
  throw new Error(
9660
10993
  `content/active.yaml: invalid YAML \u2014 ${doc.errors[0]?.message ?? "parse error"}`
@@ -9681,7 +11014,10 @@ function runRefine(args) {
9681
11014
  }
9682
11015
  const platform = parsePlatform(args.platformArg);
9683
11016
  const lang = parseLang(defaultsLang(args.pascorc, args.langArg));
9684
- const variants = parseVariants(args.variantsArg, args.pascorc.defaults?.variants ?? 3);
11017
+ const variants = parseVariants(
11018
+ args.variantsArg,
11019
+ args.pascorc.defaults?.variants ?? DEFAULT_VARIANTS
11020
+ );
9685
11021
  if (args.choose !== void 0 && !isSafeVariantId(args.choose)) {
9686
11022
  throw new UsageError(
9687
11023
  "--choose <variantId> must be a safe id (letters, digits, dot, underscore, dash; no slash or '..')"
@@ -9728,7 +11064,12 @@ function runRefine(args) {
9728
11064
  const slice = compile(profile, platform, lang);
9729
11065
  const identity = slice.identity;
9730
11066
  const links = (identity.links ?? []).map((l) => `${l.platform ?? "?"}:${l.url}`).join(", ");
9731
- const experience = slice.experience.map((e) => `${e.role} @ ${e.org} (${e.start}\u2013${e.end ?? "present"})`).join(" | ");
11067
+ const experience = slice.experience.map((e) => `${textOf(e.role, lang)} @ ${e.org} (${e.start}\u2013${e.end ?? "present"})`).join(" | ");
11068
+ const variantPrefix = platform.replace(/[^a-z]/g, "");
11069
+ const variantExamples = Array.from(
11070
+ { length: Math.min(variants, 3) },
11071
+ (_, i) => `${variantPrefix}-${lang}-${i + 1}`
11072
+ ).join(", ");
9732
11073
  const projects = slice.projects.map((p) => p.name).join(", ");
9733
11074
  const publications = slice.publications.map((p) => p.title).join(" | ");
9734
11075
  const anchors = slice.keywords.geo.associations?.join(", ") ?? "";
@@ -9744,9 +11085,9 @@ function runRefine(args) {
9744
11085
  `Language: ${lang}`,
9745
11086
  `Skill (communicative strategy \u2014 READ IT FIRST): ${skillLine(skill)}`,
9746
11087
  `Research (dated sources): ${researchPath}`,
9747
- `Variant id convention: ${platform.replace(/[^a-z]/g, "")}-${lang}-<n> (e.g. gh-en-1, gh-en-2, gh-en-3).`,
11088
+ `Variant id convention: ${variantPrefix}-${lang}-<n> (e.g. ${variantExamples}).`,
9748
11089
  ``,
9749
- `Facts (language-independent \u2014 MUST stay byte-identical across variants and platforms):`,
11090
+ `Facts (language-independent \u2014 MUST stay byte-identical across variants and platforms, except \`role\`: a langMap shown in ${lang}, falling back to en):`,
9750
11091
  ` name: ${identity.name}`,
9751
11092
  ` location: ${identity.location ?? "\u2014"}`,
9752
11093
  ` links: ${links || "\u2014"}`,
@@ -9783,11 +11124,11 @@ function runRefine(args) {
9783
11124
 
9784
11125
  // src/commands/skills-export.ts
9785
11126
  import { cpSync as cpSync2, existsSync as existsSync17, mkdirSync as mkdirSync7, readdirSync as readdirSync8, statSync as statSync8 } from "fs";
9786
- import { join as join18, resolve as resolve7, sep as sep2 } from "path";
11127
+ import { join as join17, resolve as resolve7, sep as sep2 } from "path";
9787
11128
  function countFiles(dir) {
9788
11129
  let total = 0;
9789
11130
  for (const entry of readdirSync8(dir, { withFileTypes: true })) {
9790
- if (entry.isDirectory()) total += countFiles(join18(dir, entry.name));
11131
+ if (entry.isDirectory()) total += countFiles(join17(dir, entry.name));
9791
11132
  else total += 1;
9792
11133
  }
9793
11134
  return total;
@@ -9865,7 +11206,7 @@ function runSkillsExport(args) {
9865
11206
 
9866
11207
  // src/commands/status.ts
9867
11208
  import { writeFileSync as writeFileSync11 } from "fs";
9868
- import { join as join20 } from "path";
11209
+ import { join as join19 } from "path";
9869
11210
 
9870
11211
  // src/report-shape.ts
9871
11212
  function summaryOf(data) {
@@ -9918,8 +11259,8 @@ function filterReport(reportData, platform) {
9918
11259
 
9919
11260
  // src/verify.ts
9920
11261
  import { createHash as createHash4 } from "crypto";
9921
- import { existsSync as existsSync18, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
9922
- import { join as join19 } from "path";
11262
+ import { existsSync as existsSync18, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
11263
+ import { join as join18 } from "path";
9923
11264
  import { parseDocument as parseDocument6, stringify as stringify4 } from "yaml";
9924
11265
 
9925
11266
  // src/capabilities.ts
@@ -9946,13 +11287,13 @@ var depsOf = (args, profile) => ({
9946
11287
  fetchText: args.deps?.fetchText ?? (async () => ({ status: 0 }))
9947
11288
  });
9948
11289
  function ledgerPathOf(workspace) {
9949
- return join19(workspace, "content", "deployments.yaml");
11290
+ return join18(workspace, "content", "deployments.yaml");
9950
11291
  }
9951
11292
  function readLedger2(args, kind) {
9952
11293
  const ledgerPath2 = ledgerPathOf(args.workspace);
9953
11294
  let ledger = {};
9954
11295
  if (existsSync18(ledgerPath2)) {
9955
- const doc = parseDocument6(readFileSync15(ledgerPath2, "utf8"));
11296
+ const doc = parseDocument6(readFileSync17(ledgerPath2, "utf8"));
9956
11297
  if (doc.errors.length > 0) {
9957
11298
  return {
9958
11299
  fail: report("status", false, EXIT.FAIL, {
@@ -10059,7 +11400,7 @@ function finishVerify(args, profile, verdicts, requests) {
10059
11400
  });
10060
11401
  }
10061
11402
  section(
10062
- `verified \u2014 ${requests} fetch(es) for ${verdicts.length} record(s)${args.platformArg !== void 0 ? ` (surface: ${args.platformArg})` : ""}`
11403
+ `checked ${verdicts.length} record(s) \u2014 ${requests} fetch(es)${args.platformArg !== void 0 ? ` (surface: ${args.platformArg})` : ""}`
10063
11404
  );
10064
11405
  for (const { key, verdict } of verdicts) {
10065
11406
  line(` ${key} \u2192 ${verdict.surface}: ${certToken(verdict.certification)}`);
@@ -10135,23 +11476,23 @@ async function verifySurface(surface, existing, capability, fetchText) {
10135
11476
  write: true
10136
11477
  };
10137
11478
  }
10138
- let digest;
11479
+ let digest2;
10139
11480
  if (response.text !== void 0) {
10140
11481
  try {
10141
- digest = digestOf(extractor(response.text));
11482
+ digest2 = digestOf(extractor(response.text));
10142
11483
  } catch {
10143
- digest = void 0;
11484
+ digest2 = void 0;
10144
11485
  }
10145
11486
  }
10146
11487
  const verdict = certificationVerdict(
10147
11488
  { certification: stored },
10148
- { status: response.status, ...digest !== void 0 ? { digest } : {} }
11489
+ { status: response.status, ...digest2 !== void 0 ? { digest: digest2 } : {} }
10149
11490
  );
10150
11491
  return {
10151
11492
  surface,
10152
11493
  certification: attemptRecord(verdict, {
10153
11494
  ...evidence,
10154
- ...digest !== void 0 ? { liveHash: digest } : {}
11495
+ ...digest2 !== void 0 ? { liveHash: digest2 } : {}
10155
11496
  }),
10156
11497
  fetched: true,
10157
11498
  write: true
@@ -10162,9 +11503,9 @@ async function verifySurface(surface, existing, capability, fetchText) {
10162
11503
  var CHECKLIST_HEADER = `# CHECKLIST \u2014 deploy status
10163
11504
 
10164
11505
  <!-- AUTO-GENERATED by \`pasco status --write-checklist\` (D18, SPEC \xA719) \u2014 do not edit by hand.
10165
- Regenerate after every deploy confirm: pasco status --mark <ledgerKey> <surface>.
11506
+ Regenerate after every deploy confirm: pasco status --mark <ledgerKey> <platform>.
10166
11507
  Source of truth: content/deployments.yaml (only \`pasco\` mutates it). -->`;
10167
- function loadOrReport2(args) {
11508
+ function loadOrReport3(args) {
10168
11509
  try {
10169
11510
  return { profile: load(args.workspace) };
10170
11511
  } catch (err) {
@@ -10234,13 +11575,13 @@ function markDeployed(args, profile) {
10234
11575
  }
10235
11576
  let deployedAt = (/* @__PURE__ */ new Date()).toISOString();
10236
11577
  if (args.markAt !== void 0) {
10237
- const at = new Date(args.markAt);
10238
- if (Number.isNaN(at.getTime())) {
11578
+ const at2 = new Date(args.markAt);
11579
+ if (Number.isNaN(at2.getTime())) {
10239
11580
  return report("status", false, EXIT.FAIL, {
10240
11581
  error: { kind: "mark", message: `--at "${args.markAt}" is not a valid ISO 8601 date-time` }
10241
11582
  });
10242
11583
  }
10243
- deployedAt = at.toISOString();
11584
+ deployedAt = at2.toISOString();
10244
11585
  }
10245
11586
  const read = readLedger2(args, "mark");
10246
11587
  if ("fail" in read) return read.fail;
@@ -10466,7 +11807,7 @@ async function runStatus(args) {
10466
11807
  }
10467
11808
  });
10468
11809
  }
10469
- const loaded = loadOrReport2(args);
11810
+ const loaded = loadOrReport3(args);
10470
11811
  if ("fail" in loaded) return loaded.fail;
10471
11812
  const profile = loaded.profile;
10472
11813
  if (args.markKey !== void 0) return markDeployed(args, profile);
@@ -10479,7 +11820,7 @@ async function runStatus(args) {
10479
11820
  const publication = publicationOf(args, profile, reportData);
10480
11821
  if (args.writeChecklist === true) {
10481
11822
  writeFileSync11(
10482
- join20(args.workspace, "CHECKLIST.md"),
11823
+ join19(args.workspace, "CHECKLIST.md"),
10483
11824
  checklistBody(reportData, completion, profile, publicationAll),
10484
11825
  "utf8"
10485
11826
  );
@@ -10547,23 +11888,40 @@ async function runStatus(args) {
10547
11888
  line(` coverage: ${publicationCoverageLine(publication)}`);
10548
11889
  }
10549
11890
  line(
10550
- " \u2192 pasco status --mark <ledgerKey> <surface> confirms a paste; --verify [platform] runs the opt-in C1 live check; --write-checklist regenerates CHECKLIST.md"
11891
+ " \u2192 pasco status --mark <ledgerKey> <platform> confirms a paste; --verify [platform] runs the opt-in C1 live check; --write-checklist regenerates CHECKLIST.md"
10551
11892
  );
10552
11893
  return EXIT.OK;
10553
11894
  }
10554
11895
 
10555
11896
  // src/commands/validate.ts
11897
+ function unrecognizedKeyHint(issues, floor) {
11898
+ if (floor?.declared != null && !floor.met) return null;
11899
+ if (!issues.some((issue2) => issue2.message.trim().toLowerCase().startsWith("unrecognized key"))) {
11900
+ return null;
11901
+ }
11902
+ return `hint: the data has a field this engine does not know \u2014 the data may be newer than this CLI; upgrade with \`${ENGINE_UPGRADE_COMMAND}\`, then run \`pasco doctor\``;
11903
+ }
11904
+ function geoSectionLine(s) {
11905
+ return ` ${s.label}: ${s.applicable === false ? "n/a" : `${s.score}/100`} \u2014 ${s.detail}`;
11906
+ }
10556
11907
  function runValidate(args) {
10557
11908
  let profile;
10558
11909
  try {
10559
11910
  profile = load(args.workspace);
10560
11911
  } catch (err) {
10561
11912
  if (err instanceof SchemaValidationError) {
11913
+ const declared = readWorkspacePascorc(args.workspace).engine;
11914
+ const floor = args.floor ?? {
11915
+ declared: typeof declared === "string" ? declared : null,
11916
+ met: true
11917
+ };
11918
+ const hint = unrecognizedKeyHint(err.issues, floor);
10562
11919
  return report("validate", false, EXIT.FAIL, {
10563
11920
  error: {
10564
11921
  kind: "schema",
10565
11922
  message: `schema validation failed (D12 fail-on-schema) \u2014 ${err.issues.length} violation(s)`,
10566
- violations: [...err.issues]
11923
+ violations: [...err.issues],
11924
+ ...hint === null ? {} : { hint }
10567
11925
  }
10568
11926
  });
10569
11927
  }
@@ -10587,19 +11945,26 @@ function runValidate(args) {
10587
11945
  if (drift.length === 0) {
10588
11946
  line("no drift warnings");
10589
11947
  } else {
10590
- for (const issue of drift) {
10591
- const where = issue.path ? ` (${issue.path})` : "";
10592
- const surfaces = issue.surfaces?.length ? ` [${issue.surfaces.join(", ")}]` : "";
10593
- line(` warn: [${issue.code}]${surfaces} ${issue.message}${where}`);
11948
+ for (const issue2 of drift) {
11949
+ const where = issue2.path ? ` (${issue2.path})` : "";
11950
+ const surfaces = issue2.surfaces?.length ? ` [${issue2.surfaces.join(", ")}]` : "";
11951
+ line(` warn: [${issue2.code}]${surfaces} ${issue2.message}${where}`);
10594
11952
  }
10595
11953
  line(` ${drift.length} warning(s) \u2014 exit 0 (D12: facts warn, schema fails)`);
10596
11954
  }
10597
11955
  section("geo");
11956
+ line(
11957
+ ` basis: heuristic index over the declared fields (${geo.basis}) \u2014 not a measurement of citations`
11958
+ );
10598
11959
  line(` overall score: ${geo.score}/100`);
10599
11960
  for (const s of Object.values(geo.sections)) {
10600
- line(` ${s.label}: ${s.score}/100 \u2014 ${s.detail}`);
11961
+ line(geoSectionLine(s));
10601
11962
  }
10602
11963
  for (const c of geo.checks) {
11964
+ if (c.applicable === false) {
11965
+ line(` n/a: [${c.code}] ${c.detail}`);
11966
+ continue;
11967
+ }
10603
11968
  if (c.score > 0) continue;
10604
11969
  const prefix = c.severity === "info" ? "info" : "warn";
10605
11970
  line(` ${prefix}: [${c.code}] ${c.detail}`);
@@ -10608,84 +11973,96 @@ function runValidate(args) {
10608
11973
  }
10609
11974
 
10610
11975
  // src/index.ts
10611
- var VERSION = (() => {
10612
- try {
10613
- const pkg = JSON.parse(readFileSync16(new URL("../package.json", import.meta.url), "utf8"));
10614
- return pkg.version;
10615
- } catch {
10616
- return "0.0.0";
10617
- }
10618
- })();
11976
+ var VERSION = CLI_VERSION;
10619
11977
  var GLOBAL_HELP = `pasco \u2014 profile-as-code CLI v${VERSION}
10620
11978
 
11979
+ The cycle:
11980
+ pasco init <dir> scaffold a workspace (content/, README.md, .pascorc)
11981
+ edit content/*.yaml the single source of truth \u2014 edit these, not profiles/
11982
+ pasco validate schema + drift + geo (warnings)
11983
+ pasco build [platform] write the per-platform artifacts
11984
+
11985
+ Each command's tag says where it sits in the cycle: [write] \xB7 [read] \xB7 [check].
11986
+ The rules behind it (fact-present, build's order, char caps): README.md in the installed package.
11987
+
10621
11988
  Commands:
10622
- init [dir] Scaffold a ${WORKSPACE_NAME} workspace (content/, README.md, .pascorc)
10623
- validate [--json] Schema (FAIL, exit 1) + drift/geo (WARN, exit 0) \u2014 D12
10624
- doctor [--json] Engine \xB7 schema \xB7 workspace on one line (SPEC \xA77.6, #348):
11989
+ init [dir] [write] Scaffold a ${WORKSPACE_NAME} workspace (content/, README.md, .pascorc)
11990
+ validate [--json] [check] Schema (FAIL, exit 1) + drift/geo (WARN, exit 0) \u2014 D12
11991
+ doctor [--json] [check] Engine \xB7 schema \xB7 workspace on one line (SPEC \xA77.6, #348):
10625
11992
  installed version, the profile contract it validates,
10626
11993
  and whether the workspace loads; also reports the
10627
11994
  .pascorc engine requirement (unmet \u21D2 exit 1)
10628
- build [platform] [--lang] Deterministic base texts per platform+lang
10629
- preview [--out <dir>] ONE self-contained HTML page of the rendered surfaces
11995
+ build [platform] [--lang] [write] Deterministic base texts per platform+lang
11996
+ preview [--out <dir>] [read] ONE self-contained HTML page of the rendered surfaces
10630
11997
  [--no-open] [--lang <l>] (inline CSS, no JS, no external asset) written to
10631
11998
  <workspace>/.pasco/preview and opened locally; --out writes
10632
11999
  elsewhere and never opens (the CI-artifact transport). Warns when
10633
12000
  the destination sits in a repo without an ignore entry.
10634
12001
  Not a publication
10635
- edit <contract-path> Resolve one contract field; --set writes it (comment-preserving,
12002
+ edit <contract-path> [write] Resolve one contract field; --set writes it (comment-preserving,
10636
12003
  [--set <text> [--check]] validated, atomic) and --set --check previews the published lines
10637
- diff [--json] Changes since the last release tag (git-based \u2192 CHANGELOG hint)
10638
- refine <platform> [--lang] PARTIAL: emit agent task template; --choose <id> records the
12004
+ edit --list [<block>] [read] Every addressable contract path of the workspace, one per line
12005
+ diff [--json] [read] Changes since the last release tag (git-based \u2192 CHANGELOG hint)
12006
+ refine <platform> [--lang] [write] PARTIAL: emit agent task template; --choose <id> records the
10639
12007
  [--variants N] chosen variant in content/active.yaml (agent-driven, D11/D13;
10640
12008
  full variant flow M3)
10641
- check [platform] Executes offline rules: brand handle matrix (M6/\xA717,
12009
+ check [platform] [check] Executes offline rules: brand handle matrix (M6/\xA717,
10642
12010
  brand flavor), registry checks (M7/\xA718.8) and
10643
12011
  media-transport rules (M8/\xA718.6) and max-chars/min-chars
10644
12012
  over headlines/bios (offline, #240) and fact-present
10645
12013
  (the built surface's facts vs the SSOT, #283);
10646
12014
  remaining rows M3/M4
10647
- audit [--json] Offline diagnostic projection: findings only (no score,
12015
+ audit [--json] [check] Offline diagnostic projection: findings only (no score,
10648
12016
  no prioritisation) in three fixed classes \u2014 dangling-ref,
10649
12017
  contradiction, unsourced-claim; stricter than
10650
12018
  validate/check on those classes; exit 1 iff any FAIL
10651
- discover <url>... [--json] Provisional profile from public surfaces: reads each
10652
- surface's schema.org JSON-LD + links, writes nothing and
12019
+ discover <url>... [--json] [read] Provisional profile from public surfaces: reads each
12020
+ [--observations <file>]
12021
+ surface's schema.org Person/Organization JSON-LD + links
12022
+ (a GitHub profile has no such node), writes nothing and
10653
12023
  marks every fact unattested; reports the audit classes
10654
- (dangling-ref \xB7 contradiction \xB7 unsourced-claim)
10655
- entity [--json] [--jsonld] The schema.org entity projection: facts, @id, sameAs,
12024
+ (dangling-ref \xB7 contradiction \xB7 unsourced-claim). A
12025
+ surface the machine cannot read gets a hand-read row
12026
+ via --observations (YAML: surface, field, value, url,
12027
+ retrieved_at) \u2014 no request, same audit
12028
+ entity [--json] [--jsonld] [read] The schema.org entity projection: facts, @id, sameAs,
10656
12029
  [--out <file> [--force]] [--bare] [--lang <l>]
10657
12030
  readiness gaps and the FAQPage node as JSON, or the
10658
12031
  paste-ready JSON-LD blocks (entity + FAQPage when faq[]
10659
12032
  is declared) for surfaces the engine does not render.
10660
12033
  Exit 1 when @id is absent (no declared home)
10661
- cite-check [<answers.ndjson>] Match the workspace's entity against saved answer-engine
12034
+ cite-check [<answers.ndjson>] [check] Match the workspace's entity against saved answer-engine
10662
12035
  [--json] answers (NDJSON; stdin when no path). Offline, no model,
10663
12036
  no clock: per answer it reports cited \xB7 confidence \xB7 spans
10664
12037
  \xB7 reasons, never a rate; exit 2 on malformed NDJSON
10665
- post <subcommand> MVP (week-1 experiment): seed \u2192 task bundle \u2192 choose \u2192
12038
+ post <subcommand> [write] MVP (week-1 experiment): seed \u2192 task bundle \u2192 choose \u2192
10666
12039
  check \u2192 optional Postiz draft payload. No LLM call.
10667
12040
  Subcommands: seed \xB7 task \xB7 choose \xB7 check \xB7 export \xB7 import \xB7 list
10668
- status [platform] [--verify] Per-entry deploy report \u2014 origin \xB7 surfaces \xB7 in-sync/stale/never
12041
+ status [platform] [--verify] [read] Per-entry deploy report \u2014 origin \xB7 surfaces \xB7 in-sync/stale/never
10669
12042
  + certification (D18/\xA719.5); --verify runs the opt-in C1
10670
12043
  live check (the engine's ONE network path, \xA719.5)
10671
- presence [--json] The presence board (D23/\xA722): every surface a release
12044
+ presence [--json] [read] The presence board (D23/\xA722): every surface a release
10672
12045
  has addressed, at its newest release, with the
10673
12046
  ledger-derived state (in-sync/stale/never)
10674
- release plan [id] [--json] One release's ordered fan-out (repo-meta \u2192 profile \u2192
12047
+ release plan [id] [--json] [read] One release's ordered fan-out (repo-meta \u2192 profile \u2192
10675
12048
  distribution), each surface's state and the per-kind
10676
12049
  factual spine; the copy lives in the shipped skills
10677
- export ai-policy Write the AI-policy artifacts (SPEC \xA77.3, M9): robots.txt
12050
+ achievements [--targets] [read] The career impact log (D24/\xA723): every row's DERIVED
12051
+ [--json] status (planned \xB7 measured \xB7 achieved \xB7 partial), the
12052
+ formatted metric and how the result is known (basis);
12053
+ --targets prints only the planned goals
12054
+ export ai-policy [write] Write the AI-policy artifacts (SPEC \xA77.3, M9): robots.txt
10678
12055
  [--out <dir> | --check] [--lang <l>] (two named groups \u2014 listed retrieval crawlers
10679
12056
  allowed, listed training crawlers disallowed, no wildcard) +
10680
12057
  llms.txt (the workspace index) + AI-POLICY.md, into <dir>;
10681
12058
  --check (exit 1) fails an unsourced, stale or incomplete entry
10682
- export profile [--json] The normalized profile for programmatic consumers (SPEC \xA77.5,
12059
+ export profile [--json] [read] The normalized profile for programmatic consumers (SPEC \xA77.5,
10683
12060
  [--now <iso8601>] #348): { schemaVersion, engine, source: { commit, generatedAt },
10684
12061
  profile } \u2014 every root the workspace declares, plus every list
10685
12062
  root defaulted to [], from the same load() the other commands
10686
12063
  use. No clock is read: generatedAt is null unless --now pins
10687
12064
  it. Offline; byte-identical across runs
10688
- skills:export <dir> Copy the skill channels this package ships into <dir>
12065
+ skills:export <dir> [write] Copy the skill channels this package ships into <dir>
10689
12066
  [--force] [--list] (--force overlays; --list shows them); no harness paths
10690
12067
 
10691
12068
  Common flags (after the command):
@@ -10705,7 +12082,8 @@ NDJSON, never a silent skip). Drift warnings exit 0 (D12: facts warn, schema fai
10705
12082
  pasco is free (MIT). Consider a tip ;)
10706
12083
  https://ko-fi.com/christianluciani \xB7 https://github.com/sponsors/ChristianLuciani
10707
12084
 
10708
- See packages/cli/README.md for details.
12085
+ See README.md in the installed package, or
12086
+ https://github.com/ChristianLuciani/profile-as-code for details.
10709
12087
  `;
10710
12088
  function parseCommandFlags(args, extra = {}) {
10711
12089
  let values;
@@ -10730,20 +12108,37 @@ function parseCommandFlags(args, extra = {}) {
10730
12108
  return { flags: values, positionals };
10731
12109
  }
10732
12110
  var COMMAND_HELP = {
10733
- discover: `pasco discover <url>... [--json] [--allow-host <hosts>]
12111
+ discover: `pasco discover <url>... [--json] [--allow-host <hosts>] [--observations <file>]
10734
12112
  Read public surfaces and build a PROVISIONAL profile so the audit's checks can
10735
12113
  run for a prospect who has no workspace. Every populated fact carries its
10736
12114
  source (surface, URL, retrieval time) and attested:false; fields that could not
10737
12115
  be discovered are listed as absent, never defaulted. Facts come only from the
10738
- surfaces' schema.org JSON-LD and their links - never prose mining.
12116
+ surfaces' schema.org JSON-LD and their links - never prose mining. A surface
12117
+ QUALIFIES only when it declares a schema.org Person or Organization node: a
12118
+ GitHub profile carries none, and neither does a product/application page
12119
+ (SoftwareApplication), so point discover at a personal site or page that does.
12120
+ An unsourced quantitative sentence is reported only when it is ABOUT the entity
12121
+ (it names it or its handle, or is first-person) - a badge counter or a nameless
12122
+ footer is not a claim about the person. Identical sentences are reported once.
12123
+ A surface the machine cannot read (a wall: http-999/403; no Person node) is a
12124
+ job for a human: --observations takes a YAML sequence of rows the operator read
12125
+ by hand, each with surface, field (name | link | experience), value, url and
12126
+ retrieved_at. Those rows make NO request - their URL is a provenance label - and
12127
+ they enter the same provisional profile, so the same classes run over them.
12128
+ Two surfaces that declare different names, or experience rows that disagree,
12129
+ are reported as a contradiction; what the machine-read surfaces cannot supply
12130
+ (a page text for unsourced-claim) is simply not part of a manual row.
10739
12131
  Requests are sequential, never retried, credential-free and confined to a
10740
12132
  closed host allowlist: the platform hosts the engine ships, plus --allow-host
10741
12133
  (comma-separated). A URL outside it is reported as
10742
12134
  host-not-allowed with NO request made. Nothing is written: no content/, no
10743
12135
  ledger, no artifact.
10744
- --json machine-readable report: surfaces, unchecked URLs, the
10745
- provisional profile, its per-fact provenance and the findings
12136
+ --json machine-readable report: surfaces (with via: fetch|manual),
12137
+ unchecked URLs, the provisional profile, its per-fact provenance
12138
+ and the findings
10746
12139
  --allow-host extra hostnames the operator declares, comma-separated
12140
+ --observations YAML rows read by hand (no request made); refused with the row
12141
+ number when a row is malformed
10747
12142
  Exit 1 iff a FAIL finding exists (dangling-ref or contradiction).`,
10748
12143
  init: `pasco init [dir] [--flavor person|brand]
10749
12144
  Scaffold a ${WORKSPACE_NAME} workspace skeleton (SPEC \xA79): content/identity.yaml,
@@ -10754,7 +12149,9 @@ README.md, .pascorc. Minimal but pasco validate-clean. Refuses to overwrite.
10754
12149
  dir target directory (default: current directory)`,
10755
12150
  validate: `pasco validate [--json] [--workspace <path>]
10756
12151
  Load + validate the workspace (D12): schema violations FAIL (exit 1); drift and
10757
- GEO are warnings (exit 0). See schema/README.md "Validation semantics".
12152
+ GEO are warnings (exit 0). See
12153
+ https://github.com/ChristianLuciani/profile-as-code/blob/main/schema/README.md
12154
+ "Validation semantics".
10758
12155
  A .pascorc "engine" requirement is enforced BEFORE the load: an installed engine
10759
12156
  older than the declared minimum fails with the installed version, the required
10760
12157
  version and the upgrade command (SPEC \xA77.6).`,
@@ -10764,8 +12161,8 @@ Engine \xB7 schema \xB7 workspace on one line (SPEC \xA77.6, #348):
10764
12161
  schema the profile contract version this build validates (from the schema $id)
10765
12162
  workspace the resolved path, whether it loads, and the effective .pascorc
10766
12163
  engine requirement (the strictest declaration in every config the
10767
- resolution consulted, with the file it came from \xB7 met / NOT MET /
10768
- INVALID \xB7 upgrade command)
12164
+ resolution consulted \xB7 absent when none declares one \xB7 met / NOT
12165
+ MET / INVALID, with the declaring file and the upgrade command)
10769
12166
  It writes nothing, reads no clock and touches no network. Exit: 0 healthy
10770
12167
  (workspace loads and any declared engine requirement is met) \xB7 1 unhealthy (the
10771
12168
  workspace fails to load, or the requirement is unmet/malformed) \xB7 2 usage. It
@@ -10788,17 +12185,48 @@ is left out of the page and listed (never inlined); a remote image becomes a tex
10788
12185
  build: `pasco build [platform] [--view <name>] [--lang <l>] [--json] [--workspace <path>]
10789
12186
  Deterministic base texts per platform+lang (compile() slices). No platform =
10790
12187
  all platforms. lang defaults to .pascorc defaults.lang or 'en'.
12188
+ The artifact path carries no lang: one path per surface, so building another
12189
+ lang OVERWRITES the artifact the previous build wrote (one README per repo on
12190
+ GitHub; for the paste surfaces, re-build before pasting the other language).
10791
12191
  A .pascorc "engine" requirement is enforced before any render (SPEC \xA77.6).
10792
12192
  --view resolves a declared view (D22/\xA721): its rule may carry the platform, the
10793
12193
  lang and the fact selection, so the surface shows a SUBSET of the SSOT. Every
10794
12194
  omission is reported as a warning; an addressed key that resolves to nothing is
10795
12195
  an error. No selection policy = byte-identical output.`,
10796
12196
  edit: `pasco edit <contract-path> [--set <text> [--check]] [--json] [--workspace <path>]
12197
+ pasco edit --list [<block>] [--json] [--workspace <path>]
12198
+ pasco edit --create <root> [--check] [--json] [--workspace <path>]
10797
12199
  Resolve ONE contract field (epic #247, S1\u2013S5 \u2014 read \xB7 preview \xB7 write \xB7 surface impact \xB7 cap pre-flight \xB7 agent contract):
10798
12200
  positioning.statement.<lang>
10799
12201
  headlines[platform=<p>,lang=<l>].text
10800
12202
  bios[platform=<p>,lang=<l>,length=<len>].text
12203
+ identity.definition.<lang>
12204
+ faq[id=<slug>,lang=<l>].q | .a
12205
+ faq[id=<slug>].source
10801
12206
  identity.links[platform=<p>,label=<l>].url (read-only; label optional)
12207
+ --list is the read-only discovery surface (#411): it prints every addressable row
12208
+ of this workspace, one per line, built by the same constructors resolution uses \u2014
12209
+ no second grammar. <block> scopes it to positioning | headlines | bios |
12210
+ identity.definition | identity.links | faq; an unknown block, --set, --check,
12211
+ --create or an address next to it is usage (exit 2). A link row whose platform
12212
+ cannot be expressed as a selector is a warning; a label the grammar cannot
12213
+ express is dropped and the row is listed by its platform when that resolves to
12214
+ one row (the warning says so). Any address that resolves to more than one row
12215
+ (a duplicate headline/bio, a repeated faq id, a label-less link sharing its
12216
+ platform) is a warning too \u2014 never printed as a path the command would reject.
12217
+ It never locates a CST node, takes the edit lock or writes; an empty workspace
12218
+ prints the header alone (exit 0). --json payload { addresses: [{ address, kind,
12219
+ readOnly }], warnings }.
12220
+ --create <root> seeds a root the workspace does NOT declare yet \u2014 a DIFFERENT
12221
+ operation from the splice: the splice never invents bytes and --create never
12222
+ overwrites one. It writes a schema-valid template (--create faq, or the file form
12223
+ --create content/faq.yaml), refuses when the root already exists under any
12224
+ extension case (idempotent: nothing is written), and runs the same validation
12225
+ (load()+schema over a temp copy) and cap pre-flight as --set. --check previews
12226
+ the seed: it validates it and writes nothing. Seedable roots:
12227
+ ${SEEDABLE_ROOTS.join(", ")} (3 placeholder pairs, the schema minimum) \u2014 replace every <placeholder> with the owner's own
12228
+ facts, never invented copy. --json payload { mode, root, file, created, preview,
12229
+ seed, limits, warnings }.
10802
12230
  Without --set it prints the current value. With --set it WRITES the value \u2014 a
10803
12231
  byte-range splice over the original text (the CST locates, it never
10804
12232
  re-serializes), so comments, layout and CRLF survive and the diff is one line;
@@ -10814,16 +12242,24 @@ failure is only detected by the write.
10814
12242
  With --set --check it PREVIEWS instead: it renders the surface the way
10815
12243
  \`pasco build\` does \u2014 active variant (#238) \u2192 renderer \u2192 shared paste source,
10816
12244
  current vs proposed \u2014 and prints the artifact lines the edit changes, saying so
10817
- (with the reason) when the edit does not reach the artifact.
12245
+ (with the reason) when the edit does not reach the artifact. A faq[] value has no
12246
+ pasco build artifact: its published surface is the FAQPage node of
12247
+ \`pasco entity --jsonld\` (#360), and --check previews that node's bytes.
12248
+ identity.definition reaches the entity JSON-LD that github-readme/github-org
12249
+ embed, so its preview is the artifact's JSON-LD block.
10818
12250
  Every run against a resolved, writable address also prints the edited unit's
10819
12251
  SURFACE IMPACT from the \xA719 ledger: each
10820
12252
  recorded surface prints stale (the edit changed the hash) or in-sync (it still
10821
12253
  matches), and a unit with no record rolls up to never \u2014 derived, never stored.
10822
- The block also carries the measured drift count.
12254
+ faq[] has no \xA719 unit, so it prints no surface-impact block (rather than naming a
12255
+ fabricated one). The block also carries the measured drift count.
10823
12256
  A headline/bio value is pre-flighted against the catalog's own max-chars cap (the
10824
12257
  workspace's checks.yaml, else the sourced caps shipped with pasco) and prints the
10825
12258
  cap, its cited source and tier; over-cap is a WARNING (D12), never a block, and a platform
10826
- with no sourced cap reads \`cap: unknown\`.
12259
+ with no sourced cap reads \`cap unknown\`. identity.definition and faq[].q|.a are
12260
+ pre-flighted against the contract's own caps (160 / 90 / 320 chars per value,
12261
+ #288): over one is a schema failure (D12), so the pre-flight warns and the write
12262
+ is refused by validation.
10827
12263
  An active variant (#238) on the addressed platform is NAMED (edit never chooses the
10828
12264
  live variant \u2014 that is refine --choose).
10829
12265
  \`--json\` payload { address, current, proposed, rendered, ok, noop, written,
@@ -10831,12 +12267,15 @@ warnings, stale, state, drift, limit, over, variant, reason } inside the usual
10831
12267
  envelope
10832
12268
  (command \xB7 ok \xB7 exitCode);
10833
12269
  warnings carries a non-blocking finding the write proceeded through.
10834
- Exit: 0 a read / written / noop / the preview changes the artifact \xB7 1 the address
10835
- did not resolve to exactly one row, the target is read-only or changed under the
10836
- command (stale \u2014 nothing was written), the validation refused the result, or
10837
- (preview) the artifact does not change \xB7 2 usage (malformed address, unknown
10838
- platform or language, read-only field, block or multi-line scalar, anchored/aliased
10839
- scalar, a value with a line break, an empty --set, --check without --set).`,
12270
+ Exit: 0 a read / written / noop / the preview changes the artifact / the seed was
12271
+ written (or previewed) \xB7 1 the address did not resolve to exactly one row, the
12272
+ target is read-only or changed under the command (stale \u2014 nothing was written),
12273
+ --create found the root already present (nothing was written), the validation
12274
+ refused the result, or (preview) the artifact does not change \xB7 2 usage
12275
+ (malformed address, unknown platform or language, read-only field, block or
12276
+ multi-line scalar, anchored/aliased scalar, a value with a line break, an empty
12277
+ --set, --check without --set, --create with no seedable root or next to --set /
12278
+ --list / an address).`,
10840
12279
  diff: `pasco diff [--json] [--workspace <path>]
10841
12280
  Git-based: what changed since the last tag matching release-* (.pascorc
10842
12281
  diff.tagPattern) \u2014 commits + changed content files \u2192 CHANGELOG hint.`,
@@ -10856,8 +12295,7 @@ carries and the other does not, a URL the surface adds, prose no token
10856
12295
  explains), else WARN (D12). A declared surface with no artifact reports the
10857
12296
  rule as not run, never OK; declared: [] says no catalog declares the premise.
10858
12297
  Remaining rows in content/checks.yaml ship with the skills checks.yaml union
10859
- (M3) and the full pasco check (M4). Prints its owner line
10860
- (src/commands/check.ts).`,
12298
+ (M3) and the full pasco check (M4).`,
10861
12299
  audit: `pasco audit [--json] [--workspace <path>]
10862
12300
  The offline diagnostic projection over the identity (SPEC \xA77.1, issue #283):
10863
12301
  findings only \u2014 no score, no prioritisation, no branding (those belong to the
@@ -10873,8 +12311,10 @@ URL resolves \u2014 canonicalization, which validate warns about) and geoCheck's
10873
12311
  coverage codes.
10874
12312
  Composes load (schema), driftCheck with validate's #238 variant-store
10875
12313
  reconciliation, geoCheck (the faq-coverage premise gate only), the
10876
- completeness evidence premise and fact-present (the built surface's facts vs
10877
- the SSOT render, symmetric; it runs only where the catalog declares it).
12314
+ completeness evidence premise, the achievements evidence posture (a public
12315
+ measured/attested row with no pointer is unsourced-claim, SPEC \xA723.3) and
12316
+ fact-present (the built surface's facts vs the SSOT render, symmetric; it runs
12317
+ only where the catalog declares it).
10878
12318
  Touches no network, and no finding derives from content/deployments.yaml.
10879
12319
  --json emits { schemaVersion: "1", scope: { catalog, declared, checked, notRun },
10880
12320
  findings: [ { class, severity, path, detail } ] }. scope is the artifact side:
@@ -10894,8 +12334,8 @@ the schema.org entity the renderers embed, exposed as an artifact.
10894
12334
  is null when no link declares role: identity \u2014 the engine never invents
10895
12335
  an @id. gaps are the structured-data geoCheck codes that scored below 1,
10896
12336
  verbatim. faqJsonLd is the FAQPage node when faq[] is declared, else null.
10897
- --jsonld the SAME assembler output the renderers embed (core/src/structured-data.ts,
10898
- one home \u2014 never a second emitter), serialized as paste-ready
12337
+ --jsonld the SAME assembler output the renderers embed (the engine's single
12338
+ assembler, one home \u2014 never a second emitter), serialized as paste-ready
10899
12339
  <script type="application/ld+json"> block(s): the entity block, plus the
10900
12340
  FAQPage block when faq[] is declared (never an empty FAQPage, no invented
10901
12341
  @id; faq[].source travels as acceptedAnswer.citation).
@@ -11019,6 +12459,10 @@ overlaid, nothing is deleted). No harness-specific destination is invented.`,
11019
12459
  post: `pasco post <seed|task|choose|check|export|import|list> [...] [--json] [--workspace <path>]
11020
12460
  MVP (week-1 experiment): a post is a PROJECTION of a seed, not an identity fact.
11021
12461
  Two seed formats (#266): short (distribution) and longform (the essay/newsletter).
12462
+ Short rules ship for: linkedin, x
12463
+ Long-form rules ship for: devto, ghost, hashnode, listmonk, medium, substack, wordpress
12464
+ (long-form needs --format longform; short is the default. A channel in neither
12465
+ list has no post rule \u2014 the usage error names the rule file it wanted.)
11022
12466
  seed <id> --text <file> [--format short|longform]
11023
12467
  create posts/<id>/seed.md + post.yaml
11024
12468
  task <id> --channel <c1,c2> [--variants N] [--format short|longform]
@@ -11037,9 +12481,28 @@ Two seed formats (#266): short (distribution) and longform (the essay/newsletter
11037
12481
  API channels \u2192 Postiz POST /posts payload
11038
12482
  (integration IDs from postiz.yaml; REPLACE_ME
11039
12483
  when missing); paste channels \u2192 paste-checklist.md
11040
- import <dir> --channel <c> import historical .md posts as seeds
12484
+ import <dir> --channel linkedin|x import historical .md posts as seeds (a
12485
+ short-rule channel); each is recorded
12486
+ published by assumption (certification:
12487
+ unverifiable, url: null; no URL-recording
12488
+ command yet)
11041
12489
  list list seeds, formats, derivedFrom and chosen variants`,
11042
- status: `pasco status [platform] [--mark <ledgerKey> <surface>] [--at <iso8601>] [--verify] [--write-checklist] [--completeness] [--json] [--workspace <path>]
12490
+ achievements: `pasco achievements [--targets] [--json] [--workspace <path>]
12491
+ The career impact log (D24, SPEC \xA723, issue #446): one row per RESULT \u2014 a metric
12492
+ or a milestone \u2014 with the units it belongs to (refs), how the result is known
12493
+ (basis: measured | estimated | reported | attested) and its required visibility.
12494
+ The status is DERIVED from the numbers, never stored: planned (a target with no
12495
+ value) \xB7 measured (a value with no goal) \xB7 achieved (the direction is met) \xB7
12496
+ partial (both numbers, goal missed). A milestone has no status. Rows are grouped
12497
+ by their first ref; Unattached collects the rest.
12498
+ --targets prints only the planned rows (the goal view).
12499
+ --json emits { schemaVersion: "1", scope, achievements: [...] } where each row
12500
+ carries its derived status (null for a milestone) and scope counts the rows this
12501
+ view returned \u2014 an empty scope reads as "nothing declared here", never as a
12502
+ fabricated zero.
12503
+ Exit: 0 a successful read \xB7 1 the workspace does not load (a dangling refs[]
12504
+ entry fails load, the same class as a dangling artifactRefs).`,
12505
+ status: `pasco status [platform] [--mark <ledgerKey> <platform>] [--at <iso8601>] [--verify] [--write-checklist] [--completeness] [--json] [--workspace <path>]
11043
12506
  Per-entry deploy-status report (D18, \xA719): ledger key \xB7 origin (template/user) \xB7
11044
12507
  surfaces published \xB7 sync state (in-sync / stale / never) \xB7 certification
11045
12508
  (verified / attested / unverifiable / out-of-band-drift, \xA719.5) \u2014 plus untracked
@@ -11048,11 +12511,11 @@ It ends with the DERIVED publication truth per surface (SPEC \xA720.1, #242):
11048
12511
  <surface>: <sync> \xB7 artifact: <matches-SSOT|diverges|absent> \xB7 limits: <ok|over(cap)>
11049
12512
  \u2014 the artifact on disk vs what the SSOT renders TODAY (offline; offline cap
11050
12513
  warnings carry their source). Derived, never stored: nothing here writes.
11051
- --mark <ledgerKey> <surface> confirm a manual paste: records deployedAt + the
12514
+ --mark <ledgerKey> <platform> confirm a manual paste: records deployedAt + the
11052
12515
  CURRENT contentHash in content/deployments.yaml (only pasco writes it,
11053
12516
  D14) plus certification.state attested \u2014 and the artifactHash ANCHOR when
11054
12517
  the surface can produce one. --at overrides the timestamp. The anchor is
11055
- the digest of profiles/<surface>/README.md (what \`pasco build\` wrote, so
12518
+ the digest of profiles/<platform>/README.md (what \`pasco build\` wrote, so
11056
12519
  \`--lang\` is honoured); with no build artifact on disk it renders at
11057
12520
  .pascorc defaults.lang instead.
11058
12521
  --verify [platform] \xA719.5 C1: the opt-in live check \u2014 the engine's ONE
@@ -11096,7 +12559,8 @@ the command prints the per-kind FACTUAL SPINE derived from the release row
11096
12559
  shipped per-platform skills, never in the engine (rule 7).
11097
12560
  <release-id> selects a release; absent = the newest by date (then id).
11098
12561
  --json emits { schemaVersion: "1", release, surfaces, baseText } inside the
11099
- usual envelope.
12562
+ usual envelope; every surfaces[] row carries its 1-based \`order\`, the same
12563
+ number the human ORDER column prints (issue #421).
11100
12564
  Exit: 0 a successful read \xB7 1 the workspace does not load, declares no
11101
12565
  releases[], the id is not declared, or a surface is not in the registry \xB7
11102
12566
  2 usage (a subcommand other than plan).`
@@ -11159,7 +12623,11 @@ async function runMain() {
11159
12623
  const engineSources = readEnginePascorcSources(process.cwd(), flags.workspace, workspace);
11160
12624
  const engineGate = enforceEngineRequirement("validate", engineSources);
11161
12625
  if (engineGate !== null) return engineGate;
11162
- return runValidate({ workspace });
12626
+ const requirement = evaluateEngineRequirements(engineSources, VERSION);
12627
+ return runValidate({
12628
+ workspace,
12629
+ floor: { declared: requirement.declared, met: requirement.met }
12630
+ });
11163
12631
  }
11164
12632
  case "doctor": {
11165
12633
  const { flags, positionals } = parseCommandFlags(rest);
@@ -11246,19 +12714,74 @@ async function runMain() {
11246
12714
  case "edit": {
11247
12715
  const { flags, positionals } = parseCommandFlags(rest, {
11248
12716
  set: { type: "string" },
11249
- check: { type: "boolean" }
12717
+ check: { type: "boolean" },
12718
+ list: { type: "boolean" },
12719
+ create: { type: "string" }
11250
12720
  });
11251
12721
  if (flags.help === true) {
11252
12722
  process.stdout.write(`${COMMAND_HELP.edit}
11253
12723
  `);
11254
12724
  return EXIT.OK;
11255
12725
  }
11256
- if (positionals.length !== 1) {
11257
- throw new UsageError("usage: pasco edit <contract-path> [--set <text> [--check]]");
12726
+ let listBlock;
12727
+ let createRoot2;
12728
+ if (flags.list === true) {
12729
+ if (flags.set !== void 0 || flags.check === true || flags.create !== void 0) {
12730
+ throw new UsageError(
12731
+ "--list is read-only: it cannot be combined with --set, --check or --create"
12732
+ );
12733
+ }
12734
+ if (positionals.length > 1) {
12735
+ throw new UsageError("pasco edit --list [<block>]");
12736
+ }
12737
+ if (positionals.length === 1) {
12738
+ const value = positionals[0];
12739
+ try {
12740
+ listBlock = parseEditListBlock(value);
12741
+ } catch (err) {
12742
+ try {
12743
+ parseEditAddress(value);
12744
+ } catch {
12745
+ throw err;
12746
+ }
12747
+ throw new UsageError(
12748
+ `--list takes a block, not an address: run \`pasco edit --list\` to list the addresses, or \`pasco edit '${value}'\` to read one`
12749
+ );
12750
+ }
12751
+ }
12752
+ } else if (flags.create !== void 0) {
12753
+ if (flags.set !== void 0) {
12754
+ throw new UsageError(
12755
+ "--create seeds a missing root: it cannot be combined with --set (use --create <root> --check to preview the seed)"
12756
+ );
12757
+ }
12758
+ if (positionals.length > 0) {
12759
+ throw new UsageError("pasco edit --create <root> takes no address");
12760
+ }
12761
+ createRoot2 = parseCreateRoot(flags.create);
12762
+ } else if (positionals.length !== 1) {
12763
+ throw new UsageError("pasco edit <contract-path> [--set <text> [--check]]");
11258
12764
  }
11259
12765
  setJsonMode(flags.json === true);
11260
12766
  const workspace = resolveWorkspace(process.cwd(), flags.workspace);
11261
12767
  const pascorc = readWorkspacePascorc(workspace);
12768
+ if (flags.list === true) {
12769
+ return runEdit({
12770
+ workspace,
12771
+ list: true,
12772
+ check: false,
12773
+ ...listBlock === void 0 ? {} : { listBlock },
12774
+ pascorc
12775
+ });
12776
+ }
12777
+ if (createRoot2 !== void 0) {
12778
+ return runEdit({
12779
+ workspace,
12780
+ createRoot: createRoot2,
12781
+ check: flags.check === true,
12782
+ pascorc
12783
+ });
12784
+ }
11262
12785
  return runEdit({
11263
12786
  workspace,
11264
12787
  addressArg: positionals[0],
@@ -11327,20 +12850,25 @@ async function runMain() {
11327
12850
  }
11328
12851
  case "discover": {
11329
12852
  const { flags, positionals } = parseCommandFlags(rest, {
11330
- "allow-host": { type: "string" }
12853
+ "allow-host": { type: "string" },
12854
+ observations: { type: "string" }
11331
12855
  });
11332
12856
  if (flags.help === true) {
11333
12857
  process.stdout.write(`${COMMAND_HELP.discover}
11334
12858
  `);
11335
12859
  return EXIT.OK;
11336
12860
  }
11337
- if (positionals.length === 0)
11338
- throw new UsageError("usage: pasco discover <url>... [--json] [--allow-host <hosts>]");
12861
+ const observationsPath = typeof flags.observations === "string" ? flags.observations : void 0;
12862
+ if (positionals.length === 0 && observationsPath === void 0)
12863
+ throw new UsageError(
12864
+ "usage: pasco discover <url>... [--observations <file>] [--json] [--allow-host <hosts>]"
12865
+ );
11339
12866
  setJsonMode(flags.json === true);
11340
12867
  const allowHosts = typeof flags["allow-host"] === "string" ? flags["allow-host"].split(",").map((host) => host.trim()).filter((host) => host !== "") : void 0;
11341
12868
  return await runDiscover({
11342
12869
  urls: positionals,
11343
- ...allowHosts !== void 0 ? { allowHosts } : {}
12870
+ ...allowHosts !== void 0 ? { allowHosts } : {},
12871
+ ...observationsPath !== void 0 ? { observationsPath } : {}
11344
12872
  });
11345
12873
  }
11346
12874
  case "audit": {
@@ -11492,6 +13020,25 @@ async function runMain() {
11492
13020
  const workspace = resolveWorkspace(process.cwd(), flags.workspace);
11493
13021
  return runReleasePlan({ workspace, releaseArg: positionals[1] });
11494
13022
  }
13023
+ case "achievements": {
13024
+ const { flags, positionals } = parseCommandFlags(rest, {
13025
+ targets: { type: "boolean" }
13026
+ });
13027
+ if (flags.help === true) {
13028
+ process.stdout.write(`${COMMAND_HELP.achievements}
13029
+ `);
13030
+ return EXIT.OK;
13031
+ }
13032
+ if (positionals.length > 0) {
13033
+ throw new UsageError("usage: pasco achievements [--targets] [--json] [--workspace <path>]");
13034
+ }
13035
+ setJsonMode(flags.json === true);
13036
+ const workspace = resolveWorkspace(process.cwd(), flags.workspace);
13037
+ return runAchievements({
13038
+ workspace,
13039
+ ...flags.targets === true ? { targets: true } : {}
13040
+ });
13041
+ }
11495
13042
  case "status": {
11496
13043
  const { flags, positionals } = parseCommandFlags(rest, {
11497
13044
  mark: { type: "string" },
@@ -11508,7 +13055,7 @@ async function runMain() {
11508
13055
  return EXIT.OK;
11509
13056
  }
11510
13057
  if (flags.mark !== void 0 && positionals.length !== 1) {
11511
- throw new UsageError("usage: pasco status --mark <ledgerKey> <surface> [--at <iso8601>]");
13058
+ throw new UsageError("usage: pasco status --mark <ledgerKey> <platform> [--at <iso8601>]");
11512
13059
  }
11513
13060
  if (flags.mark === void 0 && positionals.length > 1) {
11514
13061
  throw new UsageError(
@@ -11660,7 +13207,7 @@ try {
11660
13207
  exitCode = await runMain();
11661
13208
  } catch (err) {
11662
13209
  if (err instanceof UsageError) {
11663
- process.stderr.write(`usage: ${err.message}
13210
+ process.stderr.write(`usage: ${err.message.replace(/^usage: */, "")}
11664
13211
  `);
11665
13212
  exitCode = EXIT.USAGE;
11666
13213
  } else if (err instanceof ConfigError) {