| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
fix(chat): harden runtime sync, legacy migration, and record validation (#1050) * fix(chat): harden runtime sync and legacy migration * fix(storage): validate HTTP runtime appends client-side * fix(storage): harden runtime validation and legacy guards * fix(chat): skip malformed legacy messages | 1 个月前 | |
fix(chat): harden runtime sync, legacy migration, and record validation (#1050) * fix(chat): harden runtime sync and legacy migration * fix(storage): validate HTTP runtime appends client-side * fix(storage): harden runtime validation and legacy guards * fix(chat): skip malformed legacy messages | 1 个月前 | |
fix(dsl): migrate away legacy rotate/height fields on line elements (#1261) * fix(dsl): strip legacy rotate/height from line elements on migration and import (#1260) * fix(dsl): cover whiteboard slides in the legacy line strip, drop redundant import wiring Round-1 review findings (#1261): the strip only walked scenes[*].content.canvas.elements, leaving the same bug class latent on interactive whiteboard slides (scenes[*].whiteboards[*].elements); and the import-site sanitize was redundant because the document store already runs the ladder on save, while its comment misdescribed that mechanism. Extend the strip to whiteboards and let the ladder alone own the cleanup. * fix(dsl): cover stage-level whiteboard boards and row envelopes in the legacy line strip Round-2 terminal audit findings (#1261): the strip walked only the document envelope's scene surfaces, so dirty line elements under stage.whiteboard (the stage-level explainer boards) and under bare Scene/Stage row envelopes passed through while the runner stamped the document current. Walk every line-element surface of every migratable envelope: scene canvas, scene whiteboards, and stage whiteboard, at document, Scene-row, and Stage-row roots. * fix(dsl): gate the canvas strip on the slide discriminant; run the ladder on legacy-only exports Round-3 terminal audit findings (#1261): the canvas walk fired on any canvas-shaped content regardless of the scene kind, so a non-slide scene carrying a canvas-shaped app extension would have fields deleted from it — gate on content.type ('slide' or absent, the dirty-line epoch predates schema enforcement). And the lock-free exportDatabase fallback hand-stamped DSL_VERSION onto a payload the ladder never walked, so a backup could read as current on restore and permanently skip the migration — build the export unstamped and let migrate stamp it. * fix(agent-runtime): bring stale-stamped documents current before incremental scene writes Round-4 audit finding (#1261): putScene rejects documents whose stored DSL stamp is older than the current one, and the aggregate read migrates in memory only, so the first server-side tool write into a course stored at an older version (every pre-bump course, including previously imported ones) failed with a version error. Route the server tools' incremental scene writes through a wrapper whose not-current fallback reloads the migrated aggregate and full-saves it with the scene spliced in — the server-side counterpart of the app autosave's catch-and-full-save. * style: prettier * docs(agent-runtime): record the reviewed concurrency window on the stale-stamp fallback | 12 天前 | |
feat(runtime): device-anonymous learner identity + RuntimeStore app bootstrap (#869 Part C, step 1) (#885) * feat(runtime): device-anonymous learnerKey + app RuntimeStore singleton (#869) * feat(runtime): cascade stage deletion into the runtime store (#869) deleteStageWithRelatedData now cascades into the runtime layer after its Dexie transaction completes. The runtime data lives in a separate IndexedDB database (maic-runtime), so it cannot join the transaction, and the cascade goes through deleteStageRuntimeSafely — a helper that never throws (warns with context instead): a broken or hung runtime DB must not brick stage deletion in the main app DB. deleteStageRuntime is idempotent, so a failed cascade can simply be retried. Covered at the helper seam with a stub RuntimeStore (success + throwing); the repo has no Dexie-in-node harness for database.ts and this change does not invent one. * fix(runtime): wire the live deletion path, serialize learner-key minting (#869) Cross-review fixes on the C1 bootstrap: - The runtime cascade was wired into deleteStageWithRelatedData, which has zero callers; the UI classroom-deletion flow goes through deleteStageData in stage-storage. Wire deleteStageRuntimeSafely into that live path too (after its Dexie work, same isolation rationale), and cover the wiring by running the real deleteStageData with its module deps mocked — the repo's established pattern for database-touching code. - getLearnerKey minted twice under concurrency. Same-bundle callers now share one in-flight promise on the default path (failures are not cached), and every path re-reads after writing and returns the PERSISTED key, so a cross-tab race converges on the stored winner instead of keeping an orphaned local mint. - Guard crypto.randomUUID with the house fallback pattern. - Honest docs: the cascade comment no longer claims a retry path exists (orphaned rows are inert today; a startup sweep is deferred to Part C2), and both lib/runtime modules note they are client-only. * fix(runtime): cross-tab mint lock and bounded deletion cascade (#869) - Read-after-write alone still let a tab keep an orphaned learner key when its re-read landed before the other tab's write. Minting now runs under the Web Locks API ('maic:learner-key') where available: grants are mutually exclusive across tabs, the loser re-reads the winner's key inside its grant, and an existing key is never overwritten (so the per-tab memo stays safe). Without navigator.locks (older browsers, non-window contexts) the memo + read-after-write behavior remains, with the residual race named in a comment and accepted — it merely splits one anonymous learner's local history. - deleteStageRuntimeSafely awaited without a bound, so a hung runtime IndexedDB could block the live deletion path — the exact failure the helper exists to isolate. The cascade now races a 5s timeout: on timeout it warns and resolves (orphaned rows stay inert), and the still -pending cascade carries a swallow handler so a late rejection cannot become an unhandled rejection. * fix(runtime): probe for the runtime DB before cascading a stage deletion (#869) deleteStageRuntimeSafely reached openDb() unconditionally, and opening CREATES the maic-runtime database — so deleting a classroom on a device that never wrote runtime data paid an open-or-create of a second IndexedDB DB, and in degraded environments burned the full 5s bound for zero cleanup value. Probe first without creating: where indexedDB.databases() is available, a missing maic-runtime entry returns immediately; where the probe API is unavailable (older Firefox), fall through to the bounded cascade — skipping there would strand real cleanup once Part C2 adds writers. The probe shares the existing try/catch + timeout envelope, so a hanging databases() cannot brick deletion either. The DB name is a module const passed explicitly to BrowserRuntimeStore so probe and store can never drift. | 2 个月前 | |
feat(storage): server-backed runtime — pluggable backend seam, HTTP contract, Postgres backend, reference server (#939) (#946) * ci: run PR checks against the runtime-server-backend integration branch (#939) * feat(storage): RuntimeStore HTTP contract + HttpRuntimeStore client (#939 Part B) (#940) * feat(storage): RuntimeStore HTTP contract + HttpRuntimeStore client (#939 Part B) - documented JSON HTTP contract for every RuntimeStore operation (server-assigned seq, learnerKey derived from auth — never trusted from the request, machine-readable error codes, idempotency notes) - HttpRuntimeStore client with injected fetch + auth headers hook; session reads (including createSession responses) migrate forward on the runtime line so an older server cannot leak stale envelopes - conformance test server bridging the contract onto the browser backend; full runRuntimeStoreContract green over HTTP plus error mapping cases * fix(storage): harden HTTP runtime backend after cross-review (#939 Part B) Cross-review fixes: - appendRecord gates payloads through assertJsonValue: fail loud on values JSON cannot represent (Map, Date, NaN, nested undefined, NUL strings) instead of silently mangling them in transit; payload domain documented in the contract - appendRecord/listRecords responses validate via validateRuntimeRecord (server-assigned seq is no longer trusted verbatim) and listRecords sorts by seq like listSessions already sorted defensively - request headers no longer require an ambient Headers constructor when fetch is injected - conformance server classifies errors structurally (existence checks, DSL validators, version stamps) instead of regex-sniffing messages that interpolate caller-controlled ids - router preserves empty path segments so empty-key calls keep browser no-op semantics instead of shifting onto other routes - stray runtimeDslVersion/seq in request bodies are ignored (store- assigned wins), matching the browser reference; docs updated - loopback integration test exercises the real listening server - docs state the conformance server is test-only; auth-derived learnerKey is Part D's reference server * fix(storage): round-2 cross-review hardening for the HTTP backend (#939 Part B) - json-value guard rewritten: additionally rejects -0, symbol-keyed and non-enumerable own properties, and non-index own properties on arrays; U+2028/U+2029 are accepted again (they round-trip through JSON per RFC 8259 — rejecting them lost legitimate pasted text) - conformance server classifies racing duplicate creates as 409 via a structural post-check, gates payloads and merge learner keys as 400 - segment() rejects '.'/'..' ids the URL layer would fold away - list responses must be arrays and mergeLearner's moved must be a finite number, else a typed MALFORMED_RESPONSE error * fix(storage): round-3 cross-review hardening for the HTTP backend (#939 Part B) - json-value guard: NUL is rejected in object keys too, and unpaired UTF-16 surrogates are rejected in string values and keys (jsonb refuses both; other JSON stacks corrupt lone surrogates to U+FFFD) - typed storage errors survive non-object 200 responses instead of degrading into TypeErrors on .id access - mergeLearner's moved must be a non-negative integer - conformance server classifies missing/non-object request bodies as 400 VALIDATION_FAILED - createSession/appendRecord gate the full init envelope, not only the payload (stray Date/undefined properties, NUL in ids) * fix(storage): round-4 convergence fixes for the HTTP backend (#939 Part B) - envelope JSON gate tolerates explicitly-undefined optional anchors (sceneId: undefined behaves like omission, matching the browser) - body-carried identifiers reject '.'/'..' so nothing persists that the URL layer cannot later address; conformance server mirrors it - mergeLearner keys pass the JSON-domain gate on client and server - json-value guard v4: rejects enumerable accessors (validation/serialize TOCTOU), Array subclasses and null-proto arrays, prototype-supplied array indices; sparse-array test now constructs a genuine hole * fix(storage): round-5 convergence fixes for the HTTP backend (#939 Part B) - undefined-stripping is limited to the DSL-declared optional anchors (sceneId/actionIndex/subAnchor); any other undefined member fails the JSON gate loud instead of being dropped silently - json-value guard: array and object prototype checks are realm-agnostic (chain-shape instead of identity), so ordinary values from another realm are accepted while subclasses stay rejected; shared isLosslessJsonString predicate exported for SQL key guards - conformance server merge route validates target-key addressability * fix(storage): close the toJSON prototype channel in the JSON guard (#939) toJSON is the single channel through which a prototype can alter JSON output — prototype properties never serialize and own accessors are already rejected — so refusing any value with a callable toJSON closes prototype influence on serialization entirely, including prototypes crafted to pass the realm-agnostic chain-shape check. * fix(storage): probe toJSON via descriptor walk, not property read (#939) Reading value.toJSON would execute an inherited accessor, letting a stateful getter hide from validation and reappear at stringify time. The probe now walks own descriptors up the prototype chain without invoking any user code. Post-validation mutation of the caller's object graph is documented as out of scope — it is equally unpreventable for every other validated property. * fix(storage): toJSON probe mirrors JSON.stringify semantics exactly (#939) The own-most descriptor decides: absent is safe, a non-callable data value is an ordinary shadowing member and is safe, a callable data value is rejected, and an accessor is rejected because it cannot be inspected without invoking it. No daylight remains between the guard and the serializer on this property. * feat(storage): PgRuntimeStore — Postgres runtime backend (#939 Part C) (#941) * feat(storage): PgRuntimeStore — Postgres runtime backend over an injected queryable (#939 Part C) - PgRuntimeStore implements RuntimeStore over a minimal injected Queryable (node-postgres and PGlite both satisfy it) — the package keeps zero runtime dependencies beyond @openmaic/dsl - runtime_sessions / runtime_records schema exported as RUNTIME_PG_SCHEMA with idempotent ensureSchema() - appends serialize per session via a session-row lock; MAX(seq)+1 and the insert share one transaction, UNIQUE(session_id, seq) plus bounded retry backstop non-cooperating writers - envelope semantics mirror the browser backend: version stamping, validation gates, migrate-on-read, fail-loud on future-stamped rows - full runRuntimeStoreContract green on PGlite plus PG-specific cases (concurrent-append seq atomicity, ensureSchema and mergeLearner idempotence) * fix(storage): require pinned transactions + JSON payload gate in PG backend (#939 Part C) Cross-review fixes: - withTransaction is now required; the BEGIN/COMMIT fallback is removed (on a pg Pool it spread BEGIN/body/COMMIT across different connections — no real transaction, leaked idle-in-transaction clients, broken mergeLearner atomicity; on a shared pinned client concurrent calls interleaved transactions) - single-statement deletes no longer wrap in a transaction hook call - appendRecord gates payloads through assertJsonValue: fail loud on values JSON cannot represent (Map, Date, NaN, nested undefined, NUL strings) instead of silently persisting something different - append retry also covers 40001/40P01; READ COMMITTED assumption documented at the retry loop - ensureSchema documented as create-only; redundant stage index dropped - deterministic interleaving test proves the 23505 retry path; real PostgreSQL contract lane added (postgres:16 service workflow, pg driver suite skipped locally without PG_CONTRACT_URL) * fix(storage): round-2 cross-review hardening for the PG backend (#939 Part C) - json-value guard rewritten (shared with the HTTP backend): additionally rejects -0, symbol-keyed and non-enumerable own properties, and non-index own properties on arrays; accepts U+2028/U+2029 - storage-pg-contract workflow now triggers for the runtime-server-backend integration branch and fails loud (STORAGE_PG_CONTRACT_REQUIRED=1) when PG_CONTRACT_URL is missing instead of silently skipping - loadSession distinguishes corrupt non-object rows from absent rows so getSession fails loud instead of reporting the session missing * fix(storage): round-3 cross-review hardening for the PG backend (#939 Part C) - json-value guard (shared): NUL rejected in object keys, unpaired UTF-16 surrogates rejected in values and keys - createSession/appendRecord gate the full persisted envelope through assertJsonValue, not only the payload — stray Date/undefined properties and NUL in ids fail loud instead of silently diverging from the returned record or leaking raw 22P05 - real-PG lane covers a genuine 23505 conflict from a second connection and recovery after an aborted transaction - retry-set asymmetry and mergeLearner's unbounded lock set documented * fix(storage): round-4 convergence fixes for the PG backend (#939 Part C) - NUL/lone-surrogate lookup and delete keys resolve to absent/no-op instead of leaking 22021 driver errors; setSessionStatus reports the session missing; mergeLearner from-key moves 0 - mergeLearner destination key passes the JSON-domain gate fail-loud - envelope JSON gate tolerates explicitly-undefined optional anchors, matching the browser backend - json-value guard v4 (shared) + tests for accessors, Array subclasses, prototype-supplied indices * fix(storage): round-5 convergence fixes for the PG backend (#939 Part C) - appendRecord pre-checks the session key like every other lookup path, so a NUL/lone-surrogate sessionId reports 'no session' instead of leaking a 22021 driver error - the queryable-key predicate now structurally reuses the shared isLosslessJsonString export instead of restating the string rule - undefined-stripping limited to the DSL-declared optional anchors - json-value guard: realm-agnostic prototype checks (shared) * fix(storage): close the toJSON prototype channel in the JSON guard (#939) Shared guard change with the HTTP backend branch; adds the crafted null-proto-prototype regression test. Both final-gate reviewers independently converged on this same channel. * fix(storage): probe toJSON via descriptor walk, not property read (#939) Reading value.toJSON would execute an inherited accessor, letting a stateful getter hide from validation and reappear at stringify time. The probe now walks own descriptors up the prototype chain without invoking any user code. Post-validation mutation of the caller's object graph is documented as out of scope — it is equally unpreventable for every other validated property. * fix(storage): toJSON probe mirrors JSON.stringify semantics exactly (#939) The own-most descriptor decides: absent is safe, a non-callable data value is an ordinary shadowing member and is safe, a callable data value is rejected, and an accessor is rejected because it cannot be inspected without invoking it. No daylight remains between the guard and the serializer on this property. * fix(storage): conform HTTP/PG backends to the post-#926 RuntimeStore interface (#943) The chat cutover (#926) added deleteAllRuntime() to the RuntimeStore contract while the HTTP and Postgres backends were developed in parallel against the pre-#926 interface. Implements the method on both backends (single-statement wipe on PG via the FK cascade; DELETE /runtime on the HTTP contract, documented as an operator-gated administrative endpoint), restoring a green typecheck and contract suite on the integration branch. * feat(runtime): injectable RuntimeStore backend + learner-key provider (#939 Part A) (#944) * wip(runtime): backend injection seam — pending gate verification * fix(runtime): cross-review hardening for the storage injection seam - stage-deletion IndexedDB probe applies only to the default browser backend; an injected store always receives deleteStageRuntime - explicit kv argument takes precedence over the configured learner-key provider, matching the store seam's explicit-beats-global rule - configured learner-key resolution is latched with in-flight dedup, mirroring the store singleton; identity changes require app-level handling - factory retry semantics documented; seal errors explain the module-level bootstrap requirement; isRuntimeStorageConfigured() probe and a test-only reset added - client-bootstrap-only contract documented (SSR/HMR caveats) * fix(runtime): snapshot configuration and make the test reset complete - configureRuntimeStorage copies the option fields so mutating the caller's object after configuring cannot swap the sealed backend or identity provider - resetRuntimeStorageForTests now clears every latched consumer cache (store singleton, learner-key in-flight promise) via a reset-hook registry, so a reset-then-reconfigure test actually gets the new backend * fix(runtime): reset also clears the default learner-key caches resetRuntimeStorageForTests left defaultInFlight/defaultKv latched, so a default-path test could leak its anonymous key or KV store into the next test despite the documented full-reset promise. * feat(storage): runtime reference server — auth-derived learnerKey (#939 Part D) (#945) * wip(storage): reference server — pending deleteAllRuntime route + gates * feat(storage): runtime reference server — auth-derived learnerKey enforcement (#939 Part D) - createRuntimeHttpHandler(store, options) wires the documented HTTP contract onto any injected RuntimeStore over node http - authenticate is required; every learner-scoped operation verifies the path/body learnerKey against the authenticated principal (403 FORBIDDEN_LEARNER) — the client-supplied value is never trusted - mergeLearner requires an explicit authorizeMerge grant and admin surfaces (stage cascade, DELETE /runtime) require authorizeAdmin; both default-deny - runnable reference entry demonstrates a pg Pool withTransaction and bearer-token authentication, marked as demo-only - contract suite green through HttpRuntimeStore -> listening reference handler -> PgRuntimeStore(pglite); security matrix tested (401/403 paths, admin default-deny); threat model documented * fix(storage): cross-review hardening for the reference server (#939 Part D) - principal learnerKey is optional: admin/merge-only credentials no longer fabricate learner identity; learner-scoped routes 403 without ownership - full session/record envelopes pass the JSON-domain gate at the handler, so NUL/lone-surrogate identifiers map to 400 instead of 500 - payload validation follows the injected store's validator map (options.payloadValidators) instead of imposing DSL defaults - reference factory accepts authenticate/authorizeMerge/authorizeAdmin/ payloadValidators overrides; docs no longer claim the factory binds to localhost; demo-impersonation warning hardened - 500 responses carry a generic message; details go to the server log - ownership checks precede version checks and unowned sessions read as 404, closing existence/version oracles; cross-learner denial matrix tested per route - merge/delete concurrency documented as linearizable-equivalent with the ownership re-check narrowed to the delete call * fix(storage): align reference-server semantics with the store contract - future-stamped sessions read and delete through unchanged; 409 FUTURE_VERSION applies only to guarded writes (status, append, merge) - check-then-write races reclassify structurally via a post-failure re-fetch: missing session 404, non-active session 400, never a message-sniffed or generic 500 * fix(storage): close the reference CLI's pg pool on startup failure ensureSchema opens connections inside createReferenceRuntimeServer, so a failed schema init or occupied port left the pool holding database resources until its idle timeout. * fix(storage): close the records-route existence oracle cosarah's review point on #946: the records list answered 200 [] for an absent session but 404 for another learner's, so the 404 leaked that an id exists. Absent and foreign sessions now answer identically (404 SESSION_NOT_FOUND) and the HTTP client maps that code back to an empty list, preserving the store contract's absent-lists-as-empty semantics. Contract doc records the server MAY/SHOULD and the client MUST. | 1 个月前 | |
feat(storage): server-backed runtime — pluggable backend seam, HTTP contract, Postgres backend, reference server (#939) (#946) * ci: run PR checks against the runtime-server-backend integration branch (#939) * feat(storage): RuntimeStore HTTP contract + HttpRuntimeStore client (#939 Part B) (#940) * feat(storage): RuntimeStore HTTP contract + HttpRuntimeStore client (#939 Part B) - documented JSON HTTP contract for every RuntimeStore operation (server-assigned seq, learnerKey derived from auth — never trusted from the request, machine-readable error codes, idempotency notes) - HttpRuntimeStore client with injected fetch + auth headers hook; session reads (including createSession responses) migrate forward on the runtime line so an older server cannot leak stale envelopes - conformance test server bridging the contract onto the browser backend; full runRuntimeStoreContract green over HTTP plus error mapping cases * fix(storage): harden HTTP runtime backend after cross-review (#939 Part B) Cross-review fixes: - appendRecord gates payloads through assertJsonValue: fail loud on values JSON cannot represent (Map, Date, NaN, nested undefined, NUL strings) instead of silently mangling them in transit; payload domain documented in the contract - appendRecord/listRecords responses validate via validateRuntimeRecord (server-assigned seq is no longer trusted verbatim) and listRecords sorts by seq like listSessions already sorted defensively - request headers no longer require an ambient Headers constructor when fetch is injected - conformance server classifies errors structurally (existence checks, DSL validators, version stamps) instead of regex-sniffing messages that interpolate caller-controlled ids - router preserves empty path segments so empty-key calls keep browser no-op semantics instead of shifting onto other routes - stray runtimeDslVersion/seq in request bodies are ignored (store- assigned wins), matching the browser reference; docs updated - loopback integration test exercises the real listening server - docs state the conformance server is test-only; auth-derived learnerKey is Part D's reference server * fix(storage): round-2 cross-review hardening for the HTTP backend (#939 Part B) - json-value guard rewritten: additionally rejects -0, symbol-keyed and non-enumerable own properties, and non-index own properties on arrays; U+2028/U+2029 are accepted again (they round-trip through JSON per RFC 8259 — rejecting them lost legitimate pasted text) - conformance server classifies racing duplicate creates as 409 via a structural post-check, gates payloads and merge learner keys as 400 - segment() rejects '.'/'..' ids the URL layer would fold away - list responses must be arrays and mergeLearner's moved must be a finite number, else a typed MALFORMED_RESPONSE error * fix(storage): round-3 cross-review hardening for the HTTP backend (#939 Part B) - json-value guard: NUL is rejected in object keys too, and unpaired UTF-16 surrogates are rejected in string values and keys (jsonb refuses both; other JSON stacks corrupt lone surrogates to U+FFFD) - typed storage errors survive non-object 200 responses instead of degrading into TypeErrors on .id access - mergeLearner's moved must be a non-negative integer - conformance server classifies missing/non-object request bodies as 400 VALIDATION_FAILED - createSession/appendRecord gate the full init envelope, not only the payload (stray Date/undefined properties, NUL in ids) * fix(storage): round-4 convergence fixes for the HTTP backend (#939 Part B) - envelope JSON gate tolerates explicitly-undefined optional anchors (sceneId: undefined behaves like omission, matching the browser) - body-carried identifiers reject '.'/'..' so nothing persists that the URL layer cannot later address; conformance server mirrors it - mergeLearner keys pass the JSON-domain gate on client and server - json-value guard v4: rejects enumerable accessors (validation/serialize TOCTOU), Array subclasses and null-proto arrays, prototype-supplied array indices; sparse-array test now constructs a genuine hole * fix(storage): round-5 convergence fixes for the HTTP backend (#939 Part B) - undefined-stripping is limited to the DSL-declared optional anchors (sceneId/actionIndex/subAnchor); any other undefined member fails the JSON gate loud instead of being dropped silently - json-value guard: array and object prototype checks are realm-agnostic (chain-shape instead of identity), so ordinary values from another realm are accepted while subclasses stay rejected; shared isLosslessJsonString predicate exported for SQL key guards - conformance server merge route validates target-key addressability * fix(storage): close the toJSON prototype channel in the JSON guard (#939) toJSON is the single channel through which a prototype can alter JSON output — prototype properties never serialize and own accessors are already rejected — so refusing any value with a callable toJSON closes prototype influence on serialization entirely, including prototypes crafted to pass the realm-agnostic chain-shape check. * fix(storage): probe toJSON via descriptor walk, not property read (#939) Reading value.toJSON would execute an inherited accessor, letting a stateful getter hide from validation and reappear at stringify time. The probe now walks own descriptors up the prototype chain without invoking any user code. Post-validation mutation of the caller's object graph is documented as out of scope — it is equally unpreventable for every other validated property. * fix(storage): toJSON probe mirrors JSON.stringify semantics exactly (#939) The own-most descriptor decides: absent is safe, a non-callable data value is an ordinary shadowing member and is safe, a callable data value is rejected, and an accessor is rejected because it cannot be inspected without invoking it. No daylight remains between the guard and the serializer on this property. * feat(storage): PgRuntimeStore — Postgres runtime backend (#939 Part C) (#941) * feat(storage): PgRuntimeStore — Postgres runtime backend over an injected queryable (#939 Part C) - PgRuntimeStore implements RuntimeStore over a minimal injected Queryable (node-postgres and PGlite both satisfy it) — the package keeps zero runtime dependencies beyond @openmaic/dsl - runtime_sessions / runtime_records schema exported as RUNTIME_PG_SCHEMA with idempotent ensureSchema() - appends serialize per session via a session-row lock; MAX(seq)+1 and the insert share one transaction, UNIQUE(session_id, seq) plus bounded retry backstop non-cooperating writers - envelope semantics mirror the browser backend: version stamping, validation gates, migrate-on-read, fail-loud on future-stamped rows - full runRuntimeStoreContract green on PGlite plus PG-specific cases (concurrent-append seq atomicity, ensureSchema and mergeLearner idempotence) * fix(storage): require pinned transactions + JSON payload gate in PG backend (#939 Part C) Cross-review fixes: - withTransaction is now required; the BEGIN/COMMIT fallback is removed (on a pg Pool it spread BEGIN/body/COMMIT across different connections — no real transaction, leaked idle-in-transaction clients, broken mergeLearner atomicity; on a shared pinned client concurrent calls interleaved transactions) - single-statement deletes no longer wrap in a transaction hook call - appendRecord gates payloads through assertJsonValue: fail loud on values JSON cannot represent (Map, Date, NaN, nested undefined, NUL strings) instead of silently persisting something different - append retry also covers 40001/40P01; READ COMMITTED assumption documented at the retry loop - ensureSchema documented as create-only; redundant stage index dropped - deterministic interleaving test proves the 23505 retry path; real PostgreSQL contract lane added (postgres:16 service workflow, pg driver suite skipped locally without PG_CONTRACT_URL) * fix(storage): round-2 cross-review hardening for the PG backend (#939 Part C) - json-value guard rewritten (shared with the HTTP backend): additionally rejects -0, symbol-keyed and non-enumerable own properties, and non-index own properties on arrays; accepts U+2028/U+2029 - storage-pg-contract workflow now triggers for the runtime-server-backend integration branch and fails loud (STORAGE_PG_CONTRACT_REQUIRED=1) when PG_CONTRACT_URL is missing instead of silently skipping - loadSession distinguishes corrupt non-object rows from absent rows so getSession fails loud instead of reporting the session missing * fix(storage): round-3 cross-review hardening for the PG backend (#939 Part C) - json-value guard (shared): NUL rejected in object keys, unpaired UTF-16 surrogates rejected in values and keys - createSession/appendRecord gate the full persisted envelope through assertJsonValue, not only the payload — stray Date/undefined properties and NUL in ids fail loud instead of silently diverging from the returned record or leaking raw 22P05 - real-PG lane covers a genuine 23505 conflict from a second connection and recovery after an aborted transaction - retry-set asymmetry and mergeLearner's unbounded lock set documented * fix(storage): round-4 convergence fixes for the PG backend (#939 Part C) - NUL/lone-surrogate lookup and delete keys resolve to absent/no-op instead of leaking 22021 driver errors; setSessionStatus reports the session missing; mergeLearner from-key moves 0 - mergeLearner destination key passes the JSON-domain gate fail-loud - envelope JSON gate tolerates explicitly-undefined optional anchors, matching the browser backend - json-value guard v4 (shared) + tests for accessors, Array subclasses, prototype-supplied indices * fix(storage): round-5 convergence fixes for the PG backend (#939 Part C) - appendRecord pre-checks the session key like every other lookup path, so a NUL/lone-surrogate sessionId reports 'no session' instead of leaking a 22021 driver error - the queryable-key predicate now structurally reuses the shared isLosslessJsonString export instead of restating the string rule - undefined-stripping limited to the DSL-declared optional anchors - json-value guard: realm-agnostic prototype checks (shared) * fix(storage): close the toJSON prototype channel in the JSON guard (#939) Shared guard change with the HTTP backend branch; adds the crafted null-proto-prototype regression test. Both final-gate reviewers independently converged on this same channel. * fix(storage): probe toJSON via descriptor walk, not property read (#939) Reading value.toJSON would execute an inherited accessor, letting a stateful getter hide from validation and reappear at stringify time. The probe now walks own descriptors up the prototype chain without invoking any user code. Post-validation mutation of the caller's object graph is documented as out of scope — it is equally unpreventable for every other validated property. * fix(storage): toJSON probe mirrors JSON.stringify semantics exactly (#939) The own-most descriptor decides: absent is safe, a non-callable data value is an ordinary shadowing member and is safe, a callable data value is rejected, and an accessor is rejected because it cannot be inspected without invoking it. No daylight remains between the guard and the serializer on this property. * fix(storage): conform HTTP/PG backends to the post-#926 RuntimeStore interface (#943) The chat cutover (#926) added deleteAllRuntime() to the RuntimeStore contract while the HTTP and Postgres backends were developed in parallel against the pre-#926 interface. Implements the method on both backends (single-statement wipe on PG via the FK cascade; DELETE /runtime on the HTTP contract, documented as an operator-gated administrative endpoint), restoring a green typecheck and contract suite on the integration branch. * feat(runtime): injectable RuntimeStore backend + learner-key provider (#939 Part A) (#944) * wip(runtime): backend injection seam — pending gate verification * fix(runtime): cross-review hardening for the storage injection seam - stage-deletion IndexedDB probe applies only to the default browser backend; an injected store always receives deleteStageRuntime - explicit kv argument takes precedence over the configured learner-key provider, matching the store seam's explicit-beats-global rule - configured learner-key resolution is latched with in-flight dedup, mirroring the store singleton; identity changes require app-level handling - factory retry semantics documented; seal errors explain the module-level bootstrap requirement; isRuntimeStorageConfigured() probe and a test-only reset added - client-bootstrap-only contract documented (SSR/HMR caveats) * fix(runtime): snapshot configuration and make the test reset complete - configureRuntimeStorage copies the option fields so mutating the caller's object after configuring cannot swap the sealed backend or identity provider - resetRuntimeStorageForTests now clears every latched consumer cache (store singleton, learner-key in-flight promise) via a reset-hook registry, so a reset-then-reconfigure test actually gets the new backend * fix(runtime): reset also clears the default learner-key caches resetRuntimeStorageForTests left defaultInFlight/defaultKv latched, so a default-path test could leak its anonymous key or KV store into the next test despite the documented full-reset promise. * feat(storage): runtime reference server — auth-derived learnerKey (#939 Part D) (#945) * wip(storage): reference server — pending deleteAllRuntime route + gates * feat(storage): runtime reference server — auth-derived learnerKey enforcement (#939 Part D) - createRuntimeHttpHandler(store, options) wires the documented HTTP contract onto any injected RuntimeStore over node http - authenticate is required; every learner-scoped operation verifies the path/body learnerKey against the authenticated principal (403 FORBIDDEN_LEARNER) — the client-supplied value is never trusted - mergeLearner requires an explicit authorizeMerge grant and admin surfaces (stage cascade, DELETE /runtime) require authorizeAdmin; both default-deny - runnable reference entry demonstrates a pg Pool withTransaction and bearer-token authentication, marked as demo-only - contract suite green through HttpRuntimeStore -> listening reference handler -> PgRuntimeStore(pglite); security matrix tested (401/403 paths, admin default-deny); threat model documented * fix(storage): cross-review hardening for the reference server (#939 Part D) - principal learnerKey is optional: admin/merge-only credentials no longer fabricate learner identity; learner-scoped routes 403 without ownership - full session/record envelopes pass the JSON-domain gate at the handler, so NUL/lone-surrogate identifiers map to 400 instead of 500 - payload validation follows the injected store's validator map (options.payloadValidators) instead of imposing DSL defaults - reference factory accepts authenticate/authorizeMerge/authorizeAdmin/ payloadValidators overrides; docs no longer claim the factory binds to localhost; demo-impersonation warning hardened - 500 responses carry a generic message; details go to the server log - ownership checks precede version checks and unowned sessions read as 404, closing existence/version oracles; cross-learner denial matrix tested per route - merge/delete concurrency documented as linearizable-equivalent with the ownership re-check narrowed to the delete call * fix(storage): align reference-server semantics with the store contract - future-stamped sessions read and delete through unchanged; 409 FUTURE_VERSION applies only to guarded writes (status, append, merge) - check-then-write races reclassify structurally via a post-failure re-fetch: missing session 404, non-active session 400, never a message-sniffed or generic 500 * fix(storage): close the reference CLI's pg pool on startup failure ensureSchema opens connections inside createReferenceRuntimeServer, so a failed schema init or occupied port left the pool holding database resources until its idle timeout. * fix(storage): close the records-route existence oracle cosarah's review point on #946: the records list answered 200 [] for an absent session but 404 for another learner's, so the 404 leaked that an id exists. Absent and foreign sessions now answer identically (404 SESSION_NOT_FOUND) and the HTTP client maps that code back to an empty list, preserving the store contract's absent-lists-as-empty semantics. Contract doc records the server MAY/SHOULD and the client MUST. | 1 个月前 | |
feat(courses): add folder grouping (#1005) * feat(courses): add folder grouping behind a feature flag Group courses into user-created folders on the home page. Folders are device-local organization metadata kept in the existing IndexedDB database (new folders + course-to-folder mapping tables, Dexie v16); the course document aggregate owned by the DocumentStore is untouched. - Create / rename (inline) / delete folders (keep or remove members) - Move a course between folders via a hover menu - Navigate into a folder with a breadcrumb back to all courses - Search flattens the list, annotating each course with its folder - Deleting a course cleans up its folder membership - All UI strings internationalized across 9 locales - Gated behind NEXT_PUBLIC_ENABLE_COURSE_FOLDERS (default OFF) * test(courses): update db mocks and version assertion for folder tables The folder feature adds the `stageFolders` table to the deletion cascade and bumps the Dexie schema to v16. Update the affected test db mocks to include the new table and bump the version assertion accordingly. * fix(courses): stop course click-through when selecting from the move menu The move-to-folder menu is rendered inside the course card's clickable container. Selecting a folder item could let the click event reach the card's onClick (opening the course) because only the trigger button stopped propagation. Stop propagation on pointerdown for the trigger and on click for every menu item so selecting a destination never opens the course. * fix(courses): open a dialog for the move-menu new-folder entry The move-to-folder menu's inline "new folder" input never worked: a Radix DropdownMenu is modal, so a raw <input> inside it cannot keep focus — Radix closes the menu the instant the input is focused, dropping the field before anything can be typed. Move the new-folder entry out of the menu: it now asks the caller to open the existing NewFolderDialog and, on confirm, moves the requesting course into the freshly created folder. * feat(courses): drag-to-file folders and stacked cover thumbnails Per review feedback, make folders feel complete: - Drag a course card onto a folder tile to file it there. The tile turns into a clear drop target (ring + overlay) while a drag is over it. The hover 📂 menu remains as the accessible fallback for keyboard/touch. - Replace the placeholder folder icon with a stable stack of up to three member course covers (most recently updated frontmost). Empty folders keep the folder icon; the name and course count are always visible. * feat(courses): refine folder cover stack to a tidy fanned layout Tune the folder-tile cover stack per review: front cover centered and upright, rear covers peek out from alternating sides with a slight tilt and reduced opacity, soft shadow and hairline ring. Reads as a neat pile of course covers rather than an exaggerated fan. * fix(courses): address review — dialog mount, empty-list, rename validation, partial-delete refresh, breadcrumb dedup Per review (wyuc, CHANGES_REQUESTED): 1. Mount NewFolderDialog/DeleteFolderDialog outside the collapsible Recent subtree so they are reachable while it is collapsed; the New-folder button now expands the section before opening the dialog. 2. Keep the Recent/folder surface alive when the course list is empty so folders remain reachable and a first folder can be created. 3. Enforce folder-name validation (width + uniqueness) on rename as well as create, at the storage boundary (FolderNameError) and in the UI. 4. Refresh authoritative state on folder-delete both success and failure, so a partial "remove" failure does not leave stale cards/counts. 5. Drop the duplicated folder breadcrumb from the top section header; keep a single navigation breadcrumb in the content area. Add focused storage tests covering rename validation, membership writes, both deletion modes, and partial-failure propagation. * fix(courses): address 2nd review — folder-view layout, drop highlight, validation parity, a11y, i18n Per review (wyuc, CHANGES_REQUESTED on 9f77686f): P1 — folder view layout: opening a folder compacts the hero and surfaces the library content; the centered header shows a single path "Recent > Folder name"; the empty-folder state sits directly below it; clicking Recent returns to the root grid. The duplicate content breadcrumb is dropped (kept only for search). P2 — correctness & a11y: - Clear the folder drop highlight on every drag exit (dragenter counter) and gate it on the text/stage-id payload. - Duplicate-name check in the dialog is now case-insensitive, matching the storage boundary; FolderNameError is mapped to specific messages. - Renames report the actual submitted width, not a hardcoded 0. - createFolder/renameFolder run read-check-write in one read-write transaction so the uniqueness invariant cannot race across tabs. - The move-menu trigger is focus-visible and pointer-coarse reachable (visible on touch / keyboard), not hover-only. Extra: - Empty library (no courses, no folders) shows a dedicated hint instead of the search-empty string. - Translate all newly added folder strings across ja/ko/pt/ru/ar, fix Simplified-Chinese text in zh-TW, and add the emptyLibraryHint key. * fix(courses): address 3rd review — ungate folders, fix empty-library hero, stabilize the bar Per review (wyuc, CHANGES_REQUESTED on 19729038): P1 — ship folders unconditionally: remove NEXT_PUBLIC_ENABLE_COURSE_FOLDERS, the isCourseFoldersEnabled helper, the .env.example entry, and every flag-on/flag-off UI branch. Folder metadata is always loaded; create, move, drag, and folder navigation are always available. The IndexedDB v16 schema stays intact. P1 — full-screen landing hero only when the library is truly empty: the hero uses min-h-[calc(100dvh-8rem)] only when there are zero courses AND zero folders. A `hydrated` flag waits for both async loads before selecting the layout, so folders arriving from storage do not flip the hero from full-screen to compact. P2 — geometrically stable centered bar: the Recent bar gets a fixed height (h-9) so entering/leaving a folder (which toggles the New-folder action and the folder path) does not shift the search/import controls. * fix(courses): single stable library action bar across root, folder, and empty states Per review (wyuc, CHANGES_REQUESTED on 860a1170): P2 — remove the duplicate floating import controls. The hero section rendered a second Import Classroom / PPTX cluster whenever the course list was empty, duplicating the Recent bar's actions and floating above it as the hero switched layout modes. Import now lives only in the Recent action bar. P1 — keep folder creation reachable for a truly empty library. The Recent section is now always rendered after hydration (not gated on having courses or folders), so a new user with zero of each can still create the first folder or import. The empty-library hint renders below the single stable action bar. Invariant: one library action bar across root, folder, and empty states; state changes alter the path and enabled actions only. * chore: trigger CI after ready-for-review * fix(courses): inherit folder context when importing from inside a folder Per review (wyuc, CHANGES_REQUESTED on 37be4716): [P1] Courses imported from inside a folder were silently placed at the root. The import contract now carries the new stageId to the success callback; the page captures the active folder when the file picker opens (not when the async import resolves) and files the imported course into that folder before the list refresh, so the card appears immediately and the folder count increments. Root imports remain ungrouped. A failed folder assignment surfaces an explicit error toast instead of silently falling back. * fix(courses): stable hero on folder delete, lightweight delete menu, breadcrumb count, empty-name validation Per review (wyuc, CHANGES_REQUESTED on 5e12984f) + QA findings: P1 — deleting the last folder must not expand the hero. The full-screen landing hero is a first-visit treatment only: a session-scoped "librarySeen" flag latches true once the library bar renders, so the hero stays compact across all subsequent create/delete transitions. P2 — replace the heavy two-card delete dialog with progressive disclosure. Empty folder: an inline confirmation overlay on the card tile (matching the course-delete pattern), with the empty-folder copy. Non-empty folder: a compact dropdown beside the delete icon — "Delete folder only" (courses move to unfiled) executes directly; "Delete folder and N courses" opens a lightweight destructive confirmation. The full modal is gone. QA fixes: - Breadcrumb count is now contextual (total at root, in-folder count inside a folder) instead of always showing the global total. - Renaming a folder to empty/whitespace shows a "name cannot be empty" error and shakes, instead of silently exiting edit mode. * fix(courses): cross-review cleanup — dead i18n keys, missing translations, delete-overlay reset Post-commit cross-review (leak audit PASS, wyuc 19/19 PASS) found: - Remove 6 dead i18n keys left over from the removed two-card delete dialog (deleteFolderDesc, deleteFolderUngroupTitle, etc.) across all 9 locales. - Translate all remaining English folder strings in ja/ko/pt/ru/ar (newFolderTitle, folderNameLabel, folderCreate, deleteFolderTitle, etc.). - Fix zh-TW: convert ~17 simplified-Chinese folder strings to Traditional (新增資料夾/建立/刪除/etc.). - Delete the orphaned "feature flag" comment (flag was removed earlier). - Close the inline delete-confirm overlay before the async delete, so a failure leaves the card interactive instead of stuck behind the backdrop. * fix(courses): clear drop highlight on drag end, map limit error in dialog Two minor findings from cross-review: - Escape-cancelled drags may not fire dragleave on every folder target, leaving a highlight ring. The course card now dispatches a 'course-drag-end' window event on dragEnd (fires for both normal drop and Escape cancel); folder cards listen and clear their drop state. - FolderNameError kind 'limit' (thrown at the storage boundary when FOLDER_COUNT_LIMIT is reached in a cross-tab race) now maps to the specific folderCountLimit message in the dialog instead of falling through to a generic hint. Also fixes an SSR hydration mismatch: librarySeen is now initialized to false and read from sessionStorage in useEffect (not in the useState initializer). * fix(courses): atomic folder removal, always-compact hero, no-cover placeholder Per review (wyuc, CHANGES_REQUESTED on 470f5fea): P2 — close the orphan-membership race in 'remove' mode. deleteFolder now captures members, deletes the folder row, and clears all memberships in ONE transaction BEFORE the course-deletion cascade. The folder is gone from the moment the cascade starts, so a concurrent setStageFolder (which checks existence in its own transaction) rejects the assignment. P2 — remove the first-visit full-screen hero. The librarySeen flag caused a visible layout jump on refresh (SSR renders full-screen, then the effect reads sessionStorage and switches to compact). The hero is now always compact (mt-[10vh]); no sessionStorage, no hydration mismatch, no geometry regression. P3 — distinct no-cover fallback for non-empty folders. A folder with courses but no cached thumbnails now shows a neutral stacked-card placeholder instead of the empty-folder icon. Merged with latest main; no conflicts. --------- Co-authored-by: Percy <percy@PercydeMacBook-Pro.local> | 30 天前 | |
feat(document): dirty-set incremental saves + operation-level flush (#983) * feat(document): dirty-set incremental saves + operation-level flush + unload protection Every store mutation used to trigger a debounced FULL aggregate rewrite — editing one word rewrote the whole document, scene switches during playback rewrote it for nothing, and concurrent editors clobbered each other at document granularity. Mutations now mark precise dirt (scene:<id> / structure / stage / outline / currentScene / chats): scene edits flush as version-gated putScene, stage metadata as putStage, structure and outline changes keep the full-save path, and currentScene/chats no longer touch the document at all. Dirt entries are revisioned so a mutation landing during a write is never accidentally cleared, and failed writes retain their dirt for retry. flushStageSave() drains the debounce single-flight; agent apply operations await it so each AI edit is durable before its tool result returns; hidden-page/unload kick a best-effort immediate flush. The PendingChange type stays neutral about the change unit so a future operation-log backend can replace the flush internals at one seam. Co-authored-by: Codex <codex@openai.com> * fix(document): incremental-persistence review round — bypass-proofing, true drain, no stranded dirt Structural mutations carry the cursor they changed (and cursor resolution validates membership on load, so a stale device cursor can never resurrect a deleted scene). The Stage API now wraps the real store in a guard that diffs pre/post state and centrally classifies every raw write — whiteboard, element, canvas, and action-engine mutations can no longer bypass dirty tracking, and new API namespaces inherit the guard by construction (with a lint-like inventory test). flushStageSave is a bounded true drain: an awaiter whose mutation landed mid-flight loops until its own dirt is durable. setStage hands the old document's dirt to a departing flush instead of discarding it, and a failed flush always reschedules. Mixed dirty batches (scene+stage/outline) use the atomic full save — the incremental fast path is homogeneous-only. Chat-half failures keep the chats dirt for retry without changing the tolerated-split outer semantics. Co-authored-by: Codex <codex@openai.com> * fix(document): reviewer round — drain covers caller dirt, full-path chat retry, stamping unification flushStageSave's early-return now only fires when the failing round's snapshot actually covered the caller's dirt — an agent edit landing beside an in-flight failing round is written before the promise resolves, keeping the tool-result durability guarantee unconditional. The full-save path reports chat failures the same way the incremental path does (chats dirt survives and reschedules; the tolerated-split semantics are unchanged). The departing-stage flush retries once and its doc-comment states the best-effort semantics honestly. Stage and scene stamping are extracted and shared — fixing the incremental putScene branch silently dropping the order ?? index normalization the full path applies — and the dead outline mark in setGenerationComplete is gone. Co-authored-by: Codex <codex@openai.com> --------- Co-authored-by: Codex <codex@openai.com> Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com> | 1 个月前 | |
fix(providers): three silent-behavior fixes from the provider audit (#1196) * fix(extract): fail loudly instead of silently falling back to MinerU Cloud A request that selects the self-hosted MinerU extractor without a configured base URL used to fall back to MinerU Cloud whenever a cloud API key was available, sending documents to a third party without the operator's knowledge. The cloud fallback now requires an explicit operator opt-in (ALLOW_MINERU_CLOUD_FALLBACK, default off, documented in .env.example); otherwise the route answers a 422 that names what was configured (self-hosted MinerU) and what was unavailable (its base URL), and points at both remedies. * fix(media): remove the selectable video provider that has no adapter The Sora entry in the video provider catalog could be selected in the settings UI and server config, but had no connectivity or generation dispatch case, so choosing it failed only at execution time. A catalog entry that cannot execute is worse than an absent one: the entry, its type-union member, UI names/icons, store defaults, and the VIDEO_SORA env mapping are removed. A new catalog test pins that every selectable video provider dispatches to its own adapter in both switches. * fix(storage): delete the obsolete no-op storage provider abstraction getStorageProvider() unconditionally returned a NoopStorageProvider and the module swallowed every operation into silence, so any caller believing it had storage got neither bytes nor an error. No real caller exists anywhere in the repo (only lib/storage/client.ts, a separate module with a documented null contract and a live caller, remains). The dead entry point, its type file, and the no-op provider are deleted; a test pins the removal and that the real client upload helper is kept. | 13 天前 | |
feat(whiteboard): add destructive runtime operations (#1173) * feat(whiteboard): add destructive runtime operations * fix(whiteboard): preserve additive exact replay results * fix(whiteboard): support legacy code line ids | 13 天前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 1 个月前 | ||
| 1 个月前 | ||
| 12 天前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 30 天前 | ||
| 1 个月前 | ||
| 13 天前 | ||
| 13 天前 |