Repository navigation
feat(space): document-driven collaboration space UI with acceptance gate - #560
PeterGuy326 wants to merge 22 commits into
Conversation
…shes The ConnectionForm sync effect unconditionally reset every field, including a typed PAT, whenever the saved connection values changed. A background services refresh landing while a draft was unsaved could therefore silently discard the user's input, and the next submit went out without a token. Guard the sync effect with a dirty flag: user edits mark the form dirty and win over background refreshes; the flag clears once a save or disconnect accepts the server state. Add a regression test where a refresh lands with changed saved addresses while a PAT is typed — it fails on main and passes with this change. Refs #512
Introduces the acceptance record that lets a human accept or reject an agent's deliverable inside a space. This is deliberately separate from approval: an approval gates permission to act, an acceptance is a verdict on work already done. Agents may only push work to review; only an acceptance may move it to done. Validators are fail-closed in the style of goal.v1: allowlisted keys, closed enums, bounded text, and decision/verdict cross-checks: - accepted requires a verdict for every criterion, all passed - rejected requires a non-empty reason note - criteriaIndex values must be unique and non-negative Contract + tests only; the space/document UI lands in a follow-up once this shape is agreed. Refs #558
Renders a collaboration space (协作区) as a folder in the workspace, where a single Markdown file is the source of truth for both the document view and the structure view. No second data model, no custom DSL. Shared contract (space-doc.v1): - parseSpaceDoc / renderSpaceDoc around two fixed H2 sections, 验收标准 and 任务. The parser is total: any string yields a result, missing sections come back empty. The renderer rewrites only those two sections and keeps every other byte of the document intact, so prose the parser does not understand is never lost. - spaceDocCriteriaCount for cheap progress reads without full parse. Renderer: - SpaceModule: file tree + document/structure view switch + participants and owner + task board + acceptance gate. - AcceptanceGate: per-criterion checkbox with evidence path; 验收通过 stays disabled until every criterion is ticked; 打回 requires a note. Acceptance writes done back into the document, so the verdict is durable in the same file the agent reads. - acceptance-gate-model holds the pure rules (untickedCriteria, canAccept, canReject, buildVerdicts, buildAcceptanceRecord) so the gate is testable without DOM. - useSpaceFiles reads through window.owb.positionDocs / positionDocFile and writes through writePositionDoc; when writing is unavailable the space degrades to read-only instead of throwing. - SpaceHost bridges the shell: derives participants from positionNames with the owner pinned first. Shell wiring: - App.tsx adds "space" to the activeModule union, a rail item, and the render branch. rail.space is the only hard registration point and is covered by i18n.test.tsx. - zh/en locales gain rail.space plus the space.* copy block. Tests: - apps/server/test/space-doc.test.ts (17): round-trip fidelity, fixed point, CRLF, section isolation, heading alias, unknown status reporting. - apps/desktop/renderer/test/space-acceptance-gate.test.ts (11). - apps/desktop/renderer/test/space-module.test.tsx (5). Refs #558
CI failed at `npm run check` with 19 TS errors, all in the new server
tests. The cause is mundane: `apps/server/tsconfig.json` includes
`test/**/*.ts` and the base config sets `noUncheckedIndexedAccess`, so
every `list[0]` in an assertion is `T | undefined`. I had type-checked
`packages/shared/src` but never the test files, so this only surfaced in
CI.
- space-doc.test.ts: add a single `at(list, index)` helper that asserts
bounds and narrows, then route the 18 indexed assertions through it
instead of scattering `!`. The one error that was not an index read was
`{ ...doc.criteria[0], done: true }`, where spreading a possibly
undefined value widened every field to optional; `at()` fixes that too.
- acceptance.test.ts: `value.verdicts[1].evidencePath` -> optional chain.
This one is on the #559 branch as well.
Verified with the CI command shape: `tsc -p tsconfig.json` for the server
project (includes tests) is clean, and the two suites run from
`apps/server/dist` pass 34/34.
Refs #558
`npm run check` also runs scripts/test/package-layout.test.mjs, which asserts that SHARED_RUNTIME_FILES matches the source inventory of packages/shared/src, in order. The two modules added by this branch were never listed, so this was not just a red test: they were absent from RUNTIME_FILE_SETS and would not have shipped inside the packaged app at all. - dist/acceptance.js - dist/space-doc.js Both inserted in the alphabetical position the test compares against. Verified locally: "runtime manifest is an explicit allowlist for every packaged consumer" passes, and RUNTIME_FILE_SETS for packages/shared now reports 41 files including both new modules. (The only remaining local failure in that file is "a third-party runtime dependency is either packaged or not required", which wants the src/vendor/*.cjs produced by `npm run prepare:updater`; CI runs that step before check, so it does not reproduce there.) Refs #558
SHARED_RUNTIME_FILES must match packages/shared/src in order, and RUNTIME_FILE_SETS is what actually gets packaged. Without this entry the acceptance contract would be missing from the built app. Refs #558
…e rail item Two renderer failures on the previous run, both real. 1. i18n-cjk-gate (#146) forbids CJK string literals under renderer/src, so user-facing Chinese must live in the zh/en catalogs. `SPACE_SPINE_FILE = "目标.md"` was the one offender. It is a filename that shows in the file tree, so the catalog is the right home: new key `space.spineFile` ("目标.md" / "goal.md"), and the component resolves it through `t()`. The exported symbol becomes `SPACE_SPINE_FILE_KEY`; the module test reads the value back out of `zhCatalog` so the fixtures cannot drift from the catalog. 2. App.test.tsx asserts the exact rail label order, and the new item adds a sixth entry. Expected list is now ["协作", "组织", "项目", "协作区", "收件箱", "设置"], matching the order in App.tsx (projects then space then inbox). Also replaced the deprecated `bordered={false}` on five antd Tags with `variant="filled"`; it was emitting a warning per render in the test output. Verified locally: i18n-cjk-gate 1/1, i18n 7/7, space-module 5/5, space-acceptance-gate 11/11 (24 total), and `tsc --noEmit -p apps/desktop` is clean. Refs #558
`npm run check` runs test:desktop-main, which runs build:renderer. That build failed: packages/shared/dist/position-id.js (1:9): "createRequire" is not exported by "__vite-browser-external" position-id.js does `createRequire(import.meta.url)` to load position-id.cjs, which is fine on the server and unresolvable in a browser bundle. It becomes reachable when the renderer imports *values* from the @roleweave/shared barrel, because the barrel is `export *` over every module including position-id. The control that pinned this down: #559 adds acceptance.js to shared and touches no renderer file, and its verify passes. The failure only appears once the renderer starts pulling values out of the barrel. So the two value imports move to the subpaths that package.json already exposes for them: parseSpaceDoc, renderSpaceDoc from "@roleweave/shared/space-doc" ACCEPTANCE_SCHEMA_VERSION from "@roleweave/shared/acceptance" Types stay on the barrel — `import type` is erased and cannot drag a module into the bundle. Both files keep a comment so the next value import does not reintroduce this. Verified locally: `vite build --config apps/desktop/vite.config.ts` now completes (it failed the same way before this change, even with main's App.tsx), `tsc --noEmit -p apps/desktop` is clean, and the four renderer suites pass 24/24. Refs #558
|
CI is green on Getting there took four fixes, all of which were real bugs rather than test noise. Worth a skim before you review the diff, because two of them are things the repo enforces but a local run does not surface:
Still interested in your read on the 验收台 boundary: an agent can move a task to |
PeterGuy326
left a comment
There was a problem hiding this comment.
Review: feat(space): document-driven collaboration space UI with acceptance gate
Scope: 21 files, +2532/-3, head 373110a. This is a substantive review of the contract layer, the parser/renderer pair, and the packaging/i18n wiring. I verified each repo constraint against the tree rather than against the PR description.
Verdict: changes requested (architecture is sound; two real defects in the renderer)
The overall shape is good: fixed-H2 structure keeps the file human-readable, the gate funnels through a testable model, and the packaging/i18n wiring is correct. Two defects in renderSpaceDoc should be fixed before this lands.
Constraint verification
1. CJK gate (#146) — PASS, but the premise is inaccurate.
The gate lives in apps/desktop/renderer/test/i18n-cjk-gate.test.ts. Its SCAN_ROOTS are apps/desktop/renderer/src and packages/ui/src only, and locales is an EXCLUDED_DIR. packages/shared/src is not scanned by any gate in the tree. Therefore the two fixed H2 literals in space-doc.ts (SPACE_DOC_CRITERIA_HEADING = "验收标准", SPACE_DOC_TASKS_HEADING = "任务") will not fail CI, and no exemption mechanism is required. These are genuine format identifiers rather than user-facing copy, so leaving them as literals is defensible — but be aware the gate does not protect this file at all.
2. Shared barrel trap — PASS. SpaceModule.tsx and acceptance-gate-model.ts take value imports via the subpaths @roleweave/shared/space-doc and @roleweave/shared/acceptance, and use import type for everything from the barrel. space-doc.ts imports goalWorkItemStatuses from ./goals.js, and goals.ts is pure (import type only), so nothing pulls position-id.js/createRequire into the renderer bundle. package.json exports both subpaths. No build:renderer break.
3. runtime-layout.cjs whitelist — PASS. SHARED_RUNTIME_FILES dist/* entries match walkFiles(packages/shared/src) exactly (37 entries) and in the required sorted order: dist/acceptance.js correctly precedes agent-binding.js, and dist/space-doc.js sits between sessions.js and task-board.js. Note this only holds because acceptance.ts is present in the branch — see the divergence note below.
4. rail.space registration — PASS. Present in both zh.ts and en.ts with identical space.* key sets; i18n.test.tsx asserts zh/en key parity and non-empty values, which holds.
5. at(list, i) assertion style — PASS in apps/server/test/space-doc.test.ts (helper defined and used throughout). Minor: apps/server/test/acceptance.test.ts uses value.verdicts[1]?.evidencePath and bare result.value access instead of the same helper — not a failure, just inconsistent with the house style used in the sibling file.
6. Parser total / renderer surgical — the parser holds; the renderer does not. Two defects below.
Defects
B1 — renderSpaceDoc silently discards content for a section whose H2 is missing. (Must-fix)
packages/shared/src/space-doc.ts:318-325 only registers a replacement, and the emit loop (:329-348) only rewrites a heading that already exists in source; it never inserts a missing ## 验收标准 or ## 任务.
Repro:
source = "# 目标:X\n\n## 验收标准\n- [ ] a\n"
doc.tasks = [{ title: "new", ... }]
renderSpaceDoc(doc, source) // => "# 目标:X\n\n## 验收标准\n- [ ] a" — "new" is gone, no ## 任务 created
This is reachable from the shipped flow, not just synthetic input: in SpaceModule.tsx (handleDecide), accepting a space marks every criterion done and calls renderSpaceDoc(next, source). If the spine document has no ## 验收标准 heading (a realistic draft state), the render produces no structural change, writeFile persists an unchanged file, and the user is told the criteria were marked done while nothing changed on disk. That is silent data loss on the one mutation this PR exists to perform.
Suggest: either synthesise a missing section (append ## 验收标准 / ## 任务 with its canonical body at a deterministic position before EOF), or make renderSpaceDoc return the original string unchanged and have the caller surface an explicit "no section to update" state instead of pretending the write succeeded.
B2 — renderSpaceDoc is not byte-preserving around the rewritten sections. (Should-fix)
The contract comment at space-doc.ts:293-297 promises to leave every other byte untouched. Two violations:
The blank line separating a rewritten section from the next heading is eaten:
source = "...| done | eng | 2026-01-01 |\n\n## 风险\n\n无。\n"
rendered = "...| done | eng | 2026-01-01 |\n## 风险\n\n无。\n" // blank line before ## 风险 is gone
Same for a criteria section followed by prose. The loop skips the original body up to the next H2 and pushes the replacement without re-emitting the trailing separator line.
Trailing newline(s) at EOF are dropped when the last section is structured:
"# 目标:X\n\n## 验收标准\n- [ ] a\n" -> "...- [ ] a" // no final newline
"...| a | todo |\n" -> "...| a | todo | |" // no final newline
That produces "no newline at end of file" on every write, which will churn diffs and trip git diff --check/formatters.
The existing round-trip tests (space-doc.test.ts) assert via .includes(...) and fixed-point equality, so both of these pass CI while the bytes still drift. Recommend adding assertions for the exact separator line and the terminal newline.
B3 — tautological guard. (Should-fix / clarity)
space-doc.ts:319: if (criterionLines.length > 0 || doc.criteria.length > 0). criterionLines is doc.criteria.map(...), so both operands are always equal; one is dead. If the intent was "also rewrite an existing-but-empty criteria section", the || does not express it (the section is rewritten whenever it is present, empty or not, regardless of this condition) — the condition is simply a complicated doc.criteria.length > 0.
B4 — English catalog embeds raw Chinese headings. (Minor)
packages/ui/src/locales/en.ts: space.structure.noCriteria = This document has no "## 验收标准" section yet. and space.structure.unparseable = A "## 任务" table was found.... locales/ is excluded from the CJK gate, so this passes, but shipping Chinese literals in the en catalog is inconsistent with the rest of the file and will read oddly in an English UI. Consider referencing the headings from the shared constants or phrasing without them.
Branch divergence (affects mergeability now)
373110ais based onfaafeb4e, which is 1 commit behind currentmain(6f2cb283, the #557 goals/progress merge).ghreports the PR asCONFLICTING/DIRTY.- The concrete conflict is real, not cosmetic: #557 modified
apps/desktop/renderer/src/App.tsxandpackages/ui/src/locales/{en,zh}.ts, and this PR also edits all three. Both branches append key blocks at the end of the locale files and both touch theApp.tsximport block /activeModuleunion, so a rebase is required. Thespace/code itself does not overlap with #557, so the resolution is mechanical. - Ownership issue: PR #559 (
feat(space): acceptance.v1 contract...) is still OPEN and shares this exact basefaafeb4e. This PR carries a byte-for-byte copy ofpackages/shared/src/acceptance.ts(verified identical, 13277 bytes) plus its own edits topackages/shared/src/index.ts,packages/shared/package.json,runtime-layout.cjs, andapps/server/test/acceptance.test.ts— the same files #559 touches. Whichever of the two merges first, the other must be rebased; merging both as-is will conflict on the shared contract file. Recommend deciding which PR ownsacceptance.ts(it reads like #559's deliverable) and rebasing the other onto it, so this PR stays UI-only.
No blocking issues with
Packaging inventory/order, i18n key parity, barrel-import discipline, the gate's decision invariants (canAccept/canReject/buildAcceptanceRecord cross-checks match the acceptance.v1 validator), and the renderer test coverage. Nice touch: agents can only reach review, and only acceptance moves a criterion to done (SpaceModule.handleDecide) — that separation is enforced and tested.
Requested changes
- Fix B1 (missing-section handling) or make the failed write explicit — this is the one that silently loses data.
- Fix B2 (preserve the separator blank line and the terminal newline); extend the round-trip tests to assert exact bytes.
- Simplify the B3 guard.
- Rebase on
main; coordinateacceptance.tsownership with #559 before merging.
B4 is optional polish.
* fix(services): keep unsaved connection drafts across background refreshes The ConnectionForm sync effect unconditionally reset every field, including a typed PAT, whenever the saved connection values changed. A background services refresh landing while a draft was unsaved could therefore silently discard the user's input, and the next submit went out without a token. Guard the sync effect with a dirty flag: user edits mark the form dirty and win over background refreshes; the flag clears once a save or disconnect accepts the server state. Add a regression test where a refresh lands with changed saved addresses while a PAT is typed — it fails on main and passes with this change. Refs #512 * feat(space): add acceptance.v1 contract for human acceptance verdicts Introduces the acceptance record that lets a human accept or reject an agent's deliverable inside a space. This is deliberately separate from approval: an approval gates permission to act, an acceptance is a verdict on work already done. Agents may only push work to review; only an acceptance may move it to done. Validators are fail-closed in the style of goal.v1: allowlisted keys, closed enums, bounded text, and decision/verdict cross-checks: - accepted requires a verdict for every criterion, all passed - rejected requires a non-empty reason note - criteriaIndex values must be unique and non-negative Contract + tests only; the space/document UI lands in a follow-up once this shape is agreed. Refs #558 * fix(space): narrow verdict index access for noUncheckedIndexedAccess Same class of error as the space-doc suite: the base tsconfig sets noUncheckedIndexedAccess and apps/server/tsconfig.json includes test/**, so `verdicts[1].evidencePath` is a possibly-undefined read. CI caught it on #560; this branch carries the same line. Refs #558 * fix(packaging): ship acceptance.js in the runtime allowlist SHARED_RUNTIME_FILES must match packages/shared/src in order, and RUNTIME_FILE_SETS is what actually gets packaged. Without this entry the acceptance contract would be missing from the built app. Refs #558 --------- Co-authored-by: PeterGuy326 <huyz@users.noreply.github.com>
|
Review 基于当前 head 导入约定renderer 的运行时值统一从明确的 高优先级:验收写回会丢失原始文档数据位置: 验收只需要把标准的 checkbox 标记为完成,但 同样,含 65 条验收标准的文档经过 parse/render 后,第 65 条会被静默删除;解析上限不应变成破坏原文的写入行为。 建议验收写回只修改对应 checkbox,保留任务表、未识别内容及超限部分的原文;超限或无法安全编辑时明确阻止写入,不能静默截断。请补充非标准任务表和超限文档的回归用例。 中优先级:主线文件路径不能跟随 UI 语言变化位置: 把中文字面量移出 renderer 解决了 CJK gate,但中文 请使用与语言无关的稳定路径/契约字段,只翻译显示名称。这与 PR 正文已记录的路径集成 blocker 一致;同时仍需解决根目录主线文件与仅允许 验收边界“agent 可以推进到 review,human 才能推进到 done”的边界合理,但当前代码仅把验收 checkbox 写成 验证范围已审查 OCR delegate 列出的 15/15 个可审查文件,无跳过,并阅读相关测试和端点代码。隔离转译当前源码后运行现有文档测试:21/21 通过;上述两种数据丢失复现仍成立。本轮没有重新运行完整应用构建或真实 IPC/GUI 端到端验收;完整 CI 状态来自当前 head 的 GitHub checks。 |
sun-970
left a comment
There was a problem hiding this comment.
Review note — not approving a Draft; this is a substantive read of the diff against the declared blockers.
Verified positively:
acceptance-gate-model.tsis the right shape: accept/reject rules are pure and unit-testable, and every record is round-tripped through the realvalidateAcceptanceRecord, so the gate cannot emit a shape-invalid record.- The "never rewrite prose the parser did not understand" contract holds for ordinary sections (
otherSectionskept, H1/prose untouched, CRLF preserved). - Task column aliasing (任务/标题/task/title…) with per-cell validation +
issues[]instead of silent coercion is a good, total design.
Findings to close before this becomes ready:
-
Cap-then-regenerate is destructive on write.
parseSpaceDocstops atSPACE_DOC_MAX_CRITERIA(64) andSPACE_DOC_MAX_TASKS(128) viabreak, whilerenderSpaceDocregenerates both sections from the capped model. Saving a document that holds >64 criteria or >128 tasks would silently delete the overflow lines from the file — which contradicts "the file is the source of truth". Either surface a parse issue and refuse to render, or preserve the unparsed tail verbatim rather than regenerating the whole body. -
Task cell escaping.
renderSpaceDocjoins cells with" | "and writestask.titleraw. A title containing|(or a newline) breaks the table and the round-trip.SpaceDocTask.rawexists precisely for this, but the renderer never consults it. -
SpaceDocCriterion.markeris not honoured on render. The field's comment says the raw checkbox token is retained "so an unrelated state is not silently lost", yet the renderer emits- [x]/- [ ]fromdone— soXis normalized away. Either honourmarkerwhen rendering, or drop the field; as written the comment overstates what survives. -
Duplicate structured headings. Parse takes last-wins for a repeated
## 任务, but the renderer replaces every occurrence of the heading with the same regenerated body, so a duplicated section would duplicate the table. A single "already emitted per heading" guard, or a parse issue, closes it.
Endorsed as-is: the four product blockers listed in the PR body (spine path vs the knowledge/* write endpoint, no durable acceptance store, no server-side authority on task-state writes, no real-bridge concurrency coverage) are real and correctly scoped. Staying Draft until V6/V7 are green and the App.tsx/locales conflicts against main are resolved is the right call.
Status: draft — do not merge as a completed acceptance workflow
Draft UI follow-up to the now-merged #559 (
acceptance.v1). The base is main. This PR ownsspace-doc.v1and its UI; the acceptance contract and service-draft/security prerequisites have already landed.What works
A document/structure view derives acceptance criteria and task columns from two fixed Markdown headings. The gate requires every criterion to be ticked and a rejection to contain a reason. Records now use #559's required
criteriaCount, pass the real validator, and use a stable identity independent of the display name.Review corrections:
onAcceptor publish an accepted verdict after a document-write failure.Remaining product blockers
knowledge/*. The current shell therefore cannot persist that default document. Resolve the space-to-directory/spine mapping, use one stable filename independent of UI language, and verify the real IPC/server path.AcceptanceRecord. Add durable, version-bound verdict writes and reload them before presenting this as a completed acceptance workflow.Validation ledger
Environment: macOS arm64, Node 24.14.1, npm 11.11.0. Install via
npm ciand build the adjacent design-system at CI-pinned4a82fce41f89f1a55b6b02cdb54f7a4e9ce9d84d. No external services or real credentials are needed for these tests.npm run buildnode --test apps/server/dist/test/acceptance.test.js apps/server/dist/test/space-doc.test.jsnpm run typecheck:renderer && node scripts/run-renderer-tests.mjs renderer/test/space-module.test.tsx renderer/test/space-acceptance-gate.test.ts renderer/test/i18n.test.tsx renderer/test/i18n-cjk-gate.test.tsnpm run prepare:updater && node --test scripts/test/package-layout.test.mjsnpm run build:rendererCompatibility and rollback
No second document database is added. The two headings are format identifiers; prose outside them is not regenerated.
acceptance.v1is still unreleased and its new count is consumed here. The UI is a draft preview, not release acceptance. Revert the additive module, shell wiring, document subpath and packaging/i18n entries to roll back. Browser screenshots and Windows live GUI acceptance were not performed.Refs #558; depends on #559.
Current shared audit blocker: GHSA-ch52-4w7c-c8xp. A separate 4.3.0 experiment clears npm's version-range check but still reproduces the shared-cache reuse issue, so it was not published as a security repair. No audit threshold or required check was weakened.
2026-10-05 follow-up
Synced the updated #559 contract branch and #513 connection-draft fix, so this draft's CI includes the PAT protection its previous Ubuntu run was missing. Both #559 and #513 must land before this UI. Included dependency removal from #562 without lowering the audit gate: get 5.1.0 removes the old HTTP cache chain, build Node minimum 22.12.0, legacy Got/GLOBAL_AGENT_* download options no longer apply.
Fresh local build and renderer typecheck passed. Acceptance/doc/downloader tests passed 47/47; five renderer specs (space module, acceptance gate, service connections, i18n and CJK gate) passed 32/32. Fresh current-head CI is running. This remains Draft even if CI turns green: real document path compatibility, durable AcceptanceRecord storage, caller authorization and real IPC end-to-end acceptance are still not implemented. No claim of product acceptance or merge.
Current-head CI result
All 10 executed checks passed at
b1a8f8419943e0fe0e2df513a1fe1079b70a337e, including full Node 24 jobs, security, both staging/installer platforms and layout parity. The UI remains Draft for the integration blockers above; green tests do not establish durable end-to-end acceptance.Post-contract integration (2026-10-05)
#562, #513 and #559 are merged into main. Retargeted this draft to main and synchronized the branch at
9f703721ad6dd2344523b077baf579ceb0f2ae77; resolved additive CHANGELOG and shared-export conflicts without changing the previously tested source tree. All 11 hosted checks passed on this head, including both full platform tests, security, staging, installers and layout parity. The earlier check results above refer to their stated historical heads. All remaining product blockers above still apply; this PR stays Draft and is not authorized for merge.