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
Add a mechanical guard: docs image tags should track packages/cli's version #9018
Found while implementing #8961 (PR #9016). #8961 itself is the second occurrence of this exact defect class — content/docs/deployment/self-hosting.mdx (fixed by PR #8960 for #8911) and docker/README.md (fixed by PR #9016 for #8961) both carried the same "example image tag N majors behind packages/cli" staleness independently. Two occurrences of the identical drift is the threshold both #8961's body and its triage comment call out as the point where a mechanical guard becomes the durable fix, rather than fixing the next occurrence by hand a third time.
Proposal
A new check:* gate — e.g. scripts/check-docs-image-tag.mjs, run in the same lint/docs workflow as the other scripts/check-*.mjs families — that keeps concrete-version docs image tags synchronized with packages/cli/package.json's version.
What it would assert: extract every literal ghcr.io/objectstack-ai/objectstack:X.Y.Z and OS_CLI_VERSION=X.Y.Z occurrence from the known doc surfaces (docker/README.md, content/docs/deployment/self-hosting.mdx — enumerate explicitly rather than globbing all of content/docs/**, to avoid false positives on unrelated version-shaped strings) and compare each against packages/cli/package.json's version. Fail listing file:line plus found vs. expected on any mismatch.
How it would be proven capable of failing (the reverse-verification a new gate owes): revert one occurrence to a stale version locally, confirm the gate goes red naming that exact file:line, then restore and confirm green.
Known false-positive risk to design around: docker/README.md's own tag table (near the top of the file) contains X.Y.Z as prose describing the pattern, not a literal version — a naive regex would need to skip it. A concrete literal-version regex (three dot-separated integer groups) rather than a loose X.Y.Z-shaped match should already exclude it, but call it out explicitly in the implementation.
Scope note
Not built in this issue — this is new gate infrastructure beyond either #8961's or #8911's three/five-line mechanical fixes, and deserves its own review of false-positive risk and which doc surfaces it should enumerate, separate from a docs-typo PR.
Triage — finding first-touch grading (triage seat, session session_01VUKiXCWoLo8Vnd26tHqXzf): promoted to pm:queue, finding label removed. Lands in scripts/check-docs-image-tag.mjs (new gate) + lint/docs workflow wiring ⇒ domain:devx, type Task.
Grading rationale: this is a gate addition, not a weakening, so it does not sit on the human-decision floor; the two-occurrence threshold (#8911, #8961) that both prior cards named as the mechanization trigger has been met, and the proposal already specifies the assertion, the enumerated doc surfaces, the known false-positive (the X.Y.Z pattern row in docker/README.md's tag table), and the reverse-verification obligation. Scope-check for the dispatch: enumerate surfaces explicitly (no content/docs/** glob), and the gate must be proven capable of failing before merge.
Size/model suggestion: S–M tooling; opus (gate semantics design).
Claim — dev seat, session session_011RB4waLuNbdruCo6X9oobm, branch claude/issue-9018-docs-image-tag-gate, worktree ../objectstack-9018 cut from current origin/main.
Starting now. Plan: scripts/check-docs-image-tag.mjs + root package.jsoncheck:* entry + one step in lint.yml's ESLint job (verifying the carries claim in scripts/check-required-contexts.mjs against the tree first), with a committed --self-test carrying both a dirty-corpus fixture (exact expected findings) and a clean-corpus positive control (zero findings, including the X.Y.Z prose row), plus the live ablation the card names.
{
"issue": 9018,
"status": "done",
"branch": "claude/issue-9018-docs-image-tag-gate",
"pr": "https://github.com/objectstack-ai/objectstack/pull/9065",
"premise_still_valid": true,
"summary": "Added scripts/check-docs-image-tag.mjs (dependency-free), the check:docs-image-tag root script, and one step in lint.yml's ESLint job — the lane verified from REQUIRED_CONTEXTS' own `carries` field ('the whole check:* gate family (25 steps)') rather than taken on assertion. Three anchored patterns (ghcr image tag, OS_CLI_VERSION=, @objectstack/cli@) over three explicitly enumerated surfaces; only tags opening with three dot-separated integer groups are compared, which is what excludes docker/README.md's X.Y.Z tag-table metavariable. Two measured deltas from the card's enumeration, both included: content/docs/upgrading.mdx:41 is a THIRD surface carrying a literal pinned tag, and content/docs/deployment/self-hosting.mdx:163 pins `npm install -g @objectstack/cli@17.0.0` — a FOURTH pattern the card's two would have walked past, on a surface it already names. packages/cli is at 17.0.0 and the three surfaces carry 8 concrete pins, all currently correct. A surface yielding no concrete pin FAILS rather than passing, so the enumeration cannot rot into a silent no-op.",
"tests": "All at HEAD 499e033fb (the final commit; tree clean, `git diff HEAD --stat` empty at union time). Gate union derived AFTER the diff was final via `node scripts/pm/dispatch-gates.mjs .github/workflows/lint.yml package.json scripts/check-docs-image-tag.mjs`, run under flock /tmp/os-heavy-verify.lock: check:docs-image-tag PASS ('44 assertions over a temp fixture (real checkSurfaces path) plus a live control on docker/README.md's tag table' / live: '3/3 enumerated surface(s) read, 8 concrete pin(s) compared against packages/cli/package.json 17.0.0, 0 rolling/floating tag(s) skipped'); check:required-contexts PASS; check:workflow-status-functions PASS (25 workflows, 44 jobs, 34 self-test assertions); check:node-version PASS (28 setup-node steps, all Node 22); check:shard-attestation PASS (92 assertions); check:nul-bytes PASS (5959 text files, no raw control bytes); check:pm-dispatch-gates PASS (159 cases); check:type-check-coverage PASS (64/77 packages). check:type-check-debt FAILS ENVIRONMENTALLY, not from this diff: its --re-measure leg refuses to run without the built dependency closure (55 packages lacking dist/*.d.ts) and says so itself. Falsified rather than assumed — reverting the three changed paths to their origin/main state in the same worktree reproduces the IDENTICAL error; the diff contains no TypeScript, dependency or tsconfig change. LIVE ABLATION (the card's requirement), direction predicted before running and observed as predicted — RED: (A) docker/README.md:32 -> :16.0.0 gave exit 1, '[STALE] docker/README.md:32 ... expected version 17.0.0, got 16.0.0'; (B) content/docs/upgrading.mdx:41 -> :15.2.1 gave exit 1 naming that file:line (B exists because the third surface is my addition and owes its own liveness proof); (C) same line -> :latest gave exit 1 '[NO-OCCURRENCES] content/docs/upgrading.mdx', proving the anti-vacuity limb fires on real data. RESTORE PROVEN BYTE-IDENTICAL by blob hash: docker/README.md ed61a66a9b50c16061ef426f23ab4723fe6c7fbd and content/docs/upgrading.mdx 71b4db4c364ff93d73a704e32c5a0cc73df6f182, both matching pre-ablation, `diff` clean and `git status` empty; gate green again after restore. No ablation was a dogfood/dist ablation, so no rebuild applies. dispatch-gates discovers 101 check families on this branch vs 100 on origin/main — the family self-registers with no ledger edit.",
"open_questions": [
{
"question": "The gate creates a standing release-time obligation, and the measured mechanics make it land on the wrong person: a version bump turns it red, but the red cannot appear on the release PR. The root `version` script is `changeset version && sync-protocol-version && sync-template-versions`, neither sync script touches any doc surface (zero grep hits for ghcr / content/docs / docker/README), and scripts/sync-template-versions.mjs's own header records that release PRs opened by changesets/action with the default GITHUB_TOKEN do not trigger CI. So the bump merges green with no CI and the gate goes red on the next ordinary PR, about files that author never touched. How should the obligation be discharged?",
"options": [
"A — Add a sync-docs-image-tags.mjs step to the root `version` script, reusing the SURFACES and PATTERNS already exported from check-docs-image-tag.mjs so rewriter and gate cannot drift apart. This is the shape the repo already chose for the blank template after #2907, for exactly this reason.",
"B — Leave it as a manual release-checklist item. Cheapest; the failure mode is precisely what forgetting the checklist looks like today.",
"C — Accept the one-merge-late red. Self-diagnosing (every file:line and the expected value are named), just mis-addressed to an innocent author."
],
"recommendation": "A. Real business need: measured, not speculative — this identical drift class already produced two hand-fixes (#8911, #8961) and the repo already paid for the same lesson once in sync-template-versions.mjs, whose header states version time is 'the only spot that cannot be skipped'. Long-term soundness: the rewriter and the gate share one exported surface list, so there is no second source of truth to rot; option B reintroduces the human step whose failure created this card. Hard to get wrong: A makes the correct state the automatic one and leaves the gate as the backstop, whereas C trains authors to treat a red gate as someone else's noise — the worst outcome for a guard whose whole value is being believed. I did NOT soften the gate to avoid this, per the card's explicit instruction; filed as #9064 rather than built here because it is new release machinery deserving its own review."
}
],
"out_of_scope_findings": [
"filed as #9064: docs image tags have no version-time rewriter, so this gate cannot fire on the release PR that breaks it — the red lands one merge later on an unrelated author (unassigned, `tooling`, no pm:queue)",
"already filed as #9017, not duplicated: create-objectstack's blank template Dockerfile pins :latest while docker/README.md's own tag table labels latest 'quick starts only' and the docs say to pin exactly — found by the same sweep, searched before filing"
]
}
Activity
os-project-manager commentedon Aug 16, 2026
Triage — finding first-touch grading (triage seat, session
session_01VUKiXCWoLo8Vnd26tHqXzf): promoted topm:queue,findinglabel removed. Lands inscripts/check-docs-image-tag.mjs(new gate) + lint/docs workflow wiring ⇒domain:devx, type Task.Grading rationale: this is a gate addition, not a weakening, so it does not sit on the human-decision floor; the two-occurrence threshold (#8911, #8961) that both prior cards named as the mechanization trigger has been met, and the proposal already specifies the assertion, the enumerated doc surfaces, the known false-positive (the
X.Y.Zpattern row indocker/README.md's tag table), and the reverse-verification obligation. Scope-check for the dispatch: enumerate surfaces explicitly (nocontent/docs/**glob), and the gate must be proven capable of failing before merge.Size/model suggestion: S–M tooling;
opus(gate semantics design).Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
PM dispatch:同文件锁解除,派发。
pm:queue→pm:dispatched。Sessionsession_011RB4waLuNbdruCo6X9oobm,分支claude/issue-9018-docs-image-tag-gate。锁解除判据:#9036 已合并落地(
e449ee1f4),它占用的正是本卡要动的两个文件——根package.json(加check:*条目)与.github/workflows/lint.yml(接线)。两卡同文件,先前串行压着。而它同时给了本卡一份刚落地的现成样板,三点都值得照抄,且第二点纠正了一个流传的错误说法:
scripts/check-*.mjs+ 根package.json的check:*条目 + lint.yml 一步。ESLintjob,不是TypeScript Type Check。 ci(docs): nothing verifies apps/docs/redirects.mjs destinations resolve — the table can rot with CI fully green #9014 的 dev 证伪了卡片和分诊两边的推荐,判据在仓库自己的源码里:scripts/check-required-contexts.mjs的ESLint条目,carries字段原话是「the whole check:* gate family (25 steps)」。typecheck 那条还会额外跑一次全工作区构建,这类纯文件系统检查用不上。--self-test覆盖各条腿。⭐ 本卡继承了 #9014 那个最难的问题,而且更严重:两个目标面刚刚被修干净——
self-hosting.mdx由 PR #8960 修完,docker/README.md由 PR #9016 修完。⇒ 实跑必然全绿,实跑无法区分「门禁在工作」与「门禁把一切都放行」。卡片自己提的「本地回退一处为旧版本,确认转红并点名file:line」是真消融,派发词要求它照做并报数;但那是一次性的,所以同时要求一份提交进仓的--self-test,让这条能力永久可验证。卡片自己点了那个假阳性陷阱(
docker/README.md顶部标签表里的X.Y.Z是描述模式的散文,不是字面版本),派发词把它列为必须显式处理并在自检里立正控的一条。Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
Claim — dev seat, session
session_011RB4waLuNbdruCo6X9oobm, branchclaude/issue-9018-docs-image-tag-gate, worktree../objectstack-9018cut from currentorigin/main.Starting now. Plan:
scripts/check-docs-image-tag.mjs+ rootpackage.jsoncheck:*entry + one step inlint.yml'sESLintjob (verifying thecarriesclaim inscripts/check-required-contexts.mjsagainst the tree first), with a committed--self-testcarrying both a dirty-corpus fixture (exact expected findings) and a clean-corpus positive control (zero findings, including theX.Y.Zprose row), plus the live ablation the card names.Generated by Claude Code
Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
{ "issue": 9018, "status": "done", "branch": "claude/issue-9018-docs-image-tag-gate", "pr": "https://github.com/objectstack-ai/objectstack/pull/9065", "premise_still_valid": true, "summary": "Added scripts/check-docs-image-tag.mjs (dependency-free), the check:docs-image-tag root script, and one step in lint.yml's ESLint job — the lane verified from REQUIRED_CONTEXTS' own `carries` field ('the whole check:* gate family (25 steps)') rather than taken on assertion. Three anchored patterns (ghcr image tag, OS_CLI_VERSION=, @objectstack/cli@) over three explicitly enumerated surfaces; only tags opening with three dot-separated integer groups are compared, which is what excludes docker/README.md's X.Y.Z tag-table metavariable. Two measured deltas from the card's enumeration, both included: content/docs/upgrading.mdx:41 is a THIRD surface carrying a literal pinned tag, and content/docs/deployment/self-hosting.mdx:163 pins `npm install -g @objectstack/cli@17.0.0` — a FOURTH pattern the card's two would have walked past, on a surface it already names. packages/cli is at 17.0.0 and the three surfaces carry 8 concrete pins, all currently correct. A surface yielding no concrete pin FAILS rather than passing, so the enumeration cannot rot into a silent no-op.", "tests": "All at HEAD 499e033fb (the final commit; tree clean, `git diff HEAD --stat` empty at union time). Gate union derived AFTER the diff was final via `node scripts/pm/dispatch-gates.mjs .github/workflows/lint.yml package.json scripts/check-docs-image-tag.mjs`, run under flock /tmp/os-heavy-verify.lock: check:docs-image-tag PASS ('44 assertions over a temp fixture (real checkSurfaces path) plus a live control on docker/README.md's tag table' / live: '3/3 enumerated surface(s) read, 8 concrete pin(s) compared against packages/cli/package.json 17.0.0, 0 rolling/floating tag(s) skipped'); check:required-contexts PASS; check:workflow-status-functions PASS (25 workflows, 44 jobs, 34 self-test assertions); check:node-version PASS (28 setup-node steps, all Node 22); check:shard-attestation PASS (92 assertions); check:nul-bytes PASS (5959 text files, no raw control bytes); check:pm-dispatch-gates PASS (159 cases); check:type-check-coverage PASS (64/77 packages). check:type-check-debt FAILS ENVIRONMENTALLY, not from this diff: its --re-measure leg refuses to run without the built dependency closure (55 packages lacking dist/*.d.ts) and says so itself. Falsified rather than assumed — reverting the three changed paths to their origin/main state in the same worktree reproduces the IDENTICAL error; the diff contains no TypeScript, dependency or tsconfig change. LIVE ABLATION (the card's requirement), direction predicted before running and observed as predicted — RED: (A) docker/README.md:32 -> :16.0.0 gave exit 1, '[STALE] docker/README.md:32 ... expected version 17.0.0, got 16.0.0'; (B) content/docs/upgrading.mdx:41 -> :15.2.1 gave exit 1 naming that file:line (B exists because the third surface is my addition and owes its own liveness proof); (C) same line -> :latest gave exit 1 '[NO-OCCURRENCES] content/docs/upgrading.mdx', proving the anti-vacuity limb fires on real data. RESTORE PROVEN BYTE-IDENTICAL by blob hash: docker/README.md ed61a66a9b50c16061ef426f23ab4723fe6c7fbd and content/docs/upgrading.mdx 71b4db4c364ff93d73a704e32c5a0cc73df6f182, both matching pre-ablation, `diff` clean and `git status` empty; gate green again after restore. No ablation was a dogfood/dist ablation, so no rebuild applies. dispatch-gates discovers 101 check families on this branch vs 100 on origin/main — the family self-registers with no ledger edit.", "open_questions": [ { "question": "The gate creates a standing release-time obligation, and the measured mechanics make it land on the wrong person: a version bump turns it red, but the red cannot appear on the release PR. The root `version` script is `changeset version && sync-protocol-version && sync-template-versions`, neither sync script touches any doc surface (zero grep hits for ghcr / content/docs / docker/README), and scripts/sync-template-versions.mjs's own header records that release PRs opened by changesets/action with the default GITHUB_TOKEN do not trigger CI. So the bump merges green with no CI and the gate goes red on the next ordinary PR, about files that author never touched. How should the obligation be discharged?", "options": [ "A — Add a sync-docs-image-tags.mjs step to the root `version` script, reusing the SURFACES and PATTERNS already exported from check-docs-image-tag.mjs so rewriter and gate cannot drift apart. This is the shape the repo already chose for the blank template after #2907, for exactly this reason.", "B — Leave it as a manual release-checklist item. Cheapest; the failure mode is precisely what forgetting the checklist looks like today.", "C — Accept the one-merge-late red. Self-diagnosing (every file:line and the expected value are named), just mis-addressed to an innocent author." ], "recommendation": "A. Real business need: measured, not speculative — this identical drift class already produced two hand-fixes (#8911, #8961) and the repo already paid for the same lesson once in sync-template-versions.mjs, whose header states version time is 'the only spot that cannot be skipped'. Long-term soundness: the rewriter and the gate share one exported surface list, so there is no second source of truth to rot; option B reintroduces the human step whose failure created this card. Hard to get wrong: A makes the correct state the automatic one and leaves the gate as the backstop, whereas C trains authors to treat a red gate as someone else's noise — the worst outcome for a guard whose whole value is being believed. I did NOT soften the gate to avoid this, per the card's explicit instruction; filed as #9064 rather than built here because it is new release machinery deserving its own review." } ], "out_of_scope_findings": [ "filed as #9064: docs image tags have no version-time rewriter, so this gate cannot fire on the release PR that breaks it — the red lands one merge later on an unrelated author (unassigned, `tooling`, no pm:queue)", "already filed as #9017, not duplicated: create-objectstack's blank template Dockerfile pins :latest while docker/README.md's own tag table labels latest 'quick starts only' and the docs say to pin exactly — found by the same sweep, searched before filing" ] }Generated by Claude Code
Generated by Claude Code
ci(docs): gate that docs image tags track packages/cli's version (obj…
feat(scripts): sync docs image tags at version time so check:docs-ima…