@polycode-projects/the-mechanical-code-talker 6.0.16 → 6.0.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "6.0.16",
3
+ "version": "6.0.17",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; indexes a repo on request (tmct index) or reads any producer's graph.",
@@ -260,16 +260,24 @@ async function fetchFeedFormat(record, gate, { format, now }) {
260
260
  * carries `wikibaseItem` when the article names one, past normalizeFeedItems
261
261
  * (which only knows the snapshot's own fixed fields) so the news
262
262
  * enrichment loop can short-circuit straight to the Wikidata KB source with
263
- * the Q-id already in hand, no lookup needed. */
263
+ * the Q-id already in hand, no lookup needed.
264
+ *
265
+ * Neither `news` nor `mostread` carries a per-article timestamp in the raw
266
+ * payload — the only date this feed exposes is the UTC calendar day the
267
+ * request itself names (the `/YYYY/MM/DD/` path Wikimedia selected these
268
+ * articles for), so `publishedAt` is stamped from whichever day's page
269
+ * actually answered (the primary day, or the prior day on a 404 retry),
270
+ * never from the fetch's own clock. */
264
271
  async function fetchWikimediaFeed(record, gate, { now }) {
265
- const primaryUrl = wikimediaFeedUrl(record.url, now);
266
- let body = await pacedFetchJson(gate, primaryUrl);
272
+ let feedDay = now;
273
+ let body = await pacedFetchJson(gate, wikimediaFeedUrl(record.url, feedDay));
267
274
  if (body === null) {
268
275
  // A 404 means the day's page is not yet published; retry once against
269
276
  // the previous UTC day before giving up.
270
277
  const prevDay = new Date(now);
271
278
  prevDay.setUTCDate(prevDay.getUTCDate() - 1);
272
- body = await pacedFetchJson(gate, wikimediaFeedUrl(record.url, prevDay.toISOString()));
279
+ feedDay = prevDay.toISOString();
280
+ body = await pacedFetchJson(gate, wikimediaFeedUrl(record.url, feedDay));
273
281
  if (body === null) return null;
274
282
  }
275
283
  if (isNotModified(body)) return { items: [], bytes: 0, notModified: true };
@@ -280,12 +288,14 @@ async function fetchWikimediaFeed(record, gate, { now }) {
280
288
  ? body.mostread.articles
281
289
  : [];
282
290
 
291
+ const { yyyy, mm, dd } = utcDateParts(feedDay);
292
+ const feedDayIso = `${yyyy}-${mm}-${dd}T00:00:00.000Z`;
283
293
  const raw = articles.map((a) => ({
284
294
  guid: a?.wikibase_item || a?.normalizedtitle || a?.title || "",
285
295
  title: stripMarkup(a?.normalizedtitle || a?.displaytitle || a?.title || ""),
286
296
  url: a?.content_urls?.desktop?.page || "",
287
297
  summary: stripMarkup(a?.extract || ""),
288
- publishedAt: "",
298
+ publishedAt: feedDayIso,
289
299
  wikibaseItem: a?.wikibase_item || "",
290
300
  }));
291
301
 
@@ -548,7 +548,11 @@ function collectSources(subgraphRows, sourcesByFactId) {
548
548
  const key = src.url || src.title || "";
549
549
  if (!key || seen.has(key)) continue;
550
550
  seen.add(key);
551
- sources.push({ title: src.title || "", url: src.url || "", name: src.name || "" });
551
+ const entry = { title: src.title || "", url: src.url || "", name: src.name || "" };
552
+ // Carried through only when the source snapshot actually has one — a
553
+ // card for an undated snapshot shows no date rather than a blank field.
554
+ if (src.publishedAt) entry.publishedAt = src.publishedAt;
555
+ sources.push(entry);
552
556
  }
553
557
  return sources;
554
558
  }
@@ -657,8 +661,9 @@ export function renderNewsParagraph(hub, subgraphRows, { reportedIds = null } =
657
661
 
658
662
  /** newsworthyHubs -> one item per hub (PLAN_NEWS_FEED.md section 6.6),
659
663
  * paragraph included, sorted builtAt desc then id asc. `sourcesByFactId`
660
- * maps fact ids to snapshot source links ({ title, url, name }). The gate
661
- * (PLAN_NEWS_FEED.md section 17): `reportedRows` replaces `newsWindowRows`
664
+ * maps fact ids to snapshot source links ({ title, url, name, publishedAt?
665
+ * }); publishedAt is present only when the source snapshot carried one.
666
+ * The gate (PLAN_NEWS_FEED.md section 17): `reportedRows` replaces `newsWindowRows`
662
667
  * and `newsworthyHubs` replaces `scoreHubs` as this function's own inputs —
663
668
  * both keep their prior behaviour for every other caller. Each item's
664
669
  * two-hop sub-graph then splits into its own `reported`/`background` rows,
@@ -151,6 +151,7 @@ ${THEME_TOKENS_CSS}
151
151
  .item .newtag { font-family: ${MONO_STACK}; font-size: .64rem; color: var(--taught); margin-left: .5rem; }
152
152
  .item .paragraph { margin: .4rem 0; }
153
153
  .item .sources-links { font-size: .74rem; color: var(--muted); }
154
+ .item .sourcedate { font-family: ${MONO_STACK}; }
154
155
  .item details.facts summary { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--corpus); cursor: pointer; }
155
156
  .item details.background summary { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); cursor: pointer; }
156
157
  .item details.background p { margin: .35rem 0 0; color: var(--muted); }
@@ -482,12 +483,29 @@ ${NEWS_STYLE}
482
483
  });
483
484
  }
484
485
 
486
+ // The earliest publication date among a card's sources — a lexicographic
487
+ // min over ISO-8601 UTC strings sorts chronologically without parsing, and
488
+ // "earliest" reads as when the reported event actually happened rather
489
+ // than whichever source snapshot the subgraph walk reached last. A source
490
+ // with no publishedAt (its feed never carries one) never contributes here,
491
+ // so a card built entirely from undated sources shows no date at all.
492
+ function earliestSourceDate(sources) {
493
+ let earliest = null;
494
+ for (const s of sources || []) {
495
+ if (!s.publishedAt) continue;
496
+ if (earliest === null || s.publishedAt < earliest) earliest = s.publishedAt;
497
+ }
498
+ return earliest;
499
+ }
500
+
485
501
  function cardHtml(item) {
486
502
  const factLines = item.factLines || [];
487
503
  const factsHtml = factLines.map(function (line) { return '<div class="factrow">' + esc(line) + '</div>'; }).join("");
488
504
  const moreCount = (item.factCount || 0) - factLines.length;
489
505
  const moreHtml = moreCount > 0 ? '<div class="factrow factmore">&hellip;and ' + moreCount + ' more</div>' : "";
490
506
  const sourcesText = (item.sources || []).map(function (s) { return esc(s.title || s.url || ""); }).filter(Boolean).join(", ");
507
+ const sourceDate = earliestSourceDate(item.sources);
508
+ const dateText = sourceDate ? ' <span class="sourcedate">(' + esc(sourceDate.slice(0, 10)) + ')</span>' : "";
491
509
  const background = item.backgroundParagraph
492
510
  ? '<details class="background"><summary>what the graph already knew</summary><p>' + esc(item.backgroundParagraph) + '</p></details>'
493
511
  : "";
@@ -496,7 +514,7 @@ ${NEWS_STYLE}
496
514
  + '<span class="hub">' + esc(item.hub) + '</span><span class="tier">' + esc(item.tier || "unranked") + '</span>' + newTag
497
515
  + '<p class="paragraph">' + esc(item.paragraph) + '</p>'
498
516
  + background
499
- + (sourcesText ? '<p class="sources-links">sources: ' + sourcesText + '</p>' : "")
517
+ + (sourcesText ? '<p class="sources-links">sources: ' + sourcesText + dateText + '</p>' : "")
500
518
  + '<details class="facts"><summary>' + (item.factCount || 0) + ' fact' + (item.factCount === 1 ? "" : "s") + '</summary>' + factsHtml + moreHtml + '</details>'
501
519
  + '</div>';
502
520
  }
@@ -288,6 +288,10 @@ function buildSourcesByFactId(items) {
288
288
  for (const snap of items || []) {
289
289
  const record = recordsById.get(snap.sourceId);
290
290
  const src = { title: snap.title || "", url: snap.url || "", name: record?.name || snap.sourceId || "" };
291
+ // A snapshot with no publication timestamp (a source whose own feed never
292
+ // carries one) leaves the key off entirely — never an invented or blank
293
+ // date, and never the fetch's own clock standing in for it.
294
+ if (snap.publishedAt) src.publishedAt = snap.publishedAt;
291
295
  for (const factId of snap.factIds || []) map.set(factId, src);
292
296
  }
293
297
  return map;