@scalemule/news-publication 0.0.44 → 0.0.46

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
@@ -73,6 +73,67 @@ The React front page still lives in the Walnut Creek Times repository. This pack
73
73
 
74
74
  Patch only (`0.0.x` or `0.1.x`) until a stable release is explicitly approved. Publishing is manual: `npm publish` from this directory.
75
75
 
76
+ ## Local briefing foundation
77
+
78
+ `composeLocalBriefing(candidates, options)` is an opt-in, pure selector for an existing
79
+ publication brief. It consumes `BriefingCandidate` projections from an authorized
80
+ server adapter and returns a bounded list using existing `NewsItem` text. It does
81
+ not fetch or generate news, verify claims, store preferences, or change the current
82
+ `composeEdition` path. A server adapter and publication pilot are still required;
83
+ there is no new live BaseNews endpoint in this package.
84
+
85
+ This is an article-backed presentation helper, not the canonical BaseNews schema.
86
+ BaseNews events, issues and opportunities must also work without an article.
87
+ Its eligibility rules apply only to this optional brief. They must never become
88
+ new publishing gates for articles, questionnaires, jokes, jobs, classifieds,
89
+ celebrations or other existing publication features. Their current owner services
90
+ and workflows continue independently, including when intelligence processing is
91
+ unavailable. Underlying capture, trust and agent APIs take priority over UI rollout.
92
+
93
+ Supply the recipient `sm_application_id`, `publication_id`, resolved
94
+ `coverage_area_ids`, and optional explicit community/topic/entity/issue preferences.
95
+ `personalized: false` ignores all preferences. `limit` is a maximum from 1–12
96
+ (default 6); zero useful records yields an empty briefing. Render summaries as
97
+ text, with original reporting links, verification labels and source attribution.
98
+ `why_it_matters` is optional editorial text; reason codes explain selection and
99
+ are not factual claims about an item's consequences.
100
+
101
+ The server must derive scope from authentication, authorize destination grants,
102
+ resolve geographic relationships, enforce evidence/summary redistribution rights,
103
+ and send public, approved projections. Reuse existing Content Intelligence
104
+ classification, coverage profiles, editorial decisions and current evaluation
105
+ receipts in that adapter; this presentation selector does not replace its generic
106
+ relevance engine or editorial ledger. This helper is **not an authorization or
107
+ input-validation boundary**. Do not pass client-supplied trust/approval fields,
108
+ private artifacts, or raw community submissions. Unverified and community-submitted
109
+ information retains those labels even after editorial approval.
110
+
111
+ Each candidate references an existing canonical development, occurrence or reporting
112
+ record via a namespaced `canonical_key`, with its revision. Do not use a whole issue
113
+ ID as the key: separate developments must survive. Include withdrawal/supersession
114
+ revisions when combining cached inventories; the newest revision wins before
115
+ eligibility and ranking. Supply one selected edition per canonical revision;
116
+ conflicting copies of that revision are omitted for reconciliation, not selected
117
+ by input order. A feed lacking tombstones cannot prove that cached records
118
+ are still current. The caller must invalidate caches on corrections and grant
119
+ revocation. Treat personalized HTTP responses as `private, no-store`.
120
+
121
+ Policy 1 uses a civic priority tier, followed by geography × 4, practical value × 3,
122
+ explicit follows × 3, topics × 2, reporting recency and editorial discovery value.
123
+ All seven components and reason codes are returned. Recency describes reporting,
124
+ not event time or urgency. Civic importance comes from editors, not section names.
125
+ Keep a visible link to all important coverage whenever `remaining_important_count`
126
+ is positive. Eligibility still applies to civic items: stale or unauthorized facts
127
+ cannot bypass trust gates.
128
+
129
+ `relevant_until` is required. Confirmed labels also require a public evidence link,
130
+ `last_verified_at` and a future `review_due_at`. Expired review/relevance deadlines
131
+ exclude records; missing freshness information stays `unknown`. No date is inferred
132
+ from `updated_at`, no confidence score promotes verification, and no fallback story
133
+ fills a quota. The entire approved excerpt must end with sentence punctuation and
134
+ fit within 280 Unicode code points; otherwise it is omitted for editorial repair.
135
+ Later qualifications and corrections are never cut off to make a shorter summary.
136
+
76
137
 
77
138
  ### Reporting credit and sources
78
139
 
package/dist/index.cjs CHANGED
@@ -73,106 +73,6 @@ function resolveLayout(publication, cookieValue) {
73
73
  return publication.layout;
74
74
  }
75
75
 
76
- // src/preset.ts
77
- var walnutCreekTimes = definePublication({
78
- slug: "walnutcreektimes",
79
- name: "Walnut Creek Times",
80
- masthead: ["Walnut", "Creek", "Times"],
81
- region: "Walnut Creek",
82
- regionLabel: "Walnut Creek",
83
- towns: ["Walnut Creek", "Rossmoor", "Saranap", "Pleasant Hill"],
84
- nearbyTowns: ["Martinez", "Concord", "Contra Costa"],
85
- sections: ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health"],
86
- description: "Independent local journalism covering civic decisions, downtown commerce, neighborhood life, and community achievements across Walnut Creek, Rossmoor, Saranap, and Pleasant Hill.",
87
- tagline: "Local stories. Stronger communities.",
88
- layout: "broadsheet",
89
- layouts: ["broadsheet"],
90
- features: { smile: true, jobs: true, classifieds: true },
91
- modules: { home: ["daily-smile"] }
92
- });
93
-
94
- // src/cache.ts
95
- var NEWS_CACHE = {
96
- feedRevalidateSeconds: 30,
97
- articleRevalidateSeconds: 60,
98
- edgeMaxAgeSeconds: 60,
99
- staleWhileRevalidateSeconds: 300,
100
- feedTag: "news-feed",
101
- articleTag: "news-article",
102
- inventoryTag: "public-news-discovery-v2",
103
- paths: ["/", "/news"]
104
- };
105
- var feedCacheOptions = {
106
- revalidate: NEWS_CACHE.feedRevalidateSeconds,
107
- tags: [NEWS_CACHE.feedTag]
108
- };
109
- var articleCacheOptions = {
110
- revalidate: NEWS_CACHE.articleRevalidateSeconds,
111
- tags: [NEWS_CACHE.articleTag]
112
- };
113
- var inventoryCacheOptions = {
114
- revalidate: NEWS_CACHE.feedRevalidateSeconds,
115
- tags: [NEWS_CACHE.feedTag, NEWS_CACHE.inventoryTag]
116
- };
117
- function edgeCacheControl() {
118
- return `public, s-maxage=${NEWS_CACHE.edgeMaxAgeSeconds}, stale-while-revalidate=${NEWS_CACHE.staleWhileRevalidateSeconds}`;
119
- }
120
- function feedCacheKey(publicationId, before, section) {
121
- return `${publicationId}:${before ?? ""}:${section ?? ""}`;
122
- }
123
- function articleCacheKey(publicationId, slug) {
124
- return `${publicationId}:${slug}`;
125
- }
126
- var NewsError = class extends Error {
127
- constructor(status, message) {
128
- super(message);
129
- this.status = status;
130
- this.name = "NewsError";
131
- }
132
- status;
133
- };
134
- function shouldServeLastKnown(error) {
135
- const status = error instanceof NewsError ? error.status : error && typeof error === "object" && "status" in error ? error.status : void 0;
136
- return status !== 404;
137
- }
138
- async function withLastKnown(store, key, load, accept = () => true) {
139
- try {
140
- const value = await load();
141
- if (accept(value)) store.set(key, value);
142
- return value;
143
- } catch (error) {
144
- if (!shouldServeLastKnown(error)) throw error;
145
- const cached = store.get(key);
146
- if (cached) return cached;
147
- throw error;
148
- }
149
- }
150
- function secretsMatch(provided, secret) {
151
- if (!provided || !secret) return false;
152
- const left = Array.from(provided);
153
- const right = Array.from(secret);
154
- let mismatch = left.length === right.length ? 0 : 1;
155
- const length = Math.max(left.length, right.length);
156
- for (let index = 0; index < length; index += 1) {
157
- mismatch |= (left[index]?.codePointAt(0) ?? 0) ^ (right[index]?.codePointAt(0) ?? 0);
158
- }
159
- return mismatch === 0;
160
- }
161
- function revalidateDecision(input) {
162
- if (!secretsMatch(input.providedSecret, input.configuredSecret)) {
163
- return { status: 401, body: { error: "Unauthorized" } };
164
- }
165
- const tag = input.tag?.trim() || `${NEWS_CACHE.feedTag}, ${NEWS_CACHE.articleTag}`;
166
- const path = input.path?.trim() || NEWS_CACHE.paths.join(", ");
167
- return { status: 200, body: { revalidated: true, tag, path, now: input.now ?? Date.now() } };
168
- }
169
- function defaultRevalidateTargets() {
170
- return {
171
- tags: [NEWS_CACHE.feedTag, NEWS_CACHE.articleTag, NEWS_CACHE.inventoryTag],
172
- paths: [...NEWS_CACHE.paths]
173
- };
174
- }
175
-
176
76
  // src/editorial.ts
