Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
ec15886
docs: ADR 0027, admin page prints review
neuromechanist Oct 14, 2026
92d92c9
docs: update ADR 0022 for approve body
neuromechanist Oct 14, 2026
dec0ae2
docs: changelog for the review page (#459)
neuromechanist Oct 14, 2026
57cd59a
test: add terminal-words helper for the page
neuromechanist Oct 14, 2026
9bf524b
feat: badge the identifier-screen block reason
neuromechanist Oct 14, 2026
2b5a13f
feat: add publication review copy table
neuromechanist Oct 14, 2026
2f6eb08
test: add review copy drift test
neuromechanist Oct 14, 2026
a91fc6b
ci: drift-check the review copy contract
neuromechanist Oct 14, 2026
fd1db33
Merge website wp8
neuromechanist Oct 14, 2026
8066cfc
Merge website wp7b
neuromechanist Oct 14, 2026
7bb8eb2
Merge website wp5
neuromechanist Oct 14, 2026
14f517a
test: capture review answers from the API
neuromechanist Oct 14, 2026
c2ea323
test: read the captured review fixtures
neuromechanist Oct 14, 2026
524de4e
Merge website wpf
neuromechanist Oct 14, 2026
a0ba90e
feat: add the review panel's pure logic
neuromechanist Oct 14, 2026
25f52b1
feat: add the review calls to the admin client
neuromechanist Oct 14, 2026
9024dce
feat: word the review panel's failures
neuromechanist Oct 14, 2026
dd5f9fe
Merge website wp6
neuromechanist Oct 14, 2026
f4e3110
feat: style the review panel
neuromechanist Oct 14, 2026
892b26b
feat: add review chips and panel to the row
neuromechanist Oct 14, 2026
a480883
feat: show the review panel before approving
neuromechanist Oct 14, 2026
daa138b
test: guard the review panel page source
neuromechanist Oct 14, 2026
7bd96e1
Merge website wp7
neuromechanist Oct 14, 2026
a247f57
fix: say the unverified screen can persist
neuromechanist Oct 14, 2026
6774496
refactor: name the unavailable control's text
neuromechanist Oct 14, 2026
e0def07
fix: say an unanswered approve may have started
neuromechanist Oct 14, 2026
53df443
fix: print a fixed word for a sentence-only 403
neuromechanist Oct 14, 2026
bf4772c
fix: require the detail's id, gate and anonymous
neuromechanist Oct 14, 2026
2d8098e
refactor: drop the unused resetOverride flag
neuromechanist Oct 14, 2026
8bd95da
fix: keep results out of a closed review panel
neuromechanist Oct 14, 2026
a9885b4
docs: record the review page fixes in ADR 0027
neuromechanist Oct 14, 2026
21c9cbb
Merge fix round 1 (Opus review)
neuromechanist Oct 14, 2026
5eb4b19
fix: approve says unconfirmed on a proxy 502
neuromechanist Oct 14, 2026
f9b7efb
Merge website fix round 2
neuromechanist Oct 14, 2026
4444c20
fix: clear confirm dialog returnValue
neuromechanist Oct 14, 2026
ade0461
fix: lift identifiers_found badge in dark mode
neuromechanist Oct 14, 2026
45419dc
feat: draw the review panel as a pure tree
neuromechanist Oct 14, 2026
06e989b
fix: require the request binding on approve and deny
neuromechanist Oct 14, 2026
49738d6
fix: make the review panel model total
neuromechanist Oct 14, 2026
caedb63
test: harden the copy extractor, check limits drift
neuromechanist Oct 15, 2026
e7b4340
test: count only real consumers of copy keys
neuromechanist Oct 15, 2026
5f52db3
test: say what the review fixtures invent
neuromechanist Oct 15, 2026
fa651ab
feat: add draw-fault and stale-row panel states
neuromechanist Oct 15, 2026
2930872
fix: mount the panel tree and guard its draw
neuromechanist Oct 15, 2026
20b7ee0
Merge website fix round 3: logic and tests
neuromechanist Oct 15, 2026
35444a1
Merge website fix round 3: page and dialog
neuromechanist Oct 15, 2026
f17f2cc
refactor: row reads ACTIVE_STATUSES from the lib
neuromechanist Oct 15, 2026
356bae2
ci: check out publication-limits for drift test
neuromechanist Oct 15, 2026
26b74d2
fix: page compares the model's request id
neuromechanist Oct 15, 2026
1536af5
docs: ADR 0027 rulings and printed words
neuromechanist Oct 15, 2026
05a1550
docs: changelog names the words the page prints
neuromechanist Oct 15, 2026
786f358
docs: link the two follow-up issues in ADR 0027
neuromechanist Oct 15, 2026
79ff3ac
fix: empty statement is out of step
neuromechanist Oct 15, 2026
57d4662
fix: toggle keeps a stable name and aria-expanded
neuromechanist Oct 15, 2026
d35a142
test: strict mode asserts the limits file exists
neuromechanist Oct 15, 2026
87d0280
docs: extractor is the same rule, not the code
neuromechanist Oct 15, 2026
b665e03
fix: re-read keeps the failure that asked for it
neuromechanist Oct 15, 2026
7ec1465
docs: origin refusal prints the API's sentence
neuromechanist Oct 15, 2026
74a49e1
test: dark error text must differ from light
neuromechanist Oct 15, 2026
8e5a802
test: stylesheet scope covers the first media rule
neuromechanist Oct 15, 2026
38c4edb
test: scan every literal kind for sentences
neuromechanist Oct 15, 2026
d811983
fix: cancel a pending announcement on supersede
neuromechanist Oct 15, 2026
137c0ab
fix: keep the review toggle's label in its name
neuromechanist Oct 15, 2026
b74ca6f
Merge remote-tracking branch 'origin/staging' into feature/issue-459-…
neuromechanist Oct 15, 2026
8f52caa
test: recapture review fixtures at the dev head
neuromechanist Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,13 @@ A run's progress is only as fresh as the page's 20 second refresh.
- website#407 (list fix and interim guard) and website#408 (this change).
- nemar-cli `src/lib/api/publish.ts` (`approvePublication`, the loop) and `backend/src/services/publication-orchestrator.ts` (step 14, `hasMore`).
- `nemarDatasets/.github` `onboard-openneuro.yml` (the existing Actions-runs-the-CLI pattern).

## Update, 2026-10-14: Approve now carries a body

The decision stands: the page asks the backend to start an approval, the backend asks an executor, and the page reads the result.
What changed is the request the page sends.
Approve and Deny now send a body that names the request (`request_id`), and Approve sends the review in force and the reasons shown when it approves over a non-pass.
The page also reads the API's review detail before it offers Approve, and prints the API's own `statement` and neutral `cure` for each reason, never the terminal's sentences.
It withholds Approve when that detail cannot be read, so the old bodyless click no longer exists.
The deploy order in Consequences gains a consequence: the page needs an API that serves the review detail (nemar-cli#1731), so the API deploys first.
See ADR 0027.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# ADR 0027: The admin approval page prints the API's review words and computes no verdict

**Status:** accepted
**Date:** 2026-10-14
**Owner:** Seyed Yahya Shirazi

## Context

ADR 0022 made approval a dispatched action: the page asks the backend to start it and reads what the backend reports.
Since then the Application Programming Interface (API) has learned to derive a verdict for each publication request, from an automated review and an identifier screen, and to gate approval on it (nemar-cli ADR 0102, with its Phase 4 amendment, and ADR 0103).
The page still showed a status badge and sent an empty body, so an administrator could not see why a request was held, and the API's gates could only refuse the click after the fact.
The terminal words a reason with a command inside it (its `line`, and a `cure` that names the command to run), so the page cannot print those as they are.
The API sends each reason a neutral `statement` and `cure` beside them, and the page prints those.

## Decision

The page fetches the API's review detail for a request and prints the API's own words.
Each reason shows its `statement` and its neutral `cure`, the identifier screen shows its `headline` and `lines`, and the model's text appears only under a label that says the model wrote it.
The page does not print the terminal's sentences: the terminal's `cure` carries commands, so the page never prints `line`, `command`, `description.lines`, `description.next`, or the `message` of a refusal that names a terminal step.
It transcribes only a 22-key table of the sentences the API words for a terminal, kept in `src/lib/publication-review-copy.ts` and checked against the nemar-cli copy by a drift test, the arrangement of ADR 0015.
It never computes a verdict.
It never offers Approve for a reason no override clears, and it sends `request_id` with every approve and deny and, for an override, the review in force and exactly the reasons it showed.
The review start sends no `request_id`: it acts on the active request, and its own refusals cover a request that has moved.
The rulings below settle the cases the design left open.

- **A hard reason withholds Approve whatever `gate` says.**
The API does not gate on an advice-only review, but the page fails closed on a hard reason regardless, so an unknown `cleared_by` word is also a hard reason.
An administrator who needs to approve such a request uses the terminal.
A run that has started publishing is exempt: it passed the gates when it started, so its panel offers "Continue approval" whatever reasons now stand.
- **A blocked request, and a screen that says `blocks`, `rerun` or `wait`, withhold Approve.**
The API refuses every blocked request, even a walked-back run, so the page withholds Approve for one before it looks at anything else.
The identifier screen's `blocks` (a direct identifier), `rerun` (no verdict) and `wait` (still running) each withhold it with the shared sentence for that gate, added to the hard-reason line when both apply.
Of the other screen words, `acknowledge` offers Approve with a reason to record, and `clear` and `exempt` offer it with none.
- **When an override-class reason stands and there is no review row, the words depend on whether a review can be started.**
If the detail offers a start (`start_offered` is true), the line is the shared `gate.no_review_row` sentence.
If it does not, the line is a page-owned word in `src/lib/publication-review.ts`, outside the mirrored table: "No review of this commit is on record and none can be started from this page, so Approve is not offered."
The shared sentence ends by telling the reader to start the review, which is wrong for a request no start can reach.
- **There is no fallback to the old bodyless click.**
If the detail cannot be read, the page withholds Approve and prints the shared "could not be read just now" sentence (`approve.unavailable`).
It says it is out of step with the API only when the API is older than the page or the answer cannot be trusted, and then it withholds the whole panel and prints no reason at all.
The answer cannot be trusted when any of these holds:
`request_id` is not an integer;
`request_status` is not a string;
`gate` is not one of `enforced`, `off` or `n/a`;
`screen_gate` is not one of `clear`, `blocks`, `acknowledge`, `rerun`, `wait` or `exempt`;
`anonymous`, `applies`, `started_publishing` or `start_offered` is not a boolean;
`reasons` is not a list in which every reason has string `code`, `class`, `cleared_by`, `sentence` and `cure` and a non-empty string `statement`;
`description` or `identifier_screen` lacks a string `headline` or `tone`, or the screen's `lines` is not a list of strings;
or `review` is neither null nor a record with an integer `id`, a list of fact notes and a list of findings.
An old API that sends no `statement`, or an empty one (the page prints `statement`, never `sentence`), therefore gets the out-of-step sentence and never has a terminal `line` printed in its place.
A fallback would be a way around the page's own rules.
- **A detail that is not found prints the Worker's sentence.**
A 404 from the detail route means the request is closed, and the Worker's sentence says so; "reload" would be wrong advice, so the page prints that sentence and offers no refresh link.
- **The reason for the identifier screen is always asked for when the screen says `acknowledge`.**
The API would pass without one when this administrator's own acknowledgment is already on the request, but the page does not read that, so it asks again.
Sending it again is accepted and recorded again, so the cost is friction and no wrong approval.
- **A stopped approval that never started publishing is offered as a fresh approval.**
The API gates a stopped run like a fresh one, so the panel asks for the typed word, and for an override or a reason when they are needed.
Its button reads "Approve and publish" where the row's own button reads "Resume approval"; only a run that has started publishing is continued without the gates, and its panel button reads "Continue approval".
When the row's own button reads "Retry approval", an approve offered again, the panel's button says "Retry approval" too, so the two never name one action two ways.
- **A failure is worded by the page, never by the Worker's sentence, where that sentence could name a terminal.**
A 401 or 403 that carries only a sentence prints the page's own "sign in again" word, because an inactive account's refusal names a terminal command.
The one exception is the origin guard's 403 (code `origin_not_allowed`): its sentence names no terminal, so the page prints the Worker's sentence for it.
An approve that gets no answer (a missed deadline, a dead connection, or the proxy's own `upstream_unreachable` or `upstream_redirect` 502) says the approval may have started and asks the administrator to refresh and check before trying again, because the dispatch may have gone out; the API's lease makes a retry a harmless refusal.
- **The API deploys before the website.**
A page promoted ahead of the API gets a detail without `statement`, so it withholds the panel, prints "out of step" and offers no Approve, and it never falls back to the terminal's `line`.
The release that carries this page therefore waits for the API that carries nemar-cli#1731.

## Consequences

The page prints one authoritative reading of each request, and a cure that changes with the request (the dataset, whether a start can reach it, the detail of a failed question) needs no second copy on the website.
A stale page is caught by the API's guards, which answer with a typed refusal; the page then refetches the detail rather than rendering the refusal's own reasons.
The words with no counterpart in the API, the class tags and the "out of step" sentence among them, stay page-owned and unmirrored.
The 22-key table is held by its own tests, a rule that every key has a consumer on the page, and a drift test against nemar-cli `dev`; the strict path in nemar-cli's continuous integration needs this file on `staging` first.
An administrator cannot approve from the page a request whose only blocker is a hard reason or a missing review that cannot be started; the terminal remains for those.
The page cannot run the identifier screen again (nemarOrg/website#461) or mail the depositor a comment (nemarOrg/website#462).
The `identifiers_found` badge, "Identifiers found", also shows on the depositor's dashboard card, because both surfaces draw it from the one `PublishStateBadge` component.

## Alternatives considered

- **Transcribe all 24 reasons and their composition:** a second copy of what changes per request, which the drift tests of ADR 0015 exist to catch.
- **Run the verdict in the browser:** a second implementation of the rules would drift from the API's, which is the failure ADR 0015 exists to prevent.
- **Extend `ConfirmDialog` with a slot:** it is a shared component, and the page needs the controls (a confirmation and a recorded reason of 10 to 500 characters) before the typed `PUBLISH` dialog, which stays the last step and is unchanged.
- **Keep the bodyless click when the detail fails:** it would let an administrator past the rules the page exists to show.

## Receipts

- nemar-cli#1731 (Phase 4) of epic nemar-cli#1726; website#459.
- nemar-cli ADR 0102 (Phase 4 amendment) and ADR 0103.
- Website ADR 0015 (mirrored copy and drift tests) and ADR 0022 (dispatched approval).
- The captured API response fixtures under `test/fixtures/`, each with a note on how it was captured.
1 change: 1 addition & 0 deletions .context/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ Add new entries here as you create ADRs:
- ADR 0024 - [Embed calls are counted at the edge, from the request alone](0024-embed-calls-counted-at-the-edge.md) (accepted)
- ADR 0025 - [Catalog reads are cached for minutes to hours and fall back to a stale copy](0025-catalog-reads-cached-with-stale-if-error.md) (accepted)
- ADR 0026 - [Server-side calls to the API go over a service binding that names the visitor](0026-server-side-api-calls-go-over-a-service-binding.md) (accepted)
- ADR 0027 - [The admin approval page prints the API's review words and computes no verdict](0027-the-admin-approval-page-prints-the-apis-review-words.md) (accepted)
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ jobs:
# sparse-checkout list is the whole cost of covering another contract.
# test/private-site-contract-drift.test.ts is the third, against
# shared/contract/private-site.ts.
# test/publication-review-copy-drift.test.ts is a FOURTH, against
# shared/contract/publication-review-copy.ts; same `dev` pairing.
# The same test also compares the page's numbers with shared/publication-limits.ts, which
# sits beside the contract directory; without it that test skips, visibly.
- uses: actions/checkout@v4
with:
repository: nemarOrg/nemar-cli
Expand All @@ -113,6 +117,8 @@ jobs:
/shared/contract/account-copy.ts
/shared/contract/docs-auth.ts
/shared/contract/private-site.ts
/shared/contract/publication-review-copy.ts
/shared/publication-limits.ts
sparse-checkout-cone-mode: false
ref: dev
- name: Vitest
Expand All @@ -121,6 +127,7 @@ jobs:
NEMAR_CLI_ACCOUNT_COPY: nemar-cli-contract/shared/contract/account-copy.ts
NEMAR_CLI_DOCS_CONTRACT: nemar-cli-contract/shared/contract/docs-auth.ts
NEMAR_CLI_PRIVATE_SITE_CONTRACT: nemar-cli-contract/shared/contract/private-site.ts
NEMAR_CLI_REVIEW_COPY: nemar-cli-contract/shared/contract/publication-review-copy.ts

build:
name: build
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ The release pull request from `staging` to `main` moves those entries under the

## [Unreleased]

### Added

- **The admin publication page shows the review and the identifier screen** (#459, ADR 0027; nemar-cli#1731).
Each pending, approving or blocked request shows a review chip and a screen chip from the list.
A review panel, fetched when an administrator opens it, shows the reasons in the API's own `statement` and neutral `cure` (never the terminal's sentences, whose `cure` carries commands), the identifier screen, and the model's summary and notes under a label that says the model wrote them.
Approve is absent for a reason no override clears, needs an explicit confirmation to approve over a non-pass, and needs a recorded reason of 10 to 500 characters when the identifier screen found something to read.
Every Approve and Deny now names the request it was for, and each refusal the API gives is shown in plain words.
A review can be started from the panel when the API offers one.
There is no fallback to the old click that sent nothing: if the review detail cannot be read, the page says so and withholds Approve, and if the API predates the page or sends an answer the page cannot trust, it says it is out of step.
An approve that gets no answer, or only the proxy's own 502, says the approval may have started and asks for a refresh.
A 401 or 403 for a signed-out or not-allowed account shows the page's own "sign in again" sentence, never the API's, because an inactive account's refusal can name a terminal command.
The one exception is the origin guard's 403 (code `origin_not_allowed`), whose sentence names no terminal and is printed as the API words it.
A result for a review panel that was closed meanwhile appears in the page's error region instead of a hidden panel.
This needs the API side (nemar-cli#1731), which must be deployed to the API before the website: an older API sends no `statement`, so the page withholds the panel and Approve and says it is out of step.
- **A request blocked by the identifier screen has its own badge** (#459).
The seventh block reason, `identifier_screen_findings`, reads "Identifiers found" instead of the neutral "Blocked".
The badge also shows on the depositor's dashboard card.

### Changed

- **Account copy matches nemar-cli `dev` again** (#464).
Expand Down
4 changes: 4 additions & 0 deletions src/components/ConfirmDialog.astro
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ const bodyId = `${dialogId}-body`;
if (e.detail?.dialogId !== id) return;
if (verifyInput) verifyInput.value = "";
syncSubmitEnabled();
// An Escape close leaves returnValue as the last confirm set it, so a stale "confirm" would
// fire the confirm event on the next Escape. Start every opening from nothing.
dialog.returnValue = "";
dialog.showModal();
(verifyInput ?? cancelBtn)?.focus();
});
Expand All @@ -226,6 +229,7 @@ const bodyId = `${dialogId}-body`;
);
}
}
dialog.returnValue = "";
if (verifyInput) verifyInput.value = "";
syncSubmitEnabled();
});
Expand Down
Loading
Loading