Repository navigation
[finding] DatabaseLoader.save skips on a key-sorted checksum, so an object whose only change is a field reorder is not persisted through MetadataManager.register #21828
Description
Activity
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsPath: changing the app at runtime without code — the object designer | studio-authoring.object-designer-roundtrip (the boot-time door) | P1
Triage: first grade —
bug·priority:p3·domain:engine·area:studio·pm:queue(findingremoved). One content-hash rule forsys_metadata: the loader adopts the repository'sTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T06:55Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/metadata/src/loaders/database-loader.ts(DatabaseLoader.save, about:1404–:1414) ⇒domain:engine(the lane of #21790); rationale: the same class #21790 fixed on the designer's path, on the boot-time register path.- Direction (triage's call, as the card asks).
DatabaseLoadercompares and stores the content hashSysMetadataRepositoryuses (hashSpec(body, type), from PR fix(metadata-core): a reorder of an object's fields is a content change, so the designer's publish is no longer dropped (#21790) #21814), so one column carries one vocabulary.- ⛔ No second order-aware checksum beside it.
- ⛔ No order key: the map already holds the order.
- Why p3. It is boot-time artifact priming, the running process already sees the new order, and the stale persisted row is read from source, not measured end to end.
- Pins: a field-reorder-only
registerpersists the new order, and an unordered map's key swap still compares equal. - Serial: PR fix(metadata-core): a reorder of an object's fields is a content change, so the designer's publish is no longer dropped (#21790) #21814 (metadata: publishing a field reorder from the object designer is a silent no-op — the content hash is key-order-insensitive, so a reordered
fieldsmap hashes equal and the draft is dropped #21790) introduceshashSpec(body, type). This card starts after it lands.
Generated by Claude Code
- Direction (triage's call, as the card asks).
- addedarea:studioChanging a running app without code — authoring, publish, docs and the portalChanging a running app without code — authoring, publish, docs and the portalbugSomething isn't workingSomething isn't workingand removed
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim: PM loop round 31 · 2026-10-05T07:29Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21828-loader-one-hash
Worktree:objectstack-issue-21828
Domain:domain:engine
Seat:domain:engine#1
File surface (atorigin/main8832655af2), per triage's grade and direction 5989619889:packages/metadata/src/loaders/database-loader.ts:DatabaseLoader.save(about:1366–:1466) and its history write (about:725–:800) compare and store the content hashSysMetadataRepositoryuses,hashSpec(body, type)from PR fix(metadata-core): a reorder of an object's fields is a content change, so the designer's publish is no longer dropped (#21790) #21814. One column, one vocabulary.- ⛔ No second order-aware checksum beside it.
- ⛔ No order key.
packages/metadata/src/utils/metadata-history-utils.ts(calculateChecksum), only as far as the measured fix needs.- Tests;
.changeset/21828-*.md.
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: no - A field-reorder-only
registerstops being skipped. No accepted input, key, status or error code changes. If the fix changes an exported function's behaviour or adds an export, the dev says so, and the line and the contract-review question are re-read.
Thread-read: 5989619889
Serial constraints cleared: at 2026-10-05T07:29Z: - Triage's serial predecessor metadata: publishing a field reorder from the object designer is a silent no-op — the content hash is key-order-insensitive, so a reordered
fieldsmap hashes equal and the draft is dropped #21790 landed as PR fix(metadata-core): a reorder of an object's fields is a content change, so the designer's publish is no longer dropped (#21790) #21814 →e1790fdcfc, which carrieshashSpec(body, type). - Of the 6 open PRs, none touches
packages/metadata/src/(each file list read byfilename, the 415-file release PR paged through). - This lane's other claim, [finding] lock family, artifact layer × package axis: the _lock gate looks up the packaged artifact with no package while both reads look it up with the request's package #21803 (5989129973), holds
metadata-protocol'sitem-lock.tsandprotocol.ts, a different package.
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21828,
"status": "done",
"branch": "claude/issue-21828-loader-one-hash",
"pr": "#21852",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi (mode:subagent: the dispatching PM session, as the order states)",
"premise_still_valid": true,
"summary": "DatabaseLoader now stamps sys_metadata (and its history rows) with hashSpec(body, type), which is the hash SysMetadataRepository stamps, so the column carries one vocabulary. save now decides "unchanged" by re-hashing the stored body under that rule, so a field-reorder-only register is persisted, a key swap in an unordered map is still a no-op, and a row stamped under an older rule with an unchanged body is not rewritten. The history write takes the parent row's stamp and no longer compares stamps. calculateChecksum is unchanged and still exported (docblock note only). No spec edit and no export change. Premise reproduced at 8832655: a reorder save left version 1 and stored order title,amount. Reach reading: no in-repo host composes a DatabaseLoader (MetadataPlugin builds NodeMetadataManager without datasource/driver; setDataEngine/setDatabaseDriver have no caller), so the defect is reached only by a host that configures one.",
"tests": "At cf0537a (final HEAD, origin/main 2799155 merged, which touched only packages/spec test files): pnpm --filter @objectstack/metadata typecheck: exit 0, and the new test is in the program (tsc --noEmit --listFiles: 1 hit). pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2: "Test Files 58 passed (58) / Tests 867 passed (867)". New pins: packages/metadata/src/loaders/database-loader-21828-one-content-hash.test.ts, 8/8 (real SQLite through MetadataManager.register, read back from rows). Reverse verification on committed 9e2b5ea via scripts/ablation-replace.mjs (WRAP mode, plus a trap EXIT INT TERM restore in the driver). (A) save restored to the pre-fix key-sorted calculateChecksum stamp and comparison: 3 failed / 5 passed. Red: the reorder pin, the one-vocabulary pin, and the reorder against an order-blind stamp. Both key-swap pins stayed green. (B) new stamp but a stamp comparison: 3 failed (all three upgrade pins). (C) history stamp comparison restored: 1 failed (reorder against an order-blind stamp, history 1 not 2). The first C attempt was a no-op (replacement contained the anchor; the tool refused with anchor 1 to 1) and was re-anchored and re-run. Every mutation was proved landed (anchor 1 to 0, blob 4da1f0736df3 changed) and every restore proved (blob == HEAD, git diff HEAD empty, marker count 0). Control 8/8 green after each pass. No build was involved: the test imports the source. H2/H3 scratch probe (metadata-protocol, deleted, never committed), at 8832655 vs after the fix (metadata dist rebuilt): loader over a repo-stamped view went version 1 to 2 with an update history row before, and is a no-op after. Repo over a loader-stamped view went 1 to 2 with a history row before, and is a no-op after (both stamp sha256:2d77...). Repo over a loader-stamped object was a no-op both times. A loader reorder was skipped before, and is version 2 with stored order amount,title after.",
"mcp_calls": "0 (no MCP GitHub tool called)",
"api_writes": "2, both through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls (run 37285167446, PR #21852, draft, 13315 bytes sent and stored identical, re-read by REST: identical); (2) this os-dev-report comment, i.e. POST /repos//issues/21828/comments. Not REST: 4 git pushes of the branch (empty probe 8832655, 9e2b5ea, 235e681, cf0537a). label-write (PR assignee): refused by this session's permission classifier before any request; 0 writes (see deviations).",
"gates": {
"derived_union": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at cf0537a: 62 commands, identical to the 62 derived at 235e681. All 62 run at cf0537a, all exit 0. --ran: "62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED zero — all 62 recorded an exit code and none of them is 3)".",
"added_beyond_dispatch_list": "check-adr-0087-registration x2, check-empty-changeset x2, release-rehearsal-clone --self-test, release-pending-publish --self-test, check:engine-double-contract (no pin row asked), check:objectql-double-limit, check:objectui-changeset, check:pm-changeset-deadline-census, check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:where-matcher: all exit 0 at cf0537a.",
"artifact_roster_block": "55 run at cf0537a: 52 exit 0. The 3 PR-context ones were NOT WIRED (exit 2) without a PR and were re-run against PR #21852: check-closing-target-claim exit 0 ("PR #21852 closes #21828, and each carries a Claim: whose Branch: line names claude/issue-21828-loader-one-hash"), check-single-claim-paths exit 0, check-partof-closing-keyword exit 0.",
"symbol_anchor_sweeps": "check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors, check:adr-anchors: all exit 0 at cf0537a.",
"changeset_level_axis": "check-changeset-no-major --base origin/main --event (synthetic event carrying the PR body): exit 0, "LEVEL AXIS: this PR declares clause-② no".",
"earlier_reading": "At 235e681 check:dual-build-cjs-loads was exit 3 (PREREQUISITE NOT MET, no workspace dist) and check:lean-entry-closure exit 3 until objectql was built. Both exit 0 at cf0537a (dual-build: "106 published require entry point(s) across 66 package(s) load").",
"lint_narrowed": "Declared narrowing. pnpm exec eslint --no-inline-config --format json on the 3 changed .ts files at 235e681 (bytes identical at cf0537a): 3 files, 0 errors, 0 warnings. Population: the files match the config objects packages/**/.{ts,tsx,mts,cts} and **/.{ts,tsx,mts,cts}, and the changeset is in no lint glob. Invariance: eslint.config.mjs sets only ecmaVersion/sourceType, with no parserOptions.project and no typed rules, so untouched files cannot change verdict. Full pnpm lint is CI's.",
"not_measured": "none at cf0537a"
},
"line_budget": "n/a",
"deviations": [
"The PR assignee write was refused, verbatim: commandnode scripts/pm/label-write.mjs --repo objectstack-ai/objectstack --issue 21852 --assign os-project-manager; refusal "Permission for this action was denied by the Claude Code auto mode classifier. Reason: [External System Writes]." Per the order, nothing else was attempted. PR #21852 read back with assignees [] (labels: size/m, applied by the labeler, not by this run). The seat sets the assignee.",
"The first run of ablation C was a no-op (the replacement contained its own anchor, so ablation-replace refused and restored). It was re-anchored and re-run, and the recorded result is from the re-run.",
"Attribution: commit trailers use the model-free pair and the PR body uses the session-URL footer, per AGENTS.md, which outranks the harness attribution reminder. No harness-written trailer landed."
],
"files_changed": [
".changeset/21828-metadata-loader-one-content-hash.md (+11): @objectstack/metadata patch, Clause-②: no",
"packages/metadata/src/loaders/database-loader.ts (+81/-14)",
"packages/metadata/src/loaders/database-loader-21828-one-content-hash.test.ts (+201, new)",
"packages/metadata/src/utils/metadata-history-utils.ts (+6, docblock only)"
],
"census": [
"site | computes/compares/exposes (at 8832655) | on equality | after this PR",
"save :1404, :1413-:1414 | calculateChecksum(data) vs stored checksum | no write; loadCache set to the new body; success | contentHash stamp; content comparison (storedBodyUnchanged re-hashes the stored body)",
"save :1433 / :1466 | stamps the row on update / create | n/a | contentHash stamp",
"createHistoryRecord :725, :728 | calculateChecksum(metadata) vs previousChecksum, update only | no history row | takes the caller stamp; comparison removed",
"createHistoryRecord :782, :799-:800 | writes checksum / previous_checksum on the history row | n/a | same value as the parent row",
"registerRollback :1356, :1366, :1372 | calculateChecksum(restoredData); stored stamp read as previous; stamps the row | no comparison: always writes plus one revert row | contentHash stamp; still no comparison",
"rowToRecord :874 then load() :1002, stat() :1145 | stored stamp exposed as etag | nothing compares it in the loader (load reads no ifNoneMatch) | unchanged; rows written from now on report sha256:",
"getHistoryRecord :1217, queryHistory :1322 | history checksum / previous_checksum exposed | MetadataManager.diff passes them through as checksum1/checksum2; identical comes from the patch | unchanged"
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed (PR Acceptance notes). SysMetadataRepository.put, for a type with no ordered map, decides "unchanged" by comparing stamps (storedBodyUnchanged: storedHash === hash). An unchanged-body put over a row the loader stamped before this release (bare hex) therefore rewrites it once, with a version bump and an update history row. Measured at 8832655 by the scratch H2 probe (repo over a loader-stamped view). New loader stamps match, so after this PR the effect is confined to pre-release rows, once each. Reach not measured: no in-repo host composes a DatabaseLoader. Dedupe words: SysMetadataRepository storedBodyUnchanged legacy bare-hex stamp; loader-stamped row phantom history; sys_metadata pre-release checksum rewrite",
"carrier: none · noted, not filed (PR Acceptance notes). packages/objectql/scripts/dry-run-hash-compat.ts compares the stored checksum with a type-blind hashSpec(body), so it reports checksum_drift for every object row whose fields are not in sorted order, whichever writer stamped it since #21790. Offline read-only probe. Dedupe words: dry-run-hash-compat checksum_drift type-blind hashSpec",
"carrier: none · noted, not filed (PR Acceptance notes). MetadataManager.save(type, ...) passes type unfolded, while register folds it with canonicalMetadataServiceType. A plural spelling would hash with no ordered-map row and key a separate row. Read from source, unexercised. Dedupe words: MetadataManager.save plural type not folded canonicalMetadataServiceType"
],
"cleanup": "Worktree ../objectstack-issue-21828: node_modules removed and the worktree removed with a plain git worktree remove (no --force) in the step after this comment. The branch head cf0537a is on the remote. Scratch probe and ablation scripts live only in the session scratchpad, issue-21828/."
}
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsLanded: PR #21852 →
0fe0a59b2eonmain(merged 2026-10-05T09:46Z through the merge queue, entered 2026-10-05T09:22Z), verified at 2026-10-05T09:47Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash is on
origin/mainas a single-parent commit. Its diffstat is the reviewed one: 4 files, +299/-14. - The fix is on
main.DatabaseLoaderinpackages/metadata/src/loaders/database-loader.tsstamps and compareshashSpec(body, type), and decides "unchanged" by re-hashing the stored body. Fixes #21828closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body.- From this release on,
sys_metadata.checksumcarries one hash vocabulary from both writers, and aregisterwhose only change is a field reorder is persisted. A row stamped before this release with an unchanged body is not rewritten. - The PR landed with no assignee. The dev's
label-writewas refused by its session's permission check. The seat raised this with the maintainer and did not redo the write. - Filed from this card: [finding] dry-run-hash-compat.ts hashes stored bodies type-blind, so it reports checksum_drift for every object row whose fields are not in sorted order #21853 (
dry-run-hash-compat.tshashes type-blind). Triage rules the script retired.
Generated by Claude Code
- The squash is on
- added a commit that references this issue
on Oct 7, 2026
Filing gate: ① a product defect, class (b), on a second door. The defect class that #21790 fixed on the designer's path is still present on the
MetadataManager.registerpath.MetadataManager.registeris called frompackages/metadata/src/plugin.ts(about:1199,:1211,:1256,:1374). It persists through every writabledatasource:loader (metadata-manager.tsabout:802–:817). This is boot-time artifact priming, not the designer's save-and-publish.Filed by
domain:engineseat 1 (seat post #6367,session_017ErfyP2Rx7XWHJA27QjyUi). Reader who acts: triage grades and routes. ⛔ Not a claim.Read from source
DatabaseLoader.save(packages/metadata/src/loaders/database-loader.tsabout:1404–:1414) computescalculateChecksum(data)and skips the write when it equals the stored row'schecksum.calculateChecksum(packages/metadata/src/utils/metadata-history-utils.tsabout:18–:30) sorts every key recursively before hashing.fieldschecksums equal, and the stored row keeps the old order.sys_metadata(:314), the tableSysMetadataRepositoryreads with its ownsha256:-prefixed checksum.Contract it breaks
object.fieldsorder meaningful (object.zod.tsabout:1080–:1084;field.zod.ts:90, "Insertion order = display order").fieldsmap hashes equal and the draft is dropped #21790) made the designer's path honour that throughcanonicalize(value, type). This loader keeps its own order-blind checksum.Direction (triage's call)
sys_metadata. EitherDatabaseLoaderadoptshashSpec(body, type), or it compares content under the same ordered-map rule.Related
#21790 · PR #21814 · #21821.
Dedupe words: DatabaseLoader calculateChecksum key-sorted, register field reorder skipped, sys_metadata second checksum vocabulary, metadata-history-utils normalizeJSON. MCP
search_issuesscoped to this repo gave 0 hits.Generated by Claude Code