177
77
  var labels = {
178
78
  "Local businesses": "Business",
@@ -190,8 +90,8 @@ var EDITORIAL_SECTION_ORDER = [
190
90
  "Schools",
191
91
  "Opinion"
192
92
  ];
193
- function orderedSections(sections, order = EDITORIAL_SECTION_ORDER) {
194
- return sections.filter((section) => section.name !== "Local news").slice().sort((left, right) => {
93
+ function orderedSections(sections2, order = EDITORIAL_SECTION_ORDER) {
94
+ return sections2.filter((section) => section.name !== "Local news").slice().sort((left, right) => {
195
95
  const leftRank = order.indexOf(left.name);
196
96
  const rightRank = order.indexOf(right.name);
197
97
  const leftOrder = leftRank === -1 ? order.length : leftRank;
@@ -252,12 +152,12 @@ function composeEdition(items, selection = {}, now = Date.now()) {
252
152
  secondary.push(deferredWithDuplicateImage.shift());
253
153
  }
254
154
  for (const item of secondary) remaining.splice(remaining.indexOf(item), 1);
255
- const eligible = remaining.filter((item) => {
155
+ const eligible2 = remaining.filter((item) => {
256
156
  const age = now - Date.parse(item.published_at);
257
157
  return item.excerpt?.trim() && age >= 0 && age <= 7 * 864e5;
258
158
  });
259
- const selected = (pins.brief_slugs ?? []).flatMap((slug) => eligible.find((item) => item.slug === slug) ?? []);
260
- const candidates = [...selected, ...editorialSections(eligible).map((group) => group.items[0]), ...eligible];
159
+ const selected = (pins.brief_slugs ?? []).flatMap((slug) => eligible2.find((item) => item.slug === slug) ?? []);
160
+ const candidates = [...selected, ...editorialSections(eligible2).map((group) => group.items[0]), ...eligible2];
261
161
  const briefIds = /* @__PURE__ */ new Set();
262
162
  const briefs = candidates.filter((item) => {
263
163
  if (!item || briefIds.has(item.id) || briefIds.size >= 4) return false;
@@ -266,8 +166,8 @@ function composeEdition(items, selection = {}, now = Date.now()) {
266
166
  });
267
167
  const compact = [...briefs, ...remaining.filter((item) => !briefIds.has(item.id))].slice(0, 4);
268
168
  const compactIds = new Set(compact.map((item) => item.id));
269
- const sections = editorialSections(remaining.filter((item) => !compactIds.has(item.id)));
270
- return { lead, secondary, briefs, compact, sections, navigation: editorialSections(inventory) };
169
+ const sections2 = editorialSections(remaining.filter((item) => !compactIds.has(item.id)));
170
+ return { lead, secondary, briefs, compact, sections: sections2, navigation: editorialSections(inventory) };
271
171
  }
272
172
  function displayHeadline(article, selection = {}) {
273
173
  return selection.stories?.[article.slug]?.display_title?.trim() || article.title;
@@ -432,6 +332,265 @@ function llmsText(publication, canonical) {
432
332
  ].join("\n");
433
333
  }
434
334
 
335
+ // src/briefing.ts
336
+ var LOCAL_BRIEFING_POLICY_VERSION = "1";
337
+ var DAY = 864e5;
338
+ var verifications = /* @__PURE__ */ new Set([
339
+ "unverified",
340
+ "community_submitted",
341
+ "officially_confirmed",
342
+ "independently_verified",
343
+ "disputed"
344
+ ]);
345
+ var sections = /* @__PURE__ */ new Set(["important", "around_you", "people", "discover", "participate", "following"]);
346
+ var overlaps = (a, b = []) => a.some((id) => id.trim() !== "" && b.includes(id));
347
+ function timestamp(value) {
348
+ if (!/^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,3})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.test(value) || !validCalendarDate(value.slice(0, 10))) return NaN;
349
+ return Date.parse(value);
350
+ }
351
+ function sourcesFor(candidate) {
352
+ const seen = /* @__PURE__ */ new Set();
353
+ return candidate.sources.flatMap((source) => {
354
+ const url = publicHttps(source.url);
355
+ if (!source.id.trim() || !source.publisher.trim() || !source.title.trim() || !url || seen.has(source.id)) return [];
356
+ seen.add(source.id);
357
+ return [{ id: source.id, url, title: source.title, publisher: source.publisher }];
358
+ });
359
+ }
360
+ function eligible(candidate, coverage, now) {
361
+ if (candidate.publication_status !== "published" || candidate.visibility !== "public" || candidate.editorial_approval !== "approved" || candidate.lifecycle !== "current" || !candidate.article.id.trim() || !candidate.article.slug.trim() || !candidate.article.title.trim() || !verifications.has(candidate.verification) || !sections.has(candidate.section) || !overlaps(candidate.geography_ids, coverage)) return false;
362
+ const published = timestamp(candidate.article.published_at);
363
+ const expiry = timestamp(candidate.relevant_until);
364
+ if (!Number.isFinite(published) || published > now || !Number.isFinite(expiry) || expiry <= now) return false;
365
+ const verified = candidate.last_verified_at === null ? null : timestamp(candidate.last_verified_at);
366
+ const review = candidate.review_due_at === null ? null : timestamp(candidate.review_due_at);
367
+ if (verified !== null && (!Number.isFinite(verified) || verified > now)) return false;
368
+ if (review !== null && (!Number.isFinite(review) || review <= now || verified !== null && review <= verified)) return false;
369
+ const confirmed = candidate.verification === "officially_confirmed" || candidate.verification === "independently_verified";
370
+ return !confirmed || verified !== null && review !== null && sourcesFor(candidate).length > 0;
371
+ }
372
+ function projectionKey(candidate) {
373
+ const article = candidate.article;
374
+ return JSON.stringify([
375
+ article.id,
376
+ article.slug,
377
+ article.title,
378
+ article.excerpt,
379
+ article.published_at,
380
+ article.author_name,
381
+ publicHttps(article.canonical_url),
382
+ [...candidate.geography_ids].sort(),
383
+ [...candidate.topic_ids].sort(),
384
+ [...candidate.entity_ids].sort(),
385
+ [...candidate.issue_ids].sort(),
386
+ candidate.section,
387
+ candidate.civic_importance,
388
+ candidate.practical_value,
389
+ candidate.discovery_value,
390
+ candidate.why_it_matters,
391
+ candidate.relevant_until,
392
+ candidate.verification,
393
+ candidate.last_verified_at,
394
+ candidate.review_due_at,
395
+ sourcesFor(candidate).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
396
+ ]);
397
+ }
398
+ function composeLocalBriefing(candidates, options) {
399
+ const now = options.now ?? Date.now();
400
+ const limit = options.limit ?? 6;
401
+ if (!options.sm_application_id.trim() || !options.publication_id.trim() || !Number.isFinite(now) || !Number.isFinite(new Date(now).getTime()) || !Number.isInteger(limit) || limit < 1 || limit > 12) {
402
+ throw new RangeError("A briefing requires application/publication scope, a valid time and a limit from 1 to 12");
403
+ }
404
+ const preferences = options.personalized === false ? {} : options.preferences ?? {};
405
+ const personalized = [preferences.community_ids, preferences.topic_ids, preferences.entity_ids, preferences.issue_ids].some((ids) => ids?.some((id) => id.trim() !== ""));
406
+ const newest = /* @__PURE__ */ new Map();
407
+ for (const candidate of candidates) {
408
+ if (candidate.sm_application_id !== options.sm_application_id || candidate.publication_id !== options.publication_id || !candidate.canonical_key.trim() || !Number.isSafeInteger(candidate.revision) || candidate.revision < 1) continue;
409
+ const group = newest.get(candidate.canonical_key);
410
+ if (!group || candidate.revision > group[0].revision) newest.set(candidate.canonical_key, [candidate]);
411
+ else if (candidate.revision === group[0].revision) group.push(candidate);
412
+ }
413
+ const ranked = [];
414
+ for (const group of newest.values()) {
415
+ if (group.some((candidate2) => !eligible(candidate2, options.coverage_area_ids, now))) continue;
416
+ const candidate = group[0];
417
+ if (group.length > 1 && group.some((copy) => projectionKey(copy) !== projectionKey(candidate))) continue;
418
+ const summary = candidate.article.excerpt?.trim() ?? "";
419
+ if (!summary || Array.from(summary).length > 280 || !/[.!?。!?][”’"']?$/u.test(summary)) continue;
420
+ const community = overlaps(candidate.geography_ids, preferences.community_ids);
421
+ const entity = overlaps(candidate.entity_ids, preferences.entity_ids);
422
+ const issue = overlaps(candidate.issue_ids, preferences.issue_ids);
423
+ const topic = overlaps(candidate.topic_ids, preferences.topic_ids);
424
+ const published = timestamp(candidate.article.published_at);
425
+ const age = now - published;
426
+ const important = candidate.civic_importance === "important";
427
+ const relevance = {
428
+ geographic: community ? 2 : 1,
429
+ practical: candidate.practical_value ? 1 : 0,
430
+ relationship: entity || issue ? 1 : 0,
431
+ interest: topic ? 1 : 0,
432
+ temporal: age < DAY ? 2 : age < 7 * DAY ? 1 : 0,
433
+ civic: important ? 1 : 0,
434
+ discovery: candidate.discovery_value ? 1 : 0
435
+ };
436
+ const reasons = [];
437
+ if (important) reasons.push("important_civic_information");
438
+ reasons.push(community ? "selected_community" : "publication_coverage");
439
+ if (candidate.practical_value) reasons.push("practical_value");
440
+ if (entity) reasons.push("following_entity");
441
+ if (issue) reasons.push("following_issue");
442
+ if (topic) reasons.push("selected_topic");
443
+ if (relevance.temporal > 0) reasons.push("recent_reporting");
444
+ if (candidate.discovery_value) reasons.push("local_discovery");
445
+ const article = candidate.article;
446
+ const item = {
447
+ canonical_key: candidate.canonical_key,
448
+ revision: candidate.revision,
449
+ article: {
450
+ id: article.id,
451
+ slug: article.slug,
452
+ title: article.title,
453
+ published_at: article.published_at,
454
+ author_name: article.author_name,
455
+ canonical_url: publicHttps(article.canonical_url) ?? null
456
+ },
457
+ summary,
458
+ section: important ? "important" : entity || issue ? "following" : candidate.section,
459
+ geography_ids: [...new Set(candidate.geography_ids)],
460
+ why_it_matters: candidate.why_it_matters?.trim() || void 0,
461
+ reasons,
462
+ relevance,
463
+ verification: candidate.verification,
464
+ freshness: candidate.last_verified_at !== null && candidate.review_due_at !== null ? "current" : "unknown",
465
+ last_verified_at: candidate.last_verified_at,
466
+ review_due_at: candidate.review_due_at,
467
+ relevant_until: candidate.relevant_until,
468
+ sources: sourcesFor(candidate)
469
+ };
470
+ const score = 4 * relevance.geographic + 3 * relevance.practical + 3 * relevance.relationship + 2 * relevance.interest + relevance.temporal + relevance.discovery;
471
+ ranked.push({ item, score, published, important });
472
+ }
473
+ ranked.sort((a, b) => Number(b.important) - Number(a.important) || b.score - a.score || b.published - a.published || (a.item.canonical_key < b.item.canonical_key ? -1 : a.item.canonical_key > b.item.canonical_key ? 1 : 0));
474
+ const seenArticles = /* @__PURE__ */ new Set();
475
+ const seenUrls = /* @__PURE__ */ new Set();
476
+ const deduplicated = ranked.filter(({ item }) => {
477
+ const url = item.article.canonical_url;
478
+ if (seenArticles.has(item.article.id) || url && seenUrls.has(url)) return false;
479
+ seenArticles.add(item.article.id);
480
+ if (url) seenUrls.add(url);
481
+ return true;
482
+ });
483
+ return {
484
+ schema_version: 1,
485
+ policy_version: LOCAL_BRIEFING_POLICY_VERSION,
486
+ publication_id: options.publication_id,
487
+ generated_at: new Date(now).toISOString(),
488
+ personalized,
489
+ items: deduplicated.slice(0, limit).map(({ item }) => item),
490
+ remaining_important_count: deduplicated.slice(limit).filter(({ important }) => important).length
491
+ };
492
+ }
493
+
494
+ // src/preset.ts
495
+ var walnutCreekTimes = definePublication({
496
+ slug: "walnutcreektimes",
497
+ name: "Walnut Creek Times",
498
+ masthead: ["Walnut", "Creek", "Times"],
499
+ region: "Walnut Creek",
500
+ regionLabel: "Walnut Creek",
501
+ towns: ["Walnut Creek", "Rossmoor", "Saranap", "Pleasant Hill"],
502
+ nearbyTowns: ["Martinez", "Concord", "Contra Costa"],
503
+ sections: ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health"],
504
+ description: "Independent local journalism covering civic decisions, downtown commerce, neighborhood life, and community achievements across Walnut Creek, Rossmoor, Saranap, and Pleasant Hill.",
505
+ tagline: "Local stories. Stronger communities.",
506
+ layout: "broadsheet",
507
+ layouts: ["broadsheet"],
508
+ features: { smile: true, jobs: true, classifieds: true },
509
+ modules: { home: ["daily-smile"] }
510
+ });
511
+
512
+ // src/cache.ts
513
+ var NEWS_CACHE = {
514
+ feedRevalidateSeconds: 30,
515
+ articleRevalidateSeconds: 60,
516
+ edgeMaxAgeSeconds: 60,
517
+ staleWhileRevalidateSeconds: 300,
518
+ feedTag: "news-feed",
519
+ articleTag: "news-article",
520
+ inventoryTag: "public-news-discovery-v2",
521
+ paths: ["/", "/news"]
522
+ };
523
+ var feedCacheOptions = {
524
+ revalidate: NEWS_CACHE.feedRevalidateSeconds,
525
+ tags: [NEWS_CACHE.feedTag]
526
+ };
527
+ var articleCacheOptions = {
528
+ revalidate: NEWS_CACHE.articleRevalidateSeconds,
529
+ tags: [NEWS_CACHE.articleTag]
530
+ };
531
+ var inventoryCacheOptions = {
532
+ revalidate: NEWS_CACHE.feedRevalidateSeconds,
533
+ tags: [NEWS_CACHE.feedTag, NEWS_CACHE.inventoryTag]
534
+ };
535
+ function edgeCacheControl() {
536
+ return `public, s-maxage=${NEWS_CACHE.edgeMaxAgeSeconds}, stale-while-revalidate=${NEWS_CACHE.staleWhileRevalidateSeconds}`;
537
+ }
538
+ function feedCacheKey(publicationId, before, section) {
539
+ return `${publicationId}:${before ?? ""}:${section ?? ""}`;
540
+ }
541
+ function articleCacheKey(publicationId, slug) {
542
+ return `${publicationId}:${slug}`;
543
+ }
544
+ var NewsError = class extends Error {
545
+ constructor(status, message) {
546
+ super(message);
547
+ this.status = status;
548
+ this.name = "NewsError";
549
+ }
550
+ status;
551
+ };
552
+ function shouldServeLastKnown(error) {
553
+ const status = error instanceof NewsError ? error.status : error && typeof error === "object" && "status" in error ? error.status : void 0;
554
+ return status !== 404;
555
+ }
556
+ async function withLastKnown(store, key, load, accept = () => true) {
557
+ try {
558
+ const value = await load();
559
+ if (accept(value)) store.set(key, value);
560
+ return value;
561
+ } catch (error) {
562
+ if (!shouldServeLastKnown(error)) throw error;
563
+ const cached = store.get(key);
564
+ if (cached) return cached;
565
+ throw error;
566
+ }
567
+ }
568
+ function secretsMatch(provided, secret) {
569
+ if (!provided || !secret) return false;
570
+ const left = Array.from(provided);
571
+ const right = Array.from(secret);
572
+ let mismatch = left.length === right.length ? 0 : 1;
573
+ const length = Math.max(left.length, right.length);
574
+ for (let index = 0; index < length; index += 1) {
575
+ mismatch |= (left[index]?.codePointAt(0) ?? 0) ^ (right[index]?.codePointAt(0) ?? 0);
576
+ }
577
+ return mismatch === 0;
578
+ }
579
+ function revalidateDecision(input) {
580
+ if (!secretsMatch(input.providedSecret, input.configuredSecret)) {
581
+ return { status: 401, body: { error: "Unauthorized" } };
582
+ }
583
+ const tag = input.tag?.trim() || `${NEWS_CACHE.feedTag}, ${NEWS_CACHE.articleTag}`;
584
+ const path = input.path?.trim() || NEWS_CACHE.paths.join(", ");
585
+ return { status: 200, body: { revalidated: true, tag, path, now: input.now ?? Date.now() } };
586
+ }
587
+ function defaultRevalidateTargets() {
588
+ return {
589
+ tags: [NEWS_CACHE.feedTag, NEWS_CACHE.articleTag, NEWS_CACHE.inventoryTag],
590
+ paths: [...NEWS_CACHE.paths]
591
+ };
592
+ }
593
+
435
594
  // src/analytics.ts
436
595
  var ANALYTICS_VERSION = 1;
437
596
  var ANALYTICS_COOKIE = "napsite_analytics_v1";
@@ -3174,6 +3333,7 @@ exports.EVENTS = EVENTS;
3174
3333
  exports.FEATURE_NAMES = FEATURE_NAMES;
3175
3334
  exports.JOB_EMPLOYMENT_TYPES = JOB_EMPLOYMENT_TYPES;
3176
3335
  exports.LAYOUT_COOKIE = LAYOUT_COOKIE;
3336
+ exports.LOCAL_BRIEFING_POLICY_VERSION = LOCAL_BRIEFING_POLICY_VERSION;
3177
3337
  exports.LocalNetworkBar = LocalNetworkBar;
3178
3338
  exports.MEET_ACTIVITIES = MEET_ACTIVITIES;
3179
3339
  exports.NETWORK_PARENT = NETWORK_PARENT;
@@ -3210,6 +3370,7 @@ exports.clipChars = clipChars;
3210
3370
  exports.communityMapEmbeds = communityMapEmbeds;
3211
3371
  exports.communityPublications = communityPublications;
3212
3372
  exports.composeEdition = composeEdition;
3373
+ exports.composeLocalBriefing = composeLocalBriefing;
3213
3374
  exports.dealDollars = dealDollars;
3214
3375
  exports.dealGoPath = dealGoPath;
3215
3376
  exports.dealsFeedPath = dealsFeedPath;
package/dist/index.d.cts CHANGED
@@ -1,8 +1,8 @@
1
- import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, H as HomepageSelection, b as NearbyUpdate, c as NewsItem, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-DFDCZHbS.cjs';
2
- export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-DFDCZHbS.cjs';
1
+ import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, b as NewsItem, H as HomepageSelection, c as NearbyUpdate, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-BJFCWWi0.cjs';
2
+ export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-BJFCWWi0.cjs';
3
3
  export { M as MEET_ACTIVITIES, b as MeetActivity, c as MeetupInput, a as PreparedMeetup, P as PublicPlace, d as activityLabel, e as canHostMeetup, i as isAdult, p as prepareMeetup, s as screenMessage } from './meet-RWmfj3I3.cjs';
4
4
  export { b as CLASSIFIED_CATEGORIES, c as ClassifiedCategory, C as ClassifiedListing, E as EmploymentType, J as JOB_EMPLOYMENT_TYPES, d as PreparedClassified, e as PreparedJob, P as PublicDeal, f as PublicDealOffer, a as PublicJob, g as categoryLabel, h as classifiedPath, i as classifiedSearchPath, j as clipChars, k as dealDollars, l as dealGoPath, m as dealsFeedPath, n as employmentLabel, o as isClassifiedCategory, p as isEmploymentType, q as jobEmployerBody, r as jobOpeningBody, s as jobPostingBody, t as prepareClassified, u as prepareJobPost, v as publicJobPath, w as publicJobsPath } from './marketplace-29i5w4LS.cjs';
5
- export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-C0IGpt-B.cjs';
5
+ export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-Dx4Lv-lB.cjs';
6
6
  import { CookieCategoryDefinition, CookieConsentRecord } from '@scalemule/compliance';
7
7
  export { COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, COOKIE_CONSENT_MAX_AGE_SECONDS, CookieCategory, CookieCategoryDefinition, CookieConsentRecord, CookieDisclosure, CookiePreferences, DEFAULT_CONSENT_HUB_URL, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED, EU_EEA_UK_COUNTRIES, NETWORK_CONSENT_STORAGE_KEY, acceptAllCookies, checkUrlConsentBridge, clearCookieConsent, decorateNetworkUrl, getCookieConsent, hasConsented, isCategoryAllowed, isGeoConsentRequired, queryNetworkConsentHub, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub } from '@scalemule/compliance';
8
8
  export { CookieConsentBanner, CookieConsentBannerProps } from './cookie-consent-banner.cjs';
@@ -32,6 +32,112 @@ declare const LAYOUT_COOKIE = "news_publication_layout";
32
32
  */
33
33
  declare function resolveLayout(publication: NewsPublication, cookieValue?: string | null): string;
34
34
 
35
+ declare const LOCAL_BRIEFING_POLICY_VERSION = "1";
36
+ type BriefingVerification = "unverified" | "community_submitted" | "officially_confirmed" | "independently_verified" | "disputed";
37
+ type BriefingSection = "important" | "around_you" | "people" | "discover" | "participate" | "following";
38
+ /** Public evidence links only. Artifact URLs and private reporter notes do not belong here. */
39
+ interface BriefingSource {
40
+ id: string;
41
+ url: string;
42
+ title: string;
43
+ publisher: string;
44
+ }
45
+ /** Explicit choices only; no inferred demographics, relationships or reading history. */
46
+ interface BriefingPreferences {
47
+ community_ids?: readonly string[];
48
+ topic_ids?: readonly string[];
49
+ entity_ids?: readonly string[];
50
+ issue_ids?: readonly string[];
51
+ }
52
+ /**
53
+ * A server-authorized projection, not a new canonical record or submission API.
54
+ * Article-backed presentation only: BaseNews records need not have an article.
55
+ * Eligibility here must never gate existing publication or community workflows.
56
+ * The producer must resolve grants, evidence rights and geographic relationships.
57
+ * Keep withdrawn/superseded revisions in the input to suppress older copies.
58
+ */
59
+ interface BriefingCandidate {
60
+ sm_application_id: string;
61
+ publication_id: string;
62
+ /** Namespaced existing development/occurrence/reporting ID, never an issue-wide ID. */
63
+ canonical_key: string;
64
+ revision: number;
65
+ article: NewsItem;
66
+ publication_status: "published" | "draft";
67
+ visibility: "public" | "private";
68
+ editorial_approval: "approved" | "pending";
69
+ lifecycle: "current" | "superseded" | "withdrawn";
70
+ /** Resolved public area IDs, including applicable containing areas. No coordinates. */
71
+ geography_ids: readonly string[];
72
+ topic_ids: readonly string[];
73
+ entity_ids: readonly string[];
74
+ issue_ids: readonly string[];
75
+ section: BriefingSection;
76
+ /** Editorial assignments, never inferred from emotional language or click rates. */
77
+ civic_importance: "routine" | "important";
78
+ practical_value: boolean;
79
+ discovery_value: boolean;
80
+ why_it_matters?: string;
81
+ /** Required editorial relevance expiry; article publication time is not event time. */
82
+ relevant_until: string;
83
+ verification: BriefingVerification;
84
+ last_verified_at: string | null;
85
+ review_due_at: string | null;
86
+ sources: readonly BriefingSource[];
87
+ }
88
+ type BriefingReason = "important_civic_information" | "selected_community" | "publication_coverage" | "practical_value" | "following_entity" | "following_issue" | "selected_topic" | "recent_reporting" | "local_discovery";
89
+ interface LocalBriefingOptions {
90
+ sm_application_id: string;
91
+ publication_id: string;
92
+ coverage_area_ids: readonly string[];
93
+ preferences?: BriefingPreferences;
94
+ personalized?: boolean;
95
+ /** Maximum, never a quota. Between 1 and 12, default 6. */
96
+ limit?: number;
97
+ now?: number;
98
+ }
99
+ interface LocalBriefingItem {
100
+ canonical_key: string;
101
+ revision: number;
102
+ article: Pick<NewsItem, "id" | "slug" | "title" | "published_at" | "author_name" | "canonical_url">;
103
+ summary: string;
104
+ section: BriefingSection;
105
+ geography_ids: string[];
106
+ why_it_matters?: string;
107
+ reasons: BriefingReason[];
108
+ relevance: {
109
+ geographic: number;
110
+ practical: number;
111
+ relationship: number;
112
+ interest: number;
113
+ temporal: number;
114
+ civic: number;
115
+ discovery: number;
116
+ };
117
+ verification: BriefingVerification;
118
+ freshness: "current" | "unknown";
119
+ last_verified_at: string | null;
120
+ review_due_at: string | null;
121
+ relevant_until: string;
122
+ sources: BriefingSource[];
123
+ }
124
+ interface LocalBriefing {
125
+ schema_version: 1;
126
+ policy_version: typeof LOCAL_BRIEFING_POLICY_VERSION;
127
+ publication_id: string;
128
+ generated_at: string;
129
+ personalized: boolean;
130
+ items: LocalBriefingItem[];
131
+ /** Consumers must keep a visible route to important coverage beyond the brief. */
132
+ remaining_important_count: number;
133
+ }
134
+ /**
135
+ * Opt-in, deterministic presentation of existing public records. This does not
136
+ * authenticate callers, fetch evidence, verify claims, persist preferences or
137
+ * publish anything. Never pass raw user submissions as authorized candidates.
138
+ */
139
+ declare function composeLocalBriefing(candidates: readonly BriefingCandidate[], options: LocalBriefingOptions): LocalBriefing;
140
+
35
141
  /**
36
142
  * Walnut Creek Times, the reference paper.
37
143
  * Other papers call `definePublication` with their own name, towns, and flags.
@@ -367,4 +473,4 @@ declare function publicationListeningCandidates<T extends ListeningArticle>(item
367
473
  */
368
474
  declare const CURATED_COMMUNITY_CARDS: Record<string, Record<string, unknown>[]>;
369
475
 
370
- export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, COOKIE_CATEGORIES, CURATED_COMMUNITY_CARDS, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, type ListeningArticle, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, clearTheme, composeEdition, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
476
+ export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, type BriefingCandidate, type BriefingPreferences, type BriefingReason, type BriefingSection, type BriefingSource, type BriefingVerification, COOKIE_CATEGORIES, CURATED_COMMUNITY_CARDS, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, LOCAL_BRIEFING_POLICY_VERSION, type ListeningArticle, type LocalBriefing, type LocalBriefingItem, type LocalBriefingOptions, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, clearTheme, composeEdition, composeLocalBriefing, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, H as HomepageSelection, b as NearbyUpdate, c as NewsItem, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-DFDCZHbS.js';
2
- export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-DFDCZHbS.js';
1
+ import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, b as NewsItem, H as HomepageSelection, c as NearbyUpdate, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-BJFCWWi0.js';
2
+ export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-BJFCWWi0.js';
3
3
  export { M as MEET_ACTIVITIES, b as MeetActivity, c as MeetupInput, a as PreparedMeetup, P as PublicPlace, d as activityLabel, e as canHostMeetup, i as isAdult, p as prepareMeetup, s as screenMessage } from './meet-RWmfj3I3.js';
4
4
  export { b as CLASSIFIED_CATEGORIES, c as ClassifiedCategory, C as ClassifiedListing, E as EmploymentType, J as JOB_EMPLOYMENT_TYPES, d as PreparedClassified, e as PreparedJob, P as PublicDeal, f as PublicDealOffer, a as PublicJob, g as categoryLabel, h as classifiedPath, i as classifiedSearchPath, j as clipChars, k as dealDollars, l as dealGoPath, m as dealsFeedPath, n as employmentLabel, o as isClassifiedCategory, p as isEmploymentType, q as jobEmployerBody, r as jobOpeningBody, s as jobPostingBody, t as prepareClassified, u as prepareJobPost, v as publicJobPath, w as publicJobsPath } from './marketplace-29i5w4LS.js';
5
- export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-CxuyMF39.js';
5
+ export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-DmRw8G_B.js';
6
6
  import { CookieCategoryDefinition, CookieConsentRecord } from '@scalemule/compliance';
7
7
  export { COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, COOKIE_CONSENT_MAX_AGE_SECONDS, CookieCategory, CookieCategoryDefinition, CookieConsentRecord, CookieDisclosure, CookiePreferences, DEFAULT_CONSENT_HUB_URL, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED, EU_EEA_UK_COUNTRIES, NETWORK_CONSENT_STORAGE_KEY, acceptAllCookies, checkUrlConsentBridge, clearCookieConsent, decorateNetworkUrl, getCookieConsent, hasConsented, isCategoryAllowed, isGeoConsentRequired, queryNetworkConsentHub, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub } from '@scalemule/compliance';
8
8
  export { CookieConsentBanner, CookieConsentBannerProps } from './cookie-consent-banner.js';
@@ -32,6 +32,112 @@ declare const LAYOUT_COOKIE = "news_publication_layout";
32
32
  */
33
33
  declare function resolveLayout(publication: NewsPublication, cookieValue?: string | null): string;
34
34
 
35
+ declare const LOCAL_BRIEFING_POLICY_VERSION = "1";
36
+ type BriefingVerification = "unverified" | "community_submitted" | "officially_confirmed" | "independently_verified" | "disputed";
37
+ type BriefingSection = "important" | "around_you" | "people" | "discover" | "participate" | "following";
38
+ /** Public evidence links only. Artifact URLs and private reporter notes do not belong here. */
39
+ interface BriefingSource {
40
+ id: string;
41
+ url: string;
42
+ title: string;
43
+ publisher: string;
44
+ }
45
+ /** Explicit choices only; no inferred demographics, relationships or reading history. */
46
+ interface BriefingPreferences {
47
+ community_ids?: readonly string[];
48
+ topic_ids?: readonly string[];
49
+ entity_ids?: readonly string[];
50
+ issue_ids?: readonly string[];
51
+ }
52
+ /**
53
+ * A server-authorized projection, not a new canonical record or submission API.
54
+ * Article-backed presentation only: BaseNews records need not have an article.
55
+ * Eligibility here must never gate existing publication or community workflows.
56
+ * The producer must resolve grants, evidence rights and geographic relationships.
57
+ * Keep withdrawn/superseded revisions in the input to suppress older copies.
58
+ */
59
+ interface BriefingCandidate {
60
+ sm_application_id: string;
61
+ publication_id: string;
62
+ /** Namespaced existing development/occurrence/reporting ID, never an issue-wide ID. */
63
+ canonical_key: string;
64
+ revision: number;
65
+ article: NewsItem;
66
+ publication_status: "published" | "draft";
67
+ visibility: "public" | "private";
68
+ editorial_approval: "approved" | "pending";
69
+ lifecycle: "current" | "superseded" | "withdrawn";
70
+ /** Resolved public area IDs, including applicable containing areas. No coordinates. */
71
+ geography_ids: readonly string[];
72
+ topic_ids: readonly string[];
73
+ entity_ids: readonly string[];
74
+ issue_ids: readonly string[];
75
+ section: BriefingSection;
76
+ /** Editorial assignments, never inferred from emotional language or click rates. */
77
+ civic_importance: "routine" | "important";
78
+ practical_value: boolean;
79
+ discovery_value: boolean;
80
+ why_it_matters?: string;
81
+ /** Required editorial relevance expiry; article publication time is not event time. */
82
+ relevant_until: string;
83
+ verification: BriefingVerification;
84
+ last_verified_at: string | null;
85
+ review_due_at: string | null;
86
+ sources: readonly BriefingSource[];
87
+ }
88
+ type BriefingReason = "important_civic_information" | "selected_community" | "publication_coverage" | "practical_value" | "following_entity" | "following_issue" | "selected_topic" | "recent_reporting" | "local_discovery";
89
+ interface LocalBriefingOptions {
90
+ sm_application_id: string;
91
+ publication_id: string;
92
+ coverage_area_ids: readonly string[];
93
+ preferences?: BriefingPreferences;
94
+ personalized?: boolean;
95
+ /** Maximum, never a quota. Between 1 and 12, default 6. */
96
+ limit?: number;
97
+ now?: number;
98
+ }
99
+ interface LocalBriefingItem {
100
+ canonical_key: string;
101
+ revision: number;
102
+ article: Pick<NewsItem, "id" | "slug" | "title" | "published_at" | "author_name" | "canonical_url">;
103
+ summary: string;
104
+ section: BriefingSection;
105
+ geography_ids: string[];
106
+ why_it_matters?: string;
107
+ reasons: BriefingReason[];
108
+ relevance: {
109
+ geographic: number;
110
+ practical: number;
111
+ relationship: number;
112
+ interest: number;
113
+ temporal: number;
114
+ civic: number;
115
+ discovery: number;
116
+ };
117
+ verification: BriefingVerification;
118
+ freshness: "current" | "unknown";
119
+ last_verified_at: string | null;
120
+ review_due_at: string | null;
121
+ relevant_until: string;
122
+ sources: BriefingSource[];
123
+ }
124
+ interface LocalBriefing {
125
+ schema_version: 1;
126
+ policy_version: typeof LOCAL_BRIEFING_POLICY_VERSION;
127
+ publication_id: string;
128
+ generated_at: string;
129
+ personalized: boolean;
130
+ items: LocalBriefingItem[];
131
+ /** Consumers must keep a visible route to important coverage beyond the brief. */
132
+ remaining_important_count: number;
133
+ }
134
+ /**
135
+ * Opt-in, deterministic presentation of existing public records. This does not
136
+ * authenticate callers, fetch evidence, verify claims, persist preferences or
137
+ * publish anything. Never pass raw user submissions as authorized candidates.
138
+ */
139
+ declare function composeLocalBriefing(candidates: readonly BriefingCandidate[], options: LocalBriefingOptions): LocalBriefing;
140
+
35
141
  /**
36
142
  * Walnut Creek Times, the reference paper.
37
143
  * Other papers call `definePublication` with their own name, towns, and flags.
@@ -367,4 +473,4 @@ declare function publicationListeningCandidates<T extends ListeningArticle>(item
367
473
  */
368
474
  declare const CURATED_COMMUNITY_CARDS: Record<string, Record<string, unknown>[]>;
369
475
 
370
- export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, COOKIE_CATEGORIES, CURATED_COMMUNITY_CARDS, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, type ListeningArticle, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, clearTheme, composeEdition, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
476
+ export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, type BriefingCandidate, type BriefingPreferences, type BriefingReason, type BriefingSection, type BriefingSource, type BriefingVerification, COOKIE_CATEGORIES, CURATED_COMMUNITY_CARDS, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, LOCAL_BRIEFING_POLICY_VERSION, type ListeningArticle, type LocalBriefing, type LocalBriefingItem, type LocalBriefingOptions, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, clearTheme, composeEdition, composeLocalBriefing, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
package/dist/index.js CHANGED
@@ -14,106 +14,6 @@ export { CLASSIFIED_CATEGORIES, JOB_EMPLOYMENT_TYPES, categoryLabel, classifiedP
14
14
  export { MEET_ACTIVITIES, activityLabel, canHostMeetup, isAdult, prepareMeetup, screenMessage } from './chunk-GB36YM56.js';
15
15
  import { jsxs, jsx } from 'react/jsx-runtime';
16
16
 
17
- // src/preset.ts
18
- var walnutCreekTimes = definePublication({
19
- slug: "walnutcreektimes",
20
- name: "Walnut Creek Times",
21
- masthead: ["Walnut", "Creek", "Times"],
22
- region: "Walnut Creek",
23
- regionLabel: "Walnut Creek",
24
- towns: ["Walnut Creek", "Rossmoor", "Saranap", "Pleasant Hill"],
25
- nearbyTowns: ["Martinez", "Concord", "Contra Costa"],
26
- sections: ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health"],
27
- description: "Independent local journalism covering civic decisions, downtown commerce, neighborhood life, and community achievements across Walnut Creek, Rossmoor, Saranap, and Pleasant Hill.",
28
- tagline: "Local stories. Stronger communities.",
29
- layout: "broadsheet",
30
- layouts: ["broadsheet"],
31
- features: { smile: true, jobs: true, classifieds: true },
32
- modules: { home: ["daily-smile"] }
33
- });
34
-
35
- // src/cache.ts
36
- var NEWS_CACHE = {
37
- feedRevalidateSeconds: 30,
38
- articleRevalidateSeconds: 60,
39
- edgeMaxAgeSeconds: 60,
40
- staleWhileRevalidateSeconds: 300,
41
- feedTag: "news-feed",
42
- articleTag: "news-article",
43
- inventoryTag: "public-news-discovery-v2",
44
- paths: ["/", "/news"]
45
- };
46
- var feedCacheOptions = {
47
- revalidate: NEWS_CACHE.feedRevalidateSeconds,
48
- tags: [NEWS_CACHE.feedTag]
49
- };
50
- var articleCacheOptions = {
51
- revalidate: NEWS_CACHE.articleRevalidateSeconds,
52
- tags: [NEWS_CACHE.articleTag]
53
- };
54
- var inventoryCacheOptions = {
55
- revalidate: NEWS_CACHE.feedRevalidateSeconds,
56
- tags: [NEWS_CACHE.feedTag, NEWS_CACHE.inventoryTag]
57
- };
58
- function edgeCacheControl() {
59
- return `public, s-maxage=${NEWS_CACHE.edgeMaxAgeSeconds}, stale-while-revalidate=${NEWS_CACHE.staleWhileRevalidateSeconds}`;
60
- }
61
- function feedCacheKey(publicationId, before, section) {
62
- return `${publicationId}:${before ?? ""}:${section ?? ""}`;
63
- }
64
- function articleCacheKey(publicationId, slug) {
65
- return `${publicationId}:${slug}`;
66
- }
67
- var NewsError = class extends Error {
68
- constructor(status, message) {
69
- super(message);
70
- this.status = status;
71
- this.name = "NewsError";
72
- }
73
- status;
74
- };
75
- function shouldServeLastKnown(error) {
76
- const status = error instanceof NewsError ? error.status : error && typeof error === "object" && "status" in error ? error.status : void 0;
77
- return status !== 404;
78
- }
79
- async function withLastKnown(store, key, load, accept = () => true) {
80
- try {
81
- const value = await load();
82
- if (accept(value)) store.set(key, value);
83
- return value;
84
- } catch (error) {
85
- if (!shouldServeLastKnown(error)) throw error;
86
- const cached = store.get(key);
87
- if (cached) return cached;
88
- throw error;
89
- }
90
- }
91
- function secretsMatch(provided, secret) {
92
- if (!provided || !secret) return false;
93
- const left = Array.from(provided);
94
- const right = Array.from(secret);
95
- let mismatch = left.length === right.length ? 0 : 1;
96
- const length = Math.max(left.length, right.length);
97
- for (let index = 0; index < length; index += 1) {
98
- mismatch |= (left[index]?.codePointAt(0) ?? 0) ^ (right[index]?.codePointAt(0) ?? 0);
99
- }
100
- return mismatch === 0;
101
- }
102
- function revalidateDecision(input) {
103
- if (!secretsMatch(input.providedSecret, input.configuredSecret)) {
104
- return { status: 401, body: { error: "Unauthorized" } };
105
- }
106
- const tag = input.tag?.trim() || `${NEWS_CACHE.feedTag}, ${NEWS_CACHE.articleTag}`;
107
- const path = input.path?.trim() || NEWS_CACHE.paths.join(", ");
108
- return { status: 200, body: { revalidated: true, tag, path, now: input.now ?? Date.now() } };
109
- }
110
- function defaultRevalidateTargets() {
111
- return {
112
- tags: [NEWS_CACHE.feedTag, NEWS_CACHE.articleTag, NEWS_CACHE.inventoryTag],
113
- paths: [...NEWS_CACHE.paths]
114
- };
115
- }
116
-
117
17
  // src/editorial.ts
118
18
  var labels = {
119
19
  "Local businesses": "Business",
@@ -131,8 +31,8 @@ var EDITORIAL_SECTION_ORDER = [
131
31
  "Schools",
132
32
  "Opinion"
133
33
  ];
134
- function orderedSections(sections, order = EDITORIAL_SECTION_ORDER) {
135
- return sections.filter((section) => section.name !== "Local news").slice().sort((left, right) => {
34
+ function orderedSections(sections2, order = EDITORIAL_SECTION_ORDER) {
35
+ return sections2.filter((section) => section.name !== "Local news").slice().sort((left, right) => {
136
36
  const leftRank = order.indexOf(left.name);
137
37
  const rightRank = order.indexOf(right.name);
138
38
  const leftOrder = leftRank === -1 ? order.length : leftRank;
@@ -193,12 +93,12 @@ function composeEdition(items, selection = {}, now = Date.now()) {
193
93
  secondary.push(deferredWithDuplicateImage.shift());
194
94
  }
195
95
  for (const item of secondary) remaining.splice(remaining.indexOf(item), 1);
196
- const eligible = remaining.filter((item) => {
96
+ const eligible2 = remaining.filter((item) => {
197
97
  const age = now - Date.parse(item.published_at);
198
98
  return item.excerpt?.trim() && age >= 0 && age <= 7 * 864e5;
199
99
  });
200
- const selected = (pins.brief_slugs ?? []).flatMap((slug) => eligible.find((item) => item.slug === slug) ?? []);
201
- const candidates = [...selected, ...editorialSections(eligible).map((group) => group.items[0]), ...eligible];
100
+ const selected = (pins.brief_slugs ?? []).flatMap((slug) => eligible2.find((item) => item.slug === slug) ?? []);
101
+ const candidates = [...selected, ...editorialSections(eligible2).map((group) => group.items[0]), ...eligible2];
202
102
  const briefIds = /* @__PURE__ */ new Set();
203
103
  const briefs = candidates.filter((item) => {
204
104
  if (!item || briefIds.has(item.id) || briefIds.size >= 4) return false;
@@ -207,8 +107,8 @@ function composeEdition(items, selection = {}, now = Date.now()) {
207
107
  });
208
108
  const compact = [...briefs, ...remaining.filter((item) => !briefIds.has(item.id))].slice(0, 4);
209
109
  const compactIds = new Set(compact.map((item) => item.id));
210
- const sections = editorialSections(remaining.filter((item) => !compactIds.has(item.id)));
211
- return { lead, secondary, briefs, compact, sections, navigation: editorialSections(inventory) };
110
+ const sections2 = editorialSections(remaining.filter((item) => !compactIds.has(item.id)));
111
+ return { lead, secondary, briefs, compact, sections: sections2, navigation: editorialSections(inventory) };
212
112
  }
213
113
  function displayHeadline(article, selection = {}) {
214
114
  return selection.stories?.[article.slug]?.display_title?.trim() || article.title;
@@ -373,6 +273,265 @@ function llmsText(publication, canonical) {
373
273
  ].join("\n");
374
274
  }
375
275
 
276
+ // src/briefing.ts
277
+ var LOCAL_BRIEFING_POLICY_VERSION = "1";
278
+ var DAY = 864e5;
279
+ var verifications = /* @__PURE__ */ new Set([
280
+ "unverified",
281
+ "community_submitted",
282
+ "officially_confirmed",
283
+ "independently_verified",
284
+ "disputed"
285
+ ]);
286
+ var sections = /* @__PURE__ */ new Set(["important", "around_you", "people", "discover", "participate", "following"]);
287
+ var overlaps = (a, b = []) => a.some((id) => id.trim() !== "" && b.includes(id));
288
+ function timestamp(value) {
289
+ if (!/^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,3})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.test(value) || !validCalendarDate(value.slice(0, 10))) return NaN;
290
+ return Date.parse(value);
291
+ }
292
+ function sourcesFor(candidate) {
293
+ const seen = /* @__PURE__ */ new Set();
294
+ return candidate.sources.flatMap((source) => {
295
+ const url = publicHttps(source.url);
296
+ if (!source.id.trim() || !source.publisher.trim() || !source.title.trim() || !url || seen.has(source.id)) return [];
297
+ seen.add(source.id);
298
+ return [{ id: source.id, url, title: source.title, publisher: source.publisher }];
299
+ });
300
+ }
301
+ function eligible(candidate, coverage, now) {
302
+ if (candidate.publication_status !== "published" || candidate.visibility !== "public" || candidate.editorial_approval !== "approved" || candidate.lifecycle !== "current" || !candidate.article.id.trim() || !candidate.article.slug.trim() || !candidate.article.title.trim() || !verifications.has(candidate.verification) || !sections.has(candidate.section) || !overlaps(candidate.geography_ids, coverage)) return false;
303
+ const published = timestamp(candidate.article.published_at);
304
+ const expiry = timestamp(candidate.relevant_until);
305
+ if (!Number.isFinite(published) || published > now || !Number.isFinite(expiry) || expiry <= now) return false;
306
+ const verified = candidate.last_verified_at === null ? null : timestamp(candidate.last_verified_at);
307
+ const review = candidate.review_due_at === null ? null : timestamp(candidate.review_due_at);
308
+ if (verified !== null && (!Number.isFinite(verified) || verified > now)) return false;
309
+ if (review !== null && (!Number.isFinite(review) || review <= now || verified !== null && review <= verified)) return false;
310
+ const confirmed = candidate.verification === "officially_confirmed" || candidate.verification === "independently_verified";
311
+ return !confirmed || verified !== null && review !== null && sourcesFor(candidate).length > 0;
312
+ }
313
+ function projectionKey(candidate) {
314
+ const article = candidate.article;
315
+ return JSON.stringify([
316
+ article.id,
317
+ article.slug,
318
+ article.title,
319
+ article.excerpt,
320
+ article.published_at,
321
+ article.author_name,
322
+ publicHttps(article.canonical_url),
323
+ [...candidate.geography_ids].sort(),
324
+ [...candidate.topic_ids].sort(),
325
+ [...candidate.entity_ids].sort(),
326
+ [...candidate.issue_ids].sort(),
327
+ candidate.section,
328
+ candidate.civic_importance,
329
+ candidate.practical_value,
330
+ candidate.discovery_value,
331
+ candidate.why_it_matters,
332
+ candidate.relevant_until,
333
+ candidate.verification,
334
+ candidate.last_verified_at,
335
+ candidate.review_due_at,
336
+ sourcesFor(candidate).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
337
+ ]);
338
+ }
339
+ function composeLocalBriefing(candidates, options) {
340
+ const now = options.now ?? Date.now();
341
+ const limit = options.limit ?? 6;
342
+ if (!options.sm_application_id.trim() || !options.publication_id.trim() || !Number.isFinite(now) || !Number.isFinite(new Date(now).getTime()) || !Number.isInteger(limit) || limit < 1 || limit > 12) {
343
+ throw new RangeError("A briefing requires application/publication scope, a valid time and a limit from 1 to 12");
344
+ }
345
+ const preferences = options.personalized === false ? {} : options.preferences ?? {};
346
+ const personalized = [preferences.community_ids, preferences.topic_ids, preferences.entity_ids, preferences.issue_ids].some((ids) => ids?.some((id) => id.trim() !== ""));
347
+ const newest = /* @__PURE__ */ new Map();
348
+ for (const candidate of candidates) {
349
+ if (candidate.sm_application_id !== options.sm_application_id || candidate.publication_id !== options.publication_id || !candidate.canonical_key.trim() || !Number.isSafeInteger(candidate.revision) || candidate.revision < 1) continue;
350
+ const group = newest.get(candidate.canonical_key);
351
+ if (!group || candidate.revision > group[0].revision) newest.set(candidate.canonical_key, [candidate]);
352
+ else if (candidate.revision === group[0].revision) group.push(candidate);
353
+ }
354
+ const ranked = [];
355
+ for (const group of newest.values()) {
356
+ if (group.some((candidate2) => !eligible(candidate2, options.coverage_area_ids, now))) continue;
357
+ const candidate = group[0];
358
+ if (group.length > 1 && group.some((copy) => projectionKey(copy) !== projectionKey(candidate))) continue;
359
+ const summary = candidate.article.excerpt?.trim() ?? "";
360
+ if (!summary || Array.from(summary).length > 280 || !/[.!?。!?][”’"']?$/u.test(summary)) continue;
361
+ const community = overlaps(candidate.geography_ids, preferences.community_ids);
362
+ const entity = overlaps(candidate.entity_ids, preferences.entity_ids);
363
+ const issue = overlaps(candidate.issue_ids, preferences.issue_ids);
364
+ const topic = overlaps(candidate.topic_ids, preferences.topic_ids);
365
+ const published = timestamp(candidate.article.published_at);
366
+ const age = now - published;
367
+ const important = candidate.civic_importance === "important";
368
+ const relevance = {
369
+ geographic: community ? 2 : 1,
370
+ practical: candidate.practical_value ? 1 : 0,
371
+ relationship: entity || issue ? 1 : 0,
372
+ interest: topic ? 1 : 0,
373
+ temporal: age < DAY ? 2 : age < 7 * DAY ? 1 : 0,
374
+ civic: important ? 1 : 0,
375
+ discovery: candidate.discovery_value ? 1 : 0
376
+ };
377
+ const reasons = [];
378
+ if (important) reasons.push("important_civic_information");
379
+ reasons.push(community ? "selected_community" : "publication_coverage");
380
+ if (candidate.practical_value) reasons.push("practical_value");
381
+ if (entity) reasons.push("following_entity");
382
+ if (issue) reasons.push("following_issue");
383
+ if (topic) reasons.push("selected_topic");
384
+ if (relevance.temporal > 0) reasons.push("recent_reporting");
385
+ if (candidate.discovery_value) reasons.push("local_discovery");
386
+ const article = candidate.article;
387
+ const item = {
388
+ canonical_key: candidate.canonical_key,
389
+ revision: candidate.revision,
390
+ article: {
391
+ id: article.id,
392
+ slug: article.slug,
393
+ title: article.title,
394
+ published_at: article.published_at,
395
+ author_name: article.author_name,
396
+ canonical_url: publicHttps(article.canonical_url) ?? null
397
+ },
398
+ summary,
399
+ section: important ? "important" : entity || issue ? "following" : candidate.section,
400
+ geography_ids: [...new Set(candidate.geography_ids)],
401
+ why_it_matters: candidate.why_it_matters?.trim() || void 0,
402
+ reasons,
403
+ relevance,
404
+ verification: candidate.verification,
405
+ freshness: candidate.last_verified_at !== null && candidate.review_due_at !== null ? "current" : "unknown",
406
+ last_verified_at: candidate.last_verified_at,
407
+ review_due_at: candidate.review_due_at,
408
+ relevant_until: candidate.relevant_until,
409
+ sources: sourcesFor(candidate)
410
+ };
411
+ const score = 4 * relevance.geographic + 3 * relevance.practical + 3 * relevance.relationship + 2 * relevance.interest + relevance.temporal + relevance.discovery;
412
+ ranked.push({ item, score, published, important });
413
+ }
414
+ ranked.sort((a, b) => Number(b.important) - Number(a.important) || b.score - a.score || b.published - a.published || (a.item.canonical_key < b.item.canonical_key ? -1 : a.item.canonical_key > b.item.canonical_key ? 1 : 0));
415
+ const seenArticles = /* @__PURE__ */ new Set();
416
+ const seenUrls = /* @__PURE__ */ new Set();
417
+ const deduplicated = ranked.filter(({ item }) => {
418
+ const url = item.article.canonical_url;
419
+ if (seenArticles.has(item.article.id) || url && seenUrls.has(url)) return false;
420
+ seenArticles.add(item.article.id);
421
+ if (url) seenUrls.add(url);
422
+ return true;
423
+ });
424
+ return {
425
+ schema_version: 1,
426
+ policy_version: LOCAL_BRIEFING_POLICY_VERSION,
427
+ publication_id: options.publication_id,
428
+ generated_at: new Date(now).toISOString(),
429
+ personalized,
430
+ items: deduplicated.slice(0, limit).map(({ item }) => item),
431
+ remaining_important_count: deduplicated.slice(limit).filter(({ important }) => important).length
432
+ };
433
+ }
434
+
435
+ // src/preset.ts
436
+ var walnutCreekTimes = definePublication({
437
+ slug: "walnutcreektimes",
438
+ name: "Walnut Creek Times",
439
+ masthead: ["Walnut", "Creek", "Times"],
440
+ region: "Walnut Creek",
441
+ regionLabel: "Walnut Creek",
442
+ towns: ["Walnut Creek", "Rossmoor", "Saranap", "Pleasant Hill"],
443
+ nearbyTowns: ["Martinez", "Concord", "Contra Costa"],
444
+ sections: ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health"],
445
+ description: "Independent local journalism covering civic decisions, downtown commerce, neighborhood life, and community achievements across Walnut Creek, Rossmoor, Saranap, and Pleasant Hill.",
446
+ tagline: "Local stories. Stronger communities.",
447
+ layout: "broadsheet",
448
+ layouts: ["broadsheet"],
449
+ features: { smile: true, jobs: true, classifieds: true },
450
+ modules: { home: ["daily-smile"] }
451
+ });
452
+
453
+ // src/cache.ts
454
+ var NEWS_CACHE = {
455
+ feedRevalidateSeconds: 30,
456
+ articleRevalidateSeconds: 60,
457
+ edgeMaxAgeSeconds: 60,
458
+ staleWhileRevalidateSeconds: 300,
459
+ feedTag: "news-feed",
460
+ articleTag: "news-article",
461
+ inventoryTag: "public-news-discovery-v2",
462
+ paths: ["/", "/news"]
463
+ };
464
+ var feedCacheOptions = {
465
+ revalidate: NEWS_CACHE.feedRevalidateSeconds,
466
+ tags: [NEWS_CACHE.feedTag]
467
+ };
468
+ var articleCacheOptions = {
469
+ revalidate: NEWS_CACHE.articleRevalidateSeconds,
470
+ tags: [NEWS_CACHE.articleTag]
471
+ };
472
+ var inventoryCacheOptions = {
473
+ revalidate: NEWS_CACHE.feedRevalidateSeconds,
474
+ tags: [NEWS_CACHE.feedTag, NEWS_CACHE.inventoryTag]
475
+ };
476
+ function edgeCacheControl() {
477
+ return `public, s-maxage=${NEWS_CACHE.edgeMaxAgeSeconds}, stale-while-revalidate=${NEWS_CACHE.staleWhileRevalidateSeconds}`;
478
+ }
479
+ function feedCacheKey(publicationId, before, section) {
480
+ return `${publicationId}:${before ?? ""}:${section ?? ""}`;
481
+ }
482
+ function articleCacheKey(publicationId, slug) {
483
+ return `${publicationId}:${slug}`;
484
+ }
485
+ var NewsError = class extends Error {
486
+ constructor(status, message) {
487
+ super(message);
488
+ this.status = status;
489
+ this.name = "NewsError";
490
+ }
491
+ status;
492
+ };
493
+ function shouldServeLastKnown(error) {
494
+ const status = error instanceof NewsError ? error.status : error && typeof error === "object" && "status" in error ? error.status : void 0;
495
+ return status !== 404;
496
+ }
497
+ async function withLastKnown(store, key, load, accept = () => true) {
498
+ try {
499
+ const value = await load();
500
+ if (accept(value)) store.set(key, value);
501
+ return value;
502
+ } catch (error) {
503
+ if (!shouldServeLastKnown(error)) throw error;
504
+ const cached = store.get(key);
505
+ if (cached) return cached;
506
+ throw error;
507
+ }
508
+ }
509
+ function secretsMatch(provided, secret) {
510
+ if (!provided || !secret) return false;
511
+ const left = Array.from(provided);
512
+ const right = Array.from(secret);
513
+ let mismatch = left.length === right.length ? 0 : 1;
514
+ const length = Math.max(left.length, right.length);
515
+ for (let index = 0; index < length; index += 1) {
516
+ mismatch |= (left[index]?.codePointAt(0) ?? 0) ^ (right[index]?.codePointAt(0) ?? 0);
517
+ }
518
+ return mismatch === 0;
519
+ }
520
+ function revalidateDecision(input) {
521
+ if (!secretsMatch(input.providedSecret, input.configuredSecret)) {
522
+ return { status: 401, body: { error: "Unauthorized" } };
523
+ }
524
+ const tag = input.tag?.trim() || `${NEWS_CACHE.feedTag}, ${NEWS_CACHE.articleTag}`;
525
+ const path = input.path?.trim() || NEWS_CACHE.paths.join(", ");
526
+ return { status: 200, body: { revalidated: true, tag, path, now: input.now ?? Date.now() } };
527
+ }
528
+ function defaultRevalidateTargets() {
529
+ return {
530
+ tags: [NEWS_CACHE.feedTag, NEWS_CACHE.articleTag, NEWS_CACHE.inventoryTag],
531
+ paths: [...NEWS_CACHE.paths]
532
+ };
533
+ }
534
+
376
535
  // src/analytics.ts
377
536
  var ANALYTICS_VERSION = 1;
378
537
  var ANALYTICS_COOKIE = "napsite_analytics_v1";
@@ -593,4 +752,4 @@ async function publicationListeningCandidates(items, read, publicationId, public
593
752
  return output.sort((a, b) => (b.publishedAt ?? "").localeCompare(a.publishedAt ?? ""));
594
753
  }
595
754
 
596
- export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, EDITORIAL_SECTION_ORDER, EVENTS, NEWS_CACHE, NewsError, SESSION_IDLE_MS, UTM_FIELDS, UUID, activeNearby, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, composeEdition, defaultRevalidateTargets, displayHeadline, edgeCacheControl, editorialSections, feedCacheKey, feedCacheOptions, filterNews, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, referralCode, resolveCoupon, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
755
+ export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, EDITORIAL_SECTION_ORDER, EVENTS, LOCAL_BRIEFING_POLICY_VERSION, NEWS_CACHE, NewsError, SESSION_IDLE_MS, UTM_FIELDS, UUID, activeNearby, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, composeEdition, composeLocalBriefing, defaultRevalidateTargets, displayHeadline, edgeCacheControl, editorialSections, feedCacheKey, feedCacheOptions, filterNews, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, referralCode, resolveCoupon, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
@@ -1,4 +1,4 @@
1
- import { P as PublicationInput, N as NewsPublication } from './types-DFDCZHbS.js';
1
+ import { P as PublicationInput, N as NewsPublication } from './types-BJFCWWi0.js';
2
2
 
3
3
  /**
4
4
  * Serializable application config. Code hooks (`matchTag`, `coupons.resolve`)
@@ -1,4 +1,4 @@
1
- import { P as PublicationInput, N as NewsPublication } from './types-DFDCZHbS.cjs';
1
+ import { P as PublicationInput, N as NewsPublication } from './types-BJFCWWi0.cjs';
2
2
 
3
3
  /**
4
4
  * Serializable application config. Code hooks (`matchTag`, `coupons.resolve`)
@@ -1,5 +1,5 @@
1
- import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-C0IGpt-B.cjs';
2
- import './types-DFDCZHbS.cjs';
1
+ import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-Dx4Lv-lB.cjs';
2
+ import './types-BJFCWWi0.cjs';
3
3
 
4
4
  declare function readSnapshotFile(file: string): Promise<PublicationSnapshot | null>;
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-CxuyMF39.js';
2
- import './types-DFDCZHbS.js';
1
+ import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-DmRw8G_B.js';
2
+ import './types-BJFCWWi0.js';
3
3
 
4
4
  declare function readSnapshotFile(file: string): Promise<PublicationSnapshot | null>;
5
5
  /**
@@ -238,4 +238,4 @@ type EditorialSection = {
238
238
  items: NewsItem[];
239
239
  };
240
240
 
241
- export { type ArticleCitation as A, type CouponOffer as C, type EditorialSection as E, type FeatureName as F, type HomepageSelection as H, type NewsPublication as N, type PublicationInput as P, type ReferralSettings as R, type StoryDisplay as S, type TagRef as T, type VerifiedCard as V, type PublicationTheme as a, type NearbyUpdate as b, type NewsItem as c, type NewsArticle as d, type CardType as e, type CategoryRef as f, type CouponSettings as g, type CoverImageMetadata as h, FEATURE_NAMES as i, type FeatureFlags as j, type NewsFeed as k, type NewsLayoutId as l, type PublicationModules as m, type VerifiedCardAction as n, type VerifiedCardFact as o, type VerifiedCardImage as p };
241
+ export { type ArticleCitation as A, type CouponOffer as C, type EditorialSection as E, type FeatureName as F, type HomepageSelection as H, type NewsPublication as N, type PublicationInput as P, type ReferralSettings as R, type StoryDisplay as S, type TagRef as T, type VerifiedCard as V, type PublicationTheme as a, type NewsItem as b, type NearbyUpdate as c, type NewsArticle as d, type CardType as e, type CategoryRef as f, type CouponSettings as g, type CoverImageMetadata as h, FEATURE_NAMES as i, type FeatureFlags as j, type NewsFeed as k, type NewsLayoutId as l, type PublicationModules as m, type VerifiedCardAction as n, type VerifiedCardFact as o, type VerifiedCardImage as p };
@@ -238,4 +238,4 @@ type EditorialSection = {
238
238
  items: NewsItem[];
239
239
  };
240
240
 
241
- export { type ArticleCitation as A, type CouponOffer as C, type EditorialSection as E, type FeatureName as F, type HomepageSelection as H, type NewsPublication as N, type PublicationInput as P, type ReferralSettings as R, type StoryDisplay as S, type TagRef as T, type VerifiedCard as V, type PublicationTheme as a, type NearbyUpdate as b, type NewsItem as c, type NewsArticle as d, type CardType as e, type CategoryRef as f, type CouponSettings as g, type CoverImageMetadata as h, FEATURE_NAMES as i, type FeatureFlags as j, type NewsFeed as k, type NewsLayoutId as l, type PublicationModules as m, type VerifiedCardAction as n, type VerifiedCardFact as o, type VerifiedCardImage as p };
241
+ export { type ArticleCitation as A, type CouponOffer as C, type EditorialSection as E, type FeatureName as F, type HomepageSelection as H, type NewsPublication as N, type PublicationInput as P, type ReferralSettings as R, type StoryDisplay as S, type TagRef as T, type VerifiedCard as V, type PublicationTheme as a, type NewsItem as b, type NearbyUpdate as c, type NewsArticle as d, type CardType as e, type CategoryRef as f, type CouponSettings as g, type CoverImageMetadata as h, FEATURE_NAMES as i, type FeatureFlags as j, type NewsFeed as k, type NewsLayoutId as l, type PublicationModules as m, type VerifiedCardAction as n, type VerifiedCardFact as o, type VerifiedCardImage as p };
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { V as VerifiedCard } from './types-DFDCZHbS.cjs';
2
+ import { V as VerifiedCard } from './types-BJFCWWi0.cjs';
3
3
 
4
4
  /**
5
5
  * Validate that an action link is safe for readers.
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { V as VerifiedCard } from './types-DFDCZHbS.js';
2
+ import { V as VerifiedCard } from './types-BJFCWWi0.js';
3
3
 
4
4
  /**
5
5
  * Validate that an action link is safe for readers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.44",
3
+ "version": "0.0.46",
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
  "main": "./dist/index.cjs",
@@ -109,7 +109,7 @@
109
109
  "story-review.css"
110
110
  ],
111
111
  "peerDependencies": {
112
- "@scalemule/react": "0.0.20",
112
+ "@scalemule/react": "0.0.21",
113
113
  "react": ">=18.0.0"
114
114
  },
115
115
  "peerDependenciesMeta": {
@@ -129,7 +129,7 @@
129
129
  "prepublishOnly": "node -e \"const v=require('./package.json').version; if(!/^0\\.(0|1)\\.\\d+$/.test(v)){console.error('ERR: SDK '+v+' violates patch-only policy (0.0.x or 0.1.x only). See CONTRIBUTING.md SDK versioning section.');process.exit(1)}\" && npm run lint && npm test && npm run build"
130
130
  },
131
131
  "devDependencies": {
132
- "@scalemule/react": "0.0.20",
132
+ "@scalemule/react": "0.0.21",
133
133
  "@testing-library/react": "^16.3.3",
134
134
  "@types/node": "^22.15.0",
135
135
  "@types/react": "^19.1.0",
@@ -162,7 +162,7 @@
162
162
  },
163
163
  "dependencies": {
164
164
  "@scalemule/compliance": "^0.0.5",
165
- "@scalemule/react": "0.0.20",
165
+ "@scalemule/react": "0.0.21",
166
166
  "@scalemule/sdk": "0.0.75"
167
167
  }
168
168
  }