@scalemule/news-publication 0.0.94 → 0.0.95
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.
|
@@ -26,7 +26,8 @@ Unspecified fields and media are retained. Default: save for review.
|
|
|
26
26
|
APPROVE or changes a policy. Legacy onboarding requires an explicit
|
|
27
27
|
--enable-review-policy value. Scheduled legacy articles stop before onboarding.
|
|
28
28
|
Use --expected-generation when coordinating an edit with another editor.
|
|
29
|
-
Default edit deadline: 10000 ms.
|
|
29
|
+
Default edit deadline: 10000 ms (--timeout-ms, API work). The public page check after --release
|
|
30
|
+
has its own --page-timeout-ms (default 60000). Reuse the same patch/key/checkpoint on retry.
|
|
30
31
|
RELEASE_HELD means the revision is saved and existing review gates remain.
|
|
31
32
|
|
|
32
33
|
replace-photo accepts a local file and uses normal member/API-key credentials.
|
|
@@ -58,7 +59,7 @@ that every website/CDN cache already displays the new image.
|
|
|
58
59
|
try {
|
|
59
60
|
const { values, positionals } = parseArgs({
|
|
60
61
|
allowPositionals: true, strict: true,
|
|
61
|
-
options: Object.fromEntries(['publication', 'article', 'file-id', 'expected-version', 'metadata', 'application-id', 'api-url', 'url', 'file', 'operation-key', 'state', 'timeout-ms','patch','expected-generation','enable-review-policy','manifest','submission'].map(k => [k, { type: 'string' }]).concat([['asset',{type:'string',multiple:true}],['help', { type: 'boolean' }],['release',{type:'boolean'}]])),
|
|
62
|
+
options: Object.fromEntries(['publication', 'article', 'file-id', 'expected-version', 'metadata', 'application-id', 'api-url', 'url', 'file', 'operation-key', 'state', 'timeout-ms','page-timeout-ms','patch','expected-generation','enable-review-policy','manifest','submission'].map(k => [k, { type: 'string' }]).concat([['asset',{type:'string',multiple:true}],['help', { type: 'boolean' }],['release',{type:'boolean'}]])),
|
|
62
63
|
});
|
|
63
64
|
if (values.help || !positionals.length) {
|
|
64
65
|
console.log(help);
|
|
@@ -68,6 +69,10 @@ try {
|
|
|
68
69
|
const budget = Number(values['timeout-ms'] ?? (command==='edit'?10000:30000));
|
|
69
70
|
if (!Number.isFinite(budget) || budget < 100 || budget > 120000) throw Error('--timeout-ms must be between 100 and 120000');
|
|
70
71
|
const remaining = () => { const ms = Math.floor(budget - (performance.now()-commandStarted)); if (ms < 1) throw Error('Operation deadline reached; resume the same operation'); return ms; };
|
|
72
|
+
// The public page check has its own deadline: caches and rolling pods make it slower than the API, and
|
|
73
|
+
// a slow page must never consume the API operation's budget or vice versa.
|
|
74
|
+
const pageBudget = Number(values['page-timeout-ms'] ?? 60000);
|
|
75
|
+
if (!Number.isFinite(pageBudget) || pageBudget < 1000 || pageBudget > 300000) throw Error('--page-timeout-ms must be between 1000 and 300000');
|
|
71
76
|
if (extra.length || !['submit','status','inspect', 'replace-cover', 'replace-photo','edit'].includes(command)) throw Error('Use submit, status, inspect, edit, replace-cover or replace-photo; see --help');
|
|
72
77
|
if (values.url) {
|
|
73
78
|
if (!process.env.SCALEMULE_API_KEY || !process.env.SCALEMULE_ACCESS_TOKEN || values['application-id']) throw Error('URL resolution requires normal publication member/API-key credentials');
|
|
@@ -152,15 +157,17 @@ try {
|
|
|
152
157
|
});
|
|
153
158
|
if(values.url&&values.release&&['published','already_published'].includes(result.state)){
|
|
154
159
|
let verified=false,reason;
|
|
160
|
+
const pageStarted=performance.now();
|
|
161
|
+
const pageRemaining=()=>{const ms=Math.floor(pageBudget-(performance.now()-pageStarted));if(ms<1)throw Error('Public page deadline reached; the page may still be refreshing. Resume the same operation or raise --page-timeout-ms');return ms;};
|
|
155
162
|
try{
|
|
156
|
-
const snapshot=await client.publicArticle(checkpoint.sourceIdentity.slug,AbortSignal.timeout(
|
|
163
|
+
const snapshot=await client.publicArticle(checkpoint.sourceIdentity.slug,AbortSignal.timeout(pageRemaining()));
|
|
157
164
|
const article=snapshot.article;
|
|
158
165
|
if(!article||!article.content_html||article.id!==values.article)throw Error('Published reader snapshot is missing');
|
|
159
166
|
while(!verified){
|
|
160
|
-
const response=await fetch(values.url,{redirect:'error',cache:'no-store',signal:AbortSignal.timeout(Math.min(5000,
|
|
167
|
+
const response=await fetch(values.url,{redirect:'error',cache:'no-store',signal:AbortSignal.timeout(Math.min(5000,pageRemaining()))});
|
|
161
168
|
const html=await response.text();
|
|
162
|
-
verified=response.ok&&articleEditHtmlMatches(html,{title:article.title,headline:article.headline,content_html:article.content_html,card_titles:(article.metadata?.cards??[]).map(c=>c.title),tags:checkpoint.document.tag_ids?article.tags:undefined});
|
|
163
|
-
if(!verified)await new Promise(r=>setTimeout(r,Math.min(
|
|
169
|
+
verified=response.ok&&articleEditHtmlMatches(html,{title:article.title,headline:article.headline,seo_title:'seo_title' in patch?article.seo_title:undefined,content_html:article.content_html,card_titles:(article.metadata?.cards??[]).map(c=>c.title),tags:checkpoint.document.tag_ids?article.tags:undefined});
|
|
170
|
+
if(!verified)await new Promise(r=>setTimeout(r,Math.min(1000,pageRemaining())));
|
|
164
171
|
}
|
|
165
172
|
}catch(error){reason=error.message;}
|
|
166
173
|
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;}
|
|
@@ -36,6 +36,11 @@ function articleEditHtmlMatches(html, expected) {
|
|
|
36
36
|
(m) => text(m[1]) === shown
|
|
37
37
|
);
|
|
38
38
|
if (!headline) return false;
|
|
39
|
+
const seo = expected.seo_title?.trim();
|
|
40
|
+
if (seo) {
|
|
41
|
+
const pageTitle = text(visible.match(/<title\b[^>]*>([\s\S]*?)<\/title>/iu)?.[1] ?? "");
|
|
42
|
+
if (pageTitle !== plain(seo) && !pageTitle.startsWith(plain(seo) + " ")) return false;
|
|
43
|
+
}
|
|
39
44
|
const paragraphs = [
|
|
40
45
|
...expected.content_html.matchAll(
|
|
41
46
|
/<(p|li|h[2-6]|blockquote)\b[^>]*>([\s\S]*?)<\/\1>/giu
|
|
@@ -55,13 +60,10 @@ function articleEditHtmlMatches(html, expected) {
|
|
|
55
60
|
}
|
|
56
61
|
}))) return false;
|
|
57
62
|
}
|
|
58
|
-
const titles = [
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
].flatMap(
|
|
63
|
-
(m) => [...m[1].matchAll(/<h2\b[^>]*>([\s\S]*?)<\/h2>/giu)].map((m2) => text(m2[1]))
|
|
64
|
-
);
|
|
63
|
+
const titles = [...visible.matchAll(/<aside\b([^>]*)>([\s\S]*?)<\/aside>/giu)].filter((m) => /\bdata-entity-card=["'][^"']+["']/iu.test(m[1]) || /class=["'][^"']*\barticle-verified-card\b/iu.test(m[1])).flatMap((m) => {
|
|
64
|
+
const first = m[2].match(/<h2\b[^>]*>([\s\S]*?)<\/h2>/iu);
|
|
65
|
+
return first ? [text(first[1])] : [];
|
|
66
|
+
});
|
|
65
67
|
return JSON.stringify(titles.sort()) === JSON.stringify(expected.card_titles.map(plain).sort());
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -680,7 +682,7 @@ async function runArticleEdit(t, input) {
|
|
|
680
682
|
409,
|
|
681
683
|
state.revisionId
|
|
682
684
|
);
|
|
683
|
-
for (const key of ["title", "subtitle", "content_markdown", "excerpt"])
|
|
685
|
+
for (const key of ["title", "headline", "fly_title", "seo_title", "subtitle", "content_markdown", "excerpt"])
|
|
684
686
|
if (state.document?.[key] !== void 0 && !equal(after[key] ?? "", state.document[key]))
|
|
685
687
|
throw new ArticleEditOperationError(
|
|
686
688
|
"VERIFICATION_CONFLICT",
|
|
@@ -3,10 +3,17 @@ import { A as ArticleFormat, b as ArticleLayout } from './article-presentation-0
|
|
|
3
3
|
/** Check rendered image tags, not a URL mentioned only in serialized page data. */
|
|
4
4
|
declare function articlePhotoHtmlMatches(html: string, lead: string | string[], social: string): boolean;
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Requires visible body and actual card asides; serialized page data is not delivery.
|
|
8
|
+
* Cards are recognised by either rendering the papers use: the package's `article-verified-card`
|
|
9
|
+
* aside, or the papers' entity cards (`<aside data-entity-card="venue|event|…">`), each titled by its h2.
|
|
10
|
+
* When the edit carried a search title, the document's `<title>` must show it (a site may append its
|
|
11
|
+
* own name after it).
|
|
12
|
+
*/
|
|
7
13
|
declare function articleEditHtmlMatches(html: string, expected: {
|
|
8
14
|
title: string;
|
|
9
15
|
headline?: string | null;
|
|
16
|
+
seo_title?: string | null;
|
|
10
17
|
content_html: string;
|
|
11
18
|
card_titles: string[];
|
|
12
19
|
tags?: {
|
|
@@ -3,10 +3,17 @@ import { A as ArticleFormat, b as ArticleLayout } from './article-presentation-0
|
|
|
3
3
|
/** Check rendered image tags, not a URL mentioned only in serialized page data. */
|
|
4
4
|
declare function articlePhotoHtmlMatches(html: string, lead: string | string[], social: string): boolean;
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Requires visible body and actual card asides; serialized page data is not delivery.
|
|
8
|
+
* Cards are recognised by either rendering the papers use: the package's `article-verified-card`
|
|
9
|
+
* aside, or the papers' entity cards (`<aside data-entity-card="venue|event|…">`), each titled by its h2.
|
|
10
|
+
* When the edit carried a search title, the document's `<title>` must show it (a site may append its
|
|
11
|
+
* own name after it).
|
|
12
|
+
*/
|
|
7
13
|
declare function articleEditHtmlMatches(html: string, expected: {
|
|
8
14
|
title: string;
|
|
9
15
|
headline?: string | null;
|
|
16
|
+
seo_title?: string | null;
|
|
10
17
|
content_html: string;
|
|
11
18
|
card_titles: string[];
|
|
12
19
|
tags?: {
|
|
@@ -36,6 +36,11 @@ function articleEditHtmlMatches(html, expected) {
|
|
|
36
36
|
(m) => text(m[1]) === shown
|
|
37
37
|
);
|
|
38
38
|
if (!headline) return false;
|
|
39
|
+
const seo = expected.seo_title?.trim();
|
|
40
|
+
if (seo) {
|
|
41
|
+
const pageTitle = text(visible.match(/<title\b[^>]*>([\s\S]*?)<\/title>/iu)?.[1] ?? "");
|
|
42
|
+
if (pageTitle !== plain(seo) && !pageTitle.startsWith(plain(seo) + " ")) return false;
|
|
43
|
+
}
|
|
39
44
|
const paragraphs = [
|
|
40
45
|
...expected.content_html.matchAll(
|
|
41
46
|
/<(p|li|h[2-6]|blockquote)\b[^>]*>([\s\S]*?)<\/\1>/giu
|
|
@@ -55,13 +60,10 @@ function articleEditHtmlMatches(html, expected) {
|
|
|
55
60
|
}
|
|
56
61
|
}))) return false;
|
|
57
62
|
}
|
|
58
|
-
const titles = [
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
].flatMap(
|
|
63
|
-
(m) => [...m[1].matchAll(/<h2\b[^>]*>([\s\S]*?)<\/h2>/giu)].map((m2) => text(m2[1]))
|
|
64
|
-
);
|
|
63
|
+
const titles = [...visible.matchAll(/<aside\b([^>]*)>([\s\S]*?)<\/aside>/giu)].filter((m) => /\bdata-entity-card=["'][^"']+["']/iu.test(m[1]) || /class=["'][^"']*\barticle-verified-card\b/iu.test(m[1])).flatMap((m) => {
|
|
64
|
+
const first = m[2].match(/<h2\b[^>]*>([\s\S]*?)<\/h2>/iu);
|
|
65
|
+
return first ? [text(first[1])] : [];
|
|
66
|
+
});
|
|
65
67
|
return JSON.stringify(titles.sort()) === JSON.stringify(expected.card_titles.map(plain).sort());
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -652,7 +654,7 @@ async function runArticleEdit(t, input) {
|
|
|
652
654
|
409,
|
|
653
655
|
state.revisionId
|
|
654
656
|
);
|
|
655
|
-
for (const key of ["title", "subtitle", "content_markdown", "excerpt"])
|
|
657
|
+
for (const key of ["title", "headline", "fly_title", "seo_title", "subtitle", "content_markdown", "excerpt"])
|
|
656
658
|
if (state.document?.[key] !== void 0 && !equal(after[key] ?? "", state.document[key]))
|
|
657
659
|
throw new ArticleEditOperationError(
|
|
658
660
|
"VERIFICATION_CONFLICT",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalemule/news-publication",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.95",
|
|
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": {
|