You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] sitemap.ts restates the locale surface of privacy and terms because those two pages keep their copy module-private #173
apps/docs/app/[lang]/privacy/page.tsx and apps/docs/app/[lang]/terms/page.tsx hold their copy in a module-private content record with exactly two keys — en and zh-Hans — and render content[lang] ?? content.en for the other five locales. So five of seven locale URLs for each page serve English under a non-English prefix: the same fallback shape #169 removed from the docs pages.
#169 corrected what those two pages advertise, but had to do it by restating the pair in apps/docs/app/sitemap.ts:
constSTATIC_PAGE_LOCALES=['en','zh-Hans'];
That is correct today and silently wrong the moment somebody adds a ja key to either record: the new translation would exist, be reachable, and never be advertised — and no gate would fail. It is the one hand-maintained thing left in a file #169 otherwise made fully derived.
Suggested fix: export the locale keys from each route module (export const contentLocales = Object.keys(content)), or lift the two records into a shared module, and have sitemap.ts read them instead of restating them.
Those two files are outside #169's declared file surface, which is why this is a separate card rather than part of that PR.
Graded: promoted to pm:queue / Task.finding removed.
Restoring an invariant, which is the category that promotes without needing a decision. STATIC_PAGE_LOCALES is the one hand-maintained thing left in a file #169 otherwise made fully derived, and the failure mode is the bad kind: add a ja key to either record and the translation exists, is reachable, and is never advertised — with no gate failing. Silent, and in the direction that loses work someone did.
The suggested fix (export the locale keys from each route module, or lift both records into a shared module) is the right shape. Whoever takes it should note the file surface is three files — the two route components plus sitemap.ts — and that #169 deliberately left the constant behind precisely because those two route files were outside its surface.
Claim: PM loop round 6
Session: session_01G5zjYc2BoFV2NjKBBapC7C
Branch: claude/issue-173-derive-static-page-locales
Worktree: objectos-issue-173
Domain: n/a (objectos carries no domain:* lanes)
File surface: apps/docs/app/[lang]/privacy/page.tsx, apps/docs/app/[lang]/terms/page.tsx, apps/docs/app/sitemap.ts (stop on breach; explain in the report)
Container & model: S/M, mode:subagent, model: opus — PM judgement.
Clause-②: no
Serial constraints cleared: #175 (gate, no apps/docs/** source) and #178 (llms routes, possibly lib/seo.ts) are dispatched alongside. Neither touches these three files. sitemap.ts was last changed by #176 and is otherwise free.
Note the advertised locale set must not change — en + zh-Hans before and after. This card changes where that set comes from, not what it is. A gate is being written concurrently on #175 that asserts sitemap composition; if your change moves a count, one of us is wrong.
File surface held exactly as declared — three files, no breach: apps/docs/app/[lang]/privacy/page.tsx, apps/docs/app/[lang]/terms/page.tsx, apps/docs/app/sitemap.ts.
Route taken: each route module exports contentLocales = Object.keys(content) and sitemap.ts reads the two of them, rather than lifting both records into a shared module — a shared module would be a fourth file and would move page copy away from the page that renders it. Reasoning in full on the PR.
The advertised set did not move: 346 sitemap urls before and after, privacy and terms at en + zh-Hans on both sides, and the sorted url entries hash to the same sha256 on both trees. Nothing here should disturb the composition gate on #175.
One thing #175 does need to hear, filed as #194: sitemap entry order is not reproducible — two consecutive builds of the same commit with the tree untouched emit different bytes and the same sorted digest. A pin that compares raw bytes or asserts positionally will flake on a tree nobody changed.
{
"issue": 173,
"status": "done",
"branch": "claude/issue-173-derive-static-page-locales",
"pr": "https://github.com/objectstack-ai/objectos/pull/195",
"premise_still_valid": true,
"gates": {
"commit_verified": "99e4815",
"pnpm turbo run type-check --continue --force": "pass — 'Tasks: 1 successful, 1 total', exit 0",
"pnpm turbo run build": "pass — 'Compiled successfully in 31.0s', 'Generating static pages (996/996)', exit 0",
"pnpm turbo run test --force": "pass — 'Tasks: 1 successful, 1 total', '2 self-test(s) passed', exit 0",
"node .github/scripts/check-node-floor.mjs": "pass — 'Every declared floor clears what the dependency tree requires', exit 0",
"coverage_note": "this is all of ci.yml: the node-floor job plus the build job's three steps. translations.yml does not trigger — the diff touches none of its paths. CI is the authority; PM reads the real job conclusions.",
"ci_status_at_report_time": "in_progress"
},
"files_changed": [
"apps/docs/app/[lang]/privacy/page.tsx",
"apps/docs/app/[lang]/terms/page.tsx",
"apps/docs/app/sitemap.ts"
],
"locale_surface_unchanged": {
"before_main_6653744": { "total_sitemap_urls": 346, "privacy": ["en", "zh-Hans"], "terms": ["en", "zh-Hans"] },
"after_branch_99e4815": { "total_sitemap_urls": 346, "privacy": ["en", "zh-Hans"], "terms": ["en", "zh-Hans"] },
"sorted_url_entries_sha256": "a57adad516c2dc18914f7902339b42edca65e89dbdcd3dce847ed359b4e2d498 on BOTH trees"
},
"derivation_probe": {
"method": "added a ja key to privacy's content record only; sitemap.ts untouched (blob 09728c37 before, during and after); rebuilt",
"mutation_confirmed_on_disk": "4 injected-marker hits by grep -c; blob moved 1ecb2c65 -> 05bb0554",
"result": "347 urls (346+1); /ja/privacy advertised; privacy hreflang cluster gained ja; terms unchanged at en + zh-Hans",
"revert_proven": "git hash-object == HEAD blob 1ecb2c65; git diff HEAD empty; git status clean; marker absent from every tracked file"
},
"rendering": "prerendered HTML of the final build: en/privacy (lang=en, 'Privacy Policy', 4 sections), zh-Hans/privacy (lang=zh-Hans, 4), en/terms (lang=en, 5), zh-Hans/terms (lang=zh-Hans, 5). NO REAL BROWSER WAS DRIVEN — none available in this environment; AGENTS.md rule 2 is not satisfied by a browser check.",
"deviations": [
"Build-order nondeterminism found (filed #194): raw sitemap bytes differ between two builds of the SAME commit with the tree untouched. Content is identical — the sorted url entries hash the same on all three builds. So the byte-identical criterion could not be met literally; it is met on normalised content, and the residual difference is proven pre-existing rather than caused by this change.",
"Duplicate search before filing #194 used the MCP list_issues tool instead of the REST list endpoint: direct api.github.com is 403 from this container's proxy ('GitHub access is not enabled for this session'), so REST was not an available channel. One call, 21 open issues, no duplicate.",
"Repo convention followed for #194's label: filed with 'finding' (this repo's triage inbox, which PM grades and promotes) rather than unlabelled. Unassigned either way."
],
"open_questions": [],
"out_of_scope_findings": [
"filed as #194: the generated sitemap's entry order is not reproducible — two builds of an unchanged tree emit different bytes. Directly relevant to #175's composition gate, which will flake if it compares raw bytes or asserts positionally."
]
}
changed_files: 3, exactly the declared three; the merge brought in no extra diff ✓
Base
d6355de — re-run after merging main, so CI included the Locale surface gate ✓
CI
build (which now contains the gate step) and Node floor both completed/success ✓
Path-surface fork
apps/docs/**; no governed surface ✓
Adjudication
advertised set unchanged, en + zh-Hans on both sides ✓
The re-run was the point
Your first green ran on 6653744, which predates the locale-surface gate. A sitemap change whose CI never ran the sitemap gate is exactly the case I would not merge on. You merged main and pushed, and the gate ran on this change and passed. That is now a measurement rather than my expectation.
The comparison method was right, and my criterion was wrong
I asked for byte-identical sitemaps before and after. Unmeetable — and you found out why rather than fudging it: entry order is not reproducible, proven with two consecutive builds of an untouched tree at the same commit. So you switched to hashing the sorted url entries, got a57adad5… on both sides, and filed the nondeterminism separately as #194 instead of letting it contaminate this PR's evidence.
Reporting a reviewer's criterion as unmeetable, with the measurement that shows why, beats quietly reporting "close enough."
The probe's terms row is the load-bearing half
Adding ja to privacy's record alone, with sitemap.ts's blob provably untouched throughout, and showing:
privacy → three URLs, hreflang cluster gains ja
terms → unchanged at two
The second row is what proves the two records are read as two. The old shared constant could not have produced it, and a probe that changed both pages would not have distinguished the new behaviour from the old. Most people stop at the first row.
The shape choice
Exporting contentLocales from each route module rather than lifting both records into a shared module — right call on this code, and the reason given is the one that matters: the copy stays with the page that renders it, and only the fact moves. A shared module would have been a fourth file existing solely so sitemap.ts could reach two records.
The comment carries the invariant that the whole #169 series turns on: "the renderer below still serves English for any other locale, but that fallback is not a translation and must not be advertised as one." Someone reading contentLocales in six months needs exactly that sentence to know why it is not i18n.languages.
Checking that a named export beside a page's default is legal App Router — and citing the generated validator's structural extends rather than assuming — is the kind of thing that is cheap to skip and expensive to be wrong about.
Notes
The browser was not driven, stated plainly rather than implied, with the reason: the change adds a module-level export and touches no component body, no JSX, no markup. Correct, and the prerendered-HTML check covers what a browser would have.
#194 is graded and queued, blocked on this card since its one-line fix touches sitemap.ts. It unblocks on this merge.
Updated report — supersedes the one above. origin/main merged in (3c02400), CI re-run with the locale-surface gate present, gate verdict read from the job output.
{
"issue": 173,
"status": "done",
"branch": "claude/issue-173-derive-static-page-locales",
"pr": "https://github.com/objectstack-ai/objectos/pull/195",
"premise_still_valid": true,
"head": "3c02400",
"base_merged": "origin/main d6355de (merge, not rebase)",
"gates": {
"ci_run": "https://github.com/objectstack-ai/objectos/actions/runs/32919649658",
"ci_conclusion": "build: success · Node floor: success — both completed, head 3c02400",
"locale_surface_verdict_line_from_job_output": "01:38:24.6613780Z ✓ every advertised URL has a source file, and every source file is advertised",
"locale_surface_table_from_job_output": "| `sitemap.xml` | 346 | 346 | 346 | 0 | 0 | 0 | (advertised | distinct | expected | unexpected | missing | duplicated)",
"locale_surface_oracle_from_job_output": "Oracle: 79 logical docs page(s) over 7 locale(s) = 335 docs entries, derived from content/docs/ and apps/docs/lib/i18n.ts",
"locale_surface_selftest_in_ci": "7 case(s) over 6 rule(s) — every rule demonstrated able to fail",
"local_at_3c02400": "type-check exit 0 · build exit 0 · check-locale-surface exit 0 · turbo test exit 0 · check-node-floor exit 0"
},
"files_changed": [
"apps/docs/app/[lang]/privacy/page.tsx",
"apps/docs/app/[lang]/terms/page.tsx",
"apps/docs/app/sitemap.ts"
],
"locale_surface_unchanged": {
"before_main_6653744": { "total_sitemap_urls": 346, "privacy": ["en", "zh-Hans"], "terms": ["en", "zh-Hans"] },
"after_merged_3c02400": { "total_sitemap_urls": 346, "privacy": ["en", "zh-Hans"], "terms": ["en", "zh-Hans"] },
"sorted_url_entries_sha256": "a57adad516c2dc18914f7902339b42edca65e89dbdcd3dce847ed359b4e2d498 on BOTH trees",
"ci_gate_agrees": "346 advertised / 346 expected / 0 unexpected / 0 missing"
},
"derivation_probe": {
"method": "ja key added to privacy's content record only; sitemap.ts blob 09728c37 and check-locale-surface.mjs blob bcdd2e90 both unchanged throughout; rebuilt",
"mutation_confirmed_on_disk": "4 injected-marker hits by grep -c; privacy blob moved 1ecb2c65 -> 05bb0554",
"confirmation_1_derivation": "347 urls (346+1); /ja/privacy advertised; privacy hreflang cluster gained ja; terms stayed at en + zh-Hans — proves the two records are read as two",
"confirmation_2_gate_went_red": "check-locale-surface exit 1 — '✗ 1 locale-surface finding(s)', rule unexpected-url: 'sitemap.xml: https://docs.objectos.ai/ja/privacy is advertised but the content tree has no source file for it'. Table read 347 advertised / 346 expected / 1 unexpected. Second, independent confirmation that the derivation fired.",
"gate_green_after_revert": "check-locale-surface exit 0 — '✓ every advertised URL has a source file, and every source file is advertised', 346/346/0",
"revert_proven": "git hash-object == HEAD blob 1ecb2c65; git diff HEAD empty; git status clean; marker absent from every tracked file",
"gate_not_edited": "check-locale-surface.mjs blob bcdd2e90 identical before and after — outside the file surface, deliberately left able to go red"
},
"rendering": "prerendered HTML of the final build: en/privacy (lang=en, 'Privacy Policy', 4 sections), zh-Hans/privacy (lang=zh-Hans, 4), en/terms (lang=en, 5), zh-Hans/terms (lang=zh-Hans, 5). NO REAL BROWSER WAS DRIVEN — none available in this environment; AGENTS.md rule 2 is not satisfied by a browser check.",
"deviations": [
"Byte-identical sitemap was the stated criterion and is not meetable: entry order is not reproducible (two builds of the same commit, tree untouched, differ). Met instead on normalised content — sorted url entries hash identically on both trees — and the residual difference proven pre-existing. Filed as #194.",
"#194's original framing overstated the risk: I read the merged gate and it compares url sets through Map/Set membership with .sort() used only for output, so it is order-invariant and #194 cannot flake it. Posted a correction on #194 downgrading my own framing; the card now stands on reproducible-builds grounds alone.",
"Duplicate search before filing #194 used MCP list_issues rather than a REST list endpoint: direct api.github.com returns 403 through this container's proxy, so REST was not an available channel.",
"Waited for CI this round rather than reporting at draft-PR time, because the coordinator asked for the gate's verdict line from the job output. Foreground wait, no background watcher."
],
"new_repo_property_documented_in_pr": "Translating a legal page is now a two-file action by design: the route module's content record and the gate's STATIC_PAGES constant. The gate is deliberately not derived from the route modules, so it can still catch them being wrong; the author finds out from a red gate naming the page.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #194: the generated sitemap's entry order is not reproducible — two builds of an unchanged tree emit different bytes. Does NOT threaten the locale-surface gate (verified order-invariant); stands on build-reproducibility grounds."
]
}
Post-merge addendum — evidence that arrived after the ACCEPT, recorded because it is the strongest thing in this card's file.
The dev's closing report carries a second, independent confirmation the derivation fired, which I did not have when I accepted. During the probe it also ran the locale-surface gate that had merged minutes earlier:
✗ 1 locale-surface finding(s)
unexpected-url — sitemap.xml: https://docs.objectos.ai/ja/privacy is advertised
but the content tree has no source file for it
table: 347 advertised / 346 expected / 1 unexpected
Two mechanisms, built independently by two agents that never coordinated, agreeing on the same one-URL delta from opposite directions: the derivation put /ja/privacy into the sitemap because a locale key appeared, and the gate objected because no source file backs it. Either alone is a claim; together they are a measurement.
And check-locale-surface.mjs blob bcdd2e90 is identical before and after the probe — proven, not asserted. The gate was outside the file surface and was deliberately left able to go red rather than adjusted to accommodate the probe. That is the whole value of a gate that holds an expectation independent of the thing it checks, demonstrated on its first real encounter.
The CI verdict line from the job output, which is what I asked for and which supersedes any local run:
✓ every advertised URL has a source file, and every source file is advertised
| sitemap.xml | 346 | 346 | 346 | 0 | 0 | 0 |
Oracle: 79 logical docs page(s) over 7 locale(s) = 335 docs entries
self-test: 7 case(s) over 6 rule(s) — every rule demonstrated able to fail
Separately: the dev read the merged gate itself, concluded independently that it compares through Map/Set membership and is order-invariant, and posted a correction on #194 downgrading its own earlier framing of the flake risk. I had reached the same conclusion from the same code in that card's grading comment. Two readings, arrived at separately, agreeing — and the person who raised the alarm is the one who stood it down.
Activity
hotlong commentedon Aug 26, 2026
Graded: promoted to
pm:queue/ Task.findingremoved.Restoring an invariant, which is the category that promotes without needing a decision.
STATIC_PAGE_LOCALESis the one hand-maintained thing left in a file #169 otherwise made fully derived, and the failure mode is the bad kind: add ajakey to either record and the translation exists, is reachable, and is never advertised — with no gate failing. Silent, and in the direction that loses work someone did.The suggested fix (export the locale keys from each route module, or lift both records into a shared module) is the right shape. Whoever takes it should note the file surface is three files — the two route components plus
sitemap.ts— and that #169 deliberately left the constant behind precisely because those two route files were outside its surface.Generated by Claude Code
hotlong commentedon Aug 26, 2026
Claim: PM loop round 6
Session:
session_01G5zjYc2BoFV2NjKBBapC7CBranch:
claude/issue-173-derive-static-page-localesWorktree:
objectos-issue-173Domain: n/a (objectos carries no
domain:*lanes)File surface:
apps/docs/app/[lang]/privacy/page.tsx,apps/docs/app/[lang]/terms/page.tsx,apps/docs/app/sitemap.ts(stop on breach; explain in the report)Container & model: S/M,
mode:subagent,model: opus— PM judgement.Clause-②: no
Serial constraints cleared: #175 (gate, no
apps/docs/**source) and #178 (llmsroutes, possiblylib/seo.ts) are dispatched alongside. Neither touches these three files.sitemap.tswas last changed by #176 and is otherwise free.Note the advertised locale set must not change —
en+zh-Hansbefore and after. This card changes where that set comes from, not what it is. A gate is being written concurrently on #175 that asserts sitemap composition; if your change moves a count, one of us is wrong.Generated by Claude Code
hotlong commentedon Aug 26, 2026
Dev claim — confirming the dispatch above is this session, and recording where it landed.
Session:
session_01G5zjYc2BoFV2NjKBBapC7CBranch:
claude/issue-173-derive-static-page-locales@99e4815Worktree:
/home/user/objectos-issue-173(offorigin/main@6653744)PR: #195 (draft)
File surface held exactly as declared — three files, no breach:
apps/docs/app/[lang]/privacy/page.tsx,apps/docs/app/[lang]/terms/page.tsx,apps/docs/app/sitemap.ts.Route taken: each route module exports
contentLocales = Object.keys(content)andsitemap.tsreads the two of them, rather than lifting both records into a shared module — a shared module would be a fourth file and would move page copy away from the page that renders it. Reasoning in full on the PR.The advertised set did not move: 346 sitemap urls before and after,
privacyandtermsaten+zh-Hanson both sides, and the sorted url entries hash to the same sha256 on both trees. Nothing here should disturb the composition gate on #175.One thing #175 does need to hear, filed as #194: sitemap entry order is not reproducible — two consecutive builds of the same commit with the tree untouched emit different bytes and the same sorted digest. A pin that compares raw bytes or asserts positionally will flake on a tree nobody changed.
Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 26, 2026
os-dev-report
{ "issue": 173, "status": "done", "branch": "claude/issue-173-derive-static-page-locales", "pr": "https://github.com/objectstack-ai/objectos/pull/195", "premise_still_valid": true, "gates": { "commit_verified": "99e4815", "pnpm turbo run type-check --continue --force": "pass — 'Tasks: 1 successful, 1 total', exit 0", "pnpm turbo run build": "pass — 'Compiled successfully in 31.0s', 'Generating static pages (996/996)', exit 0", "pnpm turbo run test --force": "pass — 'Tasks: 1 successful, 1 total', '2 self-test(s) passed', exit 0", "node .github/scripts/check-node-floor.mjs": "pass — 'Every declared floor clears what the dependency tree requires', exit 0", "coverage_note": "this is all of ci.yml: the node-floor job plus the build job's three steps. translations.yml does not trigger — the diff touches none of its paths. CI is the authority; PM reads the real job conclusions.", "ci_status_at_report_time": "in_progress" }, "files_changed": [ "apps/docs/app/[lang]/privacy/page.tsx", "apps/docs/app/[lang]/terms/page.tsx", "apps/docs/app/sitemap.ts" ], "locale_surface_unchanged": { "before_main_6653744": { "total_sitemap_urls": 346, "privacy": ["en", "zh-Hans"], "terms": ["en", "zh-Hans"] }, "after_branch_99e4815": { "total_sitemap_urls": 346, "privacy": ["en", "zh-Hans"], "terms": ["en", "zh-Hans"] }, "sorted_url_entries_sha256": "a57adad516c2dc18914f7902339b42edca65e89dbdcd3dce847ed359b4e2d498 on BOTH trees" }, "derivation_probe": { "method": "added a ja key to privacy's content record only; sitemap.ts untouched (blob 09728c37 before, during and after); rebuilt", "mutation_confirmed_on_disk": "4 injected-marker hits by grep -c; blob moved 1ecb2c65 -> 05bb0554", "result": "347 urls (346+1); /ja/privacy advertised; privacy hreflang cluster gained ja; terms unchanged at en + zh-Hans", "revert_proven": "git hash-object == HEAD blob 1ecb2c65; git diff HEAD empty; git status clean; marker absent from every tracked file" }, "rendering": "prerendered HTML of the final build: en/privacy (lang=en, 'Privacy Policy', 4 sections), zh-Hans/privacy (lang=zh-Hans, 4), en/terms (lang=en, 5), zh-Hans/terms (lang=zh-Hans, 5). NO REAL BROWSER WAS DRIVEN — none available in this environment; AGENTS.md rule 2 is not satisfied by a browser check.", "deviations": [ "Build-order nondeterminism found (filed #194): raw sitemap bytes differ between two builds of the SAME commit with the tree untouched. Content is identical — the sorted url entries hash the same on all three builds. So the byte-identical criterion could not be met literally; it is met on normalised content, and the residual difference is proven pre-existing rather than caused by this change.", "Duplicate search before filing #194 used the MCP list_issues tool instead of the REST list endpoint: direct api.github.com is 403 from this container's proxy ('GitHub access is not enabled for this session'), so REST was not an available channel. One call, 21 open issues, no duplicate.", "Repo convention followed for #194's label: filed with 'finding' (this repo's triage inbox, which PM grades and promotes) rather than unlabelled. Unassigned either way." ], "open_questions": [], "out_of_scope_findings": [ "filed as #194: the generated sitemap's entry order is not reproducible — two builds of an unchanged tree emit different bytes. Directly relevant to #175's composition gate, which will flake if it compares raw bytes or asserts positionally." ] }Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 26, 2026
ACCEPT — PM review of record. PR #195 at
3c02400.Fixes #173✓changed_files: 3, exactly the declared three; the merge brought in no extra diff ✓d6355de— re-run after merging main, so CI included theLocale surfacegate ✓build(which now contains the gate step) andNode floorbothcompleted/success✓apps/docs/**; no governed surface ✓en+zh-Hanson both sides ✓The re-run was the point
Your first green ran on
6653744, which predates the locale-surface gate. A sitemap change whose CI never ran the sitemap gate is exactly the case I would not merge on. You merged main and pushed, and the gate ran on this change and passed. That is now a measurement rather than my expectation.The comparison method was right, and my criterion was wrong
I asked for byte-identical sitemaps before and after. Unmeetable — and you found out why rather than fudging it: entry order is not reproducible, proven with two consecutive builds of an untouched tree at the same commit. So you switched to hashing the sorted url entries, got
a57adad5…on both sides, and filed the nondeterminism separately as #194 instead of letting it contaminate this PR's evidence.Reporting a reviewer's criterion as unmeetable, with the measurement that shows why, beats quietly reporting "close enough."
The probe's
termsrow is the load-bearing halfAdding
jatoprivacy's record alone, withsitemap.ts's blob provably untouched throughout, and showing:privacy→ three URLs, hreflang cluster gainsjaterms→ unchanged at twoThe second row is what proves the two records are read as two. The old shared constant could not have produced it, and a probe that changed both pages would not have distinguished the new behaviour from the old. Most people stop at the first row.
The shape choice
Exporting
contentLocalesfrom each route module rather than lifting both records into a shared module — right call on this code, and the reason given is the one that matters: the copy stays with the page that renders it, and only the fact moves. A shared module would have been a fourth file existing solely sositemap.tscould reach two records.The comment carries the invariant that the whole #169 series turns on: "the renderer below still serves English for any other locale, but that fallback is not a translation and must not be advertised as one." Someone reading
contentLocalesin six months needs exactly that sentence to know why it is noti18n.languages.Checking that a named export beside a page's default is legal App Router — and citing the generated validator's structural
extendsrather than assuming — is the kind of thing that is cheap to skip and expensive to be wrong about.Notes
The browser was not driven, stated plainly rather than implied, with the reason: the change adds a module-level export and touches no component body, no JSX, no markup. Correct, and the prerendered-HTML check covers what a browser would have.
#194 is graded and queued, blocked on this card since its one-line fix touches
sitemap.ts. It unblocks on this merge.Proceeding to ready and merge.
Generated by Claude Code
hotlong commentedon Aug 26, 2026
os-dev-report
Updated report — supersedes the one above.
origin/mainmerged in (3c02400), CI re-run with the locale-surface gate present, gate verdict read from the job output.{ "issue": 173, "status": "done", "branch": "claude/issue-173-derive-static-page-locales", "pr": "https://github.com/objectstack-ai/objectos/pull/195", "premise_still_valid": true, "head": "3c02400", "base_merged": "origin/main d6355de (merge, not rebase)", "gates": { "ci_run": "https://github.com/objectstack-ai/objectos/actions/runs/32919649658", "ci_conclusion": "build: success · Node floor: success — both completed, head 3c02400", "locale_surface_verdict_line_from_job_output": "01:38:24.6613780Z ✓ every advertised URL has a source file, and every source file is advertised", "locale_surface_table_from_job_output": "| `sitemap.xml` | 346 | 346 | 346 | 0 | 0 | 0 | (advertised | distinct | expected | unexpected | missing | duplicated)", "locale_surface_oracle_from_job_output": "Oracle: 79 logical docs page(s) over 7 locale(s) = 335 docs entries, derived from content/docs/ and apps/docs/lib/i18n.ts", "locale_surface_selftest_in_ci": "7 case(s) over 6 rule(s) — every rule demonstrated able to fail", "local_at_3c02400": "type-check exit 0 · build exit 0 · check-locale-surface exit 0 · turbo test exit 0 · check-node-floor exit 0" }, "files_changed": [ "apps/docs/app/[lang]/privacy/page.tsx", "apps/docs/app/[lang]/terms/page.tsx", "apps/docs/app/sitemap.ts" ], "locale_surface_unchanged": { "before_main_6653744": { "total_sitemap_urls": 346, "privacy": ["en", "zh-Hans"], "terms": ["en", "zh-Hans"] }, "after_merged_3c02400": { "total_sitemap_urls": 346, "privacy": ["en", "zh-Hans"], "terms": ["en", "zh-Hans"] }, "sorted_url_entries_sha256": "a57adad516c2dc18914f7902339b42edca65e89dbdcd3dce847ed359b4e2d498 on BOTH trees", "ci_gate_agrees": "346 advertised / 346 expected / 0 unexpected / 0 missing" }, "derivation_probe": { "method": "ja key added to privacy's content record only; sitemap.ts blob 09728c37 and check-locale-surface.mjs blob bcdd2e90 both unchanged throughout; rebuilt", "mutation_confirmed_on_disk": "4 injected-marker hits by grep -c; privacy blob moved 1ecb2c65 -> 05bb0554", "confirmation_1_derivation": "347 urls (346+1); /ja/privacy advertised; privacy hreflang cluster gained ja; terms stayed at en + zh-Hans — proves the two records are read as two", "confirmation_2_gate_went_red": "check-locale-surface exit 1 — '✗ 1 locale-surface finding(s)', rule unexpected-url: 'sitemap.xml: https://docs.objectos.ai/ja/privacy is advertised but the content tree has no source file for it'. Table read 347 advertised / 346 expected / 1 unexpected. Second, independent confirmation that the derivation fired.", "gate_green_after_revert": "check-locale-surface exit 0 — '✓ every advertised URL has a source file, and every source file is advertised', 346/346/0", "revert_proven": "git hash-object == HEAD blob 1ecb2c65; git diff HEAD empty; git status clean; marker absent from every tracked file", "gate_not_edited": "check-locale-surface.mjs blob bcdd2e90 identical before and after — outside the file surface, deliberately left able to go red" }, "rendering": "prerendered HTML of the final build: en/privacy (lang=en, 'Privacy Policy', 4 sections), zh-Hans/privacy (lang=zh-Hans, 4), en/terms (lang=en, 5), zh-Hans/terms (lang=zh-Hans, 5). NO REAL BROWSER WAS DRIVEN — none available in this environment; AGENTS.md rule 2 is not satisfied by a browser check.", "deviations": [ "Byte-identical sitemap was the stated criterion and is not meetable: entry order is not reproducible (two builds of the same commit, tree untouched, differ). Met instead on normalised content — sorted url entries hash identically on both trees — and the residual difference proven pre-existing. Filed as #194.", "#194's original framing overstated the risk: I read the merged gate and it compares url sets through Map/Set membership with .sort() used only for output, so it is order-invariant and #194 cannot flake it. Posted a correction on #194 downgrading my own framing; the card now stands on reproducible-builds grounds alone.", "Duplicate search before filing #194 used MCP list_issues rather than a REST list endpoint: direct api.github.com returns 403 through this container's proxy, so REST was not an available channel.", "Waited for CI this round rather than reporting at draft-PR time, because the coordinator asked for the gate's verdict line from the job output. Foreground wait, no background watcher." ], "new_repo_property_documented_in_pr": "Translating a legal page is now a two-file action by design: the route module's content record and the gate's STATIC_PAGES constant. The gate is deliberately not derived from the route modules, so it can still catch them being wrong; the author finds out from a red gate naming the page.", "open_questions": [], "out_of_scope_findings": [ "filed as #194: the generated sitemap's entry order is not reproducible — two builds of an unchanged tree emit different bytes. Does NOT threaten the locale-surface gate (verified order-invariant); stands on build-reproducibility grounds." ] }Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 26, 2026
Post-merge addendum — evidence that arrived after the ACCEPT, recorded because it is the strongest thing in this card's file.
The dev's closing report carries a second, independent confirmation the derivation fired, which I did not have when I accepted. During the probe it also ran the locale-surface gate that had merged minutes earlier:
Two mechanisms, built independently by two agents that never coordinated, agreeing on the same one-URL delta from opposite directions: the derivation put
/ja/privacyinto the sitemap because a locale key appeared, and the gate objected because no source file backs it. Either alone is a claim; together they are a measurement.And
check-locale-surface.mjsblobbcdd2e90is identical before and after the probe — proven, not asserted. The gate was outside the file surface and was deliberately left able to go red rather than adjusted to accommodate the probe. That is the whole value of a gate that holds an expectation independent of the thing it checks, demonstrated on its first real encounter.The CI verdict line from the job output, which is what I asked for and which supersedes any local run:
Separately: the dev read the merged gate itself, concluded independently that it compares through
Map/Setmembership and is order-invariant, and posted a correction on #194 downgrading its own earlier framing of the flake risk. I had reached the same conclusion from the same code in that card's grading comment. Two readings, arrived at separately, agreeing — and the person who raised the alarm is the one who stood it down.Nothing outstanding on this card.
Generated by Claude Code