Repository navigation
Open a decision file the board listed, from its row - #2046
Conversation
|
Verdict: PASS at dc781ad Ran at this tip: Red first: the six changed sources ( Why it is right: Scope ( CI at this tip: 15 check-runs, one red and it is the expected one — Metadata: |
…ab#2041) The Project board listed every decisions/*.md file as a plain div: a person saw that a decision existed and could not open it. A row is now a button that asks the host for that one name and draws the text under it. The new read accepts only a name the board's own listing produced — the last board read of that Project is the set, including the names the 200-row cut dropped — so a name like decisions/../outside.md, an absolute path, a name one level down or a symlink answers `not listed` without a stat or an open, and the answer keeps the reader's own three states (text, absent, too large). The bounded byte read is now shared with project_state (readListedFile), so the 200,000-byte bound and the lstat rule cannot drift between the two readers.
dc781ad to
2c7b70a
Compare
|
@Birfy review request at The head moved from What changed since Tier is T3 ( |
|
Verdict: PASS at 2c7b70a Ran at this tip: Red first, eights on the same suites: with the seven changed sources plus Read at the tip rather than trusted from the description:
Checks at this tip ( Scope: 23 files, Metadata: body |
Fixes #2041
Owner: Dev-1
Written-by: Dev-1
Reviewer: Birfy
Claim-Id: 95933d41dc0c46eca230c105c4174339
What changed
The board listed a Project's decision files as inert rows —
data-pathanddata-sha256, no click target, no link — so a person could see that a decision existed and could not open it. A row is abuttonnow, and activating it draws that one file's text under the row; activating it again closes it, and another row replaces it. Abuttonneeds notabIndex, so keyboard focus and activation work.Host (
core/src/host/project-board.js):rt.projectDecision(projectId, name, actor)answers the reader's own three states for one name: the text withbytes/sha256/mtime,absent: <path>, ortoo large: <path> is N bytes (bound 200000).projectBoardread of that Project produced.decisionsOfhandsknownback as a map of path → the folder that path was read from, and that map is kept per Project. It is the un-truncated dedupe, so a name past the 200-row cut opens as well, and a decision a meeting archived into a folder of its own is read from that folder rather than the Project folder.not listed: …before anystatoropen.isDecisionPath(Refuse a decision name that leaves decisions/ and stop project_state following a symlink #2038) is the first check, sodecisions/../outside.md, an absolute path and a name one level down never reach the disk at all; the set is what refuses a legal-looking name the listing skipped, such as a symlink.tasks: []for a member who cannot see a Project's cards, and its decisions must not answer instead. The actor travels with the id fromstore.js(channel.jsuntouched), and visibility is worked out per call, in memory, before anything is opened — the per-Project cache holds names and folders only, never one member's view of them, so one reader cannot lend their reach to the next caller and a revoked card takes effect on the next open. A name a decided meeting archived follows that meeting's own cards throughrt.taskVisible, the rule the board paints them with; a plain record has no card, so the Project's role list decides (roleOf: owner, maintainer or member). The chain has three steps, in this order and on purpose: the cards decide when the name has cards; a meeting-linked name whosetaskIdsis empty, or whose cards are all gone, falls to that same Project role list — the cards are the only thing that carried visibility, and a record that lost them must not read as visible to nobody, so the deliberate consequence is that such a name is exactly as open as a plain record and no wider; and anything that cannot be judged at all — notaskVisible, no Project record — refuses rather than allows. A caller who may not open a name is answered with exactly the sentence a name the listing never had is answered with, so the refusal cannot be read as a list of what exists.readListedFileincore/src/host/project-state.js, extracted fromreadProjectState: the 200,000-byte bound and thelstatrule (a symlink answersabsent) are one implementation for both readers, andproject_state's own answers are unchanged.Endpoint wiring:
project-decisioninstore.js, dispatched inchannel.jsbesideproject-board, listed inauthz.js'sTASK_ENDPOINTSbeside it. Client (core/src/client/project-board.js):ProjectBoardRecordkeeps one open path, askscall('project-decision', { id, path }), and draws aprefor the body or the host's own sentence forabsent/too large/not listedunder adata-open-decisionblock — the answer is text, never markup it contains. An answer that lands after another row was opened is dropped rather than painted under the row now open. Two copy keys with zh entries.Red first
Base
3eca1e54sources with only the two new test files added,cd core && node --import ./test/harness-resolve.mjs --test test/project-board.test.js test/project-board-view.test.js— 81 tests, 68 pass, 13 fail, the same three runs in a row:#2041: activating a decision row…expected: 'button',actual: 'div'#2041: a second activation collapses…TypeError: rowAt(...).props.onClick is not a function#2041: each refused answer reads its own sentence…TypeError#2041: an answer that lands after another row opened…TypeError#2041: the overlay hands the board its own read…the board draws the decision row— no row exists to click#2041: a decision the board listed opens with its text{"ok":false,"error":{"code":"bot/not-found","message":"Unknown endpoint project-decision","details":{}}}, sofalse !== true#2041: a name the board did not list…TypeError: rt.projectDecision is not a function#2041: a listed decision over the bound…TypeError#2041: a decision past the 200-row cut…TypeError#2041: a decision deleted after the board listed it…TypeError#2041: a decision a meeting archived…TypeError#2041: a decision text is answered only to a caller who can see the Projectfalse !== true#2041: revoking a card closes the name a board read already cachedThe client case that answers a refusal walks all three the reader has —
too large,not listedandabsent— and each has to read as the host's sentence, whole, with the row still a row.The four refused names in the second case are
decisions/../outside.md, an absolute path,decisions/sub/2026-10-30-deep.mdand a symlink with a legal name; each must answernot listed: …, carry notext, and the../one points at a file over the bound, sotoo largewould be the answer if the host had stat'd it — it isnot listed, which is the proof the disk was never touched.Mutations (each applied alone, then reverted)
folderOf(project): 80/81, red ona name the board did not list…(the symlink answersabsent): the set, not the name shape, is what refuses a legal-looking name.decisions.files): 74/81, red on seven cases — the listing-to-open path, the bound, the past-cut name, the deleted one, the archived one, and both caller cases.MAX_BYTEScheck inreadListedFile: 83/86 together withtest/project-state.test.js, red on the board'sover the bound, on the caller case and on the reader's own#1702 read one returns its bytes, bounded— the bound is shared, so breaking it once breaks both.settlewriting into whichever row is open): 32/33 ontest/project-board-view.test.js, red onan answer that lands after another row opened is never drawn. Before this test existed the same arm ran 33/33 green, which is why the guard had no evidence behind it.call: readfrom the pane the overlay builds: 32/33, red onthe overlay hands the board its own read, so a row opens through it— without that line the panel'scallis undefined and an activation returns silently.Verified at the tip
3eca1e54261ea22b211e93884e26ea5d578bacb5; local commitc7a3291c50c32f8087e5040a02fd479b27ec6144, treef7139a1c4c1e5c4b276cbcb7fe86b57c29625f98; the pushed head is rebuilt from that same tree by the push path, so both trees are identical. One commit, 23 files, +1349/−121.TZ=UTC ARCH_DOCS_BASE=3eca1e54 npm run verify→ exit 0: core 2492/2492, social 541, tree 127, timeline 135, workflow 336,.github563, interop 37, live-e2e 24;arch-docs: OK (23 changed file(s)); lint 73/36/9/2/16 warnings and 0 errors, plus interop 1 and live-e2e 0;build-client --checkexit 0.node scripts/ds-bot-patches.mjs check→ pin2b10c70ok,10 patches apply cleanly in order,core/ equals ds-bot/ + patches/ds-bot/;canonical→generate is a no-op;generatere-run on this tip changed no path (git status --porcelainempty apart from one untracked dependency symlink this worktree links in), and the tree did not move.Generated set — what moved and why
This branch is rebased on
3eca1e54, which already carries #2040's and #2044's own regeneration ofclient.jsand0008, so every number here is this change's delta against that base.generaterewrote thefiles/mirrors of the five edited files (src/host/project-board.js,src/host/project-state.js,src/host/authz.js,src/client/project-board.jsand both test files) andcore/client.js.Three patches gained exactly one hunk each, and nothing else in them moved — the endpoint has to be dispatched from
store.jsandchannel.js, which those patches already carry:0002src/host/store.js@@ -160,13 +160,489 @@→@@ -160,13 +160,497 @@at patch line 10130002src/host/store.jsproject-decisioncase (2 comment + 6 code)0005src/host/channel.js@@ -319,32 +319,135 @@→@@ -319,32 +319,136 @@at line 8440005src/host/channel.jscase 'project-decision':0001src/client/i18n.js@@ -41,22 +41,74 @@→@@ -41,22 +41,76 @@at line 7510001src/client/i18n.js0008is the client bundle, one inner diff (client.js), and it moved as a whole: +128/−90. That is representation drift — inserting the new row markup and the open block moves the diff's own anchors — not extra content: what0008produces iscore/client.jsbyte for byte (core/ equals ds-bot/ + patches/ds-bot/above), and that file's own delta against the base is +50/−7, the client change itself.0003/0007/0009/0012/0014/0016are byte-identical to the base.What I did not run
core/dev/bench.sh smoke): it needs thedeepseek-harnesscheckout, which this worktree does not have. The host change is a read endpoint besideproject-boardandproject-state; nothing inindex.jsor the turn/agent path moved.packagejob (pack check, typecheck, actionlint) —verifydoes not run it.Deliberately out of scope
Editing a decision, more than one open at a time, search, and any change to how decisions are written (the issue's own scope). The endpoint never re-lists the folder: a file created after the last board read answers
not listeduntil the board polls again (≤5 s), which is the price of the set being "the listing this board produced" rather than a second, wider read path; closing a row and opening it again re-reads that one file (onelstatand one read, no directory re-list), so a file edited in the meantime is shown as it is now. The rows themselves stay as they are:project-boardsends the decision listing to any signed-in actor (#1723 leaves it that way), which is a recorded residual of #2038, not something this change widens — what this change narrows is the text behind a row.Checkpoint
{ "pr": 2046, "issue": 2041, "claim": "Dev-1", "summary": "A decision row on the Project board was inert; it is a button now, and activating it draws that one file's text under the row. The host adds rt.projectDecision, which opens a listed name only when the caller may see it: a meeting's decision follows that meeting's cards, a plain record follows the Project's roles, and anyone else gets the not listed sentence a missing name gets. The byte read is readListedFile, shared with project_state, so the bound and the symlink rule cannot drift. The endpoint is wired through store, channel and authz, and the client draws the body or the refusal.", "files": [ "core/src/host/project-board.js", "core/src/host/project-state.js", "core/src/host/store.js", "core/src/client/project-board.js", "core/test/project-board.test.js", "core/test/project-board-view.test.js", "core/src/host/authz.js", "docs/subsystems/core-host.md" ], "checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=3eca1e54 npm run verify; generate x2 + check + canonical; build-client --check; red first on 3eca1e54 with tests only", "result": "green", "at": "2c7b70a6c76bb926aed1e9a0dd338595220392db" }, "next": "none — ready for review; Test and Security are named by the Lead." }