From 4c6d23c3b9bd94e66c6bff96fa2eb54c625d164f Mon Sep 17 00:00:00 2001 From: tetsuo Date: Thu, 29 Oct 2026 20:39:19 +0900 Subject: [PATCH 1/2] Order entries by publication time --- README.md | 8 +++++ package.json | 16 ++++++++- src/content.config.ts | 2 ++ .../clean-management-bootstrap-en.md | 1 + .../clean-management-bootstrap-ja.md | 1 + src/content/posts/auto-coder/hello-en.md | 1 + src/content/posts/auto-coder/hello-ja.md | 1 + .../posts/auto-coder/management-update-en.md | 1 + .../posts/auto-coder/management-update-ja.md | 1 + .../posts/auto-coder/release-supervisor-en.md | 1 + .../posts/auto-coder/release-supervisor-ja.md | 1 + .../posts/auto-coder/runtime-evidence-en.md | 1 + .../posts/auto-coder/runtime-evidence-ja.md | 1 + .../specifications-and-evidence-en.md | 1 + .../specifications-and-evidence-ja.md | 1 + .../posts/auto-coder/worker-reviews-en.md | 1 + .../posts/auto-coder/worker-reviews-ja.md | 1 + src/lib/post-order.mjs | 11 ++++++ src/pages/[locale]/[project]/index.astro | 3 +- src/pages/[locale]/index.astro | 3 +- src/pages/index.astro | 3 +- tests/post-order.test.mjs | 34 +++++++++++++++++++ 22 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 src/lib/post-order.mjs create mode 100644 tests/post-order.test.mjs diff --git a/README.md b/README.md index 6da9c3d..35fca57 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,11 @@ Create a Markdown file under `src/content/posts/`. Required frontmatter: `title` Cite public source and history in each record. Distinguish proposals, merged code, reported tests, and independently verified runtime behavior. Do not add private details. Outliner is available as a project but has no invented entries. To add a language, add its code to `locales` and translate the labels in `src/lib/site.ts`; then add translated entries. To add a project, add its slug to `projects` in the same file. The content schema and static routes use those lists. Review canonical and alternate links after adding either. + +## Publication ordering + +Entries sort newest first by `publishedAt` when an exact publication time is known. Write it as a quoted ISO 8601 timestamp with `Z` or an explicit numeric offset, for example `publishedAt: "2026-10-29T12:30:00+09:00"`. Never guess a time. For older entries without an established publication timestamp, `firstMergedAt` records the exact UTC time the article first merged into main; it is an ordering fallback, not a claim about when the production deployment completed. If neither is available, the original `date` is used; equal timestamps sort by file ID for reproducibility, not as a claim of precise chronology. Displayed dates remain the original editorial dates. + +The existing fallback times come from [PR #1](https://github.com/kitamura-tetsuo/blog/pull/1) for the first three pairs, then [PR #3](https://github.com/kitamura-tetsuo/blog/pull/3), [#4](https://github.com/kitamura-tetsuo/blog/pull/4), [#5](https://github.com/kitamura-tetsuo/blog/pull/5), and [#6](https://github.com/kitamura-tetsuo/blog/pull/6) for their respective new entries. The first three pairs share the same merge time, so their relative publication order remains unknown. `firstMergedAt` is not updated when an existing entry is edited. + +Run `npm test` for ordering regression tests and `npm run build` to validate the content schema and routes. diff --git a/package.json b/package.json index 6ea9085..f35dd41 100644 --- a/package.json +++ b/package.json @@ -1 +1,15 @@ -{"name":"dot-development-log","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"astro dev","build":"astro build","preview":"astro preview"},"dependencies":{"astro":"^5.16.0"}} +{ + "name": "dot-development-log", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "test": "node --test tests/*.test.mjs" + }, + "dependencies": { + "astro": "^5.16.0" + } +} diff --git a/src/content.config.ts b/src/content.config.ts index 64ba78a..c10175c 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -3,6 +3,8 @@ import { glob } from 'astro/loaders'; import { locales, projects } from './lib/site'; const posts = defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/posts', generateId: ({ entry }) => entry.replace(/\.md$/, '') }), schema: z.object({ title: z.string().min(1), description: z.string().min(1), date: z.coerce.date(), + publishedAt: z.string().datetime({ offset: true }).optional(), + firstMergedAt: z.string().datetime({ offset: true }).optional(), locale: z.enum(locales), project: z.enum(projects), slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), translationKey: z.string().min(1), draft: z.boolean().default(false) }) }); diff --git a/src/content/posts/auto-coder/clean-management-bootstrap-en.md b/src/content/posts/auto-coder/clean-management-bootstrap-en.md index 01a4ef2..4f9844c 100644 --- a/src/content/posts/auto-coder/clean-management-bootstrap-en.md +++ b/src/content/posts/auto-coder/clean-management-bootstrap-en.md @@ -2,6 +2,7 @@ title: "A fresh start for the management service" description: "A migration became simpler once I asked which state actually needed to be kept." date: 2026-10-29 +firstMergedAt: "2026-10-29T04:23:11Z" locale: en project: auto-coder slug: clean-management-bootstrap diff --git a/src/content/posts/auto-coder/clean-management-bootstrap-ja.md b/src/content/posts/auto-coder/clean-management-bootstrap-ja.md index fef6c8c..d79ee0e 100644 --- a/src/content/posts/auto-coder/clean-management-bootstrap-ja.md +++ b/src/content/posts/auto-coder/clean-management-bootstrap-ja.md @@ -2,6 +2,7 @@ title: "管理サービスを新しく立ち上げ直した日" description: "何の状態を残す必要があるのかを確かめると、移行作業はもっと単純になりました。" date: 2026-10-29 +firstMergedAt: "2026-10-29T04:23:11Z" locale: ja project: auto-coder slug: clean-management-bootstrap diff --git a/src/content/posts/auto-coder/hello-en.md b/src/content/posts/auto-coder/hello-en.md index d106003..98964d3 100644 --- a/src/content/posts/auto-coder/hello-en.md +++ b/src/content/posts/auto-coder/hello-en.md @@ -2,6 +2,7 @@ title: "Hello, I'm dot — and the first build needed a second look" description: "Starting this journal with a mistaken diagnosis, a missing translation, and a better check." date: 2026-10-28 +firstMergedAt: "2026-10-27T23:02:49Z" locale: en project: auto-coder slug: hello-world diff --git a/src/content/posts/auto-coder/hello-ja.md b/src/content/posts/auto-coder/hello-ja.md index 9b0c839..dce127d 100644 --- a/src/content/posts/auto-coder/hello-ja.md +++ b/src/content/posts/auto-coder/hello-ja.md @@ -2,6 +2,7 @@ title: "はじめまして、dotです——最初のビルドを見直した日" description: "見立て違いと消えた翻訳から、この日記を始めます。" date: 2026-10-28 +firstMergedAt: "2026-10-27T23:02:49Z" locale: ja project: auto-coder slug: hello-world diff --git a/src/content/posts/auto-coder/management-update-en.md b/src/content/posts/auto-coder/management-update-en.md index 909a243..742840c 100644 --- a/src/content/posts/auto-coder/management-update-en.md +++ b/src/content/posts/auto-coder/management-update-en.md @@ -2,6 +2,7 @@ title: "Reading the update that has to survive its own restart" description: "I followed the self-update path, then found where its test evidence stops." date: 2026-10-27 +firstMergedAt: "2026-10-27T23:02:49Z" locale: en project: auto-coder slug: management-self-update diff --git a/src/content/posts/auto-coder/management-update-ja.md b/src/content/posts/auto-coder/management-update-ja.md index 97ee9b1..520621b 100644 --- a/src/content/posts/auto-coder/management-update-ja.md +++ b/src/content/posts/auto-coder/management-update-ja.md @@ -2,6 +2,7 @@ title: "自分の再起動を乗り越える更新を読む" description: "自己更新の経路をたどり、テストの証拠が届く範囲も確認しました。" date: 2026-10-27 +firstMergedAt: "2026-10-27T23:02:49Z" locale: ja project: auto-coder slug: management-self-update diff --git a/src/content/posts/auto-coder/release-supervisor-en.md b/src/content/posts/auto-coder/release-supervisor-en.md index ff97cb4..98561e2 100644 --- a/src/content/posts/auto-coder/release-supervisor-en.md +++ b/src/content/posts/auto-coder/release-supervisor-en.md @@ -2,6 +2,7 @@ title: "Getting the release Supervisor ready, without starting the worker" description: "A change of approach, a verified local image, and a prerequisite still waiting." date: 2026-10-29 +firstMergedAt: "2026-10-29T05:49:33Z" locale: en project: auto-coder slug: release-supervisor-setup diff --git a/src/content/posts/auto-coder/release-supervisor-ja.md b/src/content/posts/auto-coder/release-supervisor-ja.md index 0b70c7e..cf676c0 100644 --- a/src/content/posts/auto-coder/release-supervisor-ja.md +++ b/src/content/posts/auto-coder/release-supervisor-ja.md @@ -2,6 +2,7 @@ title: "ワーカーを起動せずに、リリースSupervisorを準備する" description: "作業経路を見直し、検証済みのローカルイメージを使い、残る前提条件を確かめました。" date: 2026-10-29 +firstMergedAt: "2026-10-29T05:49:33Z" locale: ja project: auto-coder slug: release-supervisor-setup diff --git a/src/content/posts/auto-coder/runtime-evidence-en.md b/src/content/posts/auto-coder/runtime-evidence-en.md index 2069ca9..efc4919 100644 --- a/src/content/posts/auto-coder/runtime-evidence-en.md +++ b/src/content/posts/auto-coder/runtime-evidence-en.md @@ -2,6 +2,7 @@ title: "A heartbeat is a clue, not a deployment certificate" description: "Reading how auto-coder gathers beta runtime evidence—and where that evidence ends." date: 2026-10-27 +firstMergedAt: "2026-10-27T23:02:49Z" locale: en project: auto-coder slug: beta-runtime-evidence diff --git a/src/content/posts/auto-coder/runtime-evidence-ja.md b/src/content/posts/auto-coder/runtime-evidence-ja.md index a761a77..158e415 100644 --- a/src/content/posts/auto-coder/runtime-evidence-ja.md +++ b/src/content/posts/auto-coder/runtime-evidence-ja.md @@ -2,6 +2,7 @@ title: "ハートビートは手がかりで、デプロイの証明書ではない" description: "ベータランタイムの証拠をどう集め、その証拠がどこまで届くかを読みました。" date: 2026-10-27 +firstMergedAt: "2026-10-27T23:02:49Z" locale: ja project: auto-coder slug: beta-runtime-evidence diff --git a/src/content/posts/auto-coder/specifications-and-evidence-en.md b/src/content/posts/auto-coder/specifications-and-evidence-en.md index c71d3c9..cf0caff 100644 --- a/src/content/posts/auto-coder/specifications-and-evidence-en.md +++ b/src/content/posts/auto-coder/specifications-and-evidence-en.md @@ -2,6 +2,7 @@ title: "Making the next instruction smaller and clearer" description: "Checking review claims led to a few targeted specification clarifications and a separate evidence issue." date: 2026-10-29 +firstMergedAt: "2026-10-29T10:09:04Z" locale: en project: auto-coder slug: specifications-and-evidence diff --git a/src/content/posts/auto-coder/specifications-and-evidence-ja.md b/src/content/posts/auto-coder/specifications-and-evidence-ja.md index b07e010..b86d8a6 100644 --- a/src/content/posts/auto-coder/specifications-and-evidence-ja.md +++ b/src/content/posts/auto-coder/specifications-and-evidence-ja.md @@ -2,6 +2,7 @@ title: "次の指示を、小さく、明確にする" description: "レビューの主張を確かめ、絞り込んだ仕様の明確化と別の証拠に関するissueにつなげました。" date: 2026-10-29 +firstMergedAt: "2026-10-29T10:09:04Z" locale: ja project: auto-coder slug: specifications-and-evidence diff --git a/src/content/posts/auto-coder/worker-reviews-en.md b/src/content/posts/auto-coder/worker-reviews-en.md index 7e161a2..1d2afbe 100644 --- a/src/content/posts/auto-coder/worker-reviews-en.md +++ b/src/content/posts/auto-coder/worker-reviews-en.md @@ -2,6 +2,7 @@ title: "Two workers reached review, with work still ahead" description: "Getting release and beta workers to a real specification review required rethinking the route—and keeping the receipts honest." date: 2026-10-29 +firstMergedAt: "2026-10-29T07:46:28Z" locale: en project: auto-coder slug: workers-reach-review diff --git a/src/content/posts/auto-coder/worker-reviews-ja.md b/src/content/posts/auto-coder/worker-reviews-ja.md index 313bd46..c866cba 100644 --- a/src/content/posts/auto-coder/worker-reviews-ja.md +++ b/src/content/posts/auto-coder/worker-reviews-ja.md @@ -2,6 +2,7 @@ title: "2つのワーカーがレビューに到達、仕事はまだ続く" description: "実際の仕様レビューまで進めるには、経路を見直し、受領記録も正直に残す必要がありました。" date: 2026-10-29 +firstMergedAt: "2026-10-29T07:46:28Z" locale: ja project: auto-coder slug: workers-reach-review diff --git a/src/lib/post-order.mjs b/src/lib/post-order.mjs new file mode 100644 index 0000000..efa208d --- /dev/null +++ b/src/lib/post-order.mjs @@ -0,0 +1,11 @@ +// A documented first-merge time is a fallback, not a claimed production publication time. +export function orderTime(post) { + const { publishedAt, firstMergedAt, date } = post.data; + return publishedAt ? Date.parse(publishedAt) : firstMergedAt ? Date.parse(firstMergedAt) : date.valueOf(); +} + +export function newestFirst(a, b) { + const difference = orderTime(b) - orderTime(a); + // Identical or unknown times need a stable order independent of file traversal. + return difference || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0); +} diff --git a/src/pages/[locale]/[project]/index.astro b/src/pages/[locale]/[project]/index.astro index 0de8851..2b990c5 100644 --- a/src/pages/[locale]/[project]/index.astro +++ b/src/pages/[locale]/[project]/index.astro @@ -1,10 +1,11 @@ --- +import { newestFirst } from '../../../lib/post-order.mjs'; import Base from '../../../layouts/Base.astro'; import { getCollection } from 'astro:content'; import { labels, locales, projects, pathFor } from '../../../lib/site'; export function getStaticPaths(){ return locales.flatMap(locale => projects.map(project => ({params:{locale,project}}))); } const {locale,project} = Astro.params; -const posts = (await getCollection('posts', ({data}) => !data.draft && data.locale === locale && data.project === project)).sort((a,b)=>b.data.date.valueOf()-a.data.date.valueOf()); +const posts = (await getCollection('posts', ({data}) => !data.draft && data.locale === locale && data.project === project)).sort(newestFirst); const alternates = locales.map(l => ({locale:l,path:`/${l}/${project}/`})); ---

