@sellable/mcp 0.1.239 → 0.1.242
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 +2 -1
- package/dist/server.js +13 -1
- package/dist/tools/content-posts.d.ts +315 -0
- package/dist/tools/content-posts.js +366 -15
- package/dist/tools/registry.d.ts +204 -0
- package/package.json +1 -1
- package/skills/create-post/SKILL.md +247 -17
- package/skills/create-post/references/gold-standard-post-pack.md +43 -2
- package/skills/create-post/references/hook-research-playbook.md +217 -2
- package/skills/create-post/references/post-file-contract.md +24 -2
- package/skills/create-post/references/post-validation.md +101 -6
- package/skills/create-post/references/premise-development.md +159 -0
|
@@ -80,13 +80,18 @@ export const contentPostToolDefinitions = [
|
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
name: "save_post_draft",
|
|
83
|
-
description: "Save a validated LinkedIn post draft under ~/.sellable/content/linkedin/drafts.",
|
|
83
|
+
description: "Save a validated LinkedIn post draft under ~/.sellable/content/linkedin/drafts. Use versioned draft IDs for multiple drafts of the same idea.",
|
|
84
84
|
inputSchema: {
|
|
85
85
|
type: "object",
|
|
86
86
|
properties: {
|
|
87
87
|
draftId: { type: "string" },
|
|
88
88
|
ideaId: { type: "string" },
|
|
89
89
|
hookResearchId: { type: "string" },
|
|
90
|
+
priorDraftId: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description: "Optional previous draft ID this draft is trying to improve on.",
|
|
93
|
+
},
|
|
94
|
+
iteration: draftIterationSchema(),
|
|
90
95
|
title: { type: "string" },
|
|
91
96
|
body: { type: "string" },
|
|
92
97
|
validationReceipt: {
|
|
@@ -99,11 +104,38 @@ export const contentPostToolDefinitions = [
|
|
|
99
104
|
additionalProperties: false,
|
|
100
105
|
},
|
|
101
106
|
},
|
|
107
|
+
{
|
|
108
|
+
name: "update_post_draft",
|
|
109
|
+
description: "Update an existing LinkedIn post draft in place while preserving omitted fields. Useful for adding iteration receipts, status changes, or final copy edits without rewriting the whole artifact.",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
draftId: { type: "string" },
|
|
114
|
+
hookResearchId: { type: "string" },
|
|
115
|
+
priorDraftId: { type: "string" },
|
|
116
|
+
iteration: draftIterationSchema(),
|
|
117
|
+
title: { type: "string" },
|
|
118
|
+
body: { type: "string" },
|
|
119
|
+
validationReceipt: {
|
|
120
|
+
description: "Markdown string or structured object to replace the saved validation receipt.",
|
|
121
|
+
},
|
|
122
|
+
status: { type: "string" },
|
|
123
|
+
updatedAt: { type: "string" },
|
|
124
|
+
},
|
|
125
|
+
required: ["draftId"],
|
|
126
|
+
additionalProperties: false,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
102
129
|
{
|
|
103
130
|
name: "list_post_drafts",
|
|
104
131
|
description: "List saved LinkedIn post drafts with compact sanitized previews. Use get_post_draft for full Markdown.",
|
|
105
132
|
inputSchema: listInputSchema(),
|
|
106
133
|
},
|
|
134
|
+
{
|
|
135
|
+
name: "list_post_draft_iterations",
|
|
136
|
+
description: "List all draft versions for one idea, including iteration scores, verdicts, prior draft links, status, and sanitized previews.",
|
|
137
|
+
inputSchema: idInputSchema("ideaId"),
|
|
138
|
+
},
|
|
107
139
|
{
|
|
108
140
|
name: "get_post_draft",
|
|
109
141
|
description: "Read one saved LinkedIn post draft by ID.",
|
|
@@ -121,11 +153,47 @@ export const contentPostToolDefinitions = [
|
|
|
121
153
|
publishedAt: { type: "string" },
|
|
122
154
|
finalText: { type: "string" },
|
|
123
155
|
title: { type: "string" },
|
|
156
|
+
updateDraftStatus: {
|
|
157
|
+
type: "boolean",
|
|
158
|
+
description: "Defaults to true when draftId is supplied. Marks the source draft as published and links it to the published record.",
|
|
159
|
+
},
|
|
124
160
|
},
|
|
125
161
|
required: ["publishUrl"],
|
|
126
162
|
additionalProperties: false,
|
|
127
163
|
},
|
|
128
164
|
},
|
|
165
|
+
{
|
|
166
|
+
name: "get_published_post",
|
|
167
|
+
description: "Read one published LinkedIn post record by ID. If year is omitted, searches all published year folders.",
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
publishedPostId: { type: "string" },
|
|
172
|
+
year: { type: "string" },
|
|
173
|
+
},
|
|
174
|
+
required: ["publishedPostId"],
|
|
175
|
+
additionalProperties: false,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "update_published_post_metrics",
|
|
180
|
+
description: "Append a metrics snapshot to a published LinkedIn post record and update latest metric fields for post-performance learning.",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
publishedPostId: { type: "string" },
|
|
185
|
+
year: { type: "string" },
|
|
186
|
+
capturedAt: { type: "string" },
|
|
187
|
+
metrics: {
|
|
188
|
+
type: "object",
|
|
189
|
+
additionalProperties: true,
|
|
190
|
+
},
|
|
191
|
+
note: { type: "string" },
|
|
192
|
+
},
|
|
193
|
+
required: ["publishedPostId", "metrics"],
|
|
194
|
+
additionalProperties: false,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
129
197
|
{
|
|
130
198
|
name: "list_published_posts",
|
|
131
199
|
description: "List published LinkedIn post records with compact sanitized previews.",
|
|
@@ -152,6 +220,13 @@ function idInputSchema(propertyName) {
|
|
|
152
220
|
additionalProperties: false,
|
|
153
221
|
};
|
|
154
222
|
}
|
|
223
|
+
function draftIterationSchema() {
|
|
224
|
+
return {
|
|
225
|
+
type: "object",
|
|
226
|
+
description: "Optional draft iteration metadata: version, priorDraftId, changeIntent, whatChanged, whatImproved, whatGotWorse, score, and verdict.",
|
|
227
|
+
additionalProperties: true,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
155
230
|
export function resolveContentRoot() {
|
|
156
231
|
const envValue = process.env[CONTENT_ROOT_ENV]?.trim();
|
|
157
232
|
if (envValue) {
|
|
@@ -274,14 +349,13 @@ export function savePostDraftTool(input) {
|
|
|
274
349
|
const safeResearchId = input.hookResearchId
|
|
275
350
|
? normalizeArtifactId(input.hookResearchId, "hookResearchId")
|
|
276
351
|
: undefined;
|
|
352
|
+
const iteration = normalizeDraftIteration(input.iteration, input.priorDraftId);
|
|
277
353
|
requireString(input.body, "body");
|
|
278
354
|
const id = input.draftId ??
|
|
279
355
|
`draft_${dateStamp(now)}_${slugify(input.title || safeIdeaId)}_v1`;
|
|
280
356
|
const safeId = normalizeArtifactId(id, "draftId");
|
|
281
357
|
const status = input.status || "draft";
|
|
282
|
-
|
|
283
|
-
throw new Error(`Unsupported draft status: ${status}`);
|
|
284
|
-
}
|
|
358
|
+
assertDraftStatus(status);
|
|
285
359
|
const metadata = {
|
|
286
360
|
id: safeId,
|
|
287
361
|
type: "draft",
|
|
@@ -289,6 +363,9 @@ export function savePostDraftTool(input) {
|
|
|
289
363
|
title: input.title,
|
|
290
364
|
ideaId: safeIdeaId,
|
|
291
365
|
hookResearchId: safeResearchId,
|
|
366
|
+
draftVersion: iteration?.version || inferDraftVersion(safeId),
|
|
367
|
+
priorDraftId: iteration?.priorDraftId,
|
|
368
|
+
iteration,
|
|
292
369
|
createdAt: now,
|
|
293
370
|
updatedAt: now,
|
|
294
371
|
};
|
|
@@ -304,13 +381,98 @@ export function savePostDraftTool(input) {
|
|
|
304
381
|
status,
|
|
305
382
|
ideaId: safeIdeaId,
|
|
306
383
|
hookResearchId: safeResearchId,
|
|
384
|
+
draftVersion: metadata.draftVersion,
|
|
385
|
+
priorDraftId: metadata.priorDraftId,
|
|
386
|
+
iterationScore: iteration?.score,
|
|
387
|
+
iterationVerdict: iteration?.verdict,
|
|
307
388
|
updatedAt: now,
|
|
308
389
|
preview: sanitizedPreview(input.body),
|
|
309
390
|
};
|
|
310
391
|
}
|
|
392
|
+
export function updatePostDraftTool(input) {
|
|
393
|
+
const safeDraftId = normalizeArtifactId(input.draftId, "draftId");
|
|
394
|
+
const relativePath = `${RELATIVE_DIRS.drafts}/${safeDraftId}.md`;
|
|
395
|
+
const existing = readArtifactIfExists(relativePath);
|
|
396
|
+
if (!existing) {
|
|
397
|
+
throw new Error(`No draftId found for ID: ${safeDraftId}`);
|
|
398
|
+
}
|
|
399
|
+
const now = normalizeDate(input.updatedAt);
|
|
400
|
+
const existingBody = extractMarkdownSection(existing.markdown, "Draft Body");
|
|
401
|
+
const existingReceipt = extractMarkdownSection(existing.markdown, "Validation Receipt");
|
|
402
|
+
const body = input.body ?? existingBody;
|
|
403
|
+
requireString(body, "body");
|
|
404
|
+
const validationReceipt = input.validationReceipt === undefined
|
|
405
|
+
? existingReceipt
|
|
406
|
+
: formatReceipt(input.validationReceipt);
|
|
407
|
+
requireString(validationReceipt, "validationReceipt");
|
|
408
|
+
const safeResearchId = input.hookResearchId
|
|
409
|
+
? normalizeArtifactId(input.hookResearchId, "hookResearchId")
|
|
410
|
+
: existing.metadata.hookResearchId;
|
|
411
|
+
const iteration = normalizeDraftIteration(input.iteration ?? existing.metadata.iteration, input.priorDraftId ?? existing.metadata.priorDraftId);
|
|
412
|
+
const status = input.status || existing.metadata.status || "draft";
|
|
413
|
+
assertDraftStatus(status);
|
|
414
|
+
const metadata = {
|
|
415
|
+
...existing.metadata,
|
|
416
|
+
status,
|
|
417
|
+
title: input.title ?? existing.metadata.title,
|
|
418
|
+
hookResearchId: safeResearchId,
|
|
419
|
+
draftVersion: iteration?.version || existing.metadata.draftVersion || inferDraftVersion(safeDraftId),
|
|
420
|
+
priorDraftId: iteration?.priorDraftId,
|
|
421
|
+
iteration,
|
|
422
|
+
updatedAt: now,
|
|
423
|
+
};
|
|
424
|
+
const markdown = buildMarkdown(metadata, [
|
|
425
|
+
["Draft Body", body],
|
|
426
|
+
["Validation Receipt", validationReceipt],
|
|
427
|
+
]);
|
|
428
|
+
writeArtifact(relativePath, markdown);
|
|
429
|
+
return {
|
|
430
|
+
id: safeDraftId,
|
|
431
|
+
path: relativePath,
|
|
432
|
+
status,
|
|
433
|
+
ideaId: metadata.ideaId,
|
|
434
|
+
hookResearchId: safeResearchId,
|
|
435
|
+
draftVersion: metadata.draftVersion,
|
|
436
|
+
priorDraftId: metadata.priorDraftId,
|
|
437
|
+
iterationScore: iteration?.score,
|
|
438
|
+
iterationVerdict: iteration?.verdict,
|
|
439
|
+
updatedAt: now,
|
|
440
|
+
preview: sanitizedPreview(body),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
311
443
|
export function listPostDraftsTool(input) {
|
|
312
444
|
return listArtifacts(RELATIVE_DIRS.drafts, "draft", input?.limit);
|
|
313
445
|
}
|
|
446
|
+
export function listPostDraftIterationsTool(input) {
|
|
447
|
+
const safeIdeaId = normalizeArtifactId(input.ideaId, "ideaId");
|
|
448
|
+
const drafts = readArtifactsFromDir(RELATIVE_DIRS.drafts)
|
|
449
|
+
.filter((artifact) => artifact.metadata.type === "draft" &&
|
|
450
|
+
artifact.metadata.ideaId === safeIdeaId)
|
|
451
|
+
.map((artifact) => {
|
|
452
|
+
const iteration = artifact.metadata.iteration ||
|
|
453
|
+
extractIterationFromReceipt(extractMarkdownSection(artifact.markdown, "Validation Receipt"));
|
|
454
|
+
return {
|
|
455
|
+
id: artifact.metadata.id,
|
|
456
|
+
type: artifact.metadata.type,
|
|
457
|
+
status: artifact.metadata.status,
|
|
458
|
+
title: artifact.metadata.title,
|
|
459
|
+
path: artifact.relativePath,
|
|
460
|
+
ideaId: artifact.metadata.ideaId,
|
|
461
|
+
hookResearchId: artifact.metadata.hookResearchId,
|
|
462
|
+
draftVersion: artifact.metadata.draftVersion ||
|
|
463
|
+
iteration?.version ||
|
|
464
|
+
inferDraftVersion(artifact.metadata.id),
|
|
465
|
+
priorDraftId: artifact.metadata.priorDraftId || iteration?.priorDraftId,
|
|
466
|
+
iterationScore: iteration?.score,
|
|
467
|
+
iterationVerdict: iteration?.verdict,
|
|
468
|
+
publishedPostId: artifact.metadata.publishedPostId,
|
|
469
|
+
publishedAt: artifact.metadata.publishedAt,
|
|
470
|
+
updatedAt: artifact.metadata.updatedAt,
|
|
471
|
+
preview: sanitizedPreview(previewBasis(artifact.markdown)),
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
return drafts.sort(compareDraftIterations);
|
|
475
|
+
}
|
|
314
476
|
export function getPostDraftTool(input) {
|
|
315
477
|
return getArtifact(RELATIVE_DIRS.drafts, input.draftId, "draftId");
|
|
316
478
|
}
|
|
@@ -327,6 +489,14 @@ export function markPostPublishedTool(input) {
|
|
|
327
489
|
const relativePath = `${RELATIVE_DIRS.published}/${year}/${safeId}.md`;
|
|
328
490
|
const existing = readArtifactIfExists(relativePath);
|
|
329
491
|
const createdAt = existing?.metadata.createdAt || publishedAt;
|
|
492
|
+
const finalText = input.finalText ?? extractMarkdownSection(existing?.markdown || "", "Final Text");
|
|
493
|
+
const publishMetadata = readJsonSection(existing?.markdown || "", "Publish Metadata", {});
|
|
494
|
+
const futureMetrics = readJsonSection(existing?.markdown || "", "Future Metrics", {
|
|
495
|
+
impressions: null,
|
|
496
|
+
reactions: null,
|
|
497
|
+
comments: null,
|
|
498
|
+
snapshots: [],
|
|
499
|
+
});
|
|
330
500
|
const metadata = {
|
|
331
501
|
id: safeId,
|
|
332
502
|
type: "published_post",
|
|
@@ -340,27 +510,23 @@ export function markPostPublishedTool(input) {
|
|
|
340
510
|
updatedAt: publishedAt,
|
|
341
511
|
};
|
|
342
512
|
const markdown = buildMarkdown(metadata, [
|
|
343
|
-
["Final Text",
|
|
513
|
+
["Final Text", finalText],
|
|
344
514
|
[
|
|
345
515
|
"Publish Metadata",
|
|
346
516
|
jsonBlock(stripUndefined({
|
|
517
|
+
...publishMetadata,
|
|
347
518
|
draftId: safeDraftId,
|
|
348
519
|
publishUrl: input.publishUrl,
|
|
349
520
|
activityId: activityId || undefined,
|
|
350
521
|
publishedAt,
|
|
351
522
|
})),
|
|
352
523
|
],
|
|
353
|
-
[
|
|
354
|
-
"Future Metrics",
|
|
355
|
-
jsonBlock({
|
|
356
|
-
impressions: null,
|
|
357
|
-
reactions: null,
|
|
358
|
-
comments: null,
|
|
359
|
-
snapshots: [],
|
|
360
|
-
}),
|
|
361
|
-
],
|
|
524
|
+
["Future Metrics", jsonBlock(futureMetrics)],
|
|
362
525
|
]);
|
|
363
526
|
writeArtifact(relativePath, markdown);
|
|
527
|
+
if (safeDraftId && input.updateDraftStatus !== false) {
|
|
528
|
+
markDraftAsPublished(safeDraftId, safeId, publishedAt);
|
|
529
|
+
}
|
|
364
530
|
return {
|
|
365
531
|
id: safeId,
|
|
366
532
|
path: relativePath,
|
|
@@ -369,7 +535,65 @@ export function markPostPublishedTool(input) {
|
|
|
369
535
|
publishUrl: input.publishUrl,
|
|
370
536
|
publishedAt,
|
|
371
537
|
updatedAt: publishedAt,
|
|
372
|
-
preview: sanitizedPreview(
|
|
538
|
+
preview: sanitizedPreview(finalText || input.publishUrl),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
export function getPublishedPostTool(input) {
|
|
542
|
+
const found = findPublishedArtifact(input.publishedPostId, input.year);
|
|
543
|
+
if (!found) {
|
|
544
|
+
throw new Error(`No publishedPostId found for ID: ${normalizeArtifactId(input.publishedPostId, "publishedPostId")}`);
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
id: found.artifact.metadata.id,
|
|
548
|
+
path: found.relativePath,
|
|
549
|
+
metadata: found.artifact.metadata,
|
|
550
|
+
markdown: found.artifact.markdown,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
export function updatePublishedPostMetricsTool(input) {
|
|
554
|
+
if (!input.metrics || typeof input.metrics !== "object") {
|
|
555
|
+
throw new Error("metrics is required.");
|
|
556
|
+
}
|
|
557
|
+
const found = findPublishedArtifact(input.publishedPostId, input.year);
|
|
558
|
+
if (!found) {
|
|
559
|
+
throw new Error(`No publishedPostId found for ID: ${normalizeArtifactId(input.publishedPostId, "publishedPostId")}`);
|
|
560
|
+
}
|
|
561
|
+
const capturedAt = normalizeDate(input.capturedAt);
|
|
562
|
+
const finalText = extractMarkdownSection(found.artifact.markdown, "Final Text");
|
|
563
|
+
const publishMetadata = readJsonSection(found.artifact.markdown, "Publish Metadata", {});
|
|
564
|
+
const futureMetrics = readJsonSection(found.artifact.markdown, "Future Metrics", {});
|
|
565
|
+
const existingSnapshots = Array.isArray(futureMetrics.snapshots)
|
|
566
|
+
? futureMetrics.snapshots
|
|
567
|
+
: [];
|
|
568
|
+
const snapshot = stripUndefined({
|
|
569
|
+
capturedAt,
|
|
570
|
+
metrics: input.metrics,
|
|
571
|
+
note: input.note,
|
|
572
|
+
});
|
|
573
|
+
const nextMetrics = {
|
|
574
|
+
...futureMetrics,
|
|
575
|
+
...input.metrics,
|
|
576
|
+
lastCapturedAt: capturedAt,
|
|
577
|
+
snapshots: [...existingSnapshots, snapshot],
|
|
578
|
+
};
|
|
579
|
+
const metadata = {
|
|
580
|
+
...found.artifact.metadata,
|
|
581
|
+
updatedAt: capturedAt,
|
|
582
|
+
};
|
|
583
|
+
const markdown = buildMarkdown(metadata, [
|
|
584
|
+
["Final Text", finalText],
|
|
585
|
+
["Publish Metadata", jsonBlock(publishMetadata)],
|
|
586
|
+
["Future Metrics", jsonBlock(nextMetrics)],
|
|
587
|
+
]);
|
|
588
|
+
writeArtifact(found.relativePath, markdown);
|
|
589
|
+
return {
|
|
590
|
+
id: metadata.id,
|
|
591
|
+
path: found.relativePath,
|
|
592
|
+
status: metadata.status,
|
|
593
|
+
updatedAt: capturedAt,
|
|
594
|
+
metrics: input.metrics,
|
|
595
|
+
snapshotsCount: existingSnapshots.length + 1,
|
|
596
|
+
preview: sanitizedPreview(finalText || String(metadata.publishUrl || "")),
|
|
373
597
|
};
|
|
374
598
|
}
|
|
375
599
|
export function listPublishedPostsTool(input) {
|
|
@@ -423,8 +647,135 @@ function summarizeArtifacts(artifacts, limit) {
|
|
|
423
647
|
path: artifact.relativePath,
|
|
424
648
|
updatedAt: artifact.metadata.updatedAt,
|
|
425
649
|
preview: sanitizedPreview(previewBasis(artifact.markdown)),
|
|
650
|
+
ideaId: artifact.metadata.ideaId,
|
|
651
|
+
hookResearchId: artifact.metadata.hookResearchId,
|
|
652
|
+
draftVersion: artifact.metadata.draftVersion,
|
|
653
|
+
priorDraftId: artifact.metadata.priorDraftId,
|
|
654
|
+
iterationScore: artifact.metadata.iteration?.score,
|
|
655
|
+
iterationVerdict: artifact.metadata.iteration?.verdict,
|
|
656
|
+
publishedPostId: artifact.metadata.publishedPostId,
|
|
657
|
+
publishedAt: artifact.metadata.publishedAt,
|
|
426
658
|
}));
|
|
427
659
|
}
|
|
660
|
+
function assertDraftStatus(status) {
|
|
661
|
+
if (!["draft", "ready", "needs_revision", "published", "archived"].includes(status)) {
|
|
662
|
+
throw new Error(`Unsupported draft status: ${status}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function normalizeDraftIteration(iteration, priorDraftId) {
|
|
666
|
+
if (!iteration && !priorDraftId)
|
|
667
|
+
return undefined;
|
|
668
|
+
const safePriorDraftId = priorDraftId
|
|
669
|
+
? normalizeArtifactId(priorDraftId, "priorDraftId")
|
|
670
|
+
: iteration?.priorDraftId
|
|
671
|
+
? normalizeArtifactId(iteration.priorDraftId, "priorDraftId")
|
|
672
|
+
: undefined;
|
|
673
|
+
return stripUndefined({
|
|
674
|
+
...iteration,
|
|
675
|
+
priorDraftId: safePriorDraftId,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function inferDraftVersion(draftId) {
|
|
679
|
+
const match = draftId.match(/(?:^|_)v(\d+)(?:$|_)/i);
|
|
680
|
+
return match ? `v${match[1]}` : undefined;
|
|
681
|
+
}
|
|
682
|
+
function compareDraftIterations(a, b) {
|
|
683
|
+
const aVersion = numericVersion(a.draftVersion);
|
|
684
|
+
const bVersion = numericVersion(b.draftVersion);
|
|
685
|
+
if (aVersion !== bVersion)
|
|
686
|
+
return aVersion - bVersion;
|
|
687
|
+
const updated = String(a.updatedAt).localeCompare(String(b.updatedAt));
|
|
688
|
+
if (updated !== 0)
|
|
689
|
+
return updated;
|
|
690
|
+
return a.id.localeCompare(b.id);
|
|
691
|
+
}
|
|
692
|
+
function numericVersion(version) {
|
|
693
|
+
const match = version?.match(/^v(\d+)$/i);
|
|
694
|
+
return match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
|
|
695
|
+
}
|
|
696
|
+
function extractIterationFromReceipt(receipt) {
|
|
697
|
+
if (!receipt.includes("iteration:"))
|
|
698
|
+
return undefined;
|
|
699
|
+
const version = receipt.match(/^\s*version:\s*(\S+)/m)?.[1];
|
|
700
|
+
const priorDraftId = receipt.match(/^\s*priorDraftId:\s*(\S+)/m)?.[1];
|
|
701
|
+
const verdict = receipt.match(/^\s*verdict:\s*(\S+)/m)?.[1];
|
|
702
|
+
const scoreBlock = receipt.match(/^\s*score:\s*\n((?:\s{4}[A-Za-z][A-Za-z0-9_-]*:\s*[-\d.]+\n?)+)/m)?.[1];
|
|
703
|
+
const score = scoreBlock
|
|
704
|
+
? Object.fromEntries(scoreBlock
|
|
705
|
+
.trim()
|
|
706
|
+
.split(/\n/)
|
|
707
|
+
.map((line) => line.trim().split(/:\s*/))
|
|
708
|
+
.filter(([key, value]) => key && value)
|
|
709
|
+
.map(([key, value]) => [key, Number(value)]))
|
|
710
|
+
: undefined;
|
|
711
|
+
return stripUndefined({
|
|
712
|
+
version,
|
|
713
|
+
priorDraftId: priorDraftId && priorDraftId !== "none" ? priorDraftId : undefined,
|
|
714
|
+
verdict,
|
|
715
|
+
score,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
function extractMarkdownSection(markdown, heading) {
|
|
719
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
720
|
+
const match = markdown.match(new RegExp(`(?:^|\\n)## ${escaped}\\n\\n([\\s\\S]*?)(?=\\n## |$)`));
|
|
721
|
+
return match?.[1]?.trimEnd() ?? "";
|
|
722
|
+
}
|
|
723
|
+
function readJsonSection(markdown, heading, fallback) {
|
|
724
|
+
const section = extractMarkdownSection(markdown, heading);
|
|
725
|
+
const match = section.match(/```json\n([\s\S]*?)\n```/);
|
|
726
|
+
if (!match)
|
|
727
|
+
return fallback;
|
|
728
|
+
try {
|
|
729
|
+
return JSON.parse(match[1]);
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
return fallback;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function findPublishedArtifact(publishedPostId, year) {
|
|
736
|
+
const safeId = normalizeArtifactId(publishedPostId, "publishedPostId");
|
|
737
|
+
if (year) {
|
|
738
|
+
if (!/^\d{4}$/.test(year))
|
|
739
|
+
throw new Error("year must be YYYY.");
|
|
740
|
+
const relativePath = `${RELATIVE_DIRS.published}/${year}/${safeId}.md`;
|
|
741
|
+
const artifact = readArtifactIfExists(relativePath);
|
|
742
|
+
return artifact ? { artifact, relativePath } : null;
|
|
743
|
+
}
|
|
744
|
+
const root = resolveContentRoot();
|
|
745
|
+
ensureContentLayout(root);
|
|
746
|
+
const publishedRoot = safePath(root, RELATIVE_DIRS.published);
|
|
747
|
+
if (!fs.existsSync(publishedRoot))
|
|
748
|
+
return null;
|
|
749
|
+
for (const yearDir of fs.readdirSync(publishedRoot).sort()) {
|
|
750
|
+
if (!/^\d{4}$/.test(yearDir))
|
|
751
|
+
continue;
|
|
752
|
+
const relativePath = `${RELATIVE_DIRS.published}/${yearDir}/${safeId}.md`;
|
|
753
|
+
const artifact = readArtifactIfExists(relativePath);
|
|
754
|
+
if (artifact)
|
|
755
|
+
return { artifact, relativePath };
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
function markDraftAsPublished(draftId, publishedPostId, publishedAt) {
|
|
760
|
+
const relativePath = `${RELATIVE_DIRS.drafts}/${draftId}.md`;
|
|
761
|
+
const existing = readArtifactIfExists(relativePath);
|
|
762
|
+
if (!existing)
|
|
763
|
+
return;
|
|
764
|
+
const body = extractMarkdownSection(existing.markdown, "Draft Body");
|
|
765
|
+
const receipt = extractMarkdownSection(existing.markdown, "Validation Receipt");
|
|
766
|
+
const metadata = {
|
|
767
|
+
...existing.metadata,
|
|
768
|
+
status: "published",
|
|
769
|
+
publishedPostId,
|
|
770
|
+
publishedAt,
|
|
771
|
+
updatedAt: publishedAt,
|
|
772
|
+
};
|
|
773
|
+
const markdown = buildMarkdown(metadata, [
|
|
774
|
+
["Draft Body", body],
|
|
775
|
+
["Validation Receipt", receipt],
|
|
776
|
+
]);
|
|
777
|
+
writeArtifact(relativePath, markdown);
|
|
778
|
+
}
|
|
428
779
|
function readArtifactsFromDir(relativeDir) {
|
|
429
780
|
const root = resolveContentRoot();
|
|
430
781
|
ensureContentLayout(root);
|