@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/README.md +142 -47
- package/dist/index.js +2082 -535
- package/package.json +1 -1
- package/skills/README.md +1 -1
- package/skills/github-org/SKILL.md +16 -0
- package/skills/github-org/checks.yaml +4 -1
- package/skills/github-readme/SKILL.md +1 -1
- package/skills/github-readme/checks.yaml +4 -1
- package/skills/github-readme/playbook.md +1 -1
- package/skills/hotmart/checks.yaml +4 -1
- package/skills/kofi/checks.yaml +5 -2
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
|
-
|
|
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 = (
|
|
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"] =
|
|
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,
|
|
898
|
-
const
|
|
899
|
-
if (Number.isNaN(
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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((
|
|
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
|
-
|
|
1067
|
-
|
|
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
|
|
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
|
|
1216
|
-
const
|
|
1217
|
-
const
|
|
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:
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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:
|
|
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
|
|
1324
|
-
if (
|
|
1493
|
+
const at2 = folded.text.indexOf(needle, from);
|
|
1494
|
+
if (at2 === -1)
|
|
1325
1495
|
break;
|
|
1326
|
-
const end =
|
|
1327
|
-
if (isWordBounded(folded.text,
|
|
1328
|
-
out.push({ start: folded.starts[
|
|
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 =
|
|
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
|
|
1340
|
-
while (
|
|
1341
|
-
const end =
|
|
1342
|
-
if (isWordBounded(haystack,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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((
|
|
2209
|
-
path:
|
|
2210
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
2897
|
-
if (
|
|
2898
|
-
const platform =
|
|
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
|
|
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((
|
|
4301
|
-
const mapped = Object.hasOwn(DRIFT_CLASS,
|
|
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,
|
|
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 ? "
|
|
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
|
-
|
|
4337
|
-
|
|
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)
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
`
|
|
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
|
|
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
|
|
4978
|
-
line(` warn: [${
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ${
|
|
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 =
|
|
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
|
|
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
|
|
5475
|
-
const mapped = Object.hasOwn(DRIFT_CLASS,
|
|
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,
|
|
5478
|
-
const cited =
|
|
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, `${
|
|
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 = "
|
|
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
|
|
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
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
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/
|
|
5849
|
-
|
|
5850
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
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 (
|
|
7050
|
+
else if (!selectorExpressible(row.platform)) {
|
|
6109
7051
|
parts.push(
|
|
6110
|
-
`platform ${JSON.stringify(row.platform)}
|
|
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 &&
|
|
7055
|
+
if (row.label !== void 0 && !selectorExpressible(row.label)) {
|
|
6114
7056
|
parts.push(
|
|
6115
|
-
`label ${JSON.stringify(row.label)}
|
|
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
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
}
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
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) =>
|
|
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
|
|
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.
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
7519
|
-
{ path:
|
|
7520
|
-
{ path:
|
|
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
|
|
7547
|
-
import { dirname as dirname5, join as
|
|
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
|
|
7553
|
-
import { join as
|
|
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
|
|
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
|
|
8890
|
+
return join12(postsDir(workspace), id);
|
|
7565
8891
|
}
|
|
7566
8892
|
function seedPath(workspace, id) {
|
|
7567
|
-
return
|
|
8893
|
+
return join12(seedDir(workspace, id), "seed.md");
|
|
7568
8894
|
}
|
|
7569
8895
|
function ledgerPath(workspace, id) {
|
|
7570
|
-
return
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
9484
|
+
const path = join13(workspace, "postiz.yaml");
|
|
8159
9485
|
if (!existsSync13(path)) return {};
|
|
8160
|
-
const doc = parseDocument4(
|
|
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 =
|
|
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 ??
|
|
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 =
|
|
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,
|
|
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(
|
|
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 =
|
|
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 ??
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
8904
|
-
const order = String(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
9506
|
-
const file =
|
|
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
|
-
|
|
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(
|
|
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
|
|
9631
|
-
import { join as
|
|
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 =
|
|
10987
|
+
const activePath = join16(workspace, "content", "active.yaml");
|
|
9655
10988
|
let active = {};
|
|
9656
10989
|
if (existsSync16(activePath)) {
|
|
9657
|
-
const doc = parseDocument5(
|
|
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(
|
|
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: ${
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
9922
|
-
import { join as
|
|
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
|
|
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(
|
|
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
|
-
`
|
|
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
|
|
11479
|
+
let digest2;
|
|
10139
11480
|
if (response.text !== void 0) {
|
|
10140
11481
|
try {
|
|
10141
|
-
|
|
11482
|
+
digest2 = digestOf(extractor(response.text));
|
|
10142
11483
|
} catch {
|
|
10143
|
-
|
|
11484
|
+
digest2 = void 0;
|
|
10144
11485
|
}
|
|
10145
11486
|
}
|
|
10146
11487
|
const verdict = certificationVerdict(
|
|
10147
11488
|
{ certification: stored },
|
|
10148
|
-
{ status: response.status, ...
|
|
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
|
-
...
|
|
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> <
|
|
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
|
|
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
|
|
10238
|
-
if (Number.isNaN(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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> <
|
|
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
|
|
10591
|
-
const where =
|
|
10592
|
-
const surfaces =
|
|
10593
|
-
line(` warn: [${
|
|
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(
|
|
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
|
-
|
|
10638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
|
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
|
|
10745
|
-
provisional profile, its per-fact provenance
|
|
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
|
|
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
|
|
10768
|
-
INVALID
|
|
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
|
-
|
|
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
|
|
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
|
|
10835
|
-
did not resolve to exactly one row, the
|
|
10836
|
-
command (stale \u2014 nothing was written),
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
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)
|
|
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
|
|
10877
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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> <
|
|
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/<
|
|
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
|
-
|
|
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
|
-
|
|
11257
|
-
|
|
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
|
-
|
|
11338
|
-
|
|
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> <
|
|
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) {
|