@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. Reuse the same patch/key/checkpoint on retry.
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(remaining()));
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,remaining()))});
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(300,remaining())));
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
- ...visible.matchAll(
60
- /<aside\b[^>]*class=["'][^"']*\barticle-verified-card\b[^"']*["'][^>]*>([\s\S]*?)<\/aside>/giu
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
- /** Requires visible body and actual card asides; serialized page data is not delivery. */
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
- /** Requires visible body and actual card asides; serialized page data is not delivery. */
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
- ...visible.matchAll(
60
- /<aside\b[^>]*class=["'][^"']*\barticle-verified-card\b[^"']*["'][^>]*>([\s\S]*?)<\/aside>/giu
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.94",
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": {