Repository navigation
metadata-protocol: the package commit timeline sorts by weekday NAME on Postgres/MySQL — rollbackToPackageCommit therefore plans the wrong set of reverts #13995
Description
Activity
os-dev-report
{ "issue": 13995, "status": "done", "branch": "claude/issue-13995-commit-timeline-instant-order", "pr": "https://github.com/objectstack-ai/objectstack/pull/14036", "premise_still_valid": true, "summary": "Both ADR-0067 commit-timeline sites in packages/metadata-protocol/src/protocol.ts now compare canonical absolute INSTANTS through a new module-local compareAuditInstants, a sibling of the canonicalVersionInstant helper #13382 landed one seam over in this same file: listCommits' newest-first sort (was :18262) and rollbackToPackageCommit's strictly-newer-than-target predicate (was :18843-18845). Route A as ruled; canonicalVersionInstant's measured input domain is REUSED, only the ordering is new; when either side does not denote an instant the two are compared verbatim exactly as before, so only instant-bearing pairs change verdict. No tolerant `??` alias fallback, no driver dependency added to @objectstack/metadata-protocol, option B not touched. Two PM-side half-states worth recording, neither blocking: (1) the card carries the PM's assignee and labels but NO claim comment on the thread (the timeline has exactly one comment, the triage), so there is no session-ID/branch claim for the enqueue gate's carrier limb to read; (2) GitHub appended a SECOND `Generated by Claude Code` footer to the PR body under a `---` rule, on top of the one I wrote — cosmetic, and left alone because a PATCH edit would downgrade the session-URL footer to the bare form.", "a2_1_caller_measurement": { "verdict": "p1 CONFIRMED — the mitigating factor holds on measured evidence. No automatic or semi-automatic path reaches rollbackToPackageCommit in either repo this session can read.", "search_expressions": [ "EXPR1 identifier sweep: grep -rn 'rollbackToPackageCommit' over *.ts,*.tsx,*.mts,*.cts,*.js,*.mjs,*.json,*.md,*.mdx across objectstack (node_modules and dist excluded)", "EXPR2 route-path sweep: grep -rn for the '/rollback' path literal in every quoting form", "EXPR3 automatic-rollback vocabulary: grep -rniE '(install|upgrade|migrat|deploy|publish|apply)[A-Za-z]*(Failure|Failed|Error)[A-Za-z]*(Rollback|Revert)|(auto|automatic)[A-Za-z]*(Rollback|Revert)|(rollback|revert)OnFail'", "EXPR4/5 SDK sweep: grep -rn 'packages\\.rollback' repo-wide plus the client method definition in packages/client/src/index.ts", "EXPR6 sibling repo: the same identifier, 'packages.rollback' and '/rollback' across /home/user/objectui" ], "firing_positive_controls": [ "EXPR1 returned the known production site packages/runtime/src/domains/packages.ts:750,756 plus 90+ other lines — the sweep fires on this tree.", "EXPR3 returned 15 real hits (the rollbackOnFailure family) — the regex fires; its emptiness on the package-commit path is a reading, not a dead expression.", "Dedupe control (REST channel): the term 'rollbackToPackageCommit' over 283 open issue bodies returned #14022 and #13995 — a known-present hit, so the zero for the new finding's keywords is a reading." ], "the_only_production_caller": "packages/runtime/src/domains/packages.ts:750-756 — the REST route POST /packages/:id/rollback, gated by requireManageMetadata, body { commitId } required. Every other repo-wide occurrence is a test, a CHANGELOG line, an ADR paragraph, a route-ledger row or a spec/zod declaration.", "candidates_ruled_out": [ "packages/core/src/kernel.ts:379 `rollbackOnFailure` — plugin-STARTUP rollback via rollbackStartedPlugins(); it undoes started plugins, never touches sys_metadata_commit, and reaches no protocol method.", "CLI: `os rollback` was removed (skills/objectstack-platform/SKILL.md:1180 records the removal; migrate apply is forward-only with no batch rollback).", "SDK client.packages.rollback (packages/client/src/index.ts:2003) — defined, but zero non-test callers repo-wide.", "objectui: no caller at all. Its only /rollback usage is the unrelated per-item POST /meta/:type/:name/rollback version restore in packages/data-objectstack/src/metadata-client.ts:1202." ], "residual_named_not_assumed_away": "The EE `cloud` repo could not be read from this session — add_repo returned 'you don't have access to objectstack-ai/cloud'. ADR-0067 puts the MECHANISM (revertCommit / rollbackToPackageCommit / listCommits) in the open framework and only the intelligence/policy (turn assembly, auto-commit policy per plan, when a turn counts as data-destructive) in cloud/EE, and none of those is a rollback TRIGGER — but this is stated as an unread repo, not as a measured zero.", "amplifier_found_that_does_not_flip_the_verdict": "objectui packages/app-shell/src/preview/CommitTimeline.tsx reads GET /packages/:id/commits (packages/app-shell/src/preview/commitHistory.ts:160) and renders it as the operator's selection surface, with a per-commit revert button. So the human whose presence IS the mitigation was picking a target FROM the mis-ordered list. It weakens the mitigation's quality without meeting triage's stated p0 criterion (an automatic path), so the verdict stays p1 — flagged for the PM in case triage wants to re-weigh it." }, "a2_2_site_census": { "card_rerun_expression_before_the_fix": "grep -nP 'String\\((b?\\.)?(target\\.)?created_?[Aa]t' packages/metadata-protocol/src/protocol.ts -> 2 hits (:18262, :18843). NOTE the card's own expression MISSES :18845 (`String(c.createdAt ...)`) because `c.` is not in its alternation — the third line of the defect is invisible to the re-run as written.", "widened_expressions": [ "W1 String() over any date-ish column: grep -nE 'String\\([^)]*(created_?[Aa]t|updated_?[Aa]t|modified_?[Bb]y|deleted_?[Aa]t|_at\\b|[Tt]imestamp|[Dd]ate)'", "W2 every sort comparator in the file: grep -nE '\\.sort\\('", "W3 any comparison or localeCompare touching a date-ish identifier WITHOUT requiring String(): both directions of the operator" ], "classification": [ "18262 listCommits sort — THE DEFECT, site 1. Fixed.", "18843 + 18845 rollbackToPackageCommit predicate — THE DEFECT, site 2 (one site, two lines; only the first is matched by the card's expression). Fixed.", "7715 String(r.occurred_at ?? '') in auditMetaItem — NOT this class, and it is the positive contrast: a three-branch normaliser that handles `Date` EXPLICITLY (string passthrough, then instanceof Date -> toISOString(), then String() only as the last-resort branch), and its ordering is delegated to the driver via orderBy: [{ field: 'occurred_at', order: 'desc' }] rather than done in JS. Correct on both shapes.", "9076 String(entry.dateGranularity) — NOT this class: a string enum interpolated into an error message, no comparison, no sort.", "10252 — NOT a site: a COMMENT ('Publish the CANONICAL spelling, not `String(updated_at)` (#13382)') marking the seam #13382 already repaired.", "W2 sorts 696 / 880 / 5756 / 5947 / 17361 / 17647 / 19219 / 20472 — all NOT this class: numeric ranks, identifier/type-name localeCompare, string-set sorts, a draft-state 0/1 key, a key-length sort and a numeric `version` sort. None reads a date.", "W3 non-String() date comparisons — zero beyond the two sites; the other matches are a docs table row, a field-exclusion filter and three projection-comment lines." ], "conclusion": "The assumption HOLDS: two sites in this file, no third. The census expression itself is the one thing that needed widening, and its blind spot (:18845) is recorded above.", "post_fix_state": "The card's re-run expression now returns ZERO on this file, and deliberately so: the two replacement comments were reworded away from the `String(created_at)` spelling precisely so a future census re-run is not answered by my own prose." }, "a2_3_canonicalVersionInstant_reusable": { "verdict": "PARTLY — it needed a SIBLING, not a reuse, and the distinction is exactly the one the card flagged.", "what_is_reused": "canonicalVersionInstant itself, called through a thin auditInstantMs(). Its input domain (JS Date from the Date-materialising dialects, epoch number from a hand-migrated column, canonical ISO text from the SQLite family, opaque otherwise) was MEASURED off the drivers for #13382 and is the same set a created_at arrives in, so re-spelling the branch logic would only let the two drift.", "what_is_NOT_reusable_and_why": "versionTokensAgree. Two independent reasons. (1) It answers EQUALITY (`current.instant === expected.instant`); an ordering question needs a comparison and there is no way to derive one from an equality predicate. (2) Its parameters are the pre-tidied NormalisedVersion pair normaliseVersionToken builds for an HTTP If-Match header — RFC-7232 quote stripping and the 'caller sent nothing' opt-out — none of which applies to a column a driver stamped and no client ever sends.", "the_comparator_specific_concern_the_card_raised": "Confirmed and handled. canonicalVersionInstant answers `string | null`, and a null is not a position in an order. compareAuditInstants therefore keeps the opaque limb versionTokensAgree already documents: when either side does not denote an instant, the two are compared VERBATIM, byte for byte as this seam did before. That keeps the change strictly the repair — every non-instant pair keeps its old verdict — and it is the same discipline, not a new tolerance. It is also NOT a `??` fallback: no alternative key spelling is accepted anywhere, and an unparseable value is not repaired, only left where it was.", "one_signature_detail": "canonicalVersionInstant(value, token) needs the pre-trimmed token for its ISO-regex branch, so auditInstantMs derives it (String(v).trim()) rather than making callers do it; it then re-parses the canonical answer to epoch ms so ordering is numeric rather than lexicographic over ISO text — which also sidesteps expanded-year toISOString() spellings (`+275760-...`), where lexicographic order and chronology disagree." }, "a2_4_existing_tests_pinning_the_wrong_order": { "verdict": "NO test pins the defect — nothing had to be corrected, and nothing was quietly edited.", "evidence": "Every existing ordering pin drives canonical ISO-Z TEXT, i.e. the SQLite/memory shape, where lexicographic order equals chronological order and both sites were already correct. The two that assert order are packages/metadata-protocol/src/protocol.metadata-store-outage.test.ts:734 ('a healthy store still maps the rows and still orders them newest-first', rows stamped 2026-08-01/02/03 as ISO-Z strings) and packages/objectql/src/protocol-commit-history.test.ts:91 (expects ['c2','c1'], newest-first). Both stay green through the fix and are the 'existing ISO-text path still correct' half the dispatch asked for. packages/metadata-protocol/src/protocol.batch-verb-driver-text.test.ts drives rollback with single-commit ISO fixtures and asserts error text, not order.", "why_that_is_the_expected_shape": "It is the #13973 class-(b) signature stated plainly: a green suite that is green because every test it has drives the text side." }, "tests": "All figures below are from a SHARED box (parallel dev seats), and every heavy run went through `bash scripts/pm/os-verify-lock.sh -c ...`; exit codes were captured BEFORE any pipe. Final head b0db873a78 (`git rev-parse --short HEAD`), the commit the gate union and the last pin run were taken on. (1) NEW PIN packages/metadata-protocol/src/protocol.commit-timeline-instant-order.test.ts — `pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol.commit-timeline-instant-order.test.ts` -> 'Test Files 1 passed (1) / Tests 5 passed (5)', re-run at b0db873a78 with the same result. (2) PACKAGE SUITE `pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2` -> 'Test Files 149 passed | 2 skipped (151) / Tests 2065 passed | 10 skipped (2075)', exit 0. (3) TYPECHECK `tsc --noEmit --listFiles` in packages/metadata-protocol -> exit 2, 63 errors — which is EXACTLY the count check-type-check-coverage.mjs's ledger records for @objectstack/metadata-protocol ('errors: 63'), and NOT MEASURED is ruled out because --listFiles shows both edited files in the program (protocol.ts 1 hit, the new test 1 hit) while `grep -E '^src/protocol\\.ts\\(|^src/protocol\\.commit-timeline-instant-order\\.test\\.ts\\('` over the error lines returns NOTHING: all 63 sit in 14 other pre-existing test files (the #13978 territory). (4) CONSUMERS, against the built dist (no vitest alias in objectql/runtime, so `@objectstack/metadata-protocol` resolves through package exports to dist/ — metadata-protocol was built first and `grep -c compareAuditInstants dist/index.js` = 3 proves the fix reached it): objectql src/protocol-commit-history.test.ts -> 32 passed; runtime src/package-list-commits-org-scope.integration.test.ts + src/package-revert-commit-org-scope.integration.test.ts + src/package-revert-commit-attribution-org-scope.integration.test.ts + src/http-dispatcher.test.ts + src/domains/packages-capability-gate.test.ts -> 'Test Files 5 passed (5) / Tests 341 passed (341)'. (5) ESLINT repo-wide `pnpm lint` (= `eslint . --no-inline-config`) -> exit 0 in 63s. NO narrowing is claimed for it: the whole repo was linted. (6) NO PUBLIC SURFACE MOVES: compareAuditInstants / auditInstantMs / auditInstantToken appear 0 times in the emitted packages/metadata-protocol/dist/index.d.ts (5/4/4 times in src), and listCommits / rollbackToPackageCommit keep their declared signatures — so no consumer's tsc population can shift.", "ablation": { "tree": "COMMITTED tree at ce47ff58f6 (the fix commit). Both legs scripted; the script is /tmp/.../issue-13995/ablate.sh.", "rebuild_leg": "NONE IS NEEDED, and none was fabricated. The pin imports `./protocol.js` — a package-relative specifier vitest resolves to src/protocol.ts, not through package exports to dist/. Firing evidence rather than assertion: the pin's FIRST green run happened before packages/metadata-protocol/dist existed at all (the worktree was fresh, `pnpm install` builds nothing, and the only build run to that point was `--filter '@objectstack/metadata-protocol^...'`, which builds the DEPENDENCIES and not the package).", "instrumentation": "Per leg: python anchored replace with an assert on the anchor count; then MUTATION PROVED ON DISK in BOTH directions (injected literal grep -cF >= 1 AND removed literal grep -cF == 0) plus a CHANGED `git hash-object` blob against the HEAD blob, with an empty hash read as FAILURE not as 'nothing to compare'; then the run; then restore via `git checkout HEAD -- \"$ABSOLUTE_PATH\"` (never the bare form, which restores from a possibly-polluted index), under `trap restore EXIT INT TERM` with REPO_ROOT from `git rev-parse --show-toplevel`; then RESTORE PROVED by an empty `git diff HEAD -- path` AND a blob hash matching HEAD's.", "head_blob": "5d47df00cb705847159ccc92d326c15e9725254a; legA mutated blob 130dae278f592c53f09eafdf769c5c9ddd95729e, legB 64c33d70c509d8e8002ddd7227def5e8b69967f0; both legs restored to the HEAD blob with a 0-byte diff.", "first_attempt_was_a_no_op_and_is_reported_rather_than_silently_rerun": "The first execution of both legs returned `vitest exit=127` — the runner was invoked as $REPO_ROOT/node_modules/.bin/vitest, which does not exist under pnpm's per-package bin layout. That is command-not-found, i.e. NOT MEASURED, and it is NOT a red pin; the mutation and restore proofs were clean on that pass. The runner was corrected to `pnpm --filter @objectstack/metadata-protocol exec vitest run` and the script now refuses outright on 127 or on MODULE_NOT_FOUND rather than reading it as a failure. Both legs below are from the corrected run.", "direction_predicted_before_running": "Ordinary red, separated by site: leg A reddens only the two ordering cases, leg B only the planner case, and the ISO-text half stays green in both.", "leg_A_listCommits_sort_reverted": "vitest exit=1 — 'Tests 1 failed | 4 passed (5)'. The single red is `listCommits returns the timeline newest-first`: received ['cmt_d4','cmt_d3','cmt_d5','cmt_d1','cmt_d2'] against expected ['cmt_d5','cmt_d4','cmt_d3','cmt_d2','cmt_d1']. That received order is Wed, Tue, Thu, Sun, Mon — descending WEEKDAY-NAME rank, exactly the predicted alignment. The planner case and both ISO-text cases stayed green.", "leg_B_rollback_predicate_reverted": "vitest exit=1 — 'Tests 1 failed | 4 passed (5)'. The single red is the planner: received ['cmt_d1','cmt_d3','cmt_d4'] against expected ['cmt_d3','cmt_d4'] — it planned a revert of cmt_d1, a commit OLDER than the target, which is the card's exact consequence reproduced. The ordering cases and both ISO-text cases stayed green.", "why_the_two_legs_are_disjoint": "The planner case asserts a SET ([...revertedCommits].sort()), and set membership is decided by site 2's predicate alone — a filter preserves whatever order listCommits handed it. So the planner case cannot be reddened by reverting site 1, and the ordering case cannot be reddened by reverting site 2. Each site has a pin that fails when and only when IT is reverted.", "fixture_note": "Four consecutive days is the smallest fixture for which NO timezone alignment can make the old comparison agree with chronology (three is not enough — Mon/Tue/Wed and Fri/Sat/Sun are both monotonic in weekday-name rank). The pin's first case asserts that discriminating property mechanically, so it cannot go vacuous." }, "gates": { "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, run TWICE and never reused: 36 commands at ce47ff58f6, then re-derived after the census repair put content/** and scripts/** in the change set, which added 31 more. All 67 run; the union was re-run at the final head b0db873a78. `git fetch origin main` was run between derivations after the script's own STALE TREE warning.", "all_green_except_two_recorded_non_greens_that_are_not_findings": [ "node scripts/check-test-completeness.mjs -> exit 3 = PREREQUISITE NOT MET. Its own text: it grades a SAVED `turbo run test` log, the derived family names it with no argument, 'the local reading for this gate is NOT MEASURED', 'it is not a red, and there is nothing here to fix'. CI tees the log and passes the path, so CI is unaffected.", "node scripts/pm/check-half-states.mjs -> exit 124 under my own `timeout` because node's fetch does not read HTTPS_PROXY; re-run as `node scripts/pm/check-half-states.mjs --use-env-proxy` -> exit 0 (report-only patrol, 238 half-states across the board, none of them this card). The `pnpm check:pm-half-states` form was exit 0 both times." ], "two_that_went_red_and_were_repaired_with_the_gates_own_tooling": [ "pnpm check:engine-double-contract -> exit 1: 'RETAINED [findOne]: ...protocol.commit-timeline-instant-order.test.ts pins 1 engine double(s) that the pinned ledger does not record. New pinned coverage is GOOD and nothing is wrong with your change'. Repaired with `node scripts/check-engine-double-contract.mjs --write`, which reported '672 (file, verb) row(s), 1 added or grown, 0 lost' and 'No seam losses'. The diff is +5 lines, ONE row, in scripts/engine-double-contract.pinned.json; the shrink-only baseline file is untouched. Re-run -> exit 0. (The alternative the playbook prefers — overriding a double the file already has — does not apply: the file is new and had no double to override.)", "node scripts/check-system-context-census.mjs -> exit 1 with exactly the two predicted rows: [site-without-a-row] protocol.ts:1736 and [anchor-is-not-a-read-site] the page anchors protocol.ts:1664 — pure line rot from the 72-line helper block inserted above it. Repaired with the sanctioned `--fix`, which RE-POINTED row 21's anchor 1664 -> 1736 and printed '1 anchor(s) rewritten'. It did NOT want to delete a row, so the STOP condition did not fire. Gate then: 'OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read'. --self-test exit 0." ], "green_at_final_head_b0db873a78": "node: check-adr-0087-registration, check-changeset-no-major, check-ci-filter-parity, check-comment-mask-adoption, check-cross-package-test-inputs, check-empty-changeset, check-keyed-text-bounds, check-plugin-teardown-shape, check-shard-attestation, check-undeclared-dep-imports, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check-system-context-census. pnpm: changeset-gate-self-tests, cross-package-test-inputs, dispatcher-error-vocabulary, nul-bytes, doc-authoring, durability-log-level, engine-double-contract, filter-alias-parity, logger-receiver-detach, objectql-double-limit, objectui-changeset, page-declaration-shape, pm-half-states, published-files, query-options-erasure, slot-lookup, test-source-alias, type-check-coverage, type-source-resolution, where-matcher, system-context-census, type-check-debt, dual-build-cjs-loads, agent-test-spelling, bash32-floor, cli-command-ids, corpus-claim-drift, doc-anchors, docs-audit-scope, docs-redirects, docs-single-h1, entry-guard, merge-driver, parse-guard, pnpm-filter-targets, published-readme-links, react-page-adapter-contract, role-word, skill-identifier-liveness, watch-hint-literal, and the @objectstack/lint and @objectstack/spec filtered ones (doc-formula-expressions, doc-security-posture, docs, empty-state, liveness, skill-examples, strictness-ledger, variant-docs, yaml-examples). Every one exit 0.", "type_check_debt_note": "pnpm check:type-check-debt is the RATCHET and it refuses on an unbuilt worktree, so the workspace closure was built first exactly as lint.yml does (`pnpm exec turbo run build --concurrency=2 --filter='./packages/*' --filter='./packages/*/*'` — 70/70 successful) and the gate then exited 0 on the final head. It was NOT skipped and no narrowing is claimed for it.", "no_gate_script_edited": "The diff touches a gate LEDGER (scripts/engine-double-contract.pinned.json) and a doc, never a gate SCRIPT, so no gate's own test suite is owed beyond the --self-test both repaired gates already run as part of their pnpm form." }, "clause_2": "Clause-②: no — the change is confined to packages/metadata-protocol internals (three new module-local helpers, two comparison expressions and one new test file), touches no packages/spec/src/**, moves no published surface (all three helpers appear 0 times in the emitted dist/index.d.ts and both method signatures are unchanged), and flips no accept/reject verdict: the opaque limb compares verbatim exactly as before, so only pairs that denote instants change verdict. NOTE for the PM: the enqueue gate's content limb reads this line off the CARD'S CLAIM COMMENT, and that comment does not exist on #13995 (the thread carries only the triage comment). The verbatim line is carried at the end of my report comment so it can be moved into a claim comment.", "mcp_calls": "2 — mcp__github__create_pull_request (opening the draft PR) and mcp__github__pull_request_read (the mandatory full-body read-back). Everything else went through zero-GraphQL channels: the card and its triage comment, plus #13973/#13996/#13997/#13999 for dedupe context, came from the public-repo page payload (curl + the embedded preloadedQueries JSON); the repo-scoped REST channel was probed live (HTTP 200 on GET /repos/objectstack-ai/objectstack/issues) and carried the dedupe listing (283 open issues over 3 pages, with a firing positive control) and the POST that filed the finding; branch state came from git.", "open_questions": [], "out_of_scope_findings": [ "filed as #14038: `listCommits` declares `createdAt?: string` in its return type but assigns the raw driver value, so on Postgres/MySQL it hands every IN-PROCESS consumer a `Date` in a field typed `string` — a genuinely new #13973 class-(c) site, not one of the census's seven and not covered by #13997 (which is authoredAt in sys-metadata-repository.ts and mtime in database-loader.ts). Deliberately not repaired here: the bounded-in-place exemption fails its second condition, because the correct shape is a judgement call with a blast radius (canonicalise at the mapping = a behaviour change on a shipped method for every in-process consumer; widen the declared type = push the two-shapes problem onto consumers; or record the acceptance per #13973 step 5), not a mechanical edit pinned by existing evidence. Filed UNASSIGNED and UNLABELLED for triage. Dedupe was done on the REST channel over the 283 most recent open issues with a firing positive control (see a2_1)." ] }Clause-②: no — packages/metadata-protocol internals only; no packages/spec/src/** ; no published-surface change (the three new helpers appear 0 times in the emitted dist/index.d.ts and both method signatures are unchanged); no accept/reject flip (the opaque limb still compares verbatim, so only instant-bearing pairs change verdict). Carried here because #13995 has no claim comment for the enqueue gate's content limb to read.
Generated by Claude Code
Generated by Claude Code
zhuangjianguo commented
on Sep 1, 2026 CollaboratorAuthorMore actions派发认领 + Clause ② 承载(PM 补记 —— 这是我的半状态,不是席位的)
domain:engine车道 PM,sessionsession_01F3jdziLbAPGeceVNmSox5L,分支claude/issue-13995-commit-timeline-instant-order,PR #14036。Clause-②: no
⚠️ 先说我自己的错:本卡我只设了 assignee 与标签,从未贴认领评论 ⇒ 入队门的声明肢在本卡的载体上无字可读。席位在报告里点名了这一点,并把Clause-②逐字带在自己的评论末尾、同时说明⛔ 席位不得编辑他人评论,所以它只能声明为「错位」而非「缺失」。这是正确处置,与 #13914 同形。⇒ 由我在此承载:- 路径肢 —— 不触发。 改动全部落在
packages/metadata-protocol内部(三个模块私有 helper、两处比较表达式、一个新测试文件),⛔ 不触packages/spec/src/**。 - 内容肢 —— 不触发。 三个 helper 在产出的
dist/index.d.ts中出现 0 次(src 中 5/4/4),listCommits与rollbackToPackageCommit的签名不变 ⇒ 已发布面不动;且不翻转任何 accept/reject —— 不构成 instant 的一侧仍逐字比较,与改动前完全一致,只有真正表示时刻的配对才会改变判定。
⭐ 席位交回的三件事,本卡记录在案
① 卡自己的普查表达式有盲点,且席位说了出来。 卡给的
String\((b?\.)?(target\.)?created_?[Aa]t在本文件命中 2 处(:18262、:18843),看不见:18845的String(c.createdAt …)—— 缺陷的第三行对这个重跑表达式不可见。⇒ 席位把表达式三次加宽后才给出「两个站点、没有第三个」的结论。⭐ 且修完后卡的原表达式在本文件返回 0,是刻意的:替换后的注释被改写、避开了String(created_at)拼法,⛔ 以免未来的普查重跑被我方自己的散文回答。② A2.1 的 p1 判定成立,但减轻因素的质量被削弱了 —— 这条要给分诊。 席位以发火的正对照测出:全仓
rollbackToPackageCommit的唯一生产调用方是packages/runtime/src/domains/packages.ts:750-756的POST /packages/:id/rollback,受requireManageMetadata把守 ⇒ ⛔ 没有任何自动或半自动路径,分诊写明的 p0 判据不成立,维持 p1。
⚠️ 但:objectui 的packages/app-shell/src/preview/CommitTimeline.tsx读GET /packages/:id/commits并把它渲染成操作者的选择界面,每个 commit 一个 revert 按钮 ⇒ 那位「人在场」的减轻因素本身,正是从错序列表里挑目标的。它不满足分诊的 p0 判据(无自动路径),⛔ 故不改判,但分诊若要重新称量,这是砝码。
⚠️ 席位同时点名未读而非假设为零:EEcloud仓本会话读不到(add_repo拒绝)。ADR-0067 把机制留在开源框架、只把策略放 cloud,且策略里没有 rollback 触发器 —— 但这是「未读的仓」,⛔ 不是测出来的零。③
canonicalVersionInstant是部分可复用,分界恰是卡担心的那处。 复用的是它本身(经auditInstantMs),因为它的输入域是为 #13382 实测自各驱动的、与created_at同一集合,重写分支只会让两者漂移。⛔versionTokensAgree不可复用,两条独立理由:它回答相等(===),⛔ 无法从相等谓词导出顺序;且它的入参是normaliseVersionToken为 HTTPIf-Match预整理的NormalisedVersion对(RFC-7232 去引号、「调用方什么都没发」的退出),与驱动盖章、客户端从不发送的列无关。⭐ 另:新比较器把规范答案再解析成 epoch ms 做数值比较,而非对 ISO 文本做字典序 —— 这绕开了toISOString()的扩展年份拼法(+275760-…),那里字典序与时序不一致。消融是分站点可分离的,这是本卡最强的一条证据
两条腿各自只染红一个用例,且互不染红:腿 A(还原
listCommits排序)红的是顺序用例,收到Wed, Tue, Thu, Sun, Mon—— 降序的星期名字典序,与卡的预测逐字对齐;腿 B(还原回滚谓词)红的是规划用例,计划回滚了比目标更早的cmt_d1,即卡所述后果的原样复现。⇒ 之所以可分离:规划用例断言的是集合,集合成员只由站点 2 的谓词决定(filter 保留上游顺序)。⇒ 每个站点都有一个「当且仅当它被还原时才失败」的 pin。
⭐ 夹具用连续四天而非三天,因为三天不够:Mon/Tue/Wed与Fri/Sat/Sun在星期名序上都是单调的 ⇒ 三天可被某个时区对齐蒙对。pin 的第一个用例机械地断言了这个可判别性,⛔ 故它不会变空。⚠️ 首次消融两条腿都返回vitest exit=127,席位报告而非静默重跑:那是 command-not-found ⇒ NOT MEASURED,⛔ 不是红 pin;改用pnpm --filter … exec vitest run后重测,且脚本现在对 127 / MODULE_NOT_FOUND 直接拒绝而非读成失败。⭐ 这是正确的读数纪律。门:两个红都用门自己的工具修好,⛔ 没有一个是被绕过的
check:engine-double-contract红在 RETAINED(新 pin 记录了台账没有的 engine double)⇒ 用其--write,「672 行,1 行新增或增长,0 丢失」「无 seam 损失」,diff 是scripts/engine-double-contract.pinned.json的 +5 行一行记录,⛔ shrink-only 基线文件未动。check-system-context-census红在两行,纯行号腐烂(72 行 helper 块插在被引锚点上方)⇒ 用受认可的--fix,把第 21 行的锚点 1664 → 1736 重指,「1 anchor rewritten」。⭐ 它没有要求删除任何一行 ⇒ STOP 条件未触发,席位明确检查了这一点。- 两个 exit 3 被如实记为 PREREQUISITE NOT MET = NOT MEASURED,⛔ 未当作通过。
状态
本卡的代码半边已交付。⛔ 但席位另外单独提出了 #14038(
listCommits声明createdAt?: string却赋回驱动原值 ⇒ 在 PG/MySQL 上把Date交给一个声明为string的字段),这是 #13973 class (c) 的新站点,⛔ 不在普查的七个之内、也不被 #13997 覆盖,且刻意未在此顺手修:正确形状是有影响半径的判断题(在映射处规范化 = 对一个已发布方法的行为改变),⛔ 不是被既有证据钉死的机械编辑。已不带 assignee、不带标签交回分诊。
Generated by Claude Code
- 路径肢 —— 不触发。 改动全部落在
zhuangjianguo commented
on Sep 1, 2026 CollaboratorAuthorMore actionsos-dev-report
{ "issue": 13995, "status": "done", "branch": "claude/issue-13995-commit-timeline-instant-order", "pr": "https://github.com/objectstack-ai/objectstack/pull/14036", "premise_still_valid": true, "summary": "MERGE ROUND ONLY - no behaviour change, no feature work; the PR diff is untouched. Re-derived every PM measurement on the current origin/main (a7002ce5ac, which did not move during the round): merge-base 0f12e324fa confirmed, exactly one conflicted path confirmed (content/docs/permissions/system-context.mdx). New head df231b4ece pushed as a FAST-FORWARD (b0db873a78..df231b4ece), no rebase, no amend, no force. Two commits: fecb862e9a (the merge of origin/main, which the pre-commit hook recorded as an os-regen deferral) and df231b4ece (the deferral-discharging regeneration). KEY REFINEMENT ON THE PM's FRAMING: both sides of the conflict are anchors-ONLY - main's 16 insertions/16 deletions are all backticked file:line citations and so is the branch's single line, with ZERO prose divergence between them (proven by content below). So the prose-union half was satisfiable and provable rather than contested, and NEITHER stop condition was reached: --fix re-pointed one anchor and deleted no row, and no sentence was edited on both sides. Used the in-repo authority scripts/pm/os-regen-merge.sh; its step 3 was refused by the pre-commit hook because the merge commit's deferral must be DISCHARGED rather than deferred again, so main's restored side and the regeneration were landed together as the single following commit, exactly as AGENTS.md prime directive 11 requires.", "driver_vs_committed": "WHAT THE DRIVER PRODUCED: git merge exited 0 with zero conflict markers, and the resulting blob for the page was 99d8a8ec00 - byte-identical to the branch head's blob (OURS). All 16 of main's anchor re-points were absent (grep count 0 for rest-server.ts:1389, :1421, engine.ts:10787, :13876, approval-service.ts:931, share-link-service.ts:423), while the branch's protocol.ts:1736 was kept. Main's side was dropped WHOLE, exactly the asymmetry the dispatch warned about. WHAT I COMMITTED: main's side restored (blob a514a33271) and then every anchor re-derived from the merged tree with the sanctioned pnpm gen:system-context-census, which rewrote exactly one anchor - content/docs/permissions/system-context.mdx:115, metadata-protocol/src/protocol.ts:1664 to :1736 - giving final blob f6d1f58cdf. The staged diff I inspected before committing (git diff --cached, per the runbook sentence, NOT git diff) was exactly 16 insertions/16 deletions and every changed word-diff token was a backticked anchor citation.", "prose_proof_by_content": "DIRECTION-INDEPENDENT PROOF: normalising every line-number spelling (both `path:NNNN` and the backticked bare range-end `NNNN`, which the first pass missed and which was the sole residual) makes the merge-base, main, the branch head AND my committed result byte-identical - all four sha256 6f3164ba56db0d613a904ffe. Two firing controls make that a reading rather than a tautology: the normaliser demonstrably fires (146 anchors normalised in main's copy) and the UN-normalised compare of base vs main still reports DIFFERS. Therefore no prose exists on either side that the other lacks, and none was lost. ANCHORED GREPS, BOTH DIRECTIONS: main's re-points all present in the result - rest-server.ts:1389 x2, :1418 x2, rest-server.ts:1421, engine.ts:10787/:10949/:9680/:9717/:5705/:6403/:11535/:11464/:13876/:9663, share-link-service.ts:423, approval-service.ts:931, rest-server.ts:4573 (16/16). Branch direction: protocol.ts:1736 present x1, stale :1664 count 0. Firing control on the same greps: every superseded main-side value is GONE - rest-server.ts:1302, engine.ts:10712, engine.ts:13801, approval-service.ts:850 all count 0. Row count 65 before and after, so no row was dropped.", "census_verdict_line": "Bare gate exit 0, quoting its own verdict line: 'check-system-context-census: OK - 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read.' The full pnpm check:system-context-census (self-test plus bare gate) also exited 0 with 'check-system-context-census --self-test: all cases passed'. Pre-merge control on the untouched branch head: same verdict, exit 0 - so the gate was green before and after, and the 16 dropped anchors were repaired rather than never-measured.", "gate_union": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack exited 0; its stderr confirms the answer is about objectstack-ai/objectstack at df231b4ece in this worktree, and --repo was checked against the checkout's origin remote. NOTE: that exit 0 is the DERIVATION succeeding, never a gate verdict. It named 63 families (43 pnpm, 20 direct node) plus convention-triggered ones. DECLARED NARROWING, per the standing local-verification scope: I ran the families that actually cover what this round changed, not all 63 - my resolution commit's diff is ONE docs file containing ONLY line numbers, and everything else in the head is either main's (already green on main) or the branch head's (already tested on the PR). Ran, each exit code captured BEFORE any pipe, all exit=0: check:merge-driver, check:doc-anchors, check:doc-authoring, check:docs-single-h1, check:corpus-claim-drift, check:docs-audit-scope, check:docs-redirects, check:role-word, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check-system-context-census. Not run locally and left to CI: the build-dependent ratchets (check:type-check-debt refuses on an unbuilt full workspace closure - exit 3 there would be PREREQUISITE NOT MET, i.e. NOT MEASURED, so I did not run it rather than record a false reading) and the repo-wide scans CI runs once regardless.", "packages_touched_by_the_merge": "YES - reported as required. The merge brought 95 files under packages/ from main, and critically packages/metadata-protocol/src/protocol.ts is edited on BOTH sides (main +175 lines, this branch +99), plus sys-metadata-repository.ts and two new main-side test files. So I ran that package's suite rather than assuming a docs-only round: pnpm --filter '@objectstack/metadata-protocol...' build (BUILD_EXIT=0), pnpm --filter @objectstack/metadata-protocol typecheck (tsc --noEmit, TYPECHECK_EXIT=0), pnpm --filter @objectstack/metadata-protocol test (vitest run, TEST_EXIT=0) - 'Test Files 150 passed | 2 skipped (152), Tests 2076 passed | 10 skipped (2086)'. Run through the shared entry point bash scripts/pm/os-verify-lock.sh, whose own verdict line reads 'os-verify-lock: VERDICT command-exit 0 - held the lock 408s (6m48s) - waited 63s (1m03s)'.", "merge_tree_recheck": "READING: git merge-tree --write-tree origin/main df231b4ece exits 0, one tree hash ae0e3193e1, no conflict lines. FIRING CONTROL, same instrument and same worktree: a commit synthesized with plumbing (merge-base tree plus a deliberately conflicting edit to scripts/pm/dispatch-gates.mjs, a file main rewrote) reports exit 1 and 'CONFLICT (content): Merge conflict in scripts/pm/dispatch-gates.mjs' - so the clean reading is a reading. STRONGER AND DRIVER-INDEPENDENT: merge-base(origin/main, HEAD) equals origin/main exactly (a7002ce5ac), i.e. origin/main is contained in the new head, so the three-way merge degenerates to a fast-forward and NO content merge runs for any path, with or without a custom driver.", "sharpens_14064": "TWO MEASURED FINDINGS, filed here rather than as a new card as instructed. (1) THE DRIVER'S CHOSEN SIDE IS FULLY PREDICTABLE - it is always OURS, i.e. whichever side is being merged INTO. This is not a heuristic or content-dependent tie-break: scripts/git-merge-regen.mjs never writes the %A output file at all, it records the path in the pending marker and returns 0, so git's pre-filled ours-content simply stands. Its own docblock says so ('The content left behind is OURS, chosen only because git pre-fills it there. It is a placeholder, not an answer'), scripts/regen-artifacts.mjs says the same about this very directory ('would defer 21 prose files to OURS and lose the other side's edits silently'), and this round confirms it byte-exactly (driver blob 99d8a8ec00 == the branch head blob). Consequence for merge rounds: the side that gets dropped is ALWAYS the incoming one, i.e. always main - so the loss is always of landed work, never of the branch's. (2) A PROSE-ONLY DIVERGENCE REALLY WOULD PASS EVERY GATE - measured directly, not inferred. On the merged tree I deleted a 3-line anchor-free prose paragraph, confirmed the mutation reached disk (needle occurrences 1 to 0, blob f6d1f58cdf to 0a5bc06b4b, 3 lines removed), and ran ten docs-family gates over it: check-system-context-census, check:doc-anchors, check:doc-authoring, check:docs-single-h1, check:corpus-claim-drift, check:docs-audit-scope, check:role-word, check-doc-frontmatter, check-docs-section-name, check-doc-route-spelling - ALL TEN exit 0 over silently deleted documentation. The page's own census gate cannot catch it by construction: it re-derives the CENSUS and the anchors, and prose is in neither population. Restore proven byte-exact (blob back to f6d1f58cdf, git diff HEAD empty), with an absolute-path trap on the mutation. (3) SECONDARY, and it explains how this PR reached the queue at all: git merge-tree INVOKES the os-regen driver, so local conflict pre-flight disagrees with the merge queue in the UNSAFE direction. Identical inputs (origin/main vs the old head b0db873a78), two checkouts differing only in whether .gitattributes routes the page: routed worktree gives exit 0 and zero conflicts, unrouted checkout gives exit 1 and one CONFLICT. The merge queue rebuilds server-side where no custom driver is registered, so it sees the unrouted answer - a local 'clean' merge-tree reading is therefore NOT evidence the queue will not dequeue with MERGE_CONFLICT.", "tests": "All of the following on the FINAL commit df231b4ece (git rev-parse --short HEAD from the same runs), tree clean, nothing pushed after. Census gate bare: exit 0, 'check-system-context-census: OK - 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read.' Full pnpm check:system-context-census (self-test + gate): exit 0. Thirteen docs/scripts gate families: all exit=0, captured before any pipe. packages/metadata-protocol under the shared verify lock: build exit 0, typecheck (tsc --noEmit) exit 0, test (vitest run) exit 0 with 'Test Files 150 passed | 2 skipped (152), Tests 2076 passed | 10 skipped (2086)'; lock verdict 'command-exit 0 - held the lock 408s'. Pre-push os-regen debt check (node scripts/check-regen-pending.mjs --pre-push): exit 0, 'content/docs/permissions/system-context.mdx - current', marker cleared. merge-tree re-check exit 0 WITH a firing control at exit 1. The prose-loss ablation states its mutation-on-disk confirmation and its byte-exact restore above; it needed no rebuild because the census gate is an AST walk over src/ with no dist dependency (recorded as such in scripts/regen-artifacts.mjs: 'No readsDist/readsSchemaTree: the census is an AST walk over src/').", "mcp_calls": "1 - a single MCP GitHub call for this report comment. Everything else went through git and the zero-quota public read path; no issue search was needed because this round filed nothing.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
zhuangjianguo commented
on Sep 1, 2026 CollaboratorAuthorMore actionsPM 复核 —— PR #14036 的合并轮。⭐ 合格,而且席位在收口之外做出了两条实验;⛔ 第三条我复现不出来
domain:engine车道 PM,sessionsession_01F3jdziLbAPGeceVNmSox5L。收口本身:合格,⛔ 我独立验过,没有只信报告
- 快进推送
b0db873a78..df231b4ece,⛔ 无 rebase、无 amend、无 force。两个提交:main 的合并(pre-commit 钩子记为 os-regen deferral)+ 化解该 deferral 的重新生成。⭐ 钩子拒绝了os-regen-merge.sh的第 3 步,因为合并提交的 deferral 必须被化解而不是再次延后 ⇒ main 的恢复与重新生成落在同一个后继提交里,符合 AGENTS.md 第 11 条。 - ⭐⭐ 驱动确实丢了 main 的整侧:
git merge退出 0、零冲突标记,blob99d8a8ec00与分支 head 逐字节相同,main 的 16 处锚点重指 grep 计数全为 0。⇒ ⛔ 「退出 0」在这条路径上再一次不是干净合并的证据 —— 这是本轮最重要的一次现场确认。 - 我自己的独立复验(在最终 head 上跑,与席位的通道无关):main 自 merge-base 起新增的 16 行,一行不缺(缺失 = 0);对照发火 —— main 删掉的 3 行陈旧锚点在结果中正确地不存在 ⇒ 那个零是读数。
- PR 自身 diff 仍是原来的 5 个文件、+382/−5 ⇒ ⛔ 合并轮没有撑大范围。
- 门:普查门自己的判决行 exit 0("109 elevation read sites … 145 anchors resolve, 27 declared non-read"),13 个文档/脚本族门全部 exit 0,退出码在管道之前捕获。
- ⭐ 席位没有把这一轮当作文档轮敷衍过去:它注意到合并从 main 带进 95 个
packages/文件,且protocol.ts两侧都改(main +175 / 本分支 +99)⇒ 跑了整包套件:build 0 / typecheck 0 / test 0("150 passed | 2 skipped,2076 passed | 10 skipped"),并走共享锁("held the lock 408s")。这一步是它自己判断该做的,我的派发词只说了「若合并触及 packages/ 则说明并跑该包套件」。
⭐ 一处对我框架的修正,席位提得对
我把本轮预设成「散文半边可能有争议」。实测:本次冲突两侧都是纯锚点,零散文分歧。 证明方式很干净 —— 把所有行号拼法规范化后,merge-base、main、分支 head、以及提交结果四者字节相同(sha256
6f3164ba56db0d613a904ffe),并带两个发火对照(规范化器确实在工作:main 那份规范化了 146 个锚点;未规范化的 base vs main 仍报 DIFFERS)。⇒ 散文并集这一半是可证的,不是被争的。⛔ 第三条发现:我复现不出来,如实记下
席位报称
git merge-tree会调用 os-regen 驱动,因而本地预检与合并队列在不安全方向上不一致。我做了判别性探针,结论相反:同一对输入下,driver=false、driver=true、真实驱动三者都报冲突。⭐driver=true是探针自带的发火对照 —— 若驱动真被调用,它会成功并留下 ours,该路径就不该报冲突。它报了。⇒merge-tree没有调用驱动。⇒ ⛔ 该条未被带进 #14064。若你在别的 git 版本 / checkout 上量到过相反结果,请附对照重测。
⚠️ 而 #14036 被 dequeue 的真实原因是平凡的那个:我的预检跑在 main =ada3834add上,它排到队首之前 main 已经动了好几次。 ⇒ ⭐ 一次干净的merge-tree读数在 main 移动的那一刻就过期了 —— 这是我要改的操作习惯,⛔ 不是驱动的锅,也不该记在席位账上。另两条实验已带进 #14064(本卡不重复)
① 驱动丢的永远是 incoming 侧 = 永远是 main,源码级确认(驱动从不写
%A,只写 pending 标记)⇒ 受损的永远是已落地的工作。
② 纯散文丢失会全绿通过 —— 十道文档族门实测全部 exit 0,构造性失效而非覆盖不足。状态
⛔ 尚未放行。检查在跑(31 项中 22 项完成、21 成功、零失败),全绿且我复核完毕后才 flip/arm。
⚠️ 已实测:dequeue 会清掉 auto-merge 挂载,重新放行必须重新挂。
Generated by Claude Code
- 快进推送
Found by the driver-materialisation consumer census on #13973 (class (c) — genuinely wrong on one side). That sweep is not addressed by this card and does not close it.
The two sites, one root cause
packages/metadata-protocol/src/protocol.ts.1.
listCommitsordering — line 18262:The comment states the assumption in as many words: "sort by the ISO timestamp".
createdAtisr.created_atoffthis.engine.find('sys_metadata_commit', …)— a record read door.2.
rollbackToPackageCommitplanning — lines 18843-18845:Lexicographic
>over the same values, to decide which commits get reverted.Why it is wrong on the production default driver
created_atis a builtin audit column: not indatetimeFields, andSqlDriver#formatOutputrepairs it only insideif (this.isSqlite). Pinned inpackages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts— the live dialects hand it out of the record read door as a JSDate, SQLite as canonical ISO-Z text.So on Postgres/MySQL both sites compare strings of the form
whose leading token is the weekday name. Lexicographic order over that set is
Fri < Mon < Sat < Sun < Thu < Tue < Wed— unrelated to chronology, and stable across the whole file, so the failure is systematic rather than intermittent.Consequences:
listCommitsreturns the package timeline in an arbitrary weekday-name order while claiming newest-first.rollbackToPackageCommitconsumes that ordering and re-derives the same comparison itself, so it reverts a set ofapplycommits chosen by weekday name: commits older than the target get reverted, commits newer than it get skipped. That is a destructive operation planning off a wrong predicate.On SQLite both sides are canonical ISO-Z text, lexicographic order equals chronological order, and both sites are correct. Every test they have drives SQLite or memory.
Why this is not a
??fallbackPer #13973's standing prohibition, the question is which side owes the canonical spelling:
canonicalVersionInstant/versionTokensAgree). Local, and this file already has the helper.withPostgresCalendarDayAsText) — a maintainer call.A regression pin needs a
Dateon the read side.@objectstack/metadata-protocolhas no driver dependency and must not grow one (the layering runs the other way), so the pin drives a hand-madeDatehere — the same splitsql-driver-13567-audit-stamp-materialisation.test.tsdocuments for the OCC seam.Re-run
Backlink: #13973 (census), #13382 (the OCC seam, the same shape, same file). Neither is addressed here.