@scalemule/news-publication 0.0.84 → 0.0.85
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
CHANGED
|
@@ -377,7 +377,7 @@ matches one declared slot. Credentials are normal project/member credentials.
|
|
|
377
377
|
The operation key is retained for 30 days. A legacy article must first have an
|
|
378
378
|
explicitly selected collaboration policy; this endpoint never downgrades review.
|
|
379
379
|
|
|
380
|
-
### Edit article prose, cards and
|
|
380
|
+
### Edit article prose, cards, citations and tags
|
|
381
381
|
|
|
382
382
|
```sh
|
|
383
383
|
npx --yes --package @scalemule/news-publication@0.0.81 scalemule-article edit \
|
|
@@ -385,7 +385,10 @@ npx --yes --package @scalemule/news-publication@0.0.81 scalemule-article edit \
|
|
|
385
385
|
```
|
|
386
386
|
|
|
387
387
|
The patch may contain title, subtitle, content_markdown, excerpt, section,
|
|
388
|
-
community, format, layout, cards and
|
|
388
|
+
community, format, layout, cards, citations and `tag_ids`. Tag edits require SDK
|
|
389
|
+
0.0.85+ and the deployed `versioned_editorial_tags` capability. Supply up to 32
|
|
390
|
+
existing tenant-owned tag UUIDs; an empty array clears assignments. Create or
|
|
391
|
+
select tags using the ordinary Blog tags API first. It preserves omitted fields and
|
|
389
392
|
media and saves one immutable revision. Default: save for review. Add
|
|
390
393
|
`--release` only to request release through the existing policy; the command
|
|
391
394
|
never issues APPROVE or downgrades review. On a legacy article, explicitly choose
|
|
@@ -402,7 +405,7 @@ Unknown save/release responses reconcile against the original revision; another
|
|
|
402
405
|
current revision stops rather than being overwritten. Completed replay and no-op
|
|
403
406
|
edits add no revision. `RELEASE_HELD`/exit 2 preserves the saved revision while
|
|
404
407
|
review is pending. With `--url --release`, success requires the rendered
|
|
405
|
-
headline, prose
|
|
408
|
+
headline, prose, actual card components and requested canonical tag links; public visibility pending is also
|
|
406
409
|
exit 2. A saved revision and a rendered published correction are separate states.
|
|
407
410
|
|
|
408
411
|
Use the same client method, `client.editArticle`, for a caller-supplied durable
|
|
@@ -18,7 +18,7 @@ Credentials: SCALEMULE_API_KEY and SCALEMULE_ACCESS_TOKEN (normal authorized mem
|
|
|
18
18
|
Platform admins may use their normal access token with --application-id ID.
|
|
19
19
|
Optional --api-url URL defaults to https://api.scalemule.com.
|
|
20
20
|
|
|
21
|
-
edit saves prose, excerpt, cards and
|
|
21
|
+
edit saves prose, excerpt, cards, citations and tenant-owned tag_ids in one immutable revision.
|
|
22
22
|
Unspecified fields and media are retained. Default: save for review.
|
|
23
23
|
--release explicitly requests release under the existing policy; it never issues
|
|
24
24
|
APPROVE or changes a policy. Legacy onboarding requires an explicit
|
|
@@ -157,11 +157,11 @@ try {
|
|
|
157
157
|
while(!verified){
|
|
158
158
|
const response=await fetch(values.url,{redirect:'error',cache:'no-store',signal:AbortSignal.timeout(Math.min(5000,remaining()))});
|
|
159
159
|
const html=await response.text();
|
|
160
|
-
verified=response.ok&&articleEditHtmlMatches(html,{title:article.title,content_html:article.content_html,card_titles:(article.metadata?.cards??[]).map(c=>c.title)});
|
|
160
|
+
verified=response.ok&&articleEditHtmlMatches(html,{title:article.title,content_html:article.content_html,card_titles:(article.metadata?.cards??[]).map(c=>c.title),tags:checkpoint.document.tag_ids?article.tags:undefined});
|
|
161
161
|
if(!verified)await new Promise(r=>setTimeout(r,Math.min(300,remaining())));
|
|
162
162
|
}
|
|
163
163
|
}catch(error){reason=error.message;}
|
|
164
|
-
if(!verified){console.error(JSON.stringify({...result,state:'published_public_verification_pending',stage:'public_page',message:'The exact revision is published; rendered prose/cards remain unverified. Resume the same edit.',reason}));process.exitCode=2;}
|
|
164
|
+
if(!verified){console.error(JSON.stringify({...result,state:'published_public_verification_pending',stage:'public_page',message:'The exact revision is published; rendered prose/cards/tags remain unverified. Resume the same edit.',reason}));process.exitCode=2;}
|
|
165
165
|
else console.log(JSON.stringify({...result,public_page_verified:true,total_elapsed_ms:Math.round(performance.now()-commandStarted)},null,2));
|
|
166
166
|
}else{
|
|
167
167
|
console.log(JSON.stringify({...result,total_elapsed_ms:Math.round(performance.now()-commandStarted)},null,2));
|
|
@@ -43,6 +43,17 @@ function articleEditHtmlMatches(html, expected) {
|
|
|
43
43
|
const all = text(visible);
|
|
44
44
|
if (!paragraphs.length || !paragraphs.every((p) => all.includes(p)))
|
|
45
45
|
return false;
|
|
46
|
+
if (expected.tags?.length) {
|
|
47
|
+
const links = [...visible.matchAll(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/giu)];
|
|
48
|
+
if (!expected.tags.every((tag) => links.some((link) => {
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(decode(link[1]), "https://publication.example");
|
|
51
|
+
return url.pathname === "/news" && url.searchParams.get("tag") === tag.slug && text(link[2]).replace(/^#\s*/u, "") === tag.name;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}))) return false;
|
|
56
|
+
}
|
|
46
57
|
const titles = [
|
|
47
58
|
...visible.matchAll(
|
|
48
59
|
/<aside\b[^>]*class=["'][^"']*\barticle-verified-card\b[^"']*["'][^>]*>([\s\S]*?)<\/aside>/giu
|
|
@@ -365,7 +376,8 @@ var patchKeys = /* @__PURE__ */ new Set([
|
|
|
365
376
|
"format",
|
|
366
377
|
"layout",
|
|
367
378
|
"cards",
|
|
368
|
-
"citations"
|
|
379
|
+
"citations",
|
|
380
|
+
"tag_ids"
|
|
369
381
|
]);
|
|
370
382
|
function validate(input) {
|
|
371
383
|
if (!input.operationKey || !/^[\w.:-]{1,128}$/u.test(input.operationKey) || !input.articleId || !input.patch || typeof input.patch !== "object" || Array.isArray(input.patch) || !Object.keys(input.patch).length)
|
|
@@ -374,7 +386,10 @@ function validate(input) {
|
|
|
374
386
|
);
|
|
375
387
|
for (const [key, value] of Object.entries(input.patch)) {
|
|
376
388
|
if (!patchKeys.has(key)) throw Error("Unsupported edit field: " + key);
|
|
377
|
-
if (
|
|
389
|
+
if (key === "tag_ids") {
|
|
390
|
+
if (!Array.isArray(value) || value.length > 32 || new Set(value).size !== value.length || value.some((id) => typeof id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(id) || /^0{8}-0{4}-0{4}-0{4}-0{12}$/u.test(id)))
|
|
391
|
+
throw Error("tag_ids must contain at most 32 distinct, nonempty UUIDs");
|
|
392
|
+
} else if (["cards", "citations"].includes(key)) {
|
|
378
393
|
if (!Array.isArray(value)) throw Error(key + " must be an array");
|
|
379
394
|
} else if (key === "layout") {
|
|
380
395
|
if (!value || typeof value !== "object" || typeof value.id !== "string" || !Number.isInteger(value.version))
|
|
@@ -458,6 +473,8 @@ async function runArticleEdit(t, input) {
|
|
|
458
473
|
stage,
|
|
459
474
|
"The server must expose versioned_editorial_fields before this edit; no article was changed"
|
|
460
475
|
);
|
|
476
|
+
if (input.patch.tag_ids !== void 0 && options.capabilities?.versioned_editorial_tags !== true)
|
|
477
|
+
throw new ArticleEditOperationError("CAPABILITY_UNAVAILABLE", stage, "The server must expose versioned_editorial_tags before changing tags; no article was changed");
|
|
461
478
|
if (!identityMatches(source))
|
|
462
479
|
throw new ArticleEditOperationError(
|
|
463
480
|
"ARTICLE_IDENTITY_CHANGED",
|
|
@@ -621,6 +638,8 @@ async function runArticleEdit(t, input) {
|
|
|
621
638
|
409,
|
|
622
639
|
state.revisionId
|
|
623
640
|
);
|
|
641
|
+
if (state.document?.tag_ids && !equal([...state.document.tag_ids].sort(), (after.tags ?? []).map((tag) => tag.id).sort()))
|
|
642
|
+
throw new ArticleEditOperationError("VERIFICATION_CONFLICT", stage, "Published tags differ from the saved revision", 409, state.revisionId);
|
|
624
643
|
const normalizedCards = state.document?.cards;
|
|
625
644
|
if (normalizedCards && after.metadata?.cards?.length !== normalizedCards.length)
|
|
626
645
|
throw new ArticleEditOperationError(
|
|
@@ -8,6 +8,10 @@ declare function articleEditHtmlMatches(html: string, expected: {
|
|
|
8
8
|
title: string;
|
|
9
9
|
content_html: string;
|
|
10
10
|
card_titles: string[];
|
|
11
|
+
tags?: {
|
|
12
|
+
name: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
}[];
|
|
11
15
|
}): boolean;
|
|
12
16
|
|
|
13
17
|
type PhotoOperationCheckpoint = {
|
|
@@ -48,7 +52,7 @@ declare class ArticlePhotoOperationError extends Error {
|
|
|
48
52
|
constructor(code: string, stage: string, message: string, fileId?: string | undefined, status?: number | undefined);
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations">>;
|
|
55
|
+
type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations" | "tag_ids">>;
|
|
52
56
|
type ArticleEditCheckpoint = {
|
|
53
57
|
version: 1;
|
|
54
58
|
identity: string;
|
|
@@ -93,6 +97,7 @@ type ArticleDocument = {
|
|
|
93
97
|
excerpt?: string;
|
|
94
98
|
cards?: unknown[];
|
|
95
99
|
citations?: unknown[];
|
|
100
|
+
tag_ids?: string[];
|
|
96
101
|
section: string;
|
|
97
102
|
community?: string;
|
|
98
103
|
format?: ArticleFormat;
|
|
@@ -195,6 +200,7 @@ type ArticleWorkflowOptions = {
|
|
|
195
200
|
prioritized_narration_processing?: boolean;
|
|
196
201
|
content_first_editor_document?: boolean;
|
|
197
202
|
versioned_editorial_fields?: boolean;
|
|
203
|
+
versioned_editorial_tags?: boolean;
|
|
198
204
|
managed_submission_version?: number | null;
|
|
199
205
|
multipart_submission?: boolean;
|
|
200
206
|
};
|
|
@@ -8,6 +8,10 @@ declare function articleEditHtmlMatches(html: string, expected: {
|
|
|
8
8
|
title: string;
|
|
9
9
|
content_html: string;
|
|
10
10
|
card_titles: string[];
|
|
11
|
+
tags?: {
|
|
12
|
+
name: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
}[];
|
|
11
15
|
}): boolean;
|
|
12
16
|
|
|
13
17
|
type PhotoOperationCheckpoint = {
|
|
@@ -48,7 +52,7 @@ declare class ArticlePhotoOperationError extends Error {
|
|
|
48
52
|
constructor(code: string, stage: string, message: string, fileId?: string | undefined, status?: number | undefined);
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations">>;
|
|
55
|
+
type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations" | "tag_ids">>;
|
|
52
56
|
type ArticleEditCheckpoint = {
|
|
53
57
|
version: 1;
|
|
54
58
|
identity: string;
|
|
@@ -93,6 +97,7 @@ type ArticleDocument = {
|
|
|
93
97
|
excerpt?: string;
|
|
94
98
|
cards?: unknown[];
|
|
95
99
|
citations?: unknown[];
|
|
100
|
+
tag_ids?: string[];
|
|
96
101
|
section: string;
|
|
97
102
|
community?: string;
|
|
98
103
|
format?: ArticleFormat;
|
|
@@ -195,6 +200,7 @@ type ArticleWorkflowOptions = {
|
|
|
195
200
|
prioritized_narration_processing?: boolean;
|
|
196
201
|
content_first_editor_document?: boolean;
|
|
197
202
|
versioned_editorial_fields?: boolean;
|
|
203
|
+
versioned_editorial_tags?: boolean;
|
|
198
204
|
managed_submission_version?: number | null;
|
|
199
205
|
multipart_submission?: boolean;
|
|
200
206
|
};
|
|
@@ -41,6 +41,17 @@ function articleEditHtmlMatches(html, expected) {
|
|
|
41
41
|
const all = text(visible);
|
|
42
42
|
if (!paragraphs.length || !paragraphs.every((p) => all.includes(p)))
|
|
43
43
|
return false;
|
|
44
|
+
if (expected.tags?.length) {
|
|
45
|
+
const links = [...visible.matchAll(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/giu)];
|
|
46
|
+
if (!expected.tags.every((tag) => links.some((link) => {
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL(decode(link[1]), "https://publication.example");
|
|
49
|
+
return url.pathname === "/news" && url.searchParams.get("tag") === tag.slug && text(link[2]).replace(/^#\s*/u, "") === tag.name;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}))) return false;
|
|
54
|
+
}
|
|
44
55
|
const titles = [
|
|
45
56
|
...visible.matchAll(
|
|
46
57
|
/<aside\b[^>]*class=["'][^"']*\barticle-verified-card\b[^"']*["'][^>]*>([\s\S]*?)<\/aside>/giu
|
|
@@ -363,7 +374,8 @@ var patchKeys = /* @__PURE__ */ new Set([
|
|
|
363
374
|
"format",
|
|
364
375
|
"layout",
|
|
365
376
|
"cards",
|
|
366
|
-
"citations"
|
|
377
|
+
"citations",
|
|
378
|
+
"tag_ids"
|
|
367
379
|
]);
|
|
368
380
|
function validate(input) {
|
|
369
381
|
if (!input.operationKey || !/^[\w.:-]{1,128}$/u.test(input.operationKey) || !input.articleId || !input.patch || typeof input.patch !== "object" || Array.isArray(input.patch) || !Object.keys(input.patch).length)
|
|
@@ -372,7 +384,10 @@ function validate(input) {
|
|
|
372
384
|
);
|
|
373
385
|
for (const [key, value] of Object.entries(input.patch)) {
|
|
374
386
|
if (!patchKeys.has(key)) throw Error("Unsupported edit field: " + key);
|
|
375
|
-
if (
|
|
387
|
+
if (key === "tag_ids") {
|
|
388
|
+
if (!Array.isArray(value) || value.length > 32 || new Set(value).size !== value.length || value.some((id) => typeof id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(id) || /^0{8}-0{4}-0{4}-0{4}-0{12}$/u.test(id)))
|
|
389
|
+
throw Error("tag_ids must contain at most 32 distinct, nonempty UUIDs");
|
|
390
|
+
} else if (["cards", "citations"].includes(key)) {
|
|
376
391
|
if (!Array.isArray(value)) throw Error(key + " must be an array");
|
|
377
392
|
} else if (key === "layout") {
|
|
378
393
|
if (!value || typeof value !== "object" || typeof value.id !== "string" || !Number.isInteger(value.version))
|
|
@@ -456,6 +471,8 @@ async function runArticleEdit(t, input) {
|
|
|
456
471
|
stage,
|
|
457
472
|
"The server must expose versioned_editorial_fields before this edit; no article was changed"
|
|
458
473
|
);
|
|
474
|
+
if (input.patch.tag_ids !== void 0 && options.capabilities?.versioned_editorial_tags !== true)
|
|
475
|
+
throw new ArticleEditOperationError("CAPABILITY_UNAVAILABLE", stage, "The server must expose versioned_editorial_tags before changing tags; no article was changed");
|
|
459
476
|
if (!identityMatches(source))
|
|
460
477
|
throw new ArticleEditOperationError(
|
|
461
478
|
"ARTICLE_IDENTITY_CHANGED",
|
|
@@ -619,6 +636,8 @@ async function runArticleEdit(t, input) {
|
|
|
619
636
|
409,
|
|
620
637
|
state.revisionId
|
|
621
638
|
);
|
|
639
|
+
if (state.document?.tag_ids && !equal([...state.document.tag_ids].sort(), (after.tags ?? []).map((tag) => tag.id).sort()))
|
|
640
|
+
throw new ArticleEditOperationError("VERIFICATION_CONFLICT", stage, "Published tags differ from the saved revision", 409, state.revisionId);
|
|
622
641
|
const normalizedCards = state.document?.cards;
|
|
623
642
|
if (normalizedCards && after.metadata?.cards?.length !== normalizedCards.length)
|
|
624
643
|
throw new ArticleEditOperationError(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalemule/news-publication",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.85",
|
|
4
4
|
"description": "Shared server framework for ScaleMule local news publications. Walnut Creek Times is the broadsheet default; each paper supplies its own identity and feature flags.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|