Files
gitea-agent/harness/agents/review/skills/gitea-code-review/references/native-review.md
T

203 lines
28 KiB
Markdown

# Native code-review publication adapter
Use the canonical harness source at `<HARNESS_ROOT>/harness/agents/review/skills/gitea-code-review/scripts/native_review.py`. Do not copy the helper into a standalone profile: it reuses public JSON, complete-pagination, cache-path and canonical spec-review codecs from the adjacent harness spec skill. The spec publisher/renderer and spec source files are not changed.
This adapter is a publication guard, not a substitute for the main reviewer's complete diff, surrounding-code, test and runtime review. Keep the existing approval/blocker rubric. A `blocked` review is local only and has no Native write payload.
## Supported interface
- Python API: `build_payload(plan)`, `validate_snapshot(plan, snapshot)`, `verify_publication(plan, before, after, exact)`, and `publish(plan, baseline, api, lease, journal_directory=None)`, `canonical_journal_path(plan)`, and explicit `register_legacy_journal(...)`.
- REST bridge: `ApiAdapter(authenticated_request, origin=..., credential_source=...)`.
- Primary lease bridge: `BrokerIssueLease(broker_handle, absolute_cache_policy, deadline_monotonic)` backed by `managed_lock_broker.py`; `HeldIssueLease(issue_fd, absolute_cache_policy, deadline_monotonic)` remains compatibility-only for already-admitted legacy callers. Broker requests are limited to `status`, `renew`, `acquire_repository`, `release_repository` and `release`; renew before expiry and use canonical release/wait/reap/probe cleanup.
- Read-only CLI: `preflight --plan PLAN --baseline SNAPSHOT` and `verify --plan PLAN --before BEFORE --after AFTER --exact EXACT`.
- There is intentionally **no live `publish` CLI**. Live publication uses the Python API with the workflow's existing authenticated transport and still-held Issue lock. Never invent a `capture`, `build`, `publish`, or `submit` command or guess a legacy helper schema.
Use Python 3 stdlib; no package installation is needed. Discover an absolute canonical `HARNESS_ROOT` using the repository-cache contract first. Invoke the canonical helper, not a relocated skill copy.
## Plan schema: `gitea-agent.code-review-publication/v1`
All fields are explicit; extra/missing fields fail closed.
- `schema`: the literal schema above.
- `target`: `owner`, `repository`, positive integer `issue`, positive integer `pull`, lowercase full 40-hex `head` and `base`.
- `contract_sha256`: lowercase 64-hex SHA-256 of the exact, unmodified UTF-8 Discovery Issue body.
- `reviewer`: exact `{id, login}` from authenticated `/user`; login must be `gitea-review-agent`.
- `spec_reviewer`: exact `{id, login}` from authoritative `/users/gitea-spec-agent`; never infer an account ID from historical comments.
- `spec_review_id`: the single nonstale, nondismissed, canonical Native `APPROVED` spec review on this head and contract.
- `spec_paths`: unique, safe repository-relative paths in the approved spec's evidence scope. Supply source-head-verified surrounding paths as well as changed paths; an approved spec may legitimately reference unchanged tests or callers. Do **not** restrict this set to the PR diff or read another role profile's artifacts to reconstruct it. The main review/admission must establish this scope.
- `verdict`: exactly `approve` or `request_changes`. Do not normalize malformed identifiers, hashes, states or verdicts.
- `body`: finalized UTF-8 review body, beginning with the exact code-review marker and containing exactly one `- Verdict: \`<verdict>\`` line. `marker(plan)` creates the binding line:
`<!-- gitea-agent:code-review/v1 owner=<owner> repository=<repository> issue=<issue> pr=<pull> head=<head> contract-sha256=<digest> -->`.
- `provider_contract`: three explicit booleans `supersede_same_author`, `retouch_dismissed`, `unordered_commit_files`, and `evidence`, a list of redacted, verifiable provider-contract/observation references. Disable exceptions lacking evidence; never set every flag simply to make a failing check pass.
Owner/repository currently support ASCII alphanumeric, underscore, dot and hyphen identifiers, with an alphanumeric/underscore first character. Unsupported names are rejected, not rewritten. Inline comments in a new review are not supported: the Native payload always explicitly sets `comments: []`.
## Trusted transport and lease prerequisites
Keep `gitea-access.md` and `repository-cache.md` authoritative. Before calling `publish`:
1. Admit the canonical, existing role-owned repository cache and impl-created, registered detached/locked worktree. Complete the independent code review and collect evidence on the exact source head.
2. Authenticate **one** transport from the current role's `$HERMES_HOME/.env` `GITEA_TOKEN`, or an explicitly authorized role-scoped external secret source. No ambient-token fallback, cross-profile credentials, credential-bearing Git URLs or anonymous downgrade is allowed.
3. Validate the exact fixed origin `https://gitea.jsfamily.quest/api/v1` and `GET /user` account ID/login. The transport must verify TLS, reject redirects, bound network timeouts and **not retry mutation requests**. The bridge's provenance labels are assertions supplied by this already-validated caller, not a mechanism to load or certify an arbitrary client.
4. Use the common broker Issue lease throughout publication/readback. Pass a live monotonic deadline, not an old PID/`ready.json` flag. `BrokerIssueLease` checks broker status and canonical worktree invariants; renew before expiry. `HeldIssueLease` is compatibility-only and may be used only by an already-admitted legacy caller, never to create a second lock protocol. The caller must run canonical release/wait/reap/probe cleanup. A custom supervisor lease must implement the same invariants: `check(plan)` returns `None` only on success and otherwise raises; boolean/dict success flags are unsupported. Finalization follows the [review execution procedure](../SKILL.md#실행-검증과-기존-워크트리-사용): complete required final source verification under the held lease and preserve its evidence before releasing. `verified.json` proves publication verification, not workflow cleanup. Retain the same publication journal and existing result artifacts when cleanup is incomplete; report workflow partial and reconcile only unfinished cleanup checks, without replaying a full review, suite or POST. Keep the common Issue-resource `free=true` completion requirement; do not add a released-only terminal-handle gate or a new automatic retry/heartbeat loop.
5. Use the canonical binding journal at `<harness-root>/harness/.runtime/native-review-journals`; do not select a caller-owned run directory. The publisher enforces the same journal for every retry/resume of this binding. The adapter creates mode 0700 directories and 0600 atomic/fsynced artifacts, rejects symlinks/hardlinks/foreign or unsafe modes, and holds a separate nonblocking journal mutex. Redact secrets before constructing plans/artifacts; do not log request headers, raw transport exceptions or token values. Journals contain complete source snapshots and must remain private.
The current helper is strict about worktree child directories: group/world-writable checkout paths are rejected. Do not change a real worktree's permissions to force publication; stop and reconcile an admission/publisher compatibility conflict. The legacy compatibility bridge is Linux-only: it validates an already-admitted broker-held whole-file lease represented by an exclusive `FLOCK` owned by this process in `/proc/self/fdinfo`, and never acquires or reacquires that resource. New callers use `BrokerIssueLease` and the broker public request API. It uses a root-owned, non-group/world-writable `/usr/bin/git` with a fixed minimal environment, not the caller's `PATH`.
These guards assume a cooperative, trusted role-local filesystem and transport. They do not isolate a malicious process with the same UID or pin every filesystem ancestor across subprocess path resolution. At-most-once creation is scoped to the retained canonical binding registry and held canonical Issue lease; alternate caller-selected paths are rejected. Unrelated publishers, other installations and cross-host coordination remain outside this guarantee. Do not share one lease across simultaneous publication callers or change the journal after an uncertain attempt.
The request callback is `authenticated_request(relative_api_path, payload=None) -> (decoded_json, headers, integer_status)`. `None` means GET; a dict means exactly one POST. It always uses the fixed origin and original role credential, with no authorization fallback. It is provided by the existing role-local transport; do not regenerate the full gate/publisher per review run.
`ApiAdapter.snapshot(target)` collects repository/Issue/PR metadata, identity/spec identity, all changed files, commits, reviews, historical review comments, Issue/PR comments and head statuses. Use this same collector for the baseline and subsequent snapshots. Every collection records contiguous 50-item pages, declared totals and Link headers; duplicates, inconsistent totals and truncated enumeration fail closed. A saved older collector snapshot may be replayed if it provides these complete facts, but differently shaped collectors must not be mixed for live source-drift comparison.
### Empty review-comments readback without a count header
For `GET /repos/{owner}/{repository}/pulls/{pull}/reviews/{review_id}/comments`, a successful direct JSON `[]` response without `X-Total-Count` is valid evidence of zero comments only when the exact review has integer `comments_count=0` and the complete collection agrees. Preserve the actual pagination evidence as `declared_total=None`; never fabricate a zero header or infer an empty response from an unavailable/malformed response or a missing/null count. Use `ApiAdapter.snapshot` for baseline and subsequent collections and `verify_publication` with the exact review readback.
This is not a general pagination bypass: preserve contiguous pages, identity uniqueness, observed totals and Link headers. A `rel=next` continuation must be followed to completion; a final page still advertising next, an unresolved full 50-item page without a total, conflicting totals, count mismatch or unexpected inline comments blocks verification. Endpoints supplying `X-Total-Count` retain total/cardinality checks. A readback failure is not authorization for POST blind retry: retain the same journal and reconcile the uncertain attempt through authoritative reads.
### Broker response preflight before publication
Use the [operation-specific broker response contract](../../../../../operations/repository-cache.md#operation-specific-broker-responses). `renew` supplies only `status`/`deadline`; read `repository_held` and identity from a separate `status` request. Python uses `ttl=`, not the wire/CLI name `ttl_seconds`. Keep the admitted handle and the retained canonical `harness/lib/managed_lock_broker.py` module as `broker`; do not read its token or load another profile's helper.
The following is a bounded caller example, not a new adapter or live publish CLI. `broker_handle`, `ttl_seconds`, `absolute_cache_policy`, `plan` and `native_review` are already-admitted workflow inputs. Renew before expiry, reject errors/missing or mistyped fields before publication, and pass the new deadline to the existing lease checker:
```python
import math
import time
try:
renewed = broker.request_handle(broker_handle, "renew", ttl=ttl_seconds)
except Exception:
raise RuntimeError("broker renew unavailable before publication") from None
if (type(renewed) is not dict or "error" in renewed
or set(renewed) != {"status", "deadline"}
or renewed["status"] not in ("held", "repository-held")
or type(renewed["deadline"]) not in (int, float)
or not math.isfinite(renewed["deadline"])
or time.monotonic() >= renewed["deadline"]):
raise RuntimeError("broker renew response rejected before publication")
try:
status = broker.request_handle(broker_handle, "status")
except Exception:
raise RuntimeError("broker status unavailable before publication") from None
if (type(status) is not dict or "error" in status
or type(status.get("repository_held")) is not bool
or status.get("status") != renewed["status"]
or status.get("deadline") != renewed["deadline"]):
raise RuntimeError("broker status response rejected before publication")
deadline_monotonic = renewed["deadline"]
lease = native_review.BrokerIssueLease(
broker_handle, absolute_cache_policy, deadline_monotonic)
lease.check(plan)
# Continue with the existing source/adoption/publication gates below.
```
`repository_held` is an observation, not an additional publication permission or a requirement to acquire/release the repository lock here. The existing lease checker re-reads authoritative broker status and validates identity/source; the two preliminary responses alone do not authorize a POST. A failure before calling `publish` means this wrapper has not invoked a POST, not that a prior uncertain attempt is absent. Keep the existing journal and publication result, report this phase separately, and do not automatically release/reacquire the lease, retry POST, or repeat completed review/suite work. Canonical cleanup remains unchanged.
## Live integration
After absolute root discovery, role-authenticated transport capability validation, complete review, and Issue lease acquisition, load the module once using `importlib.util.spec_from_file_location` and retain it. These are existing workflow objects, **not** placeholders for an alternative credential source:
```python
api = native_review.ApiAdapter(
authenticated_request,
origin="https://gitea.jsfamily.quest/api/v1",
credential_source="$HERMES_HOME/.env:GITEA_TOKEN",
)
lease = native_review.BrokerIssueLease(broker_handle, absolute_cache_policy, deadline_monotonic)
baseline = api.snapshot(plan["target"])
native_review.validate_snapshot(plan, baseline)
result = native_review.publish(plan, baseline, api, lease)
```
Preserve the original baseline until `publish` returns verified or the uncertainty is manually reconciled. The helper rechecks authoritative role/head/base/contract/default branch/source Issue linkage/spec approval and complete source snapshots immediately before creating. It explicitly sends `commit_id`, `body`, `event`, `comments` and performs at most one `create_review` call per durable attempt.
A matching canonical review is exact-read and verified as a write-free Native no-op; conflicting/multiple/current-head pending reviews stop rather than overwrite or duplicate. Reposting a different body/verdict on the same binding is **not** an automatic update path.
A durable `intent.json` is fsynced **before** the POST. After timeout, partial success, readback error, process crash or lease expiry, resume the same journal and perform only authoritative reads. If there is no unique match, retain the attempt and report uncertainty; never delete the journal, switch directories or silently POST again. A definitive failure may require an explicitly authorized manual recovery decision, outside this helper.
## Verification semantics
- Confirm the exact new review ID, role ID/login, reviewed head, body bytes, state, target PR URL, empty inline comments, typed flags and timezone-bearing timestamps against the complete review collection.
- Confirm precisely one new review and unchanged historical author/body/head/state/comments. No unrelated native review, comment, repository, head, branch, Issue contract or source change is silently accepted.
- Validate a source Issue repository binding when the provider includes it. A PR head may legitimately belong to a fork: the fixed base repository, exact head SHA and unchanged complete PR metadata bind it, rather than incorrectly requiring every head repository to equal the base.
- `official` is a provider merge-policy flag, not an additional verdict or role-identity requirement. Current accepted entry/publication rules require canonical role/head/contract/state and nondismissed/nonstale status; they do not require `official=True`. Changing that policy requires an explicit acceptance-criteria change.
- When explicitly supported by provider evidence, same-author official-review supersession or retouching an already-dismissed same-author formal review may change only `official`, `dismissed`, `updated_at`. Require the specific expected false/true flag transitions and a timestamp equal to this exact new review's submitted/updated timestamp. This is not a global mutable-field mask. Timestamp/pattern agreement is provider-contract consistency evidence, not proof of a unique server-side causal transaction; a coincident manual mutation matching the full permitted pattern cannot be distinguished without additional authoritative event evidence.
- Only commit `files` arrays are conditionally sorted by unique filename. File record contents, commit order, parent order and other arrays are still compared without blanket normalization. Changed-file records are compared by unique filename.
- Source Issue/PR timestamp, PR review-count and requested-reviewer changes are narrowly tied to the exact added review; unrelated derived-state mutation fails.
- The CLI is **saved-artifact replay**, not fresh network verification or approval authority. Its `read_only: true` output must not be presented as a newly published review. In live results `wrote` refers to a Native POST by that invocation; local journal writes still occur for no-ops.
## Read-only commands and regression tests
```sh
python3 "$HARNESS_ROOT/harness/agents/review/skills/gitea-code-review/scripts/native_review.py" preflight \
--plan "$RUN/plan.json" --baseline "$RUN/before.json"
python3 "$HARNESS_ROOT/harness/agents/review/skills/gitea-code-review/scripts/native_review.py" verify \
--plan "$RUN/plan.json" --before "$RUN/before.json" --after "$RUN/after.json" --exact "$RUN/exact.json"
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s harness/tests/review -p 'test_native_review.py' -v
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s harness/tests/spec -v
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s harness/tests/impl -v
```
Run the test commands from the canonical harness repository root. The existing spec suite requires `/opt/data/cache/scratch`: prepare that directory only if absent after confirming a nonsymlink, role-owned, non-group/world-writable parent, and require mode 0700 for the scratch child. Preserve existing shared parent permissions; do not chmod/chown them to satisfy a test. Use isolated/local runner variables and properly quoted command arguments rather than reusing mutable interpreter globals across review steps. Keep local stub/loopback transport checks, real temporary Git/worktree/lease checks, and saved production-wire replay clearly distinguished from a live Gitea mutation test. Introducing this helper does not authorize changing any existing product PR verdict, source, Issue state, merge or deployment.
## Reviewer verification evidence recipe
This is the existing common evidence helper, not the `native_review.py preflight/verify` publisher replay interface. Use the canonical helper at `$HARNESS_ROOT/harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py`; do not create a reviewer wrapper or copy it into another profile. The historical v2 recipe gap is addressed using the current `evidence-manifest/v3` contract.
First complete authenticated cache/worktree admission and retain the current review-role Issue broker lease. Prepare a fresh stage/run directory using the existing `verification_root.py prepare` procedure in the [canonical repository-cache contract](../../../../../operations/repository-cache.md). That procedure authenticates the live handle and Issue identity; the evidence helper does not acquire or certify a lease. Derive these absolute paths from the admitted harness/Issue identity, not from an arbitrary output directory:
- `HARNESS_ROOT`: the admitted harness root containing `harness/operations/repository-cache.md`.
- `REPO_ROOT`: `$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/checkout`.
- `VERIFICATION_RUN_ROOT`: `$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/verification/$RUN_ID`.
Require a **clean checkout**, **no source/evidence exclusions**, and prepared owner-only `0700` verification/run directories. Place the reviewed command plan at `$VERIFICATION_RUN_ROOT/evidence-plan.json` with mode `0600` before running; adapt the existing [evidence-plan template](../../../../impl/skills/gitea-issue-rework/templates/evidence-plan.json) to the actual reviewer commands, finite `timeout_seconds`, output bounds, source `hash_paths`, claims and limitations. `hash_paths` are checkout-relative, while plan/output/manifest CLI arguments are **run-relative**. Do not put artifacts in the checkout, weaken permissions, change the clean-tree check, or normalize rejected absolute artifact filenames. All roots/contract paths below are absolute; the three artifact filenames are not:
```sh
python3 -B "$HARNESS_ROOT/harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py" run \
--contract-path "$HARNESS_ROOT/harness/operations/repository-cache.md" \
--repo-root "$REPO_ROOT" --artifact-root "$VERIFICATION_RUN_ROOT" \
--plan evidence-plan.json --output evidence-manifest.json
python3 -B "$HARNESS_ROOT/harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py" validate \
--contract-path "$HARNESS_ROOT/harness/operations/repository-cache.md" \
--repo-root "$REPO_ROOT" --artifact-root "$VERIFICATION_RUN_ROOT" \
--manifest evidence-manifest.json
```
Run `validate` only after `run` succeeds, using the same roots and exact source HEAD. A nonzero result, timeout or overflow blocks adoption; retain bounded failure evidence and do not automatically rerun the suite. Existing manifests are not overwritten. Local validation binds the current source, plan and actual command records; it is not live Gitea readback, reviewer approval, Native publication verification or cleanup completion. Preserve the fresh stage/run ID, source SHA, plan/manifest digests, actual results/limitations and verified reviewer-access method under the existing reporting/cleanup contract.
Archived v2 uses only `validate-archived-v2` read-only compatibility; archived v1 uses only `validate-legacy`. Neither upgrades old records to v3 deadline/cleanup guarantees nor creates fresh publication evidence. Do not migrate archived manifests, add exclusions or change schemas to make the example pass.
## Canonical native-review journal
Native code-review publication의 정본 journal root는 `<harness-root>/harness/.runtime/native-review-journals`이다. Canonical publisher의 물리적인 harness 위치에서 결정하며 caller의 run-id, 임의 journal 경로, 환경 변수 변경으로 다른 registry를 선택하지 않는다. `canonical_journal_path(plan)`은 검증된 plan의 표시용 경로 계산일 뿐 인증·게시 권한의 증명이 아니다. 실제 registry claim 전에 현재 role identity와 held Issue lease 및 source/delegation gate를 검증한다.
Versioned binding은 고정 provider origin, exact owner/repository, Issue, PR, head SHA, contract digest, reviewer의 인증된 stable ID를 묶는다. Body, verdict, run-id는 binding key에 넣지 않는다. 같은 binding의 full-plan fingerprint가 달라지면 충돌로 차단한다. `publish(plan, baseline, api, lease)`는 정본 entry를 사용하며 기존 positional journal 인자를 넘길 경우 exact canonical path만 허용한다. Alternate path, path alias, missing/corrupt identity, symlink/hardlink, owner/mode 불일치 및 중간 상태는 fail-closed하며 기존 directory를 자동 chmod하거나 repair하지 않는다.
Private root/entry는 mode0700, artifact와 mutex는 mode0600이다. 동일 binding의 nonblocking process mutex 아래 entry 확인 → durable intent 기록 → 단일 POST → authoritative readback을 수행한다. 이전 attempt가 있으면 timeout, crash, readback 실패, lease 만료 뒤에도 read-only reconciliation만 허용하며 자동 POST 재시도는 하지 않는다. Root/entry presence anchor로 알려진 기록의 삭제와 interrupted bootstrap을 새 게시로 오인하지 않는다. Pending registration 역시 게시를 차단한다. `verified.json`은 publication 확인이며 source 최종 검증·lease release·cleanup 완료를 대신하지 않는다.
Legacy 등록은 명시적으로 승인된 `register_legacy_journal` 경로만 사용한다. 기존 journal을 자동 이동·삭제하지 않는다. Source는 이미 존재하는 private journal이어야 하며 정확히 선택한 기록의 사전 expected digest, plan/intent/baseline binding, 현재 role 인증·lease/adoption을 확인한다. 완성된 durable intent가 없는 legacy, 기존 canonical entry와 충돌하는 등록은 거절한다. Import pending을 복사 전에 fsync하고 완료 receipt 전에는 publication을 금지한다. 승인된 known artifacts만 복사하고 원본 bytes·권한을 보존하며 등록 자체는 Gitea write를 하지 않는다. 중단된 import는 자동으로 fresh attempt로 복구하지 않는다. Registered intent의 재개도 read-only 확인이며 authoritative match가 없으면 uncertainty를 유지한다.
보장은 동일 설치의 retained role-local registry와 cooperative trusted filesystem/transport 범위다. Shared harness ancestry의 배포 trust는 기존 repository-cache 계약을 따른다. Malicious same-UID process, registry와 anchor를 모두 지우는 운영자, 임의 다른 publisher/install, cross-host global exactly-once는 보장하지 않는다. 다른 host·설치로 이전하려면 별도 승인된 공유 조정/기록 이전 절차가 필요하며 새 root를 만들어 retry하지 않는다. 기존 provider collection 및 snapshot 비교 규칙은 이 journal 변경으로 완화하지 않는다.
### Explicit legacy digest and registration
등록할 source path를 운영자가 명시적으로 승인해야 한다. `legacy_journal_digest(legacy_directory)`는 기존 `publication.lock`을 nonblocking으로 잡아 source를 변경하지 않고 exact bytes inventory의 SHA-256을 반환한다. Digest는 아래 순서의 존재하는 known file들에 대해 filename·raw byte length·raw SHA-256을 담은 `gitea-agent.legacy-native-journal-digest/v1` canonical JSON의 SHA-256이다: `before.json`, `intent.json`, optional `verified.json`. Source content를 출력하거나 새 lock을 만들지 않는다.
```python
approved_digest = native_review.legacy_journal_digest(approved_legacy_directory)
# Freeze/approve this exact digest independently before registration.
native_review.register_legacy_journal(
plan, approved_legacy_directory,
expected_legacy_sha256=approved_digest,
api=api, lease=lease, declaration=adoption_declaration,
)
```
Legacy source의 entry는 `before.json`, `intent.json`, optional `verified.json`, 기존 `publication.lock`만 허용한다. 추가 파일/디렉터리, symlink/hardlink/FIFO, 중복 JSON key 및 nonfinite/overflow numeric 값은 거절한다. 승인된 before/intent는 raw bytes 그대로 복사하며 legacy verified는 `legacy-verified.json`으로 별도 보존한다. Receipt의 source→target 목록은 완전히 일치해야 하며 각 복사 기록의 길이와 raw SHA-256을 확인한 뒤 완료 처리한다. 재개 때도 모든 imported artifact의 무결성을 확인한다. 이후 `verified.json`의 현재 readback 기록 갱신은 보존된 legacy archive를 덮어쓰지 않는다. 여기서 read-only reconciliation은 provider에 대한 read-only이며 로컬 journal의 확인 결과 갱신은 수행할 수 있다.
Canonical entry가 이미 존재하면 빈 entry라도 legacy 등록으로 덮어쓰지 않는다. Bootstrap mutex 아래 pending sentinel을 먼저 durable하게 만들고, 영구 `registration.json` pending receipt → known artifacts 복사/fsync → complete receipt/fsync → sentinel 제거/fsync 순서를 따른다. Pending receipt나 sentinel, 손실된 복사 기록은 모두 게시를 차단한다. 등록이 성공해도 `publish`의 read-only reconciliation과 authoritative readback이 필요하며 등록 자체는 provider 게시 성공의 증명이 아니다. 운영자가 원본을 삭제하거나 보존 기록/anchor를 초기화해 새 시도로 처리하면 이 보장의 범위를 벗어난다.