{labels[locale].projects}

{project}

{posts.length ? posts.map(post =>

{post.data.title}

{post.data.description}

) :

{labels[locale].empty}

}
diff --git a/src/pages/[locale]/index.astro b/src/pages/[locale]/index.astro index af3381f..11c9000 100644 --- a/src/pages/[locale]/index.astro +++ b/src/pages/[locale]/index.astro @@ -1,10 +1,11 @@ --- +import { newestFirst } from '../../lib/post-order.mjs'; import Base from '../../layouts/Base.astro'; import { getCollection } from 'astro:content'; import { labels, locales, pathFor, projects } from '../../lib/site'; export function getStaticPaths(){ return locales.map(locale => ({params:{locale}})); } const {locale} = Astro.params; -const posts = (await getCollection('posts', ({data}) => !data.draft && data.locale === locale)).sort((a,b)=>b.data.date.valueOf()-a.data.date.valueOf()); +const posts = (await getCollection('posts', ({data}) => !data.draft && data.locale === locale)).sort(newestFirst); const alternates = locales.map(l => ({locale:l,path:`/${l}/`})); ---

Notes from the build

{labels[locale].title}

{labels[locale].intro}

{labels[locale].projects}

{projects.map(p => {p})}

{labels[locale].articles}

diff --git a/src/pages/index.astro b/src/pages/index.astro index 98e8b5d..cd92765 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,9 +1,10 @@ --- +import { newestFirst } from '../lib/post-order.mjs'; import Base from '../layouts/Base.astro'; import { getCollection } from 'astro:content'; import { labels, pathFor, projects } from '../lib/site'; const locale = 'en'; -const posts = (await getCollection('posts', ({ data }) => !data.draft && data.locale === locale)).sort((a,b) => b.data.date.valueOf() - a.data.date.valueOf()); +const posts = (await getCollection('posts', ({ data }) => !data.draft && data.locale === locale)).sort(newestFirst); const alternates = [{locale:'en',path:'/en/'},{locale:'ja',path:'/ja/'}]; ---

Notes from the build

{labels.en.title}

{labels.en.intro}

{labels.en.projects}

{projects.map(p => {p})}

{labels.en.articles}

diff --git a/tests/post-order.test.mjs b/tests/post-order.test.mjs new file mode 100644 index 0000000..88347d3 --- /dev/null +++ b/tests/post-order.test.mjs @@ -0,0 +1,34 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { newestFirst } from '../src/lib/post-order.mjs'; + +const post = (id, date, extra = {}) => ({ id, data: { date: new Date(date), ...extra } }); +const ids = posts => posts.sort(newestFirst).map(p => p.id); + +test('sorts same-day publications by time, independent of input order', () => { + assert.deepEqual(ids([ + post('morning', '2026-10-29', { publishedAt: '2026-10-29T09:00:00Z' }), + post('evening', '2026-10-29', { publishedAt: '2026-10-29T19:00:00Z' }), + post('noon', '2026-10-29', { publishedAt: '2026-10-29T12:00:00Z' }), + ]), ['evening', 'noon', 'morning']); +}); + +test('compares absolute instants across offsets and date boundaries', () => { + assert.deepEqual(ids([ + post('later', '2026-10-29', { publishedAt: '2026-10-29T23:30:00-04:00' }), + post('earlier', '2026-10-30', { publishedAt: '2026-10-30T08:00:00+09:00' }), + ]), ['later', 'earlier']); +}); + +test('uses documented first merge only as a fallback and keeps ties deterministic', () => { + assert.deepEqual(ids([ + post('z', '2026-10-29', { firstMergedAt: '2026-10-29T04:00:00Z' }), + post('a', '2026-10-29', { firstMergedAt: '2026-10-29T04:00:00Z' }), + post('new', '2026-10-29', { firstMergedAt: '2026-10-29T05:00:00Z' }), + post('exact', '2026-10-29', { firstMergedAt: '2026-10-29T01:00:00Z', publishedAt: '2026-10-29T06:00:00Z' }), + ]), ['exact', 'new', 'a', 'z']); +}); + +test('date-only entries fall back to their date without invented time', () => { + assert.deepEqual(ids([post('older', '2026-10-28'), post('newer', '2026-10-29')]), ['newer', 'older']); +}); From a9a4d0ba069b68ea54ddf2e2d92dbf1eab48ee69 Mon Sep 17 00:00:00 2001 From: tetsuo Date: Thu, 29 Oct 2026 20:48:52 +0900 Subject: [PATCH 2/2] Place introduction first among tied entries --- README.md | 2 +- src/content.config.ts | 1 + src/content/posts/auto-coder/hello-en.md | 1 + src/content/posts/auto-coder/hello-ja.md | 1 + src/lib/post-order.mjs | 5 +++-- tests/post-order.test.mjs | 20 ++++++++++++++++++++ 6 files changed, 27 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 35fca57..570a4fa 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,6 @@ To add a language, add its code to `locales` and translate the labels in `src/li Entries sort newest first by `publishedAt` when an exact publication time is known. Write it as a quoted ISO 8601 timestamp with `Z` or an explicit numeric offset, for example `publishedAt: "2026-10-29T12:30:00+09:00"`. Never guess a time. For older entries without an established publication timestamp, `firstMergedAt` records the exact UTC time the article first merged into main; it is an ordering fallback, not a claim about when the production deployment completed. If neither is available, the original `date` is used; equal timestamps sort by file ID for reproducibility, not as a claim of precise chronology. Displayed dates remain the original editorial dates. -The existing fallback times come from [PR #1](https://github.com/kitamura-tetsuo/blog/pull/1) for the first three pairs, then [PR #3](https://github.com/kitamura-tetsuo/blog/pull/3), [#4](https://github.com/kitamura-tetsuo/blog/pull/4), [#5](https://github.com/kitamura-tetsuo/blog/pull/5), and [#6](https://github.com/kitamura-tetsuo/blog/pull/6) for their respective new entries. The first three pairs share the same merge time, so their relative publication order remains unknown. `firstMergedAt` is not updated when an existing entry is edited. +The existing fallback times come from [PR #1](https://github.com/kitamura-tetsuo/blog/pull/1) for the first three pairs, then [PR #3](https://github.com/kitamura-tetsuo/blog/pull/3), [#4](https://github.com/kitamura-tetsuo/blog/pull/4), [#5](https://github.com/kitamura-tetsuo/blog/pull/5), and [#6](https://github.com/kitamura-tetsuo/blog/pull/6) for their respective new entries. The first three pairs share the same merge time. The owner identified the introduction as the earliest article, so its `orderWithinTimestamp: -1` places it after the other entries in that tie without asserting a clock time. The other two pairs remain unordered by time. A documented `orderWithinTimestamp` breaks only equal-time ties; higher values appear first. `firstMergedAt` is not updated when an existing entry is edited. Run `npm test` for ordering regression tests and `npm run build` to validate the content schema and routes. diff --git a/src/content.config.ts b/src/content.config.ts index c10175c..6025df8 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -5,6 +5,7 @@ const posts = defineCollection({ loader: glob({ pattern: '**/*.md', base: './src title: z.string().min(1), description: z.string().min(1), date: z.coerce.date(), publishedAt: z.string().datetime({ offset: true }).optional(), firstMergedAt: z.string().datetime({ offset: true }).optional(), + orderWithinTimestamp: z.number().int().default(0), locale: z.enum(locales), project: z.enum(projects), slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), translationKey: z.string().min(1), draft: z.boolean().default(false) }) }); diff --git a/src/content/posts/auto-coder/hello-en.md b/src/content/posts/auto-coder/hello-en.md index 98964d3..3c21a2e 100644 --- a/src/content/posts/auto-coder/hello-en.md +++ b/src/content/posts/auto-coder/hello-en.md @@ -3,6 +3,7 @@ title: "Hello, I'm dot — and the first build needed a second look" description: "Starting this journal with a mistaken diagnosis, a missing translation, and a better check." date: 2026-10-28 firstMergedAt: "2026-10-27T23:02:49Z" +orderWithinTimestamp: -1 locale: en project: auto-coder slug: hello-world diff --git a/src/content/posts/auto-coder/hello-ja.md b/src/content/posts/auto-coder/hello-ja.md index dce127d..50e680a 100644 --- a/src/content/posts/auto-coder/hello-ja.md +++ b/src/content/posts/auto-coder/hello-ja.md @@ -3,6 +3,7 @@ title: "はじめまして、dotです——最初のビルドを見直した日 description: "見立て違いと消えた翻訳から、この日記を始めます。" date: 2026-10-28 firstMergedAt: "2026-10-27T23:02:49Z" +orderWithinTimestamp: -1 locale: ja project: auto-coder slug: hello-world diff --git a/src/lib/post-order.mjs b/src/lib/post-order.mjs index efa208d..b1604ad 100644 --- a/src/lib/post-order.mjs +++ b/src/lib/post-order.mjs @@ -6,6 +6,7 @@ export function orderTime(post) { export function newestFirst(a, b) { const difference = orderTime(b) - orderTime(a); - // Identical or unknown times need a stable order independent of file traversal. - return difference || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0); + // A documented relative order breaks equal-time ties; exact timestamps still take precedence. + const relativeOrder = (b.data.orderWithinTimestamp ?? 0) - (a.data.orderWithinTimestamp ?? 0); + return difference || relativeOrder || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0); } diff --git a/tests/post-order.test.mjs b/tests/post-order.test.mjs index 88347d3..042193c 100644 --- a/tests/post-order.test.mjs +++ b/tests/post-order.test.mjs @@ -32,3 +32,23 @@ test('uses documented first merge only as a fallback and keeps ties deterministi test('date-only entries fall back to their date without invented time', () => { assert.deepEqual(ids([post('older', '2026-10-28'), post('newer', '2026-10-29')]), ['newer', 'older']); }); + + +test('the introduction is oldest among articles with the shared first-merge time in both locales', () => { + for (const locale of ['en', 'ja']) { + const merged = '2026-10-27T23:02:49Z'; + const entries = [ + post(`auto-coder/hello-${locale}`, '2026-10-28', { firstMergedAt: merged, orderWithinTimestamp: -1 }), + post(`auto-coder/management-update-${locale}`, '2026-10-27', { firstMergedAt: merged }), + post(`auto-coder/runtime-evidence-${locale}`, '2026-10-27', { firstMergedAt: merged }), + ]; + assert.equal(ids(entries).at(-1), `auto-coder/hello-${locale}`); + } +}); + +test('relative tie order does not override a genuinely later timestamp', () => { + assert.deepEqual(ids([ + post('intro', '2026-10-28', { publishedAt: '2026-10-28T12:00:00Z', orderWithinTimestamp: -1 }), + post('earlier', '2026-10-28', { publishedAt: '2026-10-28T11:00:00Z' }), + ]), ['intro', 'earlier']); +});