From b92eebc5969053ae0784d3356fad2b2e7d329d3b Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 4 Oct 2026 02:54:10 +0800 Subject: [PATCH 01/18] =?UTF-8?q?chore:=20=E6=94=B6=E6=95=9B=20tabletwo=20?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E4=B8=8E=E5=89=8D=E7=AB=AF=20COS=20=E9=97=A8?= =?UTF-8?q?=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 2 +- .github/workflows/upload.yaml | 64 ++++------- README.md | 25 ++++- calcit.cirru | 204 +++++++++++++++++++++------------- deps.cirru | 7 +- dist-server/package.json | 28 ----- package.json | 2 +- yarn.lock | 28 +++-- 8 files changed, 192 insertions(+), 168 deletions(-) delete mode 100644 dist-server/package.json diff --git a/.gitattributes b/.gitattributes index f865be5..9551477 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ -calcit.cirru -diff linguist-generated +calcit.cirru diff linguist-generated yarn.lock -diff linguist-generated diff --git a/.github/workflows/upload.yaml b/.github/workflows/upload.yaml index f6e472c..2ba63cc 100644 --- a/.github/workflows/upload.yaml +++ b/.github/workflows/upload.yaml @@ -9,17 +9,23 @@ on: permissions: contents: read +concurrency: + group: frontend-${{ github.repository }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref }} + cancel-in-progress: false + queue: max + jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 15 steps: - name: Check out repository - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + uses: actions/checkout@v7.0.1 with: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + uses: actions/setup-node@v6.5.0 with: node-version: 24 package-manager-cache: false @@ -28,12 +34,10 @@ jobs: run: | corepack enable corepack prepare yarn@4.12.0 --activate - yarn --version - name: Set up Calcit and Caps - uses: calcit-lang/setup-calcit@ca701be5a471759e442aa053cd100720bbe1f09f # v1.5.0 + uses: calcit-lang/setup-calcit@v1.5.0 with: - version: "0.27.0" tools: calcit,caps caps-version: "0.1.1" @@ -49,18 +53,18 @@ jobs: set -euo pipefail calcit calcit.cirru edit format git diff --exit-code -- calcit.cirru - calcit calcit.cirru --check-only - calcit calcit.cirru --entry server --check-only - calcit calcit.cirru analyze deprecated --summary-only - calcit calcit.cirru --entry server analyze deprecated --summary-only + calcit calcit.cirru --check-only --warn-dyn-method + calcit calcit.cirru --entry server --check-only --warn-dyn-method - name: Select CDN asset path id: asset-path env: CDN_ORIGIN: https://cos-sh.tiye.me + PR_NUMBER: ${{ github.event.pull_request.number }} run: | + set -euo pipefail if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then - asset_prefix="${GITHUB_REPOSITORY}/pr/" + asset_prefix="${GITHUB_REPOSITORY}/pr/${PR_NUMBER}/${GITHUB_RUN_ID}/${GITHUB_RUN_ATTEMPT}/" else asset_prefix="${GITHUB_REPOSITORY}/" fi @@ -79,29 +83,10 @@ jobs: cp js-out/program-ir.cirru dist-server/compact.cirru cp calcit.cirru deps.cirru package.json dist-server/ - - name: Check COS credentials - id: cos-credentials - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository - env: - COS_BUCKET: ${{ secrets.COS_BUCKET }} - COS_SECRET_ID: ${{ secrets.COS_SECRET_ID }} - COS_SECRET_KEY: ${{ secrets.COS_SECRET_KEY }} - run: | - if [[ -n "$COS_BUCKET" && -n "$COS_SECRET_ID" && -n "$COS_SECRET_KEY" ]]; then - echo "available=true" >> "$GITHUB_OUTPUT" - else - echo "available=false" >> "$GITHUB_OUTPUT" - if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then - echo "::warning title=COS upload skipped::Configure COS_BUCKET, COS_SECRET_ID, and COS_SECRET_KEY to run public upload verification." - else - echo "::error title=COS credentials missing::COS_BUCKET, COS_SECRET_ID, and COS_SECRET_KEY are required for production deployment." - exit 1 - fi - fi - - name: Upload and verify frontend assets on COS - if: steps.cos-credentials.outputs.available == 'true' - uses: worktools/cos-upload-action@17e6d213fe9f6772a6090a13c8ba044cd6cbcb09 # v1.1.1 + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + uses: worktools/cos-upload-action@v1.2.0 + timeout-minutes: 10 with: source-dir: dist bucket: ${{ secrets.COS_BUCKET }} @@ -111,11 +96,10 @@ jobs: secret-id: ${{ secrets.COS_SECRET_ID }} secret-key: ${{ secrets.COS_SECRET_KEY }} - - name: Upload web assets id: deploy if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: Pendect/action-rsyncer@2554a1e574cf229fb144f23ea5971dbdca8ae8e3 # v1.1.0 + uses: Pendect/action-rsyncer@v1.1.0 env: DEPLOY_KEY: ${{secrets.rsync_private_key}} with: @@ -125,14 +109,10 @@ jobs: src: 'dist/*' dest: 'rsync-user@tiye.me:/web-assets/repo/${{ github.repository }}' - - name: Display status from deploy - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - run: echo "${{ steps.deploy.outputs.status }}" - - name: Upload server script id: deploy-server if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: Pendect/action-rsyncer@2554a1e574cf229fb144f23ea5971dbdca8ae8e3 # v1.1.0 + uses: Pendect/action-rsyncer@v1.1.0 env: DEPLOY_KEY: ${{secrets.rsync_private_key}} with: @@ -141,7 +121,3 @@ jobs: ssh_options: '' src: 'dist-server/*' dest: 'rsync-user@tiye.me:/servers/tabletwo/' - - - name: Display status from deploy - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - run: echo "${{ steps.deploy-server.outputs.status }}" diff --git a/README.md b/README.md index e508bc3..b541539 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,30 @@ http://repo.topix.im/tabletwo/ The existing web entry and the separate `/servers/tabletwo/` rsync deployment remain in place. COS migration applies only to the frontend `dist/` assets: GitHub Actions builds them against `https://cos-sh.tiye.me/TopixIM/tabletwo/` -(`.../pr/` on pull requests), uploads them, and verifies their public URLs. +(`.../pr////` on pull requests), uploads them, and verifies +their public URLs with the COS Action's built-in verification. Server packaging +and its deployment path are unchanged; generated server packages are not tracked. + +### Calcit migration status + +The candidate uses the formal Calcit/procs 0.28.0 toolchain and COS Action 1.2.0. +Frontend dispatch accepts one Enum, while the server still receives the original +`op`/`data` envelope. Watchers have explicit state and Unit contracts; focus +grouping uses typed MapEntry values and checked String IDs. The projected store +remains a Map, preserving the existing EDN wire representation. + +Server entry checks and the seven public twig/updater definitions pass. Pure Node +replay covers article/paragraph grouping, session filters, missing users, empty +input and the Map EDN roundtrip. These checks do not start a server or access user +storage. + +The frontend is **not yet accepted**: full strict checks and JS generation fail +in published WS-EDN, Respo, Alerts/JS-FFI and Markdown dependencies. Strict Caps +resolution also finds existing dependency conflicts; ordinary Caps/toolchain and +immutable Yarn resolution pass. The existing quality baseline still fails and +has not been increased. Full UI, Vite build and COS upload are unverified. +Necessary published alpha dependencies remain until compatible formal versions +are available. Action versions use readable release tags, which are not immutable. > We need a table for two people. They sit in front of each other, seeing into the eyes, and to compose the same markdown file togather. diff --git a/calcit.cirru b/calcit.cirru index c7f4097..14fb869 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -3,13 +3,13 @@ :about "|Machine-generated snapshot. Do not edit directly — changes will be overwritten. Use `calcit query` to inspect and `calcit edit`/`calcit tree` to modify. Run `calcit docs agents --contract` before mutations; use `--full` for first orientation or changed contract digest. Manual edits must follow format and schema conventions, then run `calcit edit format`." :package |app :entries $ {} - :default $ {} (:description |) (:init-fn 'app.client/main!) (:mode :js) (:reload-fn 'app.client/reload!) + :default $ {} (:description |) (:init-fn 'app.client/main!) (:mode :js) (:reload-fn 'app.client/reload!) (:target :browser) :feature-policy $ {} - :modules $ [] |respo.calcit/ |lilac/ |recollect/ |memof/ |respo-ui.calcit/ |ws-edn.calcit/ |cumulo-util.calcit/ |respo-message.calcit/ |cumulo-reel.calcit/ |alerts.calcit/ |bisection-key/ |respo-feather.calcit/ |respo-markdown.calcit/ + :modules $ [] |respo.calcit/ |recollect/ |memof/ |respo-ui.calcit/ |ws-edn.calcit/ |cumulo-util.calcit/ |respo-message.calcit/ |cumulo-reel.calcit/ |alerts.calcit/ |bisection-key/ |respo-feather.calcit/ |respo-markdown.calcit/ :type-slots $ {} - :server $ {} (:description |) (:init-fn 'app.server/main!) (:mode :native) (:reload-fn 'app.server/reload!) + :server $ {} (:description |) (:init-fn 'app.server/main!) (:mode :native) (:reload-fn 'app.server/reload!) (:target :native) :feature-policy $ {} - :modules $ [] |lilac/ |recollect/ |memof/ |cumulo-util.calcit/ |cumulo-reel.calcit/ |bisection-key/ |calcit.std/ |calcit-wss/ + :modules $ [] |recollect/ |memof/ |cumulo-util.calcit/ |cumulo-reel.calcit/ |bisection-key/ |calcit.std/ |calcit-wss/ :type-slots $ {} :files $ {} 'app.client $ %{} 'FileEntry @@ -19,11 +19,11 @@ {} $ :states $ {} :cursor $ [] :examples $ [] - :schema $ :: 'Ref 'Dynamic + :schema $ :: 'Ref $ :: 'Map 'Tag 'Dynamic '*store $ %{} 'CodeEntry (:doc |) :code $ quote $ defatom *store nil :examples $ [] - :schema $ :: 'Ref 'Dynamic + :schema $ :: 'Ref $ :: 'JsNullish (:: 'Map 'Tag 'Dynamic) 'connect! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn connect! () let @@ -42,45 +42,55 @@ :on-close $ fn (event) (reset! *store nil) (js/console.error |Disconnected) :on-data on-server-data :examples $ [] - :schema $ :: 'Fn $ {} (:return 'Dynamic) + :schema $ :: 'Fn $ {} (:return 'ws-edn.client/WsClient) :args $ [] :features $ #{} :js-ffi 'dispatch! $ %{} 'CodeEntry (:doc |) - :code $ quote $ defn dispatch! (op op-data) - when - and config/dev? $ not= op :states - println |Dispatch op op-data - case-default op - ws-send! $ {} (:kind :op) (:op op) (:data op-data) - :states $ let[] (cursor s) op-data $ reset! *states (update-states @*states cursor s) - :effect/connect $ connect! - :examples $ [] - :schema $ :: 'Fn $ {} (:return 'Unit) - :args $ [] 'Tag 'Dynamic - 'dispatch-action! $ %{} 'CodeEntry (:doc |) - :code $ quote $ defn dispatch-action! (action) + :code $ quote $ defn dispatch! (action) let - action-enum $ if (enum? action) (assert-type action Enum) (:: :noop) - op $ if (tag? action) (assert-type action Tag) (&enum:nth action-enum 0) - params $ if (tag? action) ([]) (&enum:params action-enum) + op $ &enum:nth action 0 + params $ &enum:params action data $ if (= op :states) params $ option:unwrap-or (first params) nil - dispatch! op data + when + and config/dev? $ not= op :states + println |Dispatch op data + case-default op + ws-send! $ {} (:kind :op) (:op op) (:data data) + :states $ let[] (cursor s) data $ reset! *states + assert-type (update-states @*states cursor s) (:: 'Map 'Tag 'Dynamic) + :effect/connect $ do (connect!) &unit :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) - :args $ [] 'Dynamic + :args $ [] 'Enum + :features $ #{} :js-ffi 'main! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn main! () let - highlighter $ unsafe-coerce hljs HighlightHost + highlighter $ unsafe-coerce hljs 'app.ffi.highlight/HighlightHost if config/dev? $ load-console-formatter! highlighter .register-language! |clojure clojure-lang highlighter .register-language! |bash bash-lang highlighter .register-language! |javascript javascript-lang render-app! connect! - add-watch *store :changes $ fn (store prev) (render-app!) - add-watch *states :changes $ fn (states pre) (render-app!) - on-page-touch $ fn () $ if (nil? @*store) (connect!) + add-watch! *store :changes $ fn (store prev) + hint-fn $ {} + :args $ [] + :: 'JsNullish $ :: 'Map 'Tag 'Dynamic + :: 'JsNullish $ :: 'Map 'Tag 'Dynamic + :return 'Unit + render-app! + add-watch! *states :changes $ fn (states pre) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) (:: 'Map 'Tag 'Dynamic) + :return 'Unit + render-app! + on-page-touch $ fn () + hint-fn $ {} + :args $ [] + :return 'Unit + if (nil? @*store) (connect!) + , &unit println |App-started! :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) @@ -93,47 +103,65 @@ :schema $ :: 'Dynamic 'on-server-data $ %{} 'CodeEntry (:doc |) :code $ quote $ defn on-server-data (data) - let - data-map $ unsafe-coerce data 'Map - case-default (&map:get data-map :kind) (js/console.warn "|unknown server data kind:" data) - :patch $ let - changes $ &map:get data-map :data - when config/dev? $ js/console.log |Changes $ to-js-data changes - reset! *store $ patch-twig @*store $ assert-type changes (:: 'List 'recollect.schema/change-op) + do + let + data-map $ decode-map-as data $ :: 'Map 'Tag 'Dynamic + case-default (&map:get data-map :kind) (js/console.warn "|unknown server data kind:" data) + :patch $ let + changes $ &map:get data-map :data + when config/dev? $ js/console.log |Changes $ to-js-data changes + reset! *store $ decode-map-as + patch-twig @*store $ assert-type changes $ :: 'List 'recollect.schema/change-op + :: 'Map 'Tag 'Dynamic + , &unit :examples $ [] - :schema $ :: 'Fn $ {} (:return 'Dynamic) + :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] 'Dynamic :features $ #{} :js-ffi 'reload! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn reload! () - if - or (some? client-errors) (some? server-errors) - hud! |error $ str client-errors &newline server-errors - do (hud! |inactive nil) (remove-watch *store :changes) (remove-watch *states :changes) (clear-cache!) (render-app!) - add-watch *store :changes $ fn (store prev) (render-app!) - add-watch *states :changes $ fn (states prev) (render-app!) - println "|Code updated." + do + if + or (some? client-errors) (some? server-errors) + hud! |error $ str client-errors &newline server-errors + do (hud! |inactive nil) (remove-watch! *store :changes) (remove-watch! *states :changes) (clear-cache!) (render-app!) + add-watch! *store :changes $ fn (store prev) + hint-fn $ {} + :args $ [] + :: 'JsNullish $ :: 'Map 'Tag 'Dynamic + :: 'JsNullish $ :: 'Map 'Tag 'Dynamic + :return 'Unit + render-app! + add-watch! *states :changes $ fn (states prev) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) (:: 'Map 'Tag 'Dynamic) + :return 'Unit + render-app! + println "|Code updated." + , &unit :examples $ [] - :schema $ :: 'Fn $ {} (:return 'Dynamic) + :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] + :features $ #{} :js-ffi 'render-app! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn render-app! () render! mount-target comp-container (&map:get @*states :states) @*store - , dispatch-action! + , dispatch! :examples $ [] - :schema $ :: 'Fn $ {} (:return 'Dynamic) + :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] + :features $ #{} :js-ffi 'simulate-login! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn simulate-login! () let raw $ js/localStorage.getItem $ &map:get config/site :storage-key if (js-present? raw) do (println "|Found storage.") - dispatch! :user/log-in $ parse-cirru-edn $ unsafe-coerce raw String + dispatch! $ :: :user/log-in $ parse-cirru-edn (unsafe-coerce raw String) do $ println "|Found no storage." :examples $ [] - :schema $ :: 'Fn $ {} (:return 'Dynamic) + :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] :features $ #{} :js-ffi :ns $ %{} 'NsEntry (:doc |) @@ -178,7 +206,7 @@ :border $ str "|1px solid " $ hsl 0 0 90 :box-shadow $ str "|0 0 2px " $ hsl 0 0 0 :on-click $ fn (e d!) - d! :session/view-article $ &map:get article-map :id + d! $ :: :session/view-article $ &map:get article-map :id div {} $ :style ui/row-parted <> (&map:get article-map :title) @@ -192,7 +220,7 @@ .show edit-plugin d! $ fn (result) (println "|as result:" result) when not $ blank? $ unsafe-coerce result String - d! :article/title $ {} + d! $ :: :article/title $ {} :id $ &map:get article-map :id :title result =< 16 nil @@ -201,7 +229,8 @@ :color $ hsl 200 80 70 :cursor :pointer fn (e d!) - .show remove-plugin d! $ fn () $ d! :article/remove-one (&map:get article-map :id) + .show remove-plugin d! $ fn () $ d! + :: :article/remove-one $ &map:get article-map :id div {} $ :style $ {} :color $ hsl 0 0 80 @@ -272,7 +301,7 @@ .show create-plugin d! $ fn (result) when not $ blank? $ unsafe-coerce result String - d! :article/create result + d! $ :: :article/create result <> "|Create Article" .render create-plugin :examples $ [] @@ -339,7 +368,8 @@ {} :: 'Map 'String 'Dynamic {} - fn (info d!) (d! :session/remove-message info) + fn (info d!) + d! $ :: :session/remove-message info comp-status-color $ &map:get store-map :color when dev? $ comp-inspect |Store store $ {} (:bottom 0) (:right 0) (:max-width |100%) when dev? $ comp-reel (&map:get store-map :reel-length) @@ -355,7 +385,8 @@ span {} :style $ {} $ :cursor :pointer - :on-click $ fn (e d!) (d! :effect/connect nil) + :on-click $ fn (e d!) + d! $ :: :effect/connect nil <> "|Socket broken! Click to retry." $ {} (:font-family ui/font-fancy) (:font-weight 100) (:font-size 32) :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) @@ -441,18 +472,18 @@ event-map $ unsafe-coerce e 'Map value $ &map:get event-map :value d! cursor $ {} (:time timestamp) (:text value) - d! :paragraph/content $ {} (:id sort-id) (:time timestamp) (:text value) + d! $ :: :paragraph/content $ {} (:id sort-id) (:time timestamp) (:text value) :on-keydown $ fn (e d!) when = &map:get (unsafe-coerce e 'Map) :keycode , 27 - d! :paragraph/finish-editing sort-id + d! $ :: :paragraph/finish-editing sort-id when visible? $ comp-icon :trash {} (:font-size 14) (:cursor :pointer) :color $ hsl 200 80 70 fn (e d!) - .show remove-plugin d! $ fn () $ d! :paragraph/remove sort-id + .show remove-plugin d! $ fn () $ d! (:: :paragraph/remove sort-id) .render remove-plugin :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) @@ -490,7 +521,8 @@ span {} :style $ {} $ :cursor :pointer - :on-click $ fn (e d!) (d! :paragraph/finish-editing sort-id) + :on-click $ fn (e d!) + d! $ :: :paragraph/finish-editing sort-id d! &map:get (unsafe-coerce states 'Map) :cursor , nil @@ -570,7 +602,9 @@ 'on-submit $ %{} 'CodeEntry (:doc |) :code $ quote $ defn on-submit (username password signup?) fn (e dispatch!) - dispatch! (if signup? :user/sign-up :user/log-in) ([] username password) + dispatch! $ if signup? + :: :user/sign-up $ [] username password + :: :user/log-in $ [] username password .setItem js/localStorage (&map:get config/site :storage-key) format-cirru-edn $ [] username password :examples $ [] @@ -602,7 +636,7 @@ div {} :on-click $ fn (e d!) - d! :router/change $ {} $ :name :home + d! $ :: :router/change $ {} (:name :home) :style style-entry <> |Table $ {} $ :font-size 14 <> (str members-count) @@ -611,7 +645,7 @@ {} :style $ {} $ :cursor |pointer :on-click $ fn (e d!) - d! :router/change $ {} $ :name :profile + d! $ :: :router/change $ {} (:name :profile) <> $ if logged-in? |Me |Guest :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) @@ -675,7 +709,7 @@ data $ data-transfer .get-data |text event .stop-propagation! if (not= sort-id data) - d! :paragraph/move $ {} (:target data) (:base sort-id) + d! $ :: :paragraph/move $ {} (:target data) (:base sort-id) :on-dragover $ fn (e d!) let event $ unsafe-coerce @@ -727,18 +761,23 @@ div {} :style $ {} $ :cursor :pointer - :on-click $ fn (e d!) (d! :paragraph/finish-editing sort-id) + :on-click $ fn (e d!) + d! $ :: :paragraph/finish-editing sort-id comp-i :eye 14 $ hsl 200 80 70 div {} :style $ {} $ :cursor :pointer - :on-click $ fn (e d!) (d! :paragraph/edit sort-id) (delay-focus! 400 |.editor-area) + :on-click $ fn (e d!) + d! $ :: :paragraph/edit sort-id + delay-focus! 400 |.editor-area comp-i :edit 14 $ hsl 200 80 70 =< 16 nil div {} :style $ {} $ :cursor :pointer - :on-click $ fn (e d!) (d! :paragraph/append-to sort-id) (delay-focus! 400 |.editor-area) + :on-click $ fn (e d!) + d! $ :: :paragraph/append-to sort-id + delay-focus! 400 |.editor-area comp-i :file-plus 14 $ hsl 200 70 80 :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) @@ -794,7 +833,9 @@ button {} :style $ merge style/button $ {} - :on-click $ fn (e d!) (d! :paragraph/prepend nil) (delay-focus! 400 |.editor-area) + :on-click $ fn (e d!) + d! $ :: :paragraph/prepend nil + delay-focus! 400 |.editor-area <> |Prepend =< nil 16 list-> @@ -828,7 +869,7 @@ let paragraph-map $ unsafe-coerce (last pair) 'Map &map:get paragraph-map :content - .join-str $ str &newline &newline |---- &newline &newline + .join-string $ str &newline &newline |---- &newline &newline html $ str |
 (escape-html content) |
document $ unsafe-coerce (child :document) DocumentWriterHost document .write! html @@ -899,7 +940,8 @@ button {} :style $ merge ui/button $ {} (:color :red) (:border-color :red) - :on-click $ fn (e dispatch!) (dispatch! :user/log-out nil) + :on-click $ fn (e dispatch!) + dispatch! $ :: :user/log-out nil .removeItem js/localStorage $ :storage-key config/site <> "|Log out" :examples $ [] @@ -1315,22 +1357,26 @@ some? $ &map:get session-map :user-id some? $ &map:get session-map router-id :: 'Map 'Number $ :: 'Map 'Tag 'Dynamic - result $ -> filtered - .map-list $ fn (entry) - let-sugar - [] k session - , entry - session-map $ assert-type session 'Map + result $ -> filtered (map-entries) + map $ fn (entry) + hint-fn $ {} + :args $ [] $ :: 'MapEntry 'Number (:: 'Map 'Tag 'Dynamic) + :return $ :: 'Map 'Tag 'Dynamic + let + k $ :key entry + session-map $ :value entry article-id $ &map:get session-map router-id + user-id $ decode-map-as (&map:get session-map :user-id) 'String {} (:id article-id) :name $ option:unwrap-or - get-in users-map $ [] - assert-type (&map:get session-map :user-id) String - , :name + get-in users-map $ [] user-id :name , nil :sid k group-by $ fn (x) - &map:get (assert-type x 'Map) :id + hint-fn $ {} + :args $ [] $ :: 'Map 'Tag 'Dynamic + :return 'String + decode-map-as (&map:get x :id) 'String , result :examples $ [] :schema $ :: 'Fn $ {} (:return 'Dynamic) diff --git a/deps.cirru b/deps.cirru index 3edecbb..0fe81c8 100644 --- a/deps.cirru +++ b/deps.cirru @@ -1,10 +1,10 @@ -{} (:calcit-version |0.27.0) +{} (:calcit-version |0.28.0) :version |0.1.1 :dependencies $ {} (|Cumulo/cumulo-reel.calcit |0.0.47) |Cumulo/cumulo-util.calcit |0.0.23 - |Respo/alerts.calcit |0.10.46 - |Respo/respo-feather.calcit |0.4.21 + |Respo/alerts.calcit |0.10.47 + |Respo/respo-feather.calcit |0.4.22 |Respo/respo-markdown.calcit |0.4.46 |Respo/respo-message.calcit |0.0.28 |Respo/respo-ui.calcit |0.7.32-alpha.3 @@ -12,7 +12,6 @@ |calcit-lang/bisection-key |0.0.29 |calcit-lang/calcit-wss |0.2.32 |calcit-lang/calcit.std |0.2.35 - |calcit-lang/lilac |0.5.9 |calcit-lang/memof |0.0.36 |calcit-lang/recollect |0.0.53 |mvc-works/ws-edn.calcit |0.0.32 diff --git a/dist-server/package.json b/dist-server/package.json deleted file mode 100644 index f46b63d..0000000 --- a/dist-server/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "tabletwo", - "version": "0.1.0", - "description": "A markdown table ot two", - "main": "index.js", - "scripts": { - "compile-page": "cr --emit-js --once", - "release-page": "vite build --base=./", - "watch-page": "cr --emit-js/" - }, - "repository": { - "type": "git", - "url": "git@github.com:TopixIM/tabletwo.git" - }, - "author": "jiyinyiyong", - "license": "MIT", - "devDependencies": { - "bottom-tip": "^0.1.3", - "feather-icons": "^4.28.0", - "highlight.js": "^11.5.0", - "url-parse": "^1.5.10", - "vite": "^2.8.6" - }, - "dependencies": { - "@calcit/procs": "^0.5.25", - "escape-html": "^1.0.3" - } -} diff --git a/package.json b/package.json index 3dd5f55..3dd58b8 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "vite": "^8.2.2" }, "dependencies": { - "@calcit/procs": "0.27.0", + "@calcit/procs": "0.28.0", "escape-html": "^1.0.3", "nanoid": "^5.1.5" } diff --git a/yarn.lock b/yarn.lock index ba7db6b..afb3913 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,14 +5,22 @@ __metadata: version: 8 cacheKey: 10c0 -"@calcit/procs@npm:0.27.0": - version: 0.27.0 - resolution: "@calcit/procs@npm:0.27.0" +"@calcit/finger-vec@npm:0.1.1": + version: 0.1.1 + resolution: "@calcit/finger-vec@npm:0.1.1" + checksum: 10c0/c75e7f58924a7bb226ca4a47f6b0702021796b096c04d7fb7e9a92cb49b11ca64d654ea10d78bde29a8ac2d2d05ef9e7865efd2b358af2ca989d950b04d529a8 + languageName: node + linkType: hard + +"@calcit/procs@npm:0.28.0": + version: 0.28.0 + resolution: "@calcit/procs@npm:0.28.0" dependencies: + "@calcit/finger-vec": "npm:0.1.1" "@calcit/ternary-tree": "npm:0.0.26" - "@cirru/parser.ts": "npm:^0.0.9" + "@cirru/parser.ts": "npm:0.0.10" "@cirru/writer.ts": "npm:^0.1.9" - checksum: 10c0/85edf1724beaf5e3b895f456b9d81ae93100316802b19bc20ab7cbcc61e295892e787c2f9cca0c9c231ba1cc3c3d320ae158d3f8c9fd39eda24500c56ebe99b1 + checksum: 10c0/ba7a8569b924fc1595c3b454a0d3ba5d622f0b3aac8ce35c9210752995ba07013a1ea16ed3f8c039bfb80dbc999f5a90c118f196647fb95d5c64ef5204f978cf languageName: node linkType: hard @@ -23,10 +31,10 @@ __metadata: languageName: node linkType: hard -"@cirru/parser.ts@npm:^0.0.9": - version: 0.0.9 - resolution: "@cirru/parser.ts@npm:0.0.9" - checksum: 10c0/3b13623b8f627ac81adae0cb6a4e3664f0da09115a997ca9b78b8d5d0b0f9b9b18c1ad847e068831cb1052c7da2a00329d2af04421b1024a004eef0ddca5fd7a +"@cirru/parser.ts@npm:0.0.10": + version: 0.0.10 + resolution: "@cirru/parser.ts@npm:0.0.10" + checksum: 10c0/d66a16bcf49c60865fb8e733a5c219289a7c755c6400dea7e7a8d3166ee7841a623030f35f8ee96e232bc40f974f1b79a0ccdf089947fc5779699485102b5995 languageName: node linkType: hard @@ -717,7 +725,7 @@ __metadata: version: 0.0.0-use.local resolution: "tabletwo@workspace:." dependencies: - "@calcit/procs": "npm:0.27.0" + "@calcit/procs": "npm:0.28.0" bottom-tip: "npm:^0.1.3" cirru-color: "npm:^0.2.4" copy-text-to-clipboard: "npm:^3.2.2" From eb6828a0b8e3fe7ede511c442612cc3495fe751b Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 4 Oct 2026 13:51:51 +0800 Subject: [PATCH 02/18] =?UTF-8?q?fix:=20=E6=94=B6=E6=95=9B=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=B7=A5=E5=85=B7=E6=A0=8F=E7=9A=84=E7=8A=B6=E6=80=81?= =?UTF-8?q?=20dispatch=20=E4=B8=8E=E4=BA=8B=E4=BB=B6=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++++++ calcit.cirru | 17 +++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b541539..160a1de 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,13 @@ replay covers article/paragraph grouping, session filters, missing users, empty input and the Map EDN roundtrip. These checks do not start a server or access user storage. +The editor toolbar's close callback now sends one `:states` Enum rather than the +obsolete two-argument dispatch call. Its click props use nominal `DomProps`, and +the callback has the published EventHandler/Unit contract. Three pure native +replays verify dispatch order and empty, Tag, and mixed Tag/String cursors; they +do not validate DOM interaction or the full frontend. Other UI callbacks remain +to be reviewed, and the drag host boundary is unchanged. + The frontend is **not yet accepted**: full strict checks and JS generation fail in published WS-EDN, Respo, Alerts/JS-FFI and Markdown dependencies. Strict Caps resolution also finds existing dependency conflicts; ordinary Caps/toolchain and diff --git a/calcit.cirru b/calcit.cirru index 14fb869..9f56a49 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -519,13 +519,22 @@ .set-data! data-transfer |text $ str sort-id , respo.schema/DomProps span - {} + %{} respo.schema/DomProps :style $ {} $ :cursor :pointer :on-click $ fn (e d!) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} + :args $ [] 'Dynamic + :return 'Unit + :return 'Unit d! $ :: :paragraph/finish-editing sort-id - d! - &map:get (unsafe-coerce states 'Map) :cursor - , nil + let + states-map $ decode-map-as states $ :: 'Map 'Tag 'Dynamic + cursor-value $ .unwrap $ get states-map :cursor + cursor $ decode-map-as cursor-value $ :: 'List 'Dynamic + d! $ :: :states cursor nil + , &unit comp-i :chevron-down 14 $ hsl 200 80 70 :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) From 7403a19967e78b8a5f355472e0712dabda447362 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 4 Oct 2026 13:57:39 +0800 Subject: [PATCH 03/18] =?UTF-8?q?fix:=20=E8=BF=81=E7=A7=BB=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E9=9D=A2=E6=9D=BF=E5=9B=9E=E8=B0=83=E4=B8=8E=E5=8D=95?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E7=8A=B6=E6=80=81=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +++++++++ calcit.cirru | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 160a1de..c6fd12f 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,15 @@ replays verify dispatch order and empty, Tag, and mixed Tag/String cursors; they do not validate DOM interaction or the full frontend. Other UI callbacks remain to be reviewed, and the drag host boundary is unchanged. +The editor panel's textarea also uses nominal `DomProps`. Input, keydown and +delete callbacks have explicit Unit contracts; input sends one `:states` Enum, +checks text/cursor/clock values, and reuses one timestamp for local and wire +updates. Eight pure native cases cover empty/Unicode text, mixed cursors, Esc, +other/missing keycodes and confirmation ordering with mocked clock, confirmation +and dispatch. They do not exercise the actual dialog, clock, DOM or JS backend. +Full panel preprocessing remains blocked in the published Alerts/JS-FFI +`element-first-child` host contract, before complete component acceptance. + The frontend is **not yet accepted**: full strict checks and JS generation fail in published WS-EDN, Respo, Alerts/JS-FFI and Markdown dependencies. Strict Caps resolution also finds existing dependency conflicts; ordinary Caps/toolchain and diff --git a/calcit.cirru b/calcit.cirru index 9f56a49..4fcbbb1 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -456,7 +456,7 @@ :: 'Map 'Tag 'Dynamic comp-editor-toolbar states sort-id =< nil 8 - textarea $ {} + textarea $ %{} respo.schema/DomProps :style $ merge ui/textarea ui/flex $ {} (:width |100%) (:min-height |20vh) (:resize :vertical) (:padding 16) (:font-family ui/font-code) (:font-size 13) (:border-radius |4px) (:line-height |1.6em) (:padding-bottom 120) :border $ str "|1px solid " $ hsl 240 80 90 :background-color $ hsl 0 0 100 @@ -467,23 +467,51 @@ &map:get state :text &map:get paragraph-map :content :on-input $ fn (e d!) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} + :args $ [] 'Dynamic + :return 'Unit + :return 'Unit let - timestamp $ js/Date.now - event-map $ unsafe-coerce e 'Map - value $ &map:get event-map :value - d! cursor $ {} (:time timestamp) (:text value) + timestamp $ decode-map-as (js/Date.now) 'Number + value $ decode-map-as + .unwrap $ get e :value + , 'String + state-cursor $ decode-map-as cursor $ :: 'List 'Dynamic + d! $ :: :states state-cursor $ {} (:time timestamp) (:text value) d! $ :: :paragraph/content $ {} (:id sort-id) (:time timestamp) (:text value) + , &unit :on-keydown $ fn (e d!) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} + :args $ [] 'Dynamic + :return 'Unit + :return 'Unit when = - &map:get (unsafe-coerce e 'Map) :keycode + .unwrap-or (get e :keycode) nil , 27 d! $ :: :paragraph/finish-editing sort-id + , &unit when visible? $ comp-icon :trash {} (:font-size 14) (:cursor :pointer) :color $ hsl 200 80 70 fn (e d!) - .show remove-plugin d! $ fn () $ d! (:: :paragraph/remove sort-id) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} + :args $ [] 'Dynamic + :return 'Unit + :return 'Unit + .show remove-plugin d! $ fn () + hint-fn $ {} + :args $ [] + :return 'Unit + d! $ :: :paragraph/remove sort-id + , &unit + , &unit .render remove-plugin :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) From e3320b9e0a871fc068d82c1109aa5b1e16439bb7 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 4 Oct 2026 14:18:39 +0800 Subject: [PATCH 04/18] =?UTF-8?q?refactor:=20=E6=98=8E=E7=A1=AE=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=20Option=20=E7=8A=B6=E6=80=81=E4=B8=8E=20DOM?= =?UTF-8?q?=20=E6=8C=82=E8=BD=BD=E5=90=88=E5=90=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 +++++++++++++++ calcit.cirru | 30 +++++++++++++++++------------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c6fd12f..923eb80 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,21 @@ Frontend dispatch accepts one Enum, while the server still receives the original grouping uses typed MapEntry values and checked String IDs. The projected store remains a Map, preserving the existing EDN wire representation. +The client store is now `Ref>>`: `none` means offline +and `some` contains the original projected Map. Disconnect, patch, store watchers +and reconnect checks use this contract. Rendering temporarily unwraps to the +existing Map/nil component API; component consumers and projected fields still +need stronger types. Fully refresh after upgrading an already running page, +because HMR retains old nil/Map atoms and watcher closures. The mount target now +has the published `DomElementHost` contract without changing its lookup behavior. + +The patch entry passes strict preprocessing and targeted JS generation. Its +actual emitted function with real Recollect/procs 0.28 passes six initial/nested +Map/list/set update cases and their EDN wire roundtrips in Node 24. Offline/online +watch, disconnect and reconnect callbacks also pass pure native lifecycle replay +with mocked rendering/network/logging. These are not full client entry, Vite, +DOM or production acceptance; open Map fields remain explicit type debt. + Server entry checks and the seven public twig/updater definitions pass. Pure Node replay covers article/paragraph grouping, session filters, missing users, empty input and the Map EDN roundtrip. These checks do not start a server or access user diff --git a/calcit.cirru b/calcit.cirru index 4fcbbb1..50ac344 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -21,9 +21,9 @@ :examples $ [] :schema $ :: 'Ref $ :: 'Map 'Tag 'Dynamic '*store $ %{} 'CodeEntry (:doc |) - :code $ quote $ defatom *store nil + :code $ quote $ defatom *store (Option :none) :examples $ [] - :schema $ :: 'Ref $ :: 'JsNullish (:: 'Map 'Tag 'Dynamic) + :schema $ :: 'Ref $ :: 'Option (:: 'Map 'Tag 'Dynamic) 'connect! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn connect! () let @@ -39,7 +39,9 @@ ws-connect! (str |ws:// host |: port) {} :on-open $ fn (event) (simulate-login!) - :on-close $ fn (event) (reset! *store nil) (js/console.error |Disconnected) + :on-close $ fn (event) + reset! *store $ Option :none + js/console.error |Disconnected :on-data on-server-data :examples $ [] :schema $ :: 'Fn $ {} (:return 'ws-edn.client/WsClient) @@ -76,8 +78,8 @@ add-watch! *store :changes $ fn (store prev) hint-fn $ {} :args $ [] - :: 'JsNullish $ :: 'Map 'Tag 'Dynamic - :: 'JsNullish $ :: 'Map 'Tag 'Dynamic + :: 'Option $ :: 'Map 'Tag 'Dynamic + :: 'Option $ :: 'Map 'Tag 'Dynamic :return 'Unit render-app! add-watch! *states :changes $ fn (states pre) @@ -89,7 +91,7 @@ hint-fn $ {} :args $ [] :return 'Unit - if (nil? @*store) (connect!) + if (.none? @*store) (connect!) , &unit println |App-started! :examples $ [] @@ -100,7 +102,7 @@ :code $ quote $ def mount-target option:unwrap $ query-selector |.app :examples $ [] - :schema $ :: 'Dynamic + :schema $ :: 'js-ffi.browser/DomElementHost 'on-server-data $ %{} 'CodeEntry (:doc |) :code $ quote $ defn on-server-data (data) do @@ -110,9 +112,11 @@ :patch $ let changes $ &map:get data-map :data when config/dev? $ js/console.log |Changes $ to-js-data changes - reset! *store $ decode-map-as - patch-twig @*store $ assert-type changes $ :: 'List 'recollect.schema/change-op - :: 'Map 'Tag 'Dynamic + let + base $ .unwrap-or @*store nil + next-value $ patch-twig base $ assert-type changes (:: 'List 'recollect.schema/change-op) + next-store $ decode-map-as next-value $ :: 'Map 'Tag 'Dynamic + reset! *store $ Option :some next-store , &unit :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) @@ -128,8 +132,8 @@ add-watch! *store :changes $ fn (store prev) hint-fn $ {} :args $ [] - :: 'JsNullish $ :: 'Map 'Tag 'Dynamic - :: 'JsNullish $ :: 'Map 'Tag 'Dynamic + :: 'Option $ :: 'Map 'Tag 'Dynamic + :: 'Option $ :: 'Map 'Tag 'Dynamic :return 'Unit render-app! add-watch! *states :changes $ fn (states prev) @@ -146,7 +150,7 @@ 'render-app! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn render-app! () render! mount-target - comp-container (&map:get @*states :states) @*store + comp-container (&map:get @*states :states) (.unwrap-or @*store nil) , dispatch! :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) From ee7b46f41e46af0e1d90c9af0bcf466b0c2e4ba2 Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 5 Oct 2026 06:44:00 +0800 Subject: [PATCH 05/18] =?UTF-8?q?=E5=8A=A0=E5=BC=BA=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E5=90=88=E5=90=8C=E5=B9=B6=E8=B7=9F=E8=BF=9B?= =?UTF-8?q?=20WS=20=E6=AD=A3=E5=BC=8F=E5=8F=91=E5=B8=83=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 19 ++++++++++++++++--- calcit.cirru | 18 ++++++++++++++++-- deps.cirru | 4 ++-- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 923eb80..8615e63 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,24 @@ checks text/cursor/clock values, and reuses one timestamp for local and wire updates. Eight pure native cases cover empty/Unicode text, mixed cursors, Esc, other/missing keycodes and confirmation ordering with mocked clock, confirmation and dispatch. They do not exercise the actual dialog, clock, DOM or JS backend. -Full panel preprocessing remains blocked in the published Alerts/JS-FFI -`element-first-child` host contract, before complete component acceptance. +Full panel preprocessing still depends on unresolved published Alerts/Respo +contracts, before complete component acceptance. + +导航栏两个点击 props 改为具名 `DomProps`,回调使用已发布 EventHandler 的事件 Map、 +dispatch 与 Unit 合同。原生重放实际回调 AST,验证 home/profile 消息顺序及 Unit 返回; +没有新增测试文件或强转。完整导航公开检查仍为 1/2,定向 JS 被 Respo 的三条 +pick-attrs/pick-event ToString 告警阻止,不代表 DOM 或完整组件验收通过。 + +WS-EDN 跟进正式标签 0.0.33、Cumulo Util 0.0.24(传递依赖要求 JS-FFI alpha.11, +没有追随后续 alpha 或使用源码 hash),旧 WsClient 断言已解除。私有发布依赖及 +工具链核验通过,但严格 Caps 仍有版本冲突。当前服务端完整检查 57/57、原七个 +twig/updater 公开定义全部通过;客户端 228 passed / 17 failed / 1 cascaded / +137 blocked(383 total),失败定义来自已发布 Respo、Alerts、Markdown。 +原质量基线仍有 55 项回归,预算不放宽;项目没有 definition-attached tests, +`test --require-match` 为零匹配失败,不把它当通过。 The frontend is **not yet accepted**: full strict checks and JS generation fail -in published WS-EDN, Respo, Alerts/JS-FFI and Markdown dependencies. Strict Caps +in published Respo, Alerts and Markdown dependencies. Strict Caps resolution also finds existing dependency conflicts; ordinary Caps/toolchain and immutable Yarn resolution pass. The existing quality baseline still fails and has not been increased. Full UI, Vite build and COS upload are unverified. diff --git a/calcit.cirru b/calcit.cirru index 50ac344..bb992ee 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -675,18 +675,32 @@ div {} $ :style ui/column div - {} + %{} respo.schema/DomProps :on-click $ fn (e d!) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} + :args $ [] 'Dynamic + :return 'Unit + :return 'Unit d! $ :: :router/change $ {} (:name :home) + , &unit :style style-entry <> |Table $ {} $ :font-size 14 <> (str members-count) {} $ :font-size 20 div - {} + %{} respo.schema/DomProps :style $ {} $ :cursor |pointer :on-click $ fn (e d!) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} + :args $ [] 'Dynamic + :return 'Unit + :return 'Unit d! $ :: :router/change $ {} (:name :profile) + , &unit <> $ if logged-in? |Me |Guest :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) diff --git a/deps.cirru b/deps.cirru index 0fe81c8..10e0fd1 100644 --- a/deps.cirru +++ b/deps.cirru @@ -2,7 +2,7 @@ {} (:calcit-version |0.28.0) :version |0.1.1 :dependencies $ {} (|Cumulo/cumulo-reel.calcit |0.0.47) - |Cumulo/cumulo-util.calcit |0.0.23 + |Cumulo/cumulo-util.calcit |0.0.24 |Respo/alerts.calcit |0.10.47 |Respo/respo-feather.calcit |0.4.22 |Respo/respo-markdown.calcit |0.4.46 @@ -14,4 +14,4 @@ |calcit-lang/calcit.std |0.2.35 |calcit-lang/memof |0.0.36 |calcit-lang/recollect |0.0.53 - |mvc-works/ws-edn.calcit |0.0.32 + |mvc-works/ws-edn.calcit |0.0.33 From 4be07ed259cd9d1ad47331e7fd5fe42f9d35da5d Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 13 Oct 2026 19:47:25 +0800 Subject: [PATCH 06/18] refactor: validate Tabletwo navigation projection with attached tests --- README.md | 6 +++ calcit.cirru | 133 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 120 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 109500a..5054439 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,12 @@ yarn release-page `Ref>>`,离线时为 None,连接后为 Some。 升级已运行的页面时完整刷新,避免热重载保留旧状态和 watcher。 +导航在投影 Map 的使用边界验证 `logged-in?` 为 Bool,成员计数为 +`Option`:缺失与 nil 保留原来的空值显示,0 仍显示为 0。 +导航 props 使用只声明 style 与 EventHandler 的具名 `NavigationProps`, +通过 Respo 原有 record props 接口渲染,不改变投影或消息的 EDN 格式。 +离线分支先判断 store,再读取在线 session。 + ## 部署 COS 只接收前端 `dist/` 资产。生产前缀为 `TopixIM/tabletwo/`; diff --git a/calcit.cirru b/calcit.cirru index 69d57bb..0add138 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -324,16 +324,22 @@ [] feather.core :refer $ [] comp-icon comp-i 'app.comp.container $ %{} 'FileEntry :defs $ {} + 'NavigationView $ %{} 'CodeEntry (:doc |) + :code $ quote $ defstruct NavigationView (:logged-in? 'Bool) + :count $ :: 'Option 'Number + :examples $ [] + :schema $ :: 'StructDef 'comp-container $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-container (states store) - let - states-map $ unsafe-coerce states 'Map - store-map $ unsafe-coerce store 'Map - session $ unsafe-coerce (&map:get store-map :session) 'Map - paragraph-id $ &map:get session :paragraph-id - router $ unsafe-coerce (&map:get store-map :router) 'Map - router-data $ unsafe-coerce (&map:get router :data) 'Map - if (nil? store) (comp-offline) + if (nil? store) (comp-offline) + let + states-map $ unsafe-coerce states 'Map + store-map $ decode-map-as store $ :: 'Map 'Tag 'Dynamic + navigation-view $ read-navigation-view store-map + session $ decode-map-as (&map:get store-map :session) (:: 'Map 'Tag 'Dynamic) + paragraph-id $ &map:get session :paragraph-id + router $ unsafe-coerce (&map:get store-map :router) 'Map + router-data $ unsafe-coerce (&map:get router :data) 'Map div {} $ :style $ merge unsafe-coerce ui/global $ :: 'Map 'Tag 'Dynamic @@ -343,8 +349,8 @@ {} $ :style $ merge unsafe-coerce ui/expand $ :: 'Map 'Tag 'Dynamic unsafe-coerce ui/row $ :: 'Map 'Tag 'Dynamic - comp-navigation (&map:get store-map :logged-in?) (&map:get store-map :count) - if (&map:get store-map :logged-in?) + comp-navigation (:logged-in? navigation-view) (:count navigation-view) + if (:logged-in? navigation-view) case-default (&map:get router :name) div {} $ :style ui/flex @@ -358,7 +364,7 @@ comp-previewer (>> states :previewer) (&map:get router-data :article) (&map:get router-data :focuses) (&map:get router-data :members) paragraph-id comp-login $ >> states :login let - visible? $ and (&map:get store-map :logged-in?) (some? paragraph-id) + visible? $ and (:logged-in? navigation-view) (some? paragraph-id) = :article $ &map:get router :name comp-editor-panel (>> states :editor) paragraph-id option:unwrap-or @@ -402,6 +408,74 @@ :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) :args $ [] 'String + 'read-navigation-view $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-navigation-view (store) + let + store-map $ decode-map-as store $ :: 'Map 'Tag 'Dynamic + raw-count $ &map:get store-map :count + NavigationView :logged-in? + decode-map-as (&map:get store-map :logged-in?) 'Bool + , :count $ if (nil? raw-count) (Option :none) + Option :some $ decode-map-as raw-count 'Number + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'app.comp.container/NavigationView) + :args $ [] 'Dynamic + :tests $ [] + %{} 'TestEntry (:name |guest-count-is-absent-not-zero) + :code $ quote $ let + view $ read-navigation-view $ {} (:logged-in? false) + assert= false $ :logged-in? view + assert= (Option :none) (:count view) + :tags $ #{} :navigation-view :unit + %{} 'TestEntry (:name |present-zero-count) + :code $ quote $ let + view $ read-navigation-view $ {} (:logged-in? true) (:count 0) + assert= true $ :logged-in? view + assert= (Option :some 0) (:count view) + :tags $ #{} :navigation-view :unit + %{} 'TestEntry (:name |navigation-view-keeps-source-extra-fields) + :code $ quote $ let + store $ {} (:logged-in? false) (:count 2) (:payload |preserved) + view $ read-navigation-view store + assert= false $ :logged-in? view + assert= (Option :some 2) (:count view) + assert= |preserved $ &map:get store :payload + :tags $ #{} :navigation-view :unit + %{} 'TestEntry (:name |rejects-untyped-navigation-values) + :code $ quote $ do + assert= true $ try + do + read-navigation-view $ {} $ :logged-in? |not-a-bool + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + assert= true $ try + do + read-navigation-view $ {} (:logged-in? false) (:count |not-a-number) + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + assert= true $ try + do + read-navigation-view $ {} + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + :tags $ #{} :navigation-view :unit + %{} 'TestEntry (:name |explicit-nil-count-is-absent) + :code $ quote $ let + view $ read-navigation-view $ {} (:logged-in? false) (:count nil) + assert= (Option :none) (:count view) + :tags $ #{} :navigation-view :unit 'style-body $ %{} 'CodeEntry (:doc |) :code $ quote $ def style-body {} $ :padding "|8px 16px" @@ -665,6 +739,12 @@ [] app.config :as config 'app.comp.navigation $ %{} 'FileEntry :defs $ {} + 'NavigationProps $ %{} 'CodeEntry (:doc |) + :code $ quote $ defstruct NavigationProps + :style $ :: 'Map 'Tag 'Dynamic + :on-click 'respo.schema/EventHandler + :examples $ [] + :schema $ :: 'StructDef 'comp-navigation $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-navigation (logged-in? members-count) div @@ -675,8 +755,8 @@ div {} $ :style ui/column div - %{} respo.schema/DomProps - :on-click $ fn (e d!) + NavigationProps :on-click + fn (e d!) hint-fn $ {} :args $ [] (:: 'Map 'Tag 'Dynamic) :: 'Fn $ {} @@ -685,14 +765,14 @@ :return 'Unit d! $ :: :router/change $ {} (:name :home) , &unit - :style style-entry + , :style style-entry <> |Table $ {} $ :font-size 14 - <> (str members-count) + <> (navigation-count-text members-count) {} $ :font-size 20 div - %{} respo.schema/DomProps - :style $ {} $ :cursor |pointer - :on-click $ fn (e d!) + NavigationProps :style + {} $ :cursor |pointer + , :on-click $ fn (e d!) hint-fn $ {} :args $ [] (:: 'Map 'Tag 'Dynamic) :: 'Fn $ {} @@ -704,7 +784,22 @@ <> $ if logged-in? |Me |Guest :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) - :args $ [] 'Bool 'Number + :args $ [] 'Bool $ :: 'Option 'Number + 'navigation-count-text $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn navigation-count-text (members-count) + match members-count + (:some value) (str value) + (:none) (str nil) + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] $ :: 'Option 'Number + :tests $ [] $ %{} 'TestEntry (:name |missing-and-zero-count-display) + :code $ quote $ do + assert= (str nil) + navigation-count-text $ Option :none + assert= |0 $ navigation-count-text $ Option :some 0 + assert= |2 $ navigation-count-text $ Option :some 2 + :tags $ #{} :navigation-view :unit 'style-entry $ %{} 'CodeEntry (:doc |) :code $ quote $ def style-entry {} (:cursor :pointer) (:padding "|8px 0") From 63f6f617c73c62af191db5d2ffdef10b32a50893 Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 13 Oct 2026 21:52:18 +0800 Subject: [PATCH 07/18] fix: narrow projection boundaries and normalize require rules --- .github/workflows/upload.yaml | 3 +- README.md | 14 +- calcit.cirru | 1086 ++++++++++++++++++++++++--------- 3 files changed, 799 insertions(+), 304 deletions(-) diff --git a/.github/workflows/upload.yaml b/.github/workflows/upload.yaml index 27870ba..419f62d 100644 --- a/.github/workflows/upload.yaml +++ b/.github/workflows/upload.yaml @@ -56,6 +56,7 @@ jobs: git diff --exit-code -- calcit.cirru calcit calcit.cirru --check-only --warn-dyn-method calcit calcit.cirru --entry server --check-only --warn-dyn-method + calcit calcit.cirru test --require-match --summary-only - name: Select CDN asset path id: asset-path @@ -79,9 +80,7 @@ jobs: set -euo pipefail calcit calcit.cirru js yarn vite build --base="$VITE_BASE_URL" - calcit calcit.cirru --entry server ir mkdir -p dist-server - cp js-out/program-ir.cirru dist-server/compact.cirru cp calcit.cirru deps.cirru package.json dist-server/ - name: Check COS credentials diff --git a/README.md b/README.md index 5054439..e390ed1 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ yarn install --immutable caps verify --toolchain calcit calcit.cirru --check-only calcit calcit.cirru --entry server --check-only +calcit calcit.cirru test --require-match --summary-only calcit calcit.cirru js yarn release-page ``` @@ -31,6 +32,14 @@ yarn release-page 通过 Respo 原有 record props 接口渲染,不改变投影或消息的 EDN 格式。 离线分支先判断 store,再读取在线 session。 +投影中的标题、段落正文、登录字段和编辑时间在实际使用边界解码; +focus 分组保持 List,session key 保持 Number。登录输入派发一个 `:states` Enum, +登录保存与退出清理使用 JS FFI 模块的类型化 storage 接口。 +`:require` 规则和 `:refer` 名称列表使用 Cirru syntax list,不加多余 `[]`。 + +定义内的 `:tests` 验证空值、Unicode、0、访客投影、编辑时间选择、段落插入和消息顺序。 +当前候选双入口严格检查、28 项 native / 生成 JS 共享回放及前端构建通过。 + ## 部署 COS 只接收前端 `dist/` 资产。生产前缀为 `TopixIM/tabletwo/`; @@ -38,13 +47,16 @@ PR 前缀带 PR 编号、run 和 attempt,使用 COS Action 的内置公共 URL 上传按事件和分支排队,job / 上传分别限制为 15 / 10 分钟。 原网页入口与 `/servers/tabletwo/` rsync 服务端部署路径保持不变。 `dist-server/` 由构建生成,不提交产物。 +服务端包保存 `calcit.cirru` 和依赖清单;服务器安装匹配工具与模块后运行 +`calcit calcit.cirru --entry server`。诊断 IR 不再伪装成 `compact.cirru` 部署。 ## 限制 -- 0.29.3 候选的完整严格检查、业务回归和浏览器验收尚未完成。 +- 完整业务、宿主交互与浏览器验收尚未完成,局部回放不代表整项迁移通过。 - 旧 0.28 分支的隔离回放不能证明此候选或完整 UI 已通过。 - UI state 和投影 Map 的开放字段仍需在具体使用处验证,未宣称已全部静态化。 - 普通 Caps 的传递版本选择告警不等同严格依赖解析通过。 +- 旧质量基线仍报告回归,文件和预算保持不变,不能把它记录为通过。 ## License diff --git a/calcit.cirru b/calcit.cirru index 0add138..29ce115 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -30,12 +30,12 @@ location $ location-snapshot url-obj $ unsafe-coerce url-parse (location :href) true - , ParsedUrlHost - query $ unsafe-coerce (js-get url-obj :query) QueryHost + , 'app.ffi.url/ParsedUrlHost + query $ unsafe-coerce (js-get url-obj :query) 'app.ffi.url/QueryHost raw-host $ js-get query :host raw-port $ js-get query :port - host $ if (js-present? raw-host) (unsafe-coerce raw-host String) (location :hostname) - port $ if (js-present? raw-port) (unsafe-coerce raw-port String) (&map:get config/site :port) + host $ if (js-present? raw-host) raw-host $ location :hostname + port $ if (js-present? raw-port) raw-port $ :port config/site ws-connect! (str |ws:// host |: port) {} :on-open $ fn (event) (simulate-login!) @@ -102,7 +102,7 @@ :code $ quote $ def mount-target option:unwrap $ query-selector |.app :examples $ [] - :schema $ :: 'js-ffi.browser/DomElementHost + :schema $ :: 'respo.dom/DomElement 'on-server-data $ %{} 'CodeEntry (:doc |) :code $ quote $ defn on-server-data (data) do @@ -113,7 +113,9 @@ changes $ &map:get data-map :data when config/dev? $ js/console.log |Changes $ to-js-data changes let - base $ .unwrap-or @*store nil + base $ match @*store + (:some previous-store) previous-store + (:none) nil next-value $ patch-twig base $ assert-type changes (:: 'List 'recollect.schema/change-op) next-store $ decode-map-as next-value $ :: 'Map 'Tag 'Dynamic reset! *store $ Option :some next-store @@ -126,7 +128,7 @@ :code $ quote $ defn reload! () do if - or (some? client-errors) (some? server-errors) + or (non-nil? client-errors) (non-nil? server-errors) hud! |error $ str client-errors &newline server-errors do (hud! |inactive nil) (remove-watch! *store :changes) (remove-watch! *states :changes) (clear-cache!) (render-app!) add-watch! *store :changes $ fn (store prev) @@ -150,7 +152,10 @@ 'render-app! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn render-app! () render! mount-target - comp-container (&map:get @*states :states) (.unwrap-or @*store nil) + comp-container (&map:get @*states :states) + match @*store + (:some online-store) online-store + (:none) nil , dispatch! :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) @@ -159,7 +164,7 @@ 'simulate-login! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn simulate-login! () let - raw $ js/localStorage.getItem $ &map:get config/site :storage-key + raw $ js/localStorage.getItem $ :storage-key config/site if (js-present? raw) do (println "|Found storage.") dispatch! $ :: :user/log-in $ parse-cirru-edn (unsafe-coerce raw String) @@ -171,37 +176,38 @@ :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.client :require - [] respo.core :refer $ [] render! clear-cache! realize-ssr! - [] respo.cursor :refer $ [] update-states - [] app.comp.container :refer $ [] comp-container - [] cljs.reader :refer $ [] read-string - [] app.schema :as schema - [] app.config :as config - [] ws-edn.client :refer $ [] ws-connect! ws-send! - [] recollect.patch :refer $ [] patch-twig - [] applied-science.js-interop :as j - [] |highlight.js/lib/core :default hljs - [] |highlight.js/lib/languages/clojure :default clojure-lang - [] |highlight.js/lib/languages/bash :default bash-lang - [] |highlight.js/lib/languages/javascript :default javascript-lang - [] |url-parse :default url-parse + respo.core :refer $ render! clear-cache! realize-ssr! + respo.cursor :refer $ update-states + app.comp.container :refer $ comp-container + cljs.reader :refer $ read-string + app.schema :as schema + app.config :as config + ws-edn.client :refer $ ws-connect! ws-send! + recollect.patch :refer $ patch-twig + applied-science.js-interop :as j + |highlight.js/lib/core :default hljs + |highlight.js/lib/languages/clojure :default clojure-lang + |highlight.js/lib/languages/bash :default bash-lang + |highlight.js/lib/languages/javascript :default javascript-lang + |url-parse :default url-parse cumulo-util.core :refer $ on-page-touch |./calcit.build-errors :default client-errors |../js-out/calcit.build-errors :default server-errors |bottom-tip :default hud! - js-ffi.browser :refer $ [] location-snapshot query-selector - app.ffi.highlight :refer $ [] HighlightHost - app.ffi.url :refer $ [] ParsedUrlHost QueryHost + js-ffi.browser :refer $ location-snapshot + app.ffi.highlight :refer $ HighlightHost + app.ffi.url :refer $ ParsedUrlHost QueryHost + respo.ffi.browser :refer $ query-selector 'app.comp.articles $ %{} 'FileEntry :defs $ {} 'comp-article $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-article (states article focuses) let article-map $ unsafe-coerce article 'Map + article-title $ read-article-title article focuses-map $ unsafe-coerce focuses 'Map edit-plugin $ use-prompt (>> states :edit) - {} (:text "|New title?") - :initial $ &map:get article-map :title + {} (:text "|New title?") (:initial article-title) remove-plugin $ use-confirm (>> states :remove) {} $ :text "|Sure to delete?" div @@ -213,8 +219,7 @@ d! $ :: :session/view-article $ &map:get article-map :id div {} $ :style ui/row-parted - <> (&map:get article-map :title) - {} $ :font-size 16 + <> article-title $ {} $ :font-size 16 div ({}) comp-icon :edit {} (:font-size 14) @@ -253,7 +258,10 @@ [] (&map:get info-map :id) div {} $ :style $ {} (:margin-right 8) - <> $ &map:get info-map :name + match (read-focus-name info-map) + (:some focus-name) (<> focus-name) + (:none) + span $ {} .render edit-plugin .render remove-plugin :examples $ [] @@ -312,16 +320,71 @@ :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) :args $ [] 'Dynamic 'Dynamic :features $ #{} :js-ffi + 'read-article-title $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-article-title (value) + let + value-map $ decode-map-as value $ :: 'Map 'Tag 'Dynamic + decode-map-as (&map:get value-map :title) 'String + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] 'Dynamic + :tests $ [] + %{} 'TestEntry (:name |empty-and-unicode-text) + :code $ quote $ do + assert= | $ read-article-title $ {} (:title |) + assert= "|内容🌱" $ read-article-title $ {} (:title "|内容🌱") (:extra false) + :tags $ #{} :text-boundary :unit + %{} 'TestEntry (:name |rejects-missing-and-non-text) + :code $ quote $ do + assert= true $ try + do + read-article-title $ {} + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + assert= true $ try + do + read-article-title $ {} $ :title 4 + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + :tags $ #{} :text-boundary :unit + 'read-focus-name $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-focus-name (info) + let + info-map $ decode-map-as info $ :: 'Map 'Tag 'Dynamic + raw-name $ &map:get info-map :name + if (nil? raw-name) (Option :none) + Option :some $ decode-map-as raw-name 'String + :examples $ [] + :schema $ :: 'Fn $ {} + :args $ [] 'Dynamic + :return $ :: 'Option 'String + :tests $ [] $ %{} 'TestEntry (:name |missing-name-remains-absent) + :code $ quote $ do + assert= (Option :none) + read-focus-name $ {} + assert= (Option :none) + read-focus-name $ {} $ :name nil + assert= (Option :some |) + read-focus-name $ {} $ :name | + :tags $ #{} :text-boundary :unit :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.comp.articles :require - [] respo-ui.core :refer $ [] hsl - [] respo-ui.core :as ui - [] respo.core :refer $ [] defcomp <> >> list-> span div button textarea - [] respo.comp.space :refer $ [] =< - [] app.style :as style - [] respo-alerts.core :refer $ [] use-confirm use-prompt - [] feather.core :refer $ [] comp-icon comp-i + respo-ui.core :refer $ hsl + respo-ui.core :as ui + respo.core :refer $ defcomp <> >> list-> span div button textarea + respo.comp.space :refer $ =< + app.style :as style + respo-alerts.core :refer $ use-confirm use-prompt + feather.core :refer $ comp-icon comp-i 'app.comp.container $ %{} 'FileEntry :defs $ {} 'NavigationView $ %{} 'CodeEntry (:doc |) @@ -338,8 +401,15 @@ navigation-view $ read-navigation-view store-map session $ decode-map-as (&map:get store-map :session) (:: 'Map 'Tag 'Dynamic) paragraph-id $ &map:get session :paragraph-id - router $ unsafe-coerce (&map:get store-map :router) 'Map - router-data $ unsafe-coerce (&map:get router :data) 'Map + router $ if (:logged-in? navigation-view) + decode-map-as (&map:get store-map :router) (:: 'Map 'Tag 'Dynamic) + {} + router-data $ if (:logged-in? navigation-view) + decode-map-as (&map:get router :data) (:: 'Map 'Tag 'Dynamic) + {} + raw-color $ &map:get store-map :color + status-color $ if (nil? raw-color) (Option :none) + Option :some $ decode-map-as raw-color 'String div {} $ :style $ merge unsafe-coerce ui/global $ :: 'Map 'Tag 'Dynamic @@ -364,7 +434,7 @@ comp-previewer (>> states :previewer) (&map:get router-data :article) (&map:get router-data :focuses) (&map:get router-data :members) paragraph-id comp-login $ >> states :login let - visible? $ and (:logged-in? navigation-view) (some? paragraph-id) + visible? $ and (:logged-in? navigation-view) (non-nil? paragraph-id) = :article $ &map:get router :name comp-editor-panel (>> states :editor) paragraph-id option:unwrap-or @@ -380,9 +450,9 @@ {} fn (info d!) d! $ :: :session/remove-message info - comp-status-color $ &map:get store-map :color + comp-status-color status-color when dev? $ comp-inspect |Store store $ {} (:bottom 0) (:right 0) (:max-width |100%) - when dev? $ comp-reel (&map:get store-map :reel-length) + when dev? $ comp-reel (read-reel-length store-map) {} (:right 0) (:bottom 40) :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) @@ -403,11 +473,15 @@ :args $ [] 'comp-status-color $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-status-color (color) - div $ {} $ :style - {} (:position :absolute) (:top 8) (:right 8) (:background-color color) (:border-radius |8px) (:height 16) (:width 16) (:transition-duration |200ms) (:opacity 0.8) (:pointer-events :none) + let + indicator-style $ {} (:position :absolute) (:top 8) (:right 8) (:border-radius |8px) (:height 16) (:width 16) (:transition-duration |200ms) (:opacity 0.8) (:pointer-events :none) + div $ {} $ :style + match color + (:some color-value) (assoc indicator-style :background-color color-value) + (:none) indicator-style :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) - :args $ [] 'String + :args $ [] $ :: 'Option 'String 'read-navigation-view $ %{} 'CodeEntry (:doc |) :code $ quote $ defn read-navigation-view (store) let @@ -476,6 +550,43 @@ view $ read-navigation-view $ {} (:logged-in? false) (:count nil) assert= (Option :none) (:count view) :tags $ #{} :navigation-view :unit + 'read-reel-length $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-reel-length (store) + decode-map-as + &map:get + decode-map-as store $ :: 'Map 'Tag 'Dynamic + , :reel-length + , 'Number + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'Number) + :args $ [] 'Dynamic + :tests $ [] + %{} 'TestEntry (:name |preserves-zero-and-numeric-record-count) + :code $ quote $ do + assert= 0 $ read-reel-length $ {} (:reel-length 0) + assert= 2 $ read-reel-length $ {} (:reel-length 2) + :tags $ #{} :projection :unit + %{} 'TestEntry (:name |rejects-missing-or-text-count) + :code $ quote $ do + assert= true $ try + do + read-reel-length $ {} + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + assert= true $ try + do + read-reel-length $ {} $ :reel-length |2 + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + :tags $ #{} :projection :unit 'style-body $ %{} 'CodeEntry (:doc |) :code $ quote $ def style-body {} $ :padding "|8px 16px" @@ -484,23 +595,33 @@ :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.comp.container :require - [] hsl.core :refer $ [] hsl - [] respo-ui.core :as ui - [] respo.core :refer $ [] defcomp <> div span >> button - [] respo.comp.inspect :refer $ [] comp-inspect - [] respo.comp.space :refer $ [] =< - [] app.comp.navigation :refer $ [] comp-navigation - [] app.comp.profile :refer $ [] comp-profile - [] app.comp.login :refer $ [] comp-login - [] respo-message.comp.messages :refer $ [] comp-messages - [] cumulo-reel.comp.reel :refer $ [] comp-reel - [] app.config :refer $ [] dev? - [] app.comp.previewer :refer $ [] comp-previewer - [] app.comp.editor-panel :refer $ [] comp-editor-panel - [] app.comp.articles :refer $ [] comp-articles + hsl.core :refer $ hsl + respo-ui.core :as ui + respo.core :refer $ defcomp <> div span >> button + respo.comp.inspect :refer $ comp-inspect + respo.comp.space :refer $ =< + app.comp.navigation :refer $ comp-navigation + app.comp.profile :refer $ comp-profile + app.comp.login :refer $ comp-login + respo-message.comp.messages :refer $ comp-messages + cumulo-reel.comp.reel :refer $ comp-reel + app.config :refer $ dev? + app.comp.previewer :refer $ comp-previewer + app.comp.editor-panel :refer $ comp-editor-panel + app.comp.articles :refer $ comp-articles 'app.comp.editor-panel $ %{} 'FileEntry - :defs $ {} $ 'comp-editor-panel - %{} 'CodeEntry (:doc |) + :defs $ {} + 'EditorInputProps $ %{} 'CodeEntry (:doc |) + :code $ quote $ defstruct EditorInputProps + :style $ :: 'Map 'Tag 'Dynamic + :class-name 'String + :placeholder 'String + :value 'String + :on-input 'respo.schema/EventHandler + :on-keydown 'respo.schema/EventHandler + :examples $ [] + :schema $ :: 'StructDef + 'comp-editor-panel $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-editor-panel (states sort-id paragraph visible?) let states-map $ unsafe-coerce states 'Map @@ -534,45 +655,40 @@ :: 'Map 'Tag 'Dynamic comp-editor-toolbar states sort-id =< nil 8 - textarea $ %{} respo.schema/DomProps - :style $ merge ui/textarea ui/flex $ {} (:width |100%) (:min-height |20vh) (:resize :vertical) (:padding 16) (:font-family ui/font-code) (:font-size 13) (:border-radius |4px) (:line-height |1.6em) (:padding-bottom 120) + textarea $ EditorInputProps :style + merge ui/textarea ui/flex $ {} (:width |100%) (:min-height |20vh) (:resize :vertical) (:padding 16) (:font-family ui/font-code) (:font-size 13) (:border-radius |4px) (:line-height |1.6em) (:padding-bottom 120) :border $ str "|1px solid " $ hsl 240 80 90 :background-color $ hsl 0 0 100 - :class-name |editor-area - :placeholder |Paragraph - :value $ if - > (&map:get state :time) (&map:get paragraph-map :time) - &map:get state :text - &map:get paragraph-map :content - :on-input $ fn (e d!) - hint-fn $ {} - :args $ [] (:: 'Map 'Tag 'Dynamic) - :: 'Fn $ {} - :args $ [] 'Dynamic - :return 'Unit - :return 'Unit - let - timestamp $ decode-map-as (js/Date.now) 'Number - value $ decode-map-as - .unwrap $ get e :value - , 'String - state-cursor $ decode-map-as cursor $ :: 'List 'Dynamic - d! $ :: :states state-cursor $ {} (:time timestamp) (:text value) - d! $ :: :paragraph/content $ {} (:id sort-id) (:time timestamp) (:text value) + , :class-name |editor-area :placeholder |Paragraph :value (read-editor-text state paragraph-map) :on-input + fn (e d!) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} + :args $ [] 'Dynamic + :return 'Unit + :return 'Unit + let + timestamp $ decode-map-as (js/Date.now) 'Number + value $ decode-map-as + .unwrap $ get e :value + , 'String + state-cursor $ decode-map-as cursor $ :: 'List 'Dynamic + d! $ :: :states state-cursor $ {} (:time timestamp) (:text value) + d! $ :: :paragraph/content $ {} (:id sort-id) (:time timestamp) (:text value) + , &unit + , :on-keydown $ fn (e d!) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} + :args $ [] 'Dynamic + :return 'Unit + :return 'Unit + when + = + .unwrap-or (get e :keycode) nil + , 27 + d! $ :: :paragraph/finish-editing sort-id , &unit - :on-keydown $ fn (e d!) - hint-fn $ {} - :args $ [] (:: 'Map 'Tag 'Dynamic) - :: 'Fn $ {} - :args $ [] 'Dynamic - :return 'Unit - :return 'Unit - when - = - .unwrap-or (get e :keycode) nil - , 27 - d! $ :: :paragraph/finish-editing sort-id - , &unit when visible? $ comp-icon :trash {} (:font-size 14) (:cursor :pointer) :color $ hsl 200 80 70 @@ -595,78 +711,183 @@ :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) :args $ [] 'Dynamic 'Dynamic 'Dynamic 'Bool :features $ #{} :js-ffi + 'read-editor-text $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-editor-text (state paragraph) + let + state-map $ decode-map-as state $ :: 'Map 'Tag 'Dynamic + paragraph-map $ decode-map-as paragraph $ :: 'Map 'Tag 'Dynamic + state-time $ decode-map-as (&map:get state-map :time) 'Number + paragraph-time $ decode-map-as (&map:get paragraph-map :time) 'Number + if (> state-time paragraph-time) + decode-map-as (&map:get state-map :text) 'String + decode-map-as (&map:get paragraph-map :content) 'String + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] 'Dynamic 'Dynamic + :tests $ [] + %{} 'TestEntry (:name |strictly-newer-local-edit-wins) + :code $ quote $ do + assert= "|local🌱" $ read-editor-text + {} (:time 20) (:text "|local🌱") + {} (:time 10) (:content |remote) + assert= |remote $ read-editor-text + {} (:time 10) (:text |local) + {} (:time 10) (:content |remote) + assert= | $ read-editor-text + {} (:time 0) (:text |local) + {} (:time 1) (:content |) + :tags $ #{} :editor-text :unit + %{} 'TestEntry (:name |rejects-invalid-time-without-defaulting) + :code $ quote $ assert= true + try + do + read-editor-text + {} (:time |bad) (:text |local) + {} (:time 10) (:content |remote) + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + :tags $ #{} :editor-text :unit :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.comp.editor-panel :require - [] respo-ui.core :refer $ [] hsl - [] respo-ui.core :as ui - [] respo.core :refer $ [] defcomp <> >> list-> span div button textarea - [] respo-md.comp.md :refer $ [] comp-md-block - [] respo.comp.space :refer $ [] =< - [] app.style :as style - [] app.comp.editor-toolbar :refer $ [] comp-editor-toolbar - [] respo-alerts.core :refer $ [] use-confirm - [] feather.core :refer $ [] comp-i comp-icon + respo-ui.core :refer $ hsl + respo-ui.core :as ui + respo.core :refer $ defcomp <> >> list-> span div button textarea + respo-md.comp.md :refer $ comp-md-block + respo.comp.space :refer $ =< + app.style :as style + app.comp.editor-toolbar :refer $ comp-editor-toolbar + respo-alerts.core :refer $ use-confirm + feather.core :refer $ comp-i comp-icon 'app.comp.editor-toolbar $ %{} 'FileEntry - :defs $ {} $ 'comp-editor-toolbar - %{} 'CodeEntry (:doc |) + :defs $ {} + 'ToolbarCloseProps $ %{} 'CodeEntry (:doc |) + :code $ quote $ defstruct ToolbarCloseProps + :style $ :: 'Map 'Tag 'Dynamic + :on-click 'respo.schema/EventHandler + :examples $ [] + :schema $ :: 'StructDef + 'ToolbarDragProps $ %{} 'CodeEntry (:doc |) + :code $ quote $ defstruct ToolbarDragProps + :style $ :: 'Map 'Tag 'Dynamic + :draggable 'Bool + :on-dragstart 'respo.schema/EventHandler + :examples $ [] + :schema $ :: 'StructDef + 'comp-editor-toolbar $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-editor-toolbar (states sort-id) div - unsafe-coerce - {} - :style $ merge ui/row $ {} (:font-size 16) (:justify-content :flex-end) (:cursor :move) (:padding "|0 8px") - :draggable true - :on-dragstart $ fn (e d!) - let - event $ unsafe-coerce - option:unwrap-or (get e :event) (js-object) - , DragEventHost - data-transfer $ option:unwrap $ js-nullish->option (js-get event :data-transfer) - .set-data! data-transfer |text $ str sort-id - , respo.schema/DomProps + ToolbarDragProps :style + merge ui/row $ {} (:font-size 16) (:justify-content :flex-end) (:cursor :move) (:padding "|0 8px") + , :draggable true :on-dragstart $ fn (e d!) + hint-fn $ {} (:return 'Unit) + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} (:rest 'Dynamic) (:return 'Unit) + :args $ [] 'Dynamic + let + event $ unsafe-coerce + option:unwrap-or (get e :event) (js-object) + , DragEventHost + data-transfer $ option:unwrap $ js-nullish->option (js-get event :data-transfer) + .set-data! data-transfer |text $ str sort-id + , &unit span - %{} respo.schema/DomProps - :style $ {} $ :cursor :pointer - :on-click $ fn (e d!) - hint-fn $ {} - :args $ [] (:: 'Map 'Tag 'Dynamic) - :: 'Fn $ {} - :args $ [] 'Dynamic - :return 'Unit - :return 'Unit - d! $ :: :paragraph/finish-editing sort-id - let - states-map $ decode-map-as states $ :: 'Map 'Tag 'Dynamic - cursor-value $ .unwrap $ get states-map :cursor - cursor $ decode-map-as cursor-value $ :: 'List 'Dynamic - d! $ :: :states cursor nil - , &unit + ToolbarCloseProps :style + {} $ :cursor :pointer + , :on-click $ on-close states sort-id comp-i :chevron-down 14 $ hsl 200 80 70 :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) :args $ [] 'Dynamic 'Dynamic :features $ #{} :js-ffi + 'on-close $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn on-close (states sort-id) + let + states-map $ decode-map-as states $ :: 'Map 'Tag 'Dynamic + cursor $ decode-map-as + .unwrap $ get states-map :cursor + :: 'List 'Dynamic + paragraph-key $ decode-map-as sort-id 'String + fn (event dispatch!) + hint-fn $ {} (:return 'Unit) + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} (:rest 'Dynamic) (:return 'Unit) + :args $ [] 'Dynamic + dispatch! $ :: :paragraph/finish-editing paragraph-key + dispatch! $ :: :states cursor nil + , &unit + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'respo.schema/EventHandler) + :args $ [] 'Dynamic 'Dynamic + :tests $ [] + %{} 'TestEntry (:name |dispatches-finish-before-clear-on-mixed-cursor) + :code $ quote $ let + cursor $ [] :editor |paragraph 2 + handler $ on-close + {} $ :cursor cursor + , |c32 + calls $ ref 0 + assert= &unit $ handler ({}) + fn (op & extra-args) + hint-fn $ {} + :args $ [] 'Dynamic + :rest 'Dynamic + :return 'Unit + assert= ([]) extra-args + assert= + if + = (deref calls) 0 + :: :paragraph/finish-editing |c32 + :: :states cursor nil + , op + reset! calls $ inc $ deref calls + , &unit + assert= 2 $ deref calls + :tags $ #{} :toolbar-close :unit + %{} 'TestEntry (:name |rejects-invalid-cursor-before-creating-handler) + :code $ quote $ assert= true + try + do + on-close + {} $ :cursor |not-a-list + , |c32 + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + :tags $ #{} :toolbar-close :unit :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.comp.editor-toolbar :require - [] respo-ui.core :refer $ [] hsl - [] respo-ui.core :as ui - [] respo.core :refer $ [] defcomp <> >> list-> span div button textarea - [] respo-md.comp.md :refer $ [] comp-md-block - [] respo.comp.space :refer $ [] =< - [] app.style :as style - [] feather.core :refer $ [] comp-i + respo-ui.core :refer $ hsl + respo-ui.core :as ui + respo.core :refer $ defcomp <> >> list-> span div button textarea + respo-md.comp.md :refer $ comp-md-block + respo.comp.space :refer $ =< + app.style :as style + feather.core :refer $ comp-i app.ffi.drag :refer $ DragEventHost 'app.comp.login $ %{} 'FileEntry :defs $ {} + 'LoginFields $ %{} 'CodeEntry (:doc |) + :code $ quote $ defstruct LoginFields (:username 'String) (:password 'String) + :examples $ [] + :schema $ :: 'StructDef 'comp-login $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-login (states) let states-map $ unsafe-coerce states 'Map - cursor $ &map:get states-map :cursor - state $ unsafe-coerce + cursor $ decode-map-as (&map:get states-map :cursor) (:: 'List 'Dynamic) + state $ decode-map-as or (&map:get states-map :data) initial-state - , 'Map + :: 'Map 'Tag 'Dynamic + fields $ read-login-fields state div {} $ :style $ merge unsafe-coerce ui/flex $ :: 'Map 'Tag 'Dynamic @@ -685,26 +906,24 @@ =< nil 16 div ({}) input $ {} (:placeholder |Username) - :value $ &map:get state :username + :value $ :username fields :style $ merge ui/input $ {} (:width 200) - :on-input $ fn (e d!) - d! cursor $ assoc state :username $ &map:get (unsafe-coerce e 'Map) :value + :on-input $ on-input cursor state :username ; div ({}) input $ {} (:placeholder |Password) - :value $ &map:get state :password + :value $ :password fields :style ui/input - :on-input $ fn (e d!) - d! cursor $ assoc state :password $ &map:get (unsafe-coerce e 'Map) :value + :on-input $ on-input cursor state :password =< nil 8 div {} $ :style $ {} (:text-align :right) span $ {} (:inner-text "|Sign up") :style $ merge style/link - :on-click $ on-submit (&map:get state :username) (&map:get state :password) true + :on-click $ on-submit (:username fields) (:password fields) true =< 8 nil span $ {} (:inner-text "|Log in") :style $ merge style/link - :on-click $ on-submit (&map:get state :username) (&map:get state :password) false + :on-click $ on-submit (:username fields) (:password fields) false :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) :args $ [] 'Dynamic @@ -714,29 +933,138 @@ {} (:username |) (:password |) :examples $ [] :schema $ :: 'Dynamic + 'on-input $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn on-input (cursor state field) + fn (event dispatch!) + hint-fn $ {} (:return 'Unit) + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} (:rest 'Dynamic) (:return 'Unit) + :args $ [] 'Dynamic + dispatch! $ :: :states cursor $ assoc state field + decode-map-as (&map:get event :value) 'String + , &unit + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'respo.schema/EventHandler) + :args $ [] (:: 'List 'Dynamic) (:: 'Map 'Tag 'Dynamic) 'Tag + :tests $ [] + %{} 'TestEntry (:name |dispatches-one-state-enum-with-mixed-cursor) + :code $ quote $ let + cursor $ [] :login |panel 2 + state $ {} (:username |old) (:password |secret) (:extra false) + handler $ on-input cursor state :username + calls $ ref 0 + assert= &unit $ handler + {} $ :value "|用户🌱" + fn (op & extra-args) + hint-fn $ {} + :args $ [] 'Dynamic + :return 'Unit + :rest 'Dynamic + assert= ([]) extra-args + assert= + :: :states cursor $ assoc state :username "|用户🌱" + , op + reset! calls $ inc $ deref calls + , &unit + assert= 1 $ deref calls + assert= |old $ &map:get state :username + :tags $ #{} :login-fields :unit + %{} 'TestEntry (:name |rejects-non-text-before-dispatch) + :code $ quote $ let + calls $ ref 0 + handler $ on-input ([]) initial-state :password + assert= true $ try + do + handler + {} $ :value 9 + fn (op & extra-args) + hint-fn $ {} + :args $ [] 'Dynamic + :return 'Unit + :rest 'Dynamic + assert= ([]) extra-args + reset! calls $ inc $ deref calls + , &unit + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + assert= 0 $ deref calls + :tags $ #{} :login-fields :unit 'on-submit $ %{} 'CodeEntry (:doc |) :code $ quote $ defn on-submit (username password signup?) fn (e dispatch!) + hint-fn $ {} (:return 'Unit) + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} (:rest 'Dynamic) (:return 'Unit) + :args $ [] 'Dynamic dispatch! $ if signup? :: :user/sign-up $ [] username password :: :user/log-in $ [] username password - .setItem js/localStorage (&map:get config/site :storage-key) + storage-set! (:storage-key config/site) format-cirru-edn $ [] username password + , &unit :examples $ [] - :schema $ :: 'Fn $ {} (:return 'Dynamic) + :schema $ :: 'Fn $ {} (:return 'respo.schema/EventHandler) :args $ [] 'String 'String 'Bool :features $ #{} :js-ffi + 'read-login-fields $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-login-fields (state) + let + state-map $ decode-map-as state $ :: 'Map 'Tag 'Dynamic + LoginFields :username + decode-map-as (&map:get state-map :username) 'String + , :password $ decode-map-as (&map:get state-map :password) 'String + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'app.comp.login/LoginFields) + :args $ [] 'Dynamic + :tests $ [] + %{} 'TestEntry (:name |empty-and-unicode-fields-preserve-extras) + :code $ quote $ let + empty-fields $ read-login-fields initial-state + source $ {} (:username "|用户🌱") (:password "|密钥") (:extra false) + filled $ read-login-fields source + assert= | $ :username empty-fields + assert= | $ :password empty-fields + assert= "|用户🌱" $ :username filled + assert= "|密钥" $ :password filled + assert= false $ &map:get source :extra + :tags $ #{} :login-fields :unit + %{} 'TestEntry (:name |rejects-invalid-and-missing-fields) + :code $ quote $ do + assert= true $ try + do + read-login-fields $ {} (:username 2) (:password |) + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + assert= true $ try + do + read-login-fields $ {} $ :username | + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + :tags $ #{} :login-fields :unit :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.comp.login :require - [] respo.core :refer $ [] defcomp <> div input button span - [] respo.comp.space :refer $ [] =< - [] respo.comp.inspect :refer $ [] comp-inspect - [] respo-ui.core :as ui - [] app.schema :as schema - [] app.style :as style - [] respo-md.comp.md :refer $ [] comp-md-block - [] app.config :as config + respo.core :refer $ defcomp <> div input button span + respo.comp.space :refer $ =< + respo.comp.inspect :refer $ comp-inspect + respo-ui.core :as ui + app.schema :as schema + app.style :as style + respo-md.comp.md :refer $ comp-md-block + app.config :as config + js-ffi.browser :refer $ storage-set! 'app.comp.navigation $ %{} 'FileEntry :defs $ {} 'NavigationProps $ %{} 'CodeEntry (:doc |) @@ -808,27 +1136,29 @@ :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.comp.navigation :require - [] respo-ui.core :refer $ [] hsl - [] respo-ui.core :as ui - [] respo.core :refer $ [] defcomp <> span div - [] respo.comp.space :refer $ [] =< + respo-ui.core :refer $ hsl + respo-ui.core :as ui + respo.core :refer $ defcomp <> span div + respo.comp.space :refer $ =< 'app.comp.previewer $ %{} 'FileEntry :defs $ {} 'comp-info-list $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-info-list (focus-list) let - focus-map $ unsafe-coerce focus-list 'Map + focus-records $ read-focus-list focus-list list-> {} $ :style ui/row - -> focus-map (.to-list) - .map $ fn (info) - let - info-map $ unsafe-coerce info 'Map - [] (&map:get info-map :sid) - div - {} $ :style $ {} (:padding "|0 8px") (:border-radius |16px) (:margin-right 8) - :border $ str "|1px solid " $ hsl 0 0 90 - <> $ &map:get info-map :name + -> focus-records $ .map $ fn (info) + let + info-map info + [] (&map:get info-map :sid) + div + {} $ :style $ {} (:padding "|0 8px") (:border-radius |16px) (:margin-right 8) + :border $ str "|1px solid " $ hsl 0 0 90 + match (read-focus-name info-map) + (:none) + span $ {} + (:some focus-name) (<> focus-name) :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) :args $ [] 'Dynamic @@ -838,6 +1168,7 @@ let states-map $ unsafe-coerce states 'Map paragraph-map $ unsafe-coerce paragraph 'Map + paragraph-text $ read-paragraph-content paragraph cursor $ &map:get states-map :cursor state $ or (&map:get states-map :data) {} (:text |) (:time 0) @@ -873,20 +1204,19 @@ , DragEventHost event .prevent-default! , respo.schema/DomProps - comp-md-block (&map:get paragraph-map :content) - {} (:class-name |preview-content) - :style $ {} $ :padding "|0 16px" - :highlight $ fn (code lang) - if (contains? supprted-langs lang) - let - highlighter $ unsafe-coerce hljs HighlightHost - result $ highlighter .highlight - option:unwrap-or - get supprted-langs $ assert-type lang 'String - , | - , code - result :value - escape-html code + comp-md-block paragraph-text $ {} (:class-name |preview-content) + :style $ {} $ :padding "|0 16px" + :highlight $ fn (code lang) + if (contains? supprted-langs lang) + let + highlighter $ unsafe-coerce hljs HighlightHost + result $ highlighter .highlight + option:unwrap-or + get supprted-langs $ assert-type lang 'String + , | + , code + result :value + escape-html code div unsafe-coerce {} @@ -937,68 +1267,70 @@ :code $ quote $ defcomp comp-previewer (states article focuses members sort-id) let article-map $ unsafe-coerce article 'Map + article-title $ read-article-title article focuses-map $ unsafe-coerce focuses 'Map members-map $ unsafe-coerce members 'Map paragraphs-map $ unsafe-coerce (&map:get article-map :paragraphs) 'Map - div $ {} $ :style - merge - unsafe-coerce ui/flex $ :: 'Map 'Tag 'Dynamic - unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} (:overflow :auto) (:padding-bottom 20) (:padding-top 32) - :: 'Map 'Tag 'Dynamic div {} $ :style $ merge + unsafe-coerce ui/flex $ :: 'Map 'Tag 'Dynamic unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic unsafe-coerce - {} (:max-width 960) (:width |96%) (:margin "|0px auto") + {} (:overflow :auto) (:padding-bottom 20) (:padding-top 32) :: 'Map 'Tag 'Dynamic div - {} $ :style ui/row-parted + {} $ :style $ merge + unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic + unsafe-coerce + {} (:max-width 960) (:width |96%) (:margin "|0px auto") + :: 'Map 'Tag 'Dynamic div - {} $ :style $ merge - unsafe-coerce ui/row $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} $ :align-items :center - :: 'Map 'Tag 'Dynamic - <> (&map:get article-map :title) - {} (:font-family ui/font-fancy) (:font-size 24) - =< 8 nil - list-> + {} $ :style ui/row-parted + div {} $ :style $ merge unsafe-coerce ui/row $ :: 'Map 'Tag 'Dynamic unsafe-coerce - {} $ :display :inline-block + {} $ :align-items :center :: 'Map 'Tag 'Dynamic - -> members-map (.to-list) - .map-pair $ fn (k username) - [] k $ span - {} $ :style $ {} (:margin-right 8) - :color $ hsl 0 0 70 - <> username - div - {} $ :style ui/row - comp-text-viewer article - =< 16 nil - button - {} - :style $ merge style/button $ {} - :on-click $ fn (e d!) - d! $ :: :paragraph/prepend nil - delay-focus! 400 |.editor-area - <> |Prepend - =< nil 16 - list-> - {} $ :style $ merge - unsafe-coerce ui/flex $ :: 'Map 'Tag 'Dynamic - unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} $ :border $ str "|1px solid " (hsl 0 0 94) - :: 'Map 'Tag 'Dynamic - -> paragraphs-map (.to-list) (.sort-by first) - .map-pair $ fn (k paragraph) - [] k $ comp-paragraph (>> states k) k paragraph (&map:get focuses-map k) (= k sort-id) - =< nil 300 + <> article-title $ {} (:font-family ui/font-fancy) (:font-size 24) + =< 8 nil + list-> + {} $ :style $ merge + unsafe-coerce ui/row $ :: 'Map 'Tag 'Dynamic + unsafe-coerce + {} $ :display :inline-block + :: 'Map 'Tag 'Dynamic + -> members-map (.to-list) + .map-pair $ fn (k username) + [] k $ span + {} $ :style $ {} (:margin-right 8) + :color $ hsl 0 0 70 + if (nil? username) + span $ {} + <> $ decode-map-as username 'String + div + {} $ :style ui/row + comp-text-viewer article + =< 16 nil + button + {} + :style $ merge style/button $ {} + :on-click $ fn (e d!) + d! $ :: :paragraph/prepend nil + delay-focus! 400 |.editor-area + <> |Prepend + =< nil 16 + list-> + {} $ :style $ merge + unsafe-coerce ui/flex $ :: 'Map 'Tag 'Dynamic + unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic + unsafe-coerce + {} $ :border $ str "|1px solid " (hsl 0 0 94) + :: 'Map 'Tag 'Dynamic + -> paragraphs-map (.to-list) (.sort-by first) + .map-pair $ fn (k paragraph) + [] k $ comp-paragraph (>> states k) k paragraph (&map:get focuses-map k) (= k sort-id) + =< nil 300 :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) :args $ [] 'Dynamic 'Dynamic 'Dynamic 'Dynamic 'Dynamic @@ -1028,6 +1360,70 @@ :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) :args $ [] 'Dynamic :features $ #{} :js-ffi + 'read-focus-list $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-focus-list (value) + if (nil? value) ([]) + decode-map-as value $ :: 'List $ :: 'Map 'Tag 'Dynamic + :examples $ [] + :schema $ :: 'Fn $ {} + :args $ [] 'Dynamic + :return $ :: 'List $ :: 'Map 'Tag 'Dynamic + :tests $ [] + %{} 'TestEntry + :name |absent-focus-is-empty-and-numeric-session-key-is-preserved + :code $ quote $ let + record $ {} (:sid 7) (:name "|成员🌱") + assert= ([]) (read-focus-list nil) + assert= ([] record) + read-focus-list $ [] record + :tags $ #{} :projection :unit + %{} 'TestEntry (:name |rejects-map-instead-of-projected-focus-list) + :code $ quote $ assert= true + try + do + read-focus-list $ {} + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + :tags $ #{} :projection :unit + 'read-paragraph-content $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-paragraph-content (value) + let + value-map $ decode-map-as value $ :: 'Map 'Tag 'Dynamic + decode-map-as (&map:get value-map :content) 'String + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] 'Dynamic + :tests $ [] + %{} 'TestEntry (:name |empty-and-unicode-text) + :code $ quote $ do + assert= | $ read-paragraph-content $ {} (:content |) + assert= "|内容🌱" $ read-paragraph-content $ {} (:content "|内容🌱") (:extra false) + :tags $ #{} :text-boundary :unit + %{} 'TestEntry (:name |rejects-missing-and-non-text) + :code $ quote $ do + assert= true $ try + do + read-paragraph-content $ {} + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + assert= true $ try + do + read-paragraph-content $ {} $ :content 4 + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + :tags $ #{} :text-boundary :unit 'supprted-langs $ %{} 'CodeEntry (:doc |) :code $ quote $ def supprted-langs {} (|clojure |clojure) (|javascript |javascript) (|js |javascript) (|bash |bash) @@ -1036,21 +1432,22 @@ :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.comp.previewer :require - [] respo-ui.core :refer $ [] hsl - [] respo-ui.core :as ui - [] respo.core :refer $ [] defcomp <> >> list-> span div button textarea - [] respo-md.comp.md :refer $ [] comp-md-block - [] respo.comp.space :refer $ [] =< - [] app.style :as style - [] |highlight.js/lib/core :default hljs - [] |escape-html :default escape-html - [] clojure.string :as string - [] app.util :refer $ [] delay-focus! - [] feather.core :refer $ [] comp-i + respo-ui.core :refer $ hsl + respo-ui.core :as ui + respo.core :refer $ defcomp <> >> list-> span div button textarea + respo-md.comp.md :refer $ comp-md-block + respo.comp.space :refer $ =< + app.style :as style + |highlight.js/lib/core :default hljs + |escape-html :default escape-html + clojure.string :as string + app.util :refer $ delay-focus! + feather.core :refer $ comp-i app.ffi.drag :refer $ DragEventHost - app.ffi.highlight :refer $ [] HighlightHost HighlightResultHost - app.ffi.popup :refer $ [] DocumentWriterHost - js-ffi.browser :refer $ [] window-open + app.ffi.highlight :refer $ HighlightHost HighlightResultHost + app.ffi.popup :refer $ DocumentWriterHost + js-ffi.browser :refer $ window-open + app.comp.articles :refer $ read-article-title read-focus-name 'app.comp.profile $ %{} 'FileEntry :defs $ {} $ 'comp-profile %{} 'CodeEntry (:doc |) @@ -1092,7 +1489,7 @@ :style $ merge ui/button $ {} (:color :red) (:border-color :red) :on-click $ fn (e dispatch!) dispatch! $ :: :user/log-out nil - .removeItem js/localStorage $ :storage-key config/site + storage-remove! $ :storage-key config/site <> "|Log out" :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) @@ -1101,24 +1498,20 @@ :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.comp.profile :require - [] respo-ui.core :refer $ [] hsl - [] app.schema :as schema - [] respo-ui.core :as ui - [] respo.core :refer $ [] defcomp list-> <> span button div a - [] respo.comp.space :refer $ [] =< - [] app.style :as style - [] app.config :as config + respo-ui.core :refer $ hsl + app.schema :as schema + respo-ui.core :as ui + respo.core :refer $ defcomp list-> <> span button div a + respo.comp.space :refer $ =< + app.style :as style + app.config :as config + js-ffi.browser :refer $ storage-remove! 'app.config $ %{} 'FileEntry :defs $ {} - 'cdn? $ %{} 'CodeEntry (:doc |) - :code $ quote $ def cdn? - cond - exists? js/window - , false - (exists? js/process) (= |true js/process.env.cdn) - :else false + 'SiteConfig $ %{} 'CodeEntry (:doc |) + :code $ quote $ defstruct SiteConfig (:port 'Number) (:title 'String) (:icon 'String) (:theme 'String) (:storage-key 'String) (:storage-file 'String) :examples $ [] - :schema $ :: 'Dynamic + :schema $ :: 'StructDef 'dev? $ %{} 'CodeEntry (:doc |) :code $ quote $ def dev? = |dev $ option:unwrap-or (get-env |mode) |release @@ -1126,9 +1519,18 @@ :schema $ :: 'Dynamic 'site $ %{} 'CodeEntry (:doc |) :code $ quote $ def site - {} (:port 11003) (:title |Table2) (:icon |http://cdn.tiye.me/logo/topix.png) (:theme |#eeeeff) (:storage-key |table2) (:storage-file |storage.cirru) + SiteConfig :port 11003 :title |Table2 :icon |http://cdn.tiye.me/logo/topix.png :theme |#eeeeff :storage-key |table2 :storage-file |storage.cirru :examples $ [] - :schema $ :: 'Dynamic + :schema $ :: 'app.config/SiteConfig + :tests $ [] $ %{} 'TestEntry (:name |preserves-original-site-values) + :code $ quote $ do + assert= 11003 $ :port site + assert= |Table2 $ :title site + assert= |http://cdn.tiye.me/logo/topix.png $ :icon site + assert= |#eeeeff $ :theme site + assert= |table2 $ :storage-key site + assert= |storage.cirru $ :storage-file site + :tags $ #{} :site-config :unit :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.config 'app.ffi.drag $ %{} 'FileEntry @@ -1201,7 +1603,7 @@ :ffi $ {} (:backend :js) (:kind :external-object) (:target :browser) :schema $ :: 'Trait 'QueryHost $ %{} 'CodeEntry (:doc |) - :code $ quote $ deftrait QueryHost (:host 'Dynamic) (:port 'Dynamic) + :code $ quote $ deftrait QueryHost (:host 'String) (:port 'String) :examples $ [] :ffi $ {} (:backend :js) (:kind :external-object) (:target :browser) :schema $ :: 'Trait @@ -1262,7 +1664,7 @@ '*client-caches $ %{} 'CodeEntry (:doc |) :code $ quote $ defatom *client-caches ({}) :examples $ [] - :schema $ :: 'Dynamic + :schema $ :: 'Ref $ :: 'Map 'Number (:: 'Map 'Tag 'Dynamic) '*initial-db $ %{} 'CodeEntry (:doc |) :code $ quote $ defatom *initial-db if @@ -1277,12 +1679,12 @@ '*reader-reel $ %{} 'CodeEntry (:doc |) :code $ quote $ defatom *reader-reel @*reel :examples $ [] - :schema $ :: 'Dynamic + :schema $ :: 'Ref $ :: 'cumulo-reel.core/ReelState (:: 'Map 'Tag 'Dynamic) '*reel $ %{} 'CodeEntry (:doc |) :code $ quote $ defatom *reel - struct-with reel-schema (:base @*initial-db) (:db @*initial-db) + ReelState :base @*initial-db :db @*initial-db :records ([]) :merged? false :examples $ [] - :schema $ :: 'Dynamic + :schema $ :: 'Ref $ :: 'cumulo-reel.core/ReelState (:: 'Map 'Tag 'Dynamic) 'dispatch! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn dispatch! (op op-data sid) let @@ -1309,7 +1711,7 @@ println "|Running mode:" $ if config/dev? |dev |release let port $ let - default-port $ assert-type (&map:get config/site :port) Number + default-port $ assert-type (:port config/site) Number match (get-env |port) (:some raw) result:unwrap-or (parse-float raw) default-port @@ -1336,6 +1738,7 @@ backup-path $ get-backup-path! check-write-file! storage-path file-content check-write-file! backup-path file-content + , &unit :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] @@ -1345,7 +1748,7 @@ clear-twig-caches! reset! *reel $ refresh-reel @*reel assert-type @*initial-db $ :: 'Map 'Tag 'Dynamic - , updater + , replay-updater sync-clients! @*reader-reel :examples $ [] :schema $ :: 'Fn $ {} (:return 'Dynamic) @@ -1359,6 +1762,15 @@ :examples $ [] :schema $ :: 'Fn $ {} (:return 'Dynamic) :args $ [] + 'replay-updater $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn replay-updater (db op sid op-id op-time) + if (enum? op) + updater db op (decode-map-as sid 'Number) (decode-map-as op-id 'String) (decode-map-as op-time 'Number) + raise "|replay operation must be an Enum" + :examples $ [] + :schema $ :: 'Fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) 'Dynamic 'Dynamic 'Dynamic 'Dynamic + :return $ :: 'Map 'Tag 'Dynamic 'run-server! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn run-server! (port) wss-serve! (&{} :port port) @@ -1380,8 +1792,8 @@ 'storage-file $ %{} 'CodeEntry (:doc |) :code $ quote $ def storage-file if (empty? calcit-dirname) - str calcit-dirname $ option:unwrap $ get config/site :storage-file - str calcit-dirname |/ $ option:unwrap $ get config/site :storage-file + str calcit-dirname $ :storage-file config/site + str calcit-dirname |/ $ :storage-file config/site :examples $ [] :schema $ :: 'Dynamic 'sync-clients! $ %{} 'CodeEntry (:doc |) @@ -1392,7 +1804,9 @@ db $ :db reel-state records $ :records reel-state session $ get-in db $ [] :sessions sid - old-store $ option:unwrap-or (get @*client-caches sid) nil + old-store $ match (get @*client-caches sid) + (:some previous-store) previous-store + (:none) nil new-store $ twig-container db session records changes $ diff-twig old-store new-store $ {} (:key :id) ; when config/dev? $ println "|Changes for" sid |: changes $ count records @@ -1400,16 +1814,18 @@ not $ empty? changes wss-send! sid $ format-cirru-edn $ {} (:kind :patch) (:data changes) swap! *client-caches assoc sid new-store + , &unit finish-twig-frame! + , &unit :examples $ [] - :schema $ :: 'Fn $ {} (:return 'Dynamic) + :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] 'Dynamic :features $ #{} :js-ffi :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.server :require (app.schema :as schema) app.updater :refer $ updater - cumulo-reel.core :refer $ reel-reducer refresh-reel reel-schema + cumulo-reel.core :refer $ reel-reducer refresh-reel reel-schema ReelState app.config :as config app.twig.container :refer $ twig-container recollect.diff :refer $ diff-twig @@ -1438,8 +1854,8 @@ :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.style :require - [] respo-ui.core :refer $ [] hsl - [] respo-ui.core :as ui + respo-ui.core :refer $ hsl + respo-ui.core :as ui 'app.twig.container $ %{} 'FileEntry :defs $ {} 'twig-articles $ %{} 'CodeEntry (:doc |) @@ -1454,15 +1870,16 @@ 'twig-container $ %{} 'CodeEntry (:doc |) :code $ quote $ defn twig-container (db session records) let + records-list $ decode-map-as records $ :: 'List 'Dynamic db-map $ assert-type db 'Map session-map $ assert-type option:unwrap-or session $ {} , 'Map user-id $ &map:get session-map :user-id - logged-in? $ some? user-id + logged-in? $ non-nil? user-id router $ assert-type (&map:get session-map :router) 'Map base-data $ {} (:logged-in? logged-in?) (:session session-map) - :reel-length $ count records + :reel-length $ count records-list sessions $ assert-type (&map:get db-map :sessions) 'Map users $ assert-type (&map:get db-map :users) 'Map merge base-data $ assert-type @@ -1490,8 +1907,38 @@ {} :: 'Map 'Tag 'Dynamic :examples $ [] - :schema $ :: 'Fn $ {} (:return 'Dynamic) + :schema $ :: 'Fn $ {} :args $ [] 'Dynamic (:: 'Option 'Dynamic) 'Dynamic + :return $ :: 'Map 'Tag 'Dynamic + :tests $ [] + %{} 'TestEntry + :name |guest-projection-retains-session-and-record-count + :code $ quote $ let + session-value $ {} (:user-id nil) + :router $ {} $ :name :home + :guest-field |preserved + db $ {} + :sessions $ {} $ 7 session-value + :users $ {} + projection $ twig-container db (Option :some session-value) ([] |record-a |record-b) + assert= false $ &map:get projection :logged-in? + assert= 2 $ &map:get projection :reel-length + assert= session-value $ &map:get projection :session + assert= nil $ &map:get projection :color + assert= nil $ &map:get projection :count + :tags $ #{} :business :unit + %{} 'TestEntry (:name |rejects-non-list-records-before-db-access) + :code $ quote $ assert= true + try + do + twig-container nil (Option :none) ({}) + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + :tags $ #{} :business :unit 'twig-focuses $ %{} 'CodeEntry (:doc |) :code $ quote $ defn twig-focuses (router-id sessions users) let @@ -1504,8 +1951,8 @@ router-map $ assert-type (&map:get session-map :router) 'Map and = :article $ &map:get router-map :name - some? $ &map:get session-map :user-id - some? $ &map:get session-map router-id + non-nil? $ &map:get session-map :user-id + non-nil? $ &map:get session-map router-id :: 'Map 'Number $ :: 'Map 'Tag 'Dynamic result $ -> filtered (map-entries) map $ fn (entry) @@ -1564,7 +2011,7 @@ users-map $ assert-type users $ :: 'Map 'String (:: 'Map 'Tag 'Dynamic) filtered $ assert-type filter-kv sessions-map $ fn (k session) - some? $ &map:get (assert-type session 'Map) :user-id + non-nil? $ &map:get (assert-type session 'Map) :user-id :: 'Map 'Number $ :: 'Map 'Tag 'Dynamic filter-map-kv filtered $ fn (k session) let @@ -1580,7 +2027,7 @@ :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.twig.container :require - [] app.twig.user :refer $ [] twig-user + app.twig.user :refer $ twig-user calcit.std.rand :refer $ rand-hex-color! app.util :refer $ filter-kv 'app.twig.user $ %{} 'FileEntry @@ -1592,7 +2039,7 @@ :args $ [] 'Dynamic :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.twig.user - :require $ [] recollect.twig :refer $ [] deftwig + :require $ recollect.twig :refer $ deftwig 'app.updater $ %{} 'FileEntry :defs $ {} $ 'updater %{} 'CodeEntry (:doc |) @@ -1623,7 +2070,7 @@ :return $ :: 'Map 'Tag 'Dynamic :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.updater - :require ([] app.updater.session :as session) ([] app.updater.user :as user) ([] app.updater.router :as router) ([] app.updater.paragraph :as paragraph) ([] app.updater.article :as article) + :require (app.updater.session :as session) (app.updater.user :as user) (app.updater.router :as router) (app.updater.paragraph :as paragraph) (app.updater.article :as article) 'app.updater.article $ %{} 'FileEntry :defs $ {} 'change-title $ %{} 'CodeEntry (:doc |) @@ -1651,12 +2098,13 @@ :args $ [] 'Dynamic 'Dynamic 'Number 'String 'Number :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.updater.article - :require $ [] app.schema :as schema + :require $ app.schema :as schema 'app.updater.paragraph $ %{} 'FileEntry :defs $ {} 'append-to $ %{} 'CodeEntry (:doc |) :code $ quote $ defn append-to (db op-data sid op-id op-time) let + base-key $ decode-map-as op-data 'String article-id $ option:unwrap-or get-in db $ [] :sessions sid :article-id , nil @@ -1665,7 +2113,7 @@ get-in db $ [] :articles article-id :paragraphs {} :: 'Map 'String $ :: 'Map 'Tag 'Dynamic - new-key $ bisection-util/key-after paragraphs op-data + new-key $ bisection-util/key-after paragraphs base-key -> db update-in ([] :articles article-id :paragraphs) fn (paragraphs) @@ -1676,6 +2124,42 @@ :examples $ [] :schema $ :: 'Fn $ {} (:return 'Dynamic) :args $ [] 'Dynamic 'Dynamic 'Number 'String 'Number + :tests $ [] + %{} 'TestEntry (:name |rejects-non-string-key-before-db-access) + :code $ quote $ assert= true + try + do + append-to ({}) 9 7 |new-id 42 + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + :tags $ #{} :text-boundary :unit + %{} 'TestEntry + :name |preserves-existing-paragraph-and-updates-numeric-session-cursor + :code $ quote $ let + base-key bisection-key.core/mid-id + original $ {} (:id |existing-id) (:time 1) (:content "|原文🌱") + db $ {} + :sessions $ {} $ 7 + {} (:article-id |a) (:paragraph-id base-key) + :users $ {} + :articles $ {} $ |a + {} $ :paragraphs $ {} (base-key original) + next-db $ append-to db base-key 7 |new-id 42 + new-key $ option:unwrap $ get-in next-db ([] :sessions 7 :paragraph-id) + paragraphs $ option:unwrap $ get-in next-db ([] :articles |a :paragraphs) + paragraphs-map $ decode-map-as paragraphs $ :: 'Map 'String 'Dynamic + assert= 2 $ count paragraphs-map + assert= original $ option:unwrap $ get-in next-db ([] :articles |a :paragraphs base-key) + assert= false $ = base-key new-key + assert= + {} (:id |new-id) (:time 42) (:content |) + option:unwrap $ get-in next-db $ [] :articles |a :paragraphs new-key + assert= original $ option:unwrap $ get-in db ([] :articles |a :paragraphs base-key) + :tags $ #{} :business :unit 'edit $ %{} 'CodeEntry (:doc |) :code $ quote $ defn edit (db op-data sid op-id op-time) let @@ -1723,7 +2207,7 @@ dissoc target-key update ([] :sessions sid :paragraph-id) fn (old-key) - if (some? old-key) new-key nil + if (non-nil? old-key) new-key nil :examples $ [] :schema $ :: 'Fn $ {} (:return 'Dynamic) :args $ [] 'Dynamic 'Dynamic 'Number 'String 'Number @@ -1784,7 +2268,7 @@ :args $ [] 'Dynamic 'Dynamic 'Number 'String 'Number :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.updater.paragraph - :require ([] bisection-key.util :as bisection-util) ([] app.schema :as schema) + :require (bisection-key.util :as bisection-util) (app.schema :as schema) 'app.updater.router $ %{} 'FileEntry :defs $ {} $ 'change %{} 'CodeEntry (:doc |) @@ -1834,7 +2318,7 @@ :args $ [] 'Dynamic 'Dynamic 'Number 'String 'Number :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.updater.session - :require $ [] app.schema :as schema + :require $ app.schema :as schema 'app.updater.user $ %{} 'FileEntry :defs $ {} 'log-in $ %{} 'CodeEntry (:doc |) From 7198a17e99753bdc5f062bceee89364677325914 Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 13 Oct 2026 23:06:18 +0800 Subject: [PATCH 08/18] fix: validate host calls and route-specific projection domains --- README.md | 5 +- calcit.cirru | 334 +++++++++++++++++++++++++++++++++++---------------- deps.cirru | 6 +- 3 files changed, 237 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index e390ed1..c0e9e83 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,9 @@ focus 分组保持 List,session key 保持 Number。登录输入派发一个 ` `:require` 规则和 `:refer` 名称列表使用 Cirru syntax list,不加多余 `[]`。 定义内的 `:tests` 验证空值、Unicode、0、访客投影、编辑时间选择、段落插入和消息顺序。 -当前候选双入口严格检查、28 项 native / 生成 JS 共享回放及前端构建通过。 +当前候选双入口严格检查、32 项 native / 生成 JS 共享回放及前端构建通过。 +路由数据按实际用途解码:文章数据使用 Tag 键,profile 成员使用 Number session 键, +不会将所有路由都解释成同一种 Map。宿主 trait 使用完整类型引用,拖放 props 与回调有明确合同。 ## 部署 @@ -57,6 +59,7 @@ PR 前缀带 PR 编号、run 和 attempt,使用 COS Action 的内置公共 URL - UI state 和投影 Map 的开放字段仍需在具体使用处验证,未宣称已全部静态化。 - 普通 Caps 的传递版本选择告警不等同严格依赖解析通过。 - 旧质量基线仍报告回归,文件和预算保持不变,不能把它记录为通过。 +- 已发布 Markdown 0.4.51 的自定义高亮回调尚未恢复调用,暂不视为高亮能力验收通过。 ## License diff --git a/calcit.cirru b/calcit.cirru index 29ce115..4283f10 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -404,12 +404,14 @@ router $ if (:logged-in? navigation-view) decode-map-as (&map:get store-map :router) (:: 'Map 'Tag 'Dynamic) {} - router-data $ if (:logged-in? navigation-view) - decode-map-as (&map:get router :data) (:: 'Map 'Tag 'Dynamic) - {} + router-data $ &map:get router :data raw-color $ &map:get store-map :color status-color $ if (nil? raw-color) (Option :none) Option :some $ decode-map-as raw-color 'String + article-data $ if + = (&map:get router :name) :article + decode-map-as router-data $ :: 'Map 'Tag 'Dynamic + {} div {} $ :style $ merge unsafe-coerce ui/global $ :: 'Map 'Tag 'Dynamic @@ -425,20 +427,22 @@ div {} $ :style ui/flex <> $ str router - :profile $ comp-profile (&map:get store-map :user) router-data - :home $ comp-articles (>> states :articles) router-data + :profile $ comp-profile (&map:get store-map :user) + decode-map-as router-data $ :: 'Map 'Number 'Dynamic + :home $ comp-articles (>> states :articles) + decode-map-as router-data $ :: 'Map 'Tag 'Dynamic :article $ div {} $ :style $ merge unsafe-coerce ui/row $ :: 'Map 'Tag 'Dynamic unsafe-coerce ui/flex $ :: 'Map 'Tag 'Dynamic - comp-previewer (>> states :previewer) (&map:get router-data :article) (&map:get router-data :focuses) (&map:get router-data :members) paragraph-id + comp-previewer (>> states :previewer) (&map:get article-data :article) (&map:get article-data :focuses) (&map:get article-data :members) paragraph-id comp-login $ >> states :login let visible? $ and (:logged-in? navigation-view) (non-nil? paragraph-id) = :article $ &map:get router :name comp-editor-panel (>> states :editor) paragraph-id option:unwrap-or - get-in router-data $ [] :article :paragraphs paragraph-id + get-in article-data $ [] :article :paragraphs paragraph-id {} , visible? comp-messages @@ -791,7 +795,7 @@ let event $ unsafe-coerce option:unwrap-or (get e :event) (js-object) - , DragEventHost + , 'app.ffi.drag/DragEventHost data-transfer $ option:unwrap $ js-nullish->option (js-get event :data-transfer) .set-data! data-transfer |text $ str sort-id , &unit @@ -1142,6 +1146,14 @@ respo.comp.space :refer $ =< 'app.comp.previewer $ %{} 'FileEntry :defs $ {} + 'ParagraphDropProps $ %{} 'CodeEntry (:doc |) + :code $ quote $ defstruct ParagraphDropProps + :style $ :: 'Map 'Tag 'Dynamic + :on-drop 'respo.schema/EventHandler + :on-dragover 'respo.schema/EventHandler + :on-dragenter 'respo.schema/EventHandler + :examples $ [] + :schema $ :: 'StructDef 'comp-info-list $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-info-list (focus-list) let @@ -1173,67 +1185,98 @@ state $ or (&map:get states-map :data) {} (:text |) (:time 0) div - unsafe-coerce - {} - :style $ merge - unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} (:background-color :white) (:border-radius |6px) (:margin-bottom 12) - :border $ str "|1px solid " $ hsl 0 0 90 - :: 'Map 'Tag 'Dynamic - :on-drop $ fn (e d!) + ParagraphDropProps :style + merge + unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic + unsafe-coerce + {} (:background-color :white) (:border-radius |6px) (:margin-bottom 12) + :border $ str "|1px solid " $ hsl 0 0 90 + :: 'Map 'Tag 'Dynamic + , :on-drop + fn (e d!) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} + :args $ [] 'Dynamic + :rest 'Dynamic + :return 'Unit + :return 'Unit let event $ unsafe-coerce option:unwrap-or (get e :event) (js-object) - , DragEventHost + , 'app.ffi.drag/DragEventHost data-transfer $ option:unwrap $ js-nullish->option (js-get event :data-transfer) data $ data-transfer .get-data |text event .stop-propagation! if (not= sort-id data) d! $ :: :paragraph/move $ {} (:target data) (:base sort-id) - :on-dragover $ fn (e d!) - let - event $ unsafe-coerce - option:unwrap-or (get e :event) (js-object) - , DragEventHost - event .prevent-default! - :on-dragenter $ fn (e d!) - let - event $ unsafe-coerce - option:unwrap-or (get e :event) (js-object) - , DragEventHost - event .prevent-default! - , respo.schema/DomProps + , &unit + , :on-dragover + fn (e d!) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} + :args $ [] 'Dynamic + :rest 'Dynamic + :return 'Unit + :return 'Unit + let + event $ unsafe-coerce + option:unwrap-or (get e :event) (js-object) + , 'app.ffi.drag/DragEventHost + event .prevent-default! + , &unit + , :on-dragenter $ fn (e d!) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} + :args $ [] 'Dynamic + :rest 'Dynamic + :return 'Unit + :return 'Unit + let + event $ unsafe-coerce + option:unwrap-or (get e :event) (js-object) + , 'app.ffi.drag/DragEventHost + event .prevent-default! + , &unit comp-md-block paragraph-text $ {} (:class-name |preview-content) :style $ {} $ :padding "|0 16px" :highlight $ fn (code lang) - if (contains? supprted-langs lang) - let - highlighter $ unsafe-coerce hljs HighlightHost - result $ highlighter .highlight - option:unwrap-or - get supprted-langs $ assert-type lang 'String - , | - , code - result :value - escape-html code - div - unsafe-coerce - {} - :style $ merge - unsafe-coerce ui/row-parted $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} (:padding "|4px 8px") (:cursor :move) (:min-height 40) - :: 'Map 'Tag 'Dynamic - :draggable true - :on-dragstart $ fn (e d!) + let + code-text $ decode-map-as code 'String + if (contains? supprted-langs lang) let - event $ unsafe-coerce - option:unwrap-or (get e :event) (js-object) - , DragEventHost - data-transfer $ option:unwrap $ js-nullish->option (js-get event :data-transfer) - data-transfer .set-data! |text $ str sort-id - , respo.schema/DomProps + highlighter $ unsafe-coerce hljs 'app.ffi.highlight/HighlightHost + result $ highlighter .highlight + option:unwrap-or + get supprted-langs $ assert-type lang 'String + , | + , code-text + result :value + escape-html code-text + div + ToolbarDragProps :style + merge + unsafe-coerce ui/row-parted $ :: 'Map 'Tag 'Dynamic + unsafe-coerce + {} (:padding "|4px 8px") (:cursor :move) (:min-height 40) + :: 'Map 'Tag 'Dynamic + , :draggable true :on-dragstart $ fn (e d!) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) + :: 'Fn $ {} + :args $ [] 'Dynamic + :rest 'Dynamic + :return 'Unit + :return 'Unit + let + event $ unsafe-coerce + option:unwrap-or (get e :event) (js-object) + , 'app.ffi.drag/DragEventHost + data-transfer $ option:unwrap $ js-nullish->option (js-get event :data-transfer) + data-transfer .set-data! |text $ str sort-id + , &unit comp-info-list focus-list div {} $ :style ui/row @@ -1353,7 +1396,7 @@ &map:get paragraph-map :content .join-string $ str &newline &newline |---- &newline &newline html $ str |
 (escape-html content) |
- document $ unsafe-coerce (child :document) DocumentWriterHost + document $ unsafe-coerce (child :document) 'app.ffi.popup/DocumentWriterHost document .write! html <> |Text :examples $ [] @@ -1448,53 +1491,83 @@ app.ffi.popup :refer $ DocumentWriterHost js-ffi.browser :refer $ window-open app.comp.articles :refer $ read-article-title read-focus-name + app.comp.editor-toolbar :refer $ ToolbarDragProps 'app.comp.profile $ %{} 'FileEntry - :defs $ {} $ 'comp-profile - %{} 'CodeEntry (:doc |) + :defs $ {} + 'comp-profile $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-profile (user members) let - user-map $ unsafe-coerce user 'Map - members-map $ unsafe-coerce members 'Map - div $ {} $ :style - merge ui/flex $ {} $ :padding 16 + user-map $ decode-map-as user $ :: 'Map 'Tag 'Dynamic + members-map $ read-profile-members members div - {} $ :style $ {} (:font-family ui/font-fancy) (:font-size 32) (:font-weight 100) - <> $ str "|Hello! " $ &map:get user-map :name - =< nil 16 - div - {} $ :style ui/row - <> |Members: - =< 8 nil - list-> + {} $ :style $ merge ui/flex + {} $ :padding 16 + div + {} $ :style $ {} (:font-family ui/font-fancy) (:font-size 32) (:font-weight 100) + <> $ str "|Hello! " $ &map:get user-map :name + =< nil 16 + div {} $ :style ui/row - -> members-map (.to-list) - .map-pair $ fn (k username) - [] k $ div - {} $ :style $ {} (:padding "|0 8px") - :border $ str "|1px solid " $ hsl 0 0 80 - :border-radius |16px - :margin "|0 4px" - <> username - =< nil 48 - div ({}) - button - {} - :style $ merge ui/button - :on-click $ fn (e d!) - js/location.replace $ str js/location.origin |?time= $ .now js/Date - <> |Refresh - =< 8 nil - button - {} - :style $ merge ui/button $ {} (:color :red) (:border-color :red) - :on-click $ fn (e dispatch!) - dispatch! $ :: :user/log-out nil - storage-remove! $ :storage-key config/site - <> "|Log out" + <> |Members: + =< 8 nil + list-> + {} $ :style ui/row + -> members-map (.to-list) + .map-pair $ fn (k username) + [] k $ div + {} $ :style $ {} (:padding "|0 8px") + :border $ str "|1px solid " $ hsl 0 0 80 + :border-radius |16px + :margin "|0 4px" + if (nil? username) + span $ {} + <> $ decode-map-as username 'String + =< nil 48 + div ({}) + button + {} + :style $ merge ui/button + :on-click $ fn (e d!) + js/location.replace $ str js/location.origin |?time= $ .now js/Date + <> |Refresh + =< 8 nil + button + {} + :style $ merge ui/button $ {} (:color :red) (:border-color :red) + :on-click $ fn (e dispatch!) + dispatch! $ :: :user/log-out nil + storage-remove! $ :storage-key config/site + <> "|Log out" :examples $ [] :schema $ :: 'Fn $ {} (:return 'respo.schema/Component) :args $ [] 'Dynamic 'Dynamic :features $ #{} :js-ffi + 'read-profile-members $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-profile-members (value) + decode-map-as value $ :: 'Map 'Number 'Dynamic + :examples $ [] + :schema $ :: 'Fn $ {} + :args $ [] 'Dynamic + :return $ :: 'Map 'Number 'Dynamic + :tests $ [] + %{} 'TestEntry + :name |numeric-session-keys-preserve-string-and-absent-names + :code $ quote $ let + value $ {} (7 "|成员🌱") (0 nil) + assert= value $ read-profile-members value + :tags $ #{} :projection :unit + %{} 'TestEntry (:name |rejects-string-session-key-without-coercion) + :code $ quote $ assert= true + try + do + read-profile-members $ {} $ |7 "|成员" + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + :tags $ #{} :projection :unit :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.comp.profile :require @@ -2249,6 +2322,56 @@ :examples $ [] :schema $ :: 'Fn $ {} (:return 'Dynamic) :args $ [] 'Dynamic 'Dynamic 'Number 'String 'Number + :tests $ [] + %{} 'TestEntry + :name |removes-only-selected-paragraph-and-clears-numeric-session-cursor + :code $ quote $ let + db $ {} + :sessions $ {} $ 7 + {} (:article-id |a) (:paragraph-id |T) + :articles $ {} $ |a + {} $ :paragraphs $ {} + |T $ {} $ :content "|删除对象" + |g $ {} $ :content "|保留对象🌱" + next-db $ remove-one db |T 7 |operation-id 42 + assert= nil $ &map:get + option:unwrap $ get-in next-db $ [] :sessions 7 + , :paragraph-id + assert= + option:unwrap $ get-in db $ [] :articles |a :paragraphs |g + option:unwrap $ get-in next-db $ [] :articles |a :paragraphs |g + assert= |T $ option:unwrap $ get-in db ([] :sessions 7 :paragraph-id) + assert= (Option :none) + get-in next-db $ [] :articles |a :paragraphs |T + assert= true $ contains? + option:unwrap $ get-in next-db $ [] :sessions 7 + , :paragraph-id + :tags $ #{} :business :unit + %{} 'TestEntry + :name |missing-paragraph-keeps-content-and-clears-cursor + :code $ quote $ let + db $ {} + :sessions $ {} $ 7 + {} (:article-id |a) (:paragraph-id |T) + :articles $ {} $ |a + {} $ :paragraphs $ {} + |T $ {} $ :content "|删除对象" + |g $ {} $ :content "|保留对象🌱" + next-db $ remove-one db |absent 7 |operation-id 42 + assert= nil $ &map:get + option:unwrap $ get-in next-db $ [] :sessions 7 + , :paragraph-id + assert= + option:unwrap $ get-in db $ [] :articles |a :paragraphs |g + option:unwrap $ get-in next-db $ [] :articles |a :paragraphs |g + assert= |T $ option:unwrap $ get-in db ([] :sessions 7 :paragraph-id) + assert= + option:unwrap $ get-in db $ [] :articles |a :paragraphs + option:unwrap $ get-in next-db $ [] :articles |a :paragraphs + assert= true $ contains? + option:unwrap $ get-in next-db $ [] :sessions 7 + , :paragraph-id + :tags $ #{} :business :unit 'update-content $ %{} 'CodeEntry (:doc |) :code $ quote $ defn update-content (db op-data sid op-id op-time) let @@ -2398,15 +2521,18 @@ :defs $ {} 'delay-focus! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn delay-focus! (duration target) - js/setTimeout - fn () $ let - el $ query-selector target - if (el .some?) - .focus! $ option:unwrap el - .warn js/console "|focus target box not ready." + set-timeout! + fn () + let + el $ query-selector target + if (el .some?) + .focus! $ option:unwrap el + js/console.warn "|focus target box not ready." + , &unit , duration + , &unit :examples $ [] - :schema $ :: 'Fn $ {} (:return 'Dynamic) + :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] 'Number 'String :features $ #{} :js-ffi 'filter-kv $ %{} 'CodeEntry (:doc |) @@ -2422,4 +2548,4 @@ :return $ :: 'Map 'K 'V :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.util - :require $ js-ffi.browser :refer $ query-selector + :require $ js-ffi.browser :refer $ query-selector set-timeout! diff --git a/deps.cirru b/deps.cirru index f0a3c18..4ca7957 100644 --- a/deps.cirru +++ b/deps.cirru @@ -5,10 +5,10 @@ |Cumulo/cumulo-util.calcit |0.0.27 |Respo/alerts.calcit |0.10.51 |Respo/respo-feather.calcit |0.4.25 - |Respo/respo-markdown.calcit |0.4.50 + |Respo/respo-markdown.calcit |0.4.51 |Respo/respo-message.calcit |0.0.33 - |Respo/respo-ui.calcit |0.7.35 - |Respo/respo.calcit |0.16.116 + |Respo/respo-ui.calcit |0.7.36 + |Respo/respo.calcit |0.16.118 |calcit-lang/bisection-key |0.0.30 |calcit-lang/calcit-wss |0.2.34 |calcit-lang/calcit.std |0.2.41 From d456841e8ba411f746052408248e9cc4dbc72ed3 Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 14 Oct 2026 00:23:34 +0800 Subject: [PATCH 09/18] chore: remove obsolete quality-count baseline --- README.md | 5 +- config/calcit-quality.cirru | 752 ------------------------------------ 2 files changed, 4 insertions(+), 753 deletions(-) delete mode 100644 config/calcit-quality.cirru diff --git a/README.md b/README.md index c0e9e83..31158c0 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,10 @@ focus 分组保持 List,session key 保持 Number。登录输入派发一个 ` 路由数据按实际用途解码:文章数据使用 Tag 键,profile 成员使用 Number session 键, 不会将所有路由都解释成同一种 Map。宿主 trait 使用完整类型引用,拖放 props 与回调有明确合同。 +验收以双入口严格类型检查、定义内 `:tests`、native / 生成 JS 共享语义测试和 +真实业务回放为准。已删除 CI 不再使用的 Dynamic / unsafe 数量基线; +这不放宽类型合同,也不以增加预算代替验证。浏览器高亮等未完成项仍须单独验收。 + ## 部署 COS 只接收前端 `dist/` 资产。生产前缀为 `TopixIM/tabletwo/`; @@ -58,7 +62,6 @@ PR 前缀带 PR 编号、run 和 attempt,使用 COS Action 的内置公共 URL - 旧 0.28 分支的隔离回放不能证明此候选或完整 UI 已通过。 - UI state 和投影 Map 的开放字段仍需在具体使用处验证,未宣称已全部静态化。 - 普通 Caps 的传递版本选择告警不等同严格依赖解析通过。 -- 旧质量基线仍报告回归,文件和预算保持不变,不能把它记录为通过。 - 已发布 Markdown 0.4.51 的自定义高亮回调尚未恢复调用,暂不视为高亮能力验收通过。 ## License diff --git a/config/calcit-quality.cirru b/config/calcit-quality.cirru deleted file mode 100644 index cfb8600..0000000 --- a/config/calcit-quality.cirru +++ /dev/null @@ -1,752 +0,0 @@ -{} (:schemaVersion 2) - :definitions $ {} - |app.client/*states $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.client/*store $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 0 - |app.client/connect! $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 5 - |app.client/dispatch! $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.client/main! $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.client/mount-target $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.client/on-server-data $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 1 - |app.client/reload! $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 0 - |app.client/render-app! $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.client/simulate-login! $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 1 - |app.comp.articles/comp-article $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 4 - |app.comp.articles/comp-articles $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 8 - |app.comp.container/comp-container $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 13 - |app.comp.container/comp-offline $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 0 - |app.comp.container/comp-status-color $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.comp.container/style-body $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 1 - :unsafeCoerce 0 - |app.comp.editor-panel/comp-editor-panel $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 10 - |app.comp.editor-toolbar/comp-editor-toolbar $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 4 - |app.comp.login/comp-login $ {} (:codeDynamic 0) - :codeNil 3 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 4 - :unsafeCoerce 8 - |app.comp.login/initial-state $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 1 - :unsafeCoerce 0 - |app.comp.login/on-submit $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.comp.navigation/comp-navigation $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.comp.navigation/style-entry $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 1 - :unsafeCoerce 0 - |app.comp.previewer/comp-info-list $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 2 - |app.comp.previewer/comp-paragraph $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 16 - |app.comp.previewer/comp-previewer $ {} (:codeDynamic 0) - :codeNil 5 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 6 - :unsafeCoerce 16 - |app.comp.previewer/comp-text-viewer $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 5 - |app.comp.previewer/supprted-langs $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 1 - :unsafeCoerce 0 - |app.comp.profile/comp-profile $ {} (:codeDynamic 0) - :codeNil 5 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 6 - :unsafeCoerce 2 - |app.config/cdn? $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.config/dev? $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.config/site $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 1 - :unsafeCoerce 0 - |app.schema/article $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 2 - :unsafeCoerce 0 - |app.schema/database $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 1 - :unsafeCoerce 0 - |app.schema/notification $ {} (:codeDynamic 0) - :codeNil 3 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 4 - :unsafeCoerce 0 - |app.schema/paragraph $ {} (:codeDynamic 0) - :codeNil 2 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 3 - :unsafeCoerce 0 - |app.schema/router $ {} (:codeDynamic 0) - :codeNil 3 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 4 - :unsafeCoerce 0 - |app.schema/session $ {} (:codeDynamic 0) - :codeNil 6 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 7 - :unsafeCoerce 0 - |app.schema/user $ {} (:codeDynamic 0) - :codeNil 5 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 6 - :unsafeCoerce 0 - |app.server/*client-caches $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.server/*initial-db $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.server/*reader-reel $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.server/*reel $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.server/dispatch! $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.server/get-backup-path! $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.server/main! $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.server/on-exit! $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.server/persist-db! $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.server/reload! $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.server/render-loop! $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.server/run-server! $ {} (:codeDynamic 0) - :codeNil 2 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 3 - :unsafeCoerce 0 - |app.server/storage-file $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.server/sync-clients! $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 1 - |app.style/button $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.style/link $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 0 - :typeNotFull 0 - :unresolved 1 - :unsafeCoerce 0 - |app.twig.container/twig-articles $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 1 - |app.twig.container/twig-container $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 5 - |app.twig.container/twig-focuses $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 7 - |app.twig.container/twig-members $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 6 - |app.twig.container/twig-profile $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 5 - |app.twig.user/twig-user $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.updater.article/change-title $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 1 - |app.updater.article/create $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.updater.article/remove-one $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.updater.paragraph/append-to $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 1 - |app.updater.paragraph/edit $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 0 - |app.updater.paragraph/finish-editing $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 0 - |app.updater.paragraph/move $ {} (:codeDynamic 0) - :codeNil 2 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 3 - :unsafeCoerce 2 - |app.updater.paragraph/para-prepend $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 1 - |app.updater.paragraph/remove-one $ {} (:codeDynamic 0) - :codeNil 2 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 3 - :unsafeCoerce 0 - |app.updater.paragraph/update-content $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 1 - |app.updater.router/change $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.updater.session/connect $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.updater.session/disconnect $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.updater.session/remove-message $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 1 - |app.updater.session/view-article $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 0 - |app.updater.user/log-in $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 4 - |app.updater.user/log-out $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 0 - |app.updater.user/sign-up $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 2 - |app.updater/updater $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 0 - |app.util/delay-focus! $ {} (:codeDynamic 0) - :codeNil 0 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 1 - :unsafeCoerce 1 - |app.util/filter-kv $ {} (:codeDynamic 0) - :codeNil 1 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 1 - :typeNone 1 - :typeNotFull 1 - :unresolved 2 - :unsafeCoerce 0 - :metrics $ {} (:codeDynamic 0) - :codeNil 60 - :declaredOptional 0 - :deprecatedCalls 0 - :schemaDynamic 82 - :typeNone 63 - :typeNotFull 69 - :unresolved 142 - :unsafeCoerce 134 - :scope $ {} (:includeDependencies false) - :namespace nil - :namespacePrefix nil From 0d89f238488a5d0f7e10156197c14a6be8bb2dc4 Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 14 Oct 2026 11:08:04 +0800 Subject: [PATCH 10/18] fix: check wire and projection boundaries with released codecs --- README.md | 12 +- calcit.cirru | 585 ++++++++++++++++++++++++++++++++------------------- deps.cirru | 4 +- 3 files changed, 377 insertions(+), 224 deletions(-) diff --git a/README.md b/README.md index 31158c0..dceabef 100644 --- a/README.md +++ b/README.md @@ -38,13 +38,18 @@ focus 分组保持 List,session key 保持 Number。登录输入派发一个 ` `:require` 规则和 `:refer` 名称列表使用 Cirru syntax list,不加多余 `[]`。 定义内的 `:tests` 验证空值、Unicode、0、访客投影、编辑时间选择、段落插入和消息顺序。 -当前候选双入口严格检查、32 项 native / 生成 JS 共享回放及前端构建通过。 +当前候选双入口严格检查、42 项 native / 生成 JS 共享回放及前端构建通过。 路由数据按实际用途解码:文章数据使用 Tag 键,profile 成员使用 Number session 键, 不会将所有路由都解释成同一种 Map。宿主 trait 使用完整类型引用,拖放 props 与回调有明确合同。 验收以双入口严格类型检查、定义内 `:tests`、native / 生成 JS 共享语义测试和 真实业务回放为准。已删除 CI 不再使用的 Dynamic / unsafe 数量基线; -这不放宽类型合同,也不以增加预算代替验证。浏览器高亮等未完成项仍须单独验收。 +这不放宽类型合同,也不以增加预算代替验证。 + +登录凭据在服务端解码为具名结构;客户端通过 Recollect 已发布的 codec 还原 +Cirru EDN patch,避免将传输中的 enum 直接当作已校验的名义值。 +Markdown 使用正式 0.4.53,高亮回调的输入与输出均有 String 合同。 +隔离浏览器已验证实际代码高亮、Number session key 和刷新后自动登录。 ## 部署 @@ -62,7 +67,8 @@ PR 前缀带 PR 编号、run 和 attempt,使用 COS Action 的内置公共 URL - 旧 0.28 分支的隔离回放不能证明此候选或完整 UI 已通过。 - UI state 和投影 Map 的开放字段仍需在具体使用处验证,未宣称已全部静态化。 - 普通 Caps 的传递版本选择告警不等同严格依赖解析通过。 -- 已发布 Markdown 0.4.51 的自定义高亮回调尚未恢复调用,暂不视为高亮能力验收通过。 +- Highlight.js 仍使用原有 legacy 调用方式,保留其弃用提示,未宣称已迁移该接口。 +- 完整 `fix --workflow strict --verify` 被 core `update-in` 的回调泛型输入证明阻断;双入口检查通过不等同完整收尾。 ## License diff --git a/calcit.cirru b/calcit.cirru index 4283f10..09ced89 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -58,9 +58,14 @@ println |Dispatch op data case-default op ws-send! $ {} (:kind :op) (:op op) (:data data) - :states $ let[] (cursor s) data $ reset! *states - assert-type (update-states @*states cursor s) (:: 'Map 'Tag 'Dynamic) + :states $ let[] (cursor s) params $ reset! *states + decode-map-as + update-states @*states + decode-map-as cursor $ :: 'List 'Dynamic + , s + :: 'Map 'Tag 'Dynamic :effect/connect $ do (connect!) &unit + , &unit :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] 'Enum @@ -94,6 +99,7 @@ if (.none? @*store) (connect!) , &unit println |App-started! + , &unit :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] @@ -105,46 +111,44 @@ :schema $ :: 'respo.dom/DomElement 'on-server-data $ %{} 'CodeEntry (:doc |) :code $ quote $ defn on-server-data (data) - do - let - data-map $ decode-map-as data $ :: 'Map 'Tag 'Dynamic - case-default (&map:get data-map :kind) (js/console.warn "|unknown server data kind:" data) - :patch $ let - changes $ &map:get data-map :data - when config/dev? $ js/console.log |Changes $ to-js-data changes - let - base $ match @*store - (:some previous-store) previous-store - (:none) nil - next-value $ patch-twig base $ assert-type changes (:: 'List 'recollect.schema/change-op) - next-store $ decode-map-as next-value $ :: 'Map 'Tag 'Dynamic - reset! *store $ Option :some next-store - , &unit + let + data-map $ decode-map-as data $ :: 'Map 'Tag 'Dynamic + case-default (&map:get data-map :kind) (js/console.warn "|unknown server data kind:" data) + :patch $ let + changes $ &map:get data-map :data + when config/dev? $ js/console.log |Changes $ to-js-data changes + let + base $ match @*store + (:some previous-store) previous-store + (:none) nil + next-value $ patch-twig base $ schema/read-patch-changes changes + next-store $ decode-map-as next-value $ :: 'Map 'Tag 'Dynamic + reset! *store $ Option :some next-store + , &unit :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] 'Dynamic :features $ #{} :js-ffi 'reload! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn reload! () - do - if - or (non-nil? client-errors) (non-nil? server-errors) - hud! |error $ str client-errors &newline server-errors - do (hud! |inactive nil) (remove-watch! *store :changes) (remove-watch! *states :changes) (clear-cache!) (render-app!) - add-watch! *store :changes $ fn (store prev) - hint-fn $ {} - :args $ [] - :: 'Option $ :: 'Map 'Tag 'Dynamic - :: 'Option $ :: 'Map 'Tag 'Dynamic - :return 'Unit - render-app! - add-watch! *states :changes $ fn (states prev) - hint-fn $ {} - :args $ [] (:: 'Map 'Tag 'Dynamic) (:: 'Map 'Tag 'Dynamic) - :return 'Unit - render-app! - println "|Code updated." - , &unit + if + or (non-nil? client-errors) (non-nil? server-errors) + hud! |error $ str client-errors &newline server-errors + do (hud! |inactive nil) (remove-watch! *store :changes) (remove-watch! *states :changes) (clear-cache!) (render-app!) + add-watch! *store :changes $ fn (store prev) + hint-fn $ {} + :args $ [] + :: 'Option $ :: 'Map 'Tag 'Dynamic + :: 'Option $ :: 'Map 'Tag 'Dynamic + :return 'Unit + render-app! + add-watch! *states :changes $ fn (states prev) + hint-fn $ {} + :args $ [] (:: 'Map 'Tag 'Dynamic) (:: 'Map 'Tag 'Dynamic) + :return 'Unit + render-app! + println "|Code updated." + , &unit :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] @@ -156,19 +160,24 @@ match @*store (:some online-store) online-store (:none) nil - , dispatch! + fn (value) + hint-fn $ {} + :args $ [] 'Dynamic + :return 'Unit + dispatch! $ schema/read-dispatch-operation value :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] :features $ #{} :js-ffi 'simulate-login! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn simulate-login! () - let - raw $ js/localStorage.getItem $ :storage-key config/site - if (js-present? raw) + match + js-ffi.browser/storage-get $ :storage-key config/site + (:some saved) do (println "|Found storage.") - dispatch! $ :: :user/log-in $ parse-cirru-edn (unsafe-coerce raw String) - do $ println "|Found no storage." + dispatch! $ :: :user/log-in $ parse-cirru-edn saved + (:none) (println "|Found no storage.") + , &unit :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] @@ -203,9 +212,11 @@ 'comp-article $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-article (states article focuses) let - article-map $ unsafe-coerce article 'Map + article-map $ decode-map-as article $ :: 'Map 'Tag 'Dynamic article-title $ read-article-title article - focuses-map $ unsafe-coerce focuses 'Map + article-id $ decode-map-as (&map:get article-map :id) 'String + focuses-map $ decode-map-as focuses $ :: 'Map 'String + :: 'List $ :: 'Map 'Tag 'Dynamic edit-plugin $ use-prompt (>> states :edit) {} (:text "|New title?") (:initial article-title) remove-plugin $ use-confirm (>> states :remove) @@ -216,7 +227,7 @@ :border $ str "|1px solid " $ hsl 0 0 90 :box-shadow $ str "|0 0 2px " $ hsl 0 0 0 :on-click $ fn (e d!) - d! $ :: :session/view-article $ &map:get article-map :id + d! $ :: :session/view-article article-id div {} $ :style ui/row-parted <> article-title $ {} $ :font-size 16 @@ -228,18 +239,15 @@ fn (e d!) .show edit-plugin d! $ fn (result) (println "|as result:" result) when - not $ blank? $ unsafe-coerce result String - d! $ :: :article/title $ {} - :id $ &map:get article-map :id - :title result + not $ blank? $ decode-map-as result 'String + d! $ :: :article/title $ {} (:id article-id) (:title result) =< 16 nil comp-icon :trash {} (:font-size 14) :color $ hsl 200 80 70 :cursor :pointer fn (e d!) - .show remove-plugin d! $ fn () $ d! - :: :article/remove-one $ &map:get article-map :id + .show remove-plugin d! $ fn () $ d! (:: :article/remove-one article-id) div {} $ :style $ {} :color $ hsl 0 0 80 @@ -247,14 +255,12 @@ {} $ :style ui/row -> assert-type - option:unwrap-or - get focuses-map $ &map:get article-map :id - [] + option:unwrap-or (get focuses-map article-id) ([]) :: 'List 'Dynamic .to-list .map $ fn (info) let - info-map $ unsafe-coerce info 'Map + info-map $ decode-map-as info $ :: 'Map 'Tag 'Dynamic [] (&map:get info-map :id) div {} $ :style $ {} (:margin-right 8) @@ -271,29 +277,23 @@ 'comp-articles $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-articles (states router-data) let - router-map $ unsafe-coerce router-data 'Map - articles $ unsafe-coerce + router-map $ decode-map-as router-data $ :: 'Map 'Tag 'Dynamic + articles $ decode-map-as or (&map:get router-map :articles) ({}) - , 'Map + :: 'Map 'String $ :: 'Map 'Tag 'Dynamic focuses $ or (&map:get router-map :focuses) ({}) create-plugin $ use-prompt (>> states :create) {} (:text "|Title of article:") (:initial |) div - {} $ :style $ merge - unsafe-coerce ui/flex $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} $ :padding 16 - :: 'Map 'Tag 'Dynamic + {} $ :style $ merge ui/flex + {} $ :padding 16 div {} $ :style $ {} (:font-size 24) (:font-weight 300) (:font-family ui/font-fancy) :color $ hsl 0 0 70 <> |Articles list-> - {} $ :style $ merge - unsafe-coerce ui/row $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} $ :flex-wrap :wrap - :: 'Map 'Tag 'Dynamic + {} $ :style $ merge ui/row + {} $ :flex-wrap :wrap -> articles (.to-list) sort $ fn (a b) &compare @@ -301,9 +301,9 @@ option:unwrap-or (first b) | .map-pair $ fn (k article) let - article-map $ unsafe-coerce article 'Map + article-map $ decode-map-as article $ :: 'Map 'Tag 'Dynamic [] k $ comp-article - >> states $ &map:get article-map :id + >> states $ decode-map-as (&map:get article-map :id) 'String , article focuses div ({}) button @@ -312,7 +312,7 @@ :on-click $ fn (e d!) .show create-plugin d! $ fn (result) when - not $ blank? $ unsafe-coerce result String + not $ blank? $ decode-map-as result 'String d! $ :: :article/create result <> "|Create Article" .render create-plugin @@ -446,7 +446,7 @@ {} , visible? comp-messages - unsafe-coerce + decode-map-as option:unwrap-or get-in store-map $ [] :session :messages {} @@ -628,35 +628,28 @@ 'comp-editor-panel $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-editor-panel (states sort-id paragraph visible?) let - states-map $ unsafe-coerce states 'Map - paragraph-map $ unsafe-coerce paragraph 'Map + states-map $ decode-map-as states 'Map + paragraph-map $ decode-map-as paragraph $ :: 'Map 'Tag 'Dynamic cursor $ &map:get states-map :cursor - state $ unsafe-coerce + state $ decode-map-as or (&map:get states-map :data) {} (:text |) (:time 0) - , 'Map + :: 'Map 'Tag 'Dynamic remove-plugin $ use-confirm (>> states :remove) {} :style $ {} (:cursor :pointer) (:position :absolute) (:right 8) (:color :red) :text "|Sure to delete?" div - {} $ :style $ merge - unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} (:transition-duration |200ms) (:transition-timing-function :linear) (:position :relative) (:transition-property :height) - :height $ if visible? |40% |0% - :background-color $ hsl 0 0 100 - :border-top $ str "|1px solid " $ hsl 0 0 90 - :padding $ if visible? 8 0 - :padding-left $ if visible? 88 false - :: 'Map 'Tag 'Dynamic + {} $ :style $ merge ui/column + {} (:transition-duration |200ms) (:transition-timing-function :linear) (:position :relative) (:transition-property :height) + :height $ if visible? |40% |0% + :background-color $ hsl 0 0 100 + :border-top $ str "|1px solid " $ hsl 0 0 90 + :padding $ if visible? 8 0 + :padding-left $ if visible? 88 false when visible? $ div - {} $ :style $ merge - unsafe-coerce ui/flex $ :: 'Map 'Tag 'Dynamic - unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} (:max-width 960) (:width |100%) (:margin :auto) - :: 'Map 'Tag 'Dynamic + {} $ :style $ merge ui/flex ui/column + {} (:max-width 960) (:width |100%) (:margin :auto) comp-editor-toolbar states sort-id =< nil 8 textarea $ EditorInputProps :style @@ -886,22 +879,17 @@ 'comp-login $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-login (states) let - states-map $ unsafe-coerce states 'Map + states-map $ decode-map-as states 'Map cursor $ decode-map-as (&map:get states-map :cursor) (:: 'List 'Dynamic) state $ decode-map-as or (&map:get states-map :data) initial-state :: 'Map 'Tag 'Dynamic fields $ read-login-fields state div - {} $ :style $ merge - unsafe-coerce ui/flex $ :: 'Map 'Tag 'Dynamic - unsafe-coerce ui/center $ :: 'Map 'Tag 'Dynamic + {} $ :style $ merge ui/flex ui/center div - {} $ :style $ merge - unsafe-coerce ui/center $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} $ :max-width 200 - :: 'Map 'Tag 'Dynamic + {} $ :style $ merge ui/center + {} $ :max-width 200 div {} $ :style $ {} div ({}) @@ -1178,20 +1166,16 @@ 'comp-paragraph $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-paragraph (states sort-id paragraph focus-list focused?) let - states-map $ unsafe-coerce states 'Map - paragraph-map $ unsafe-coerce paragraph 'Map + states-map $ decode-map-as states 'Map + paragraph-map $ decode-map-as paragraph $ :: 'Map 'Tag 'Dynamic paragraph-text $ read-paragraph-content paragraph cursor $ &map:get states-map :cursor state $ or (&map:get states-map :data) {} (:text |) (:time 0) div ParagraphDropProps :style - merge - unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} (:background-color :white) (:border-radius |6px) (:margin-bottom 12) - :border $ str "|1px solid " $ hsl 0 0 90 - :: 'Map 'Tag 'Dynamic + merge ui/column $ {} (:background-color :white) (:border-radius |6px) (:margin-bottom 12) + :border $ str "|1px solid " $ hsl 0 0 90 , :on-drop fn (e d!) hint-fn $ {} @@ -1243,25 +1227,22 @@ comp-md-block paragraph-text $ {} (:class-name |preview-content) :style $ {} $ :padding "|0 16px" :highlight $ fn (code lang) - let - code-text $ decode-map-as code 'String + hint-fn $ {} + :args $ [] 'String 'String + :return 'String + decode-map-as if (contains? supprted-langs lang) let highlighter $ unsafe-coerce hljs 'app.ffi.highlight/HighlightHost result $ highlighter .highlight - option:unwrap-or - get supprted-langs $ assert-type lang 'String - , | - , code-text + option:unwrap-or (get supprted-langs lang) | + , code result :value - escape-html code-text + escape-html code + , 'String div ToolbarDragProps :style - merge - unsafe-coerce ui/row-parted $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} (:padding "|4px 8px") (:cursor :move) (:min-height 40) - :: 'Map 'Tag 'Dynamic + merge ui/row-parted $ {} (:padding "|4px 8px") (:cursor :move) (:min-height 40) , :draggable true :on-dragstart $ fn (e d!) hint-fn $ {} :args $ [] (:: 'Map 'Tag 'Dynamic) @@ -1309,40 +1290,29 @@ 'comp-previewer $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-previewer (states article focuses members sort-id) let - article-map $ unsafe-coerce article 'Map + article-map $ decode-map-as article $ :: 'Map 'Tag 'Dynamic article-title $ read-article-title article - focuses-map $ unsafe-coerce focuses 'Map - members-map $ unsafe-coerce members 'Map - paragraphs-map $ unsafe-coerce (&map:get article-map :paragraphs) 'Map + focuses-map $ decode-map-as focuses $ :: 'Map 'String + :: 'List $ :: 'Map 'Tag 'Dynamic + members-map $ decode-map-as members $ :: 'Map 'Number 'Dynamic + paragraphs-map $ decode-map-as (&map:get article-map :paragraphs) + :: 'Map 'String $ :: 'Map 'Tag 'Dynamic div - {} $ :style $ merge - unsafe-coerce ui/flex $ :: 'Map 'Tag 'Dynamic - unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} (:overflow :auto) (:padding-bottom 20) (:padding-top 32) - :: 'Map 'Tag 'Dynamic + {} $ :style $ merge ui/flex ui/column + {} (:overflow :auto) (:padding-bottom 20) (:padding-top 32) div - {} $ :style $ merge - unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} (:max-width 960) (:width |96%) (:margin "|0px auto") - :: 'Map 'Tag 'Dynamic + {} $ :style $ merge ui/column + {} (:max-width 960) (:width |96%) (:margin "|0px auto") div {} $ :style ui/row-parted div - {} $ :style $ merge - unsafe-coerce ui/row $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} $ :align-items :center - :: 'Map 'Tag 'Dynamic + {} $ :style $ merge ui/row + {} $ :align-items :center <> article-title $ {} (:font-family ui/font-fancy) (:font-size 24) =< 8 nil list-> - {} $ :style $ merge - unsafe-coerce ui/row $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} $ :display :inline-block - :: 'Map 'Tag 'Dynamic + {} $ :style $ merge ui/row + {} $ :display :inline-block -> members-map (.to-list) .map-pair $ fn (k username) [] k $ span @@ -1364,12 +1334,8 @@ <> |Prepend =< nil 16 list-> - {} $ :style $ merge - unsafe-coerce ui/flex $ :: 'Map 'Tag 'Dynamic - unsafe-coerce ui/column $ :: 'Map 'Tag 'Dynamic - unsafe-coerce - {} $ :border $ str "|1px solid " (hsl 0 0 94) - :: 'Map 'Tag 'Dynamic + {} $ :style $ merge ui/flex ui/column + {} $ :border $ str "|1px solid " (hsl 0 0 94) -> paragraphs-map (.to-list) (.sort-by first) .map-pair $ fn (k paragraph) [] k $ comp-paragraph (>> states k) k paragraph (&map:get focuses-map k) (= k sort-id) @@ -1381,18 +1347,19 @@ 'comp-text-viewer $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-text-viewer (article) let - article-map $ unsafe-coerce article 'Map + article-map $ decode-map-as article $ :: 'Map 'Tag 'Dynamic button {} :style $ merge style/button $ {} :on-click $ fn (e d!) let child $ option:unwrap $ window-open | - paragraphs $ unsafe-coerce (&map:get article-map :paragraphs) 'Map + paragraphs $ decode-map-as (&map:get article-map :paragraphs) + :: 'Map 'String $ :: 'Map 'Tag 'Dynamic content $ str &newline "|# " (&map:get article-map :title) &newline &newline |---- &newline &newline $ -> paragraphs (.to-list) (.sort-by first) map $ fn (pair) let - paragraph-map $ unsafe-coerce (last pair) 'Map + paragraph-map $ decode-map-as (last pair) (:: 'Map 'Tag 'Dynamic) &map:get paragraph-map :content .join-string $ str &newline &newline |---- &newline &newline html $ str |
 (escape-html content) |
@@ -1709,6 +1676,80 @@ {} (:id nil) (:content |) (:time nil) :examples $ [] :schema $ :: 'Dynamic + 'read-dispatch-operation $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-dispatch-operation (value) + if (enum? value) value $ raise |Invalid-render-operation + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'Enum) + :args $ [] 'Dynamic + :tests $ [] + %{} 'TestEntry (:name |dispatch-preserves-enum-payload) + :code $ quote $ let + op $ :: :states ([] :article 42) + {} $ :text "|正文🐈" + assert= op $ read-dispatch-operation op + :tags $ #{} :operation :unit + %{} 'TestEntry (:name |dispatch-rejects-non-enum-input) + :code $ quote $ let + rejected? $ fn (value) + hint-fn $ {} + :args $ [] 'Dynamic + :return 'Bool + try + do (read-dispatch-operation value) false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + &= message |Invalid-render-operation + assert= true $ rejected? nil + assert= true $ rejected? $ {} + assert= true $ rejected? |operation + :tags $ #{} :operation :unit + 'read-patch-changes $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-patch-changes (value) (recollect.schema/decode-changes value) + :examples $ [] + :schema $ :: 'Fn $ {} + :args $ [] 'Dynamic + :return $ :: 'List 'recollect.schema/change-op + :tests $ [] + %{} 'TestEntry (:name |patch-empty-and-valid-enums) + :code $ quote $ let + changes $ [] + recollect.schema/change-op :replace $ {} $ :title "|正文🐈" + recollect.schema/change-op :vec-drop 0 + assert= ([]) + read-patch-changes $ [] + assert= changes $ read-patch-changes changes + :tags $ #{} :input-boundary :unit + %{} 'TestEntry (:name |patch-rejects-invalid-shape-before-applying) + :code $ quote $ let + rejected? $ fn (value) + hint-fn $ {} + :args $ [] 'Dynamic + :return 'Bool + try + do (read-patch-changes value) false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + assert= true $ rejected? $ {} + assert= true $ rejected? $ [] 42 + assert= true $ rejected? $ [] (:: :unknown nil) + assert= true $ rejected? $ [] (:: :vec-drop |not-a-number) + :tags $ #{} :input-boundary :unit + %{} 'TestEntry (:name |patch-reifies-real-edn-wire-enums) + :code $ quote $ let + original $ [] + recollect.schema/change-op :replace $ {} $ :title "|正文🐈" + recollect.schema/change-op :update-in ([] :sessions 42) + recollect.schema/change-op :pick :messages $ [] $ recollect.schema/change-op :map-splice (#{} |old) + {} $ |new $ {} (:text "|消息") + wire $ parse-cirru-edn $ format-cirru-edn original + assert= original $ read-patch-changes wire + :tags $ #{} :input-boundary :unit 'router $ %{} 'CodeEntry (:doc |) :code $ quote $ def router {} (:name nil) (:title nil) @@ -1743,7 +1784,7 @@ if path-exists? $ dbg storage-file do (println "|Found local EDN data") - merge schema/database $ assert-type + merge schema/database $ decode-map-as parse-cirru-edn $ read-file storage-file :: 'Map 'Tag 'Dynamic do (println "|Found no data") schema/database @@ -1791,7 +1832,8 @@ (:none) default-port run-server! port println $ str "|Server started on port:" port - do (; "|init it before doing multi-threading") (identity @*reader-reel) + ; "|init it before doing multi-threading" + identity @*reader-reel set-interval 200 $ fn () $ render-loop! set-interval 600000 $ fn () $ persist-db! on-control-c on-exit! @@ -1931,12 +1973,59 @@ respo-ui.core :as ui 'app.twig.container $ %{} 'FileEntry :defs $ {} + 'read-user-name $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-user-name (users user-id) + if (nil? user-id) (Option :none) + let + id $ decode-map-as user-id 'String + match + get-in users $ [] id :name + (:none) (Option :none) + (:some value) + Option :some $ decode-map-as value 'String + :examples $ [] + :schema $ :: 'Fn $ {} + :args $ [] + :: 'Map 'String $ :: 'Map 'Tag 'Dynamic + , 'Dynamic + :return $ :: 'Option 'String + :tests $ [] + %{} 'TestEntry (:name |user-name-preserves-guest-missing-and-unicode) + :code $ quote $ let + users $ {} + |u $ {} $ :name "|用户🐈" + |empty $ {} $ :name nil + assert= (Option :none) (read-user-name users nil) + assert= (Option :none) (read-user-name users |missing) + assert= (Option :none) (read-user-name users |empty) + assert= (Option :some "|用户🐈") (read-user-name users |u) + :tags $ #{} :projection :unit + %{} 'TestEntry (:name |user-name-rejects-invalid-id-and-name) + :code $ quote $ let + rejected? $ fn (users id) + hint-fn $ {} + :args $ [] + :: 'Map 'String $ :: 'Map 'Tag 'Dynamic + , 'Dynamic + :return 'Bool + try + do (read-user-name users id) false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + assert= true $ rejected? ({}) 42 + assert= true $ rejected? + {} $ |u $ {} (:name 42) + , |u + :tags $ #{} :projection :unit 'twig-articles $ %{} 'CodeEntry (:doc |) :code $ quote $ defn twig-articles (articles) filter-map-kv - assert-type articles $ :: 'Map 'String $ :: 'Map 'Tag 'Dynamic + decode-map-as articles $ :: 'Map 'String $ :: 'Map 'Tag 'Dynamic fn (k v) - %:: MapEntryDecision :keep k $ dissoc v :paragraphs + MapEntryDecision :keep k $ dissoc v :paragraphs :examples $ [] :schema $ :: 'Fn $ {} (:return 'Dynamic) :args $ [] 'Dynamic @@ -1944,18 +2033,20 @@ :code $ quote $ defn twig-container (db session records) let records-list $ decode-map-as records $ :: 'List 'Dynamic - db-map $ assert-type db 'Map - session-map $ assert-type + db-map $ decode-map-as db $ :: 'Map 'Tag 'Dynamic + session-map $ decode-map-as option:unwrap-or session $ {} - , 'Map + :: 'Map 'Tag 'Dynamic user-id $ &map:get session-map :user-id logged-in? $ non-nil? user-id - router $ assert-type (&map:get session-map :router) 'Map + router $ decode-map-as (&map:get session-map :router) (:: 'Map 'Tag 'Dynamic) base-data $ {} (:logged-in? logged-in?) (:session session-map) :reel-length $ count records-list - sessions $ assert-type (&map:get db-map :sessions) 'Map - users $ assert-type (&map:get db-map :users) 'Map - merge base-data $ assert-type + sessions $ decode-map-as (&map:get db-map :sessions) + :: 'Map 'Number $ :: 'Map 'Tag 'Dynamic + users $ decode-map-as (&map:get db-map :users) + :: 'Map 'String $ :: 'Map 'Tag 'Dynamic + merge base-data $ decode-map-as if logged-in? {} :user $ twig-user $ option:unwrap-or @@ -2015,13 +2106,13 @@ 'twig-focuses $ %{} 'CodeEntry (:doc |) :code $ quote $ defn twig-focuses (router-id sessions users) let - sessions-map $ assert-type sessions $ :: 'Map 'Number (:: 'Map 'Tag 'Dynamic) - users-map $ assert-type users $ :: 'Map 'String (:: 'Map 'Tag 'Dynamic) - filtered $ assert-type + sessions-map $ decode-map-as sessions $ :: 'Map 'Number (:: 'Map 'Tag 'Dynamic) + users-map $ decode-map-as users $ :: 'Map 'String (:: 'Map 'Tag 'Dynamic) + filtered $ decode-map-as filter-kv sessions-map $ fn (k session) let - session-map $ assert-type session 'Map - router-map $ assert-type (&map:get session-map :router) 'Map + session-map $ decode-map-as session $ :: 'Map 'Tag 'Dynamic + router-map $ decode-map-as (&map:get session-map :router) (:: 'Map 'Tag 'Dynamic) and = :article $ &map:get router-map :name non-nil? $ &map:get session-map :user-id @@ -2054,46 +2145,46 @@ 'twig-members $ %{} 'CodeEntry (:doc |) :code $ quote $ defn twig-members (article-id sessions users) let - sessions-map $ assert-type sessions $ :: 'Map 'Number (:: 'Map 'Tag 'Dynamic) - users-map $ assert-type users $ :: 'Map 'String (:: 'Map 'Tag 'Dynamic) + sessions-map $ decode-map-as sessions $ :: 'Map 'Number (:: 'Map 'Tag 'Dynamic) + users-map $ decode-map-as users $ :: 'Map 'String (:: 'Map 'Tag 'Dynamic) let - filtered $ assert-type + filtered $ decode-map-as filter-kv sessions-map $ fn (k session) let - session-map $ assert-type session 'Map - router $ assert-type (&map:get session-map :router) 'Map + session-map $ decode-map-as session $ :: 'Map 'Tag 'Dynamic + router $ decode-map-as (&map:get session-map :router) (:: 'Map 'Tag 'Dynamic) and = :article $ &map:get router :name = article-id $ &map:get session-map :article-id :: 'Map 'Number $ :: 'Map 'Tag 'Dynamic -> filtered $ .map-kv $ fn (k session) let - session-map $ assert-type session 'Map - [] k $ option:unwrap-or - get-in users-map $ [] - assert-type (&map:get session-map :user-id) String - , :name - , nil + session-map $ decode-map-as session $ :: 'Map 'Tag 'Dynamic + [] k $ match + read-user-name users-map $ &map:get session-map :user-id + (:none) nil + (:some name) name :examples $ [] :schema $ :: 'Fn $ {} (:return 'Dynamic) :args $ [] 'Dynamic 'Dynamic 'Dynamic 'twig-profile $ %{} 'CodeEntry (:doc |) :code $ quote $ defn twig-profile (sessions users) let - sessions-map $ assert-type sessions $ :: 'Map 'Number (:: 'Map 'Tag 'Dynamic) - users-map $ assert-type users $ :: 'Map 'String (:: 'Map 'Tag 'Dynamic) - filtered $ assert-type + sessions-map $ decode-map-as sessions $ :: 'Map 'Number (:: 'Map 'Tag 'Dynamic) + users-map $ decode-map-as users $ :: 'Map 'String (:: 'Map 'Tag 'Dynamic) + filtered $ decode-map-as filter-kv sessions-map $ fn (k session) - non-nil? $ &map:get (assert-type session 'Map) :user-id + non-nil? $ &map:get + decode-map-as session $ :: 'Map 'Tag 'Dynamic + , :user-id :: 'Map 'Number $ :: 'Map 'Tag 'Dynamic filter-map-kv filtered $ fn (k session) let - session-map $ assert-type session 'Map - %:: MapEntryDecision :keep k $ option:unwrap-or - get-in users-map $ [] - assert-type (&map:get session-map :user-id) String - , :name - , nil + session-map $ decode-map-as session $ :: 'Map 'Tag 'Dynamic + MapEntryDecision :keep k $ match + read-user-name users-map $ &map:get session-map :user-id + (:none) nil + (:some name) name :examples $ [] :schema $ :: 'Fn $ {} (:return 'Dynamic) :args $ [] 'Dynamic 'Dynamic @@ -2149,7 +2240,7 @@ 'change-title $ %{} 'CodeEntry (:doc |) :code $ quote $ defn change-title (db op-data sid op-id op-time) let - op-map $ assert-type op-data 'Map + op-map $ decode-map-as op-data $ :: 'Map 'Tag 'Dynamic assoc-in db [] :articles (&map:get op-map :id) :title &map:get op-map :title @@ -2181,7 +2272,7 @@ article-id $ option:unwrap-or get-in db $ [] :sessions sid :article-id , nil - paragraphs $ assert-type + paragraphs $ decode-map-as option:unwrap-or get-in db $ [] :articles article-id :paragraphs {} @@ -2255,13 +2346,13 @@ 'move $ %{} 'CodeEntry (:doc |) :code $ quote $ defn move (db op-data sid op-id op-time) let - op-map $ assert-type op-data $ :: 'Map 'Tag 'String + op-map $ decode-map-as op-data $ :: 'Map 'Tag 'String target-key $ &map:get op-map :target base-key $ &map:get op-map :base article-id $ option:unwrap-or get-in db $ [] :sessions sid :article-id , nil - paragraphs $ assert-type + paragraphs $ decode-map-as option:unwrap-or get-in db $ [] :articles article-id :paragraphs {} @@ -2290,7 +2381,7 @@ article-id $ option:unwrap-or get-in db $ [] :sessions sid :article-id , nil - paragraphs $ assert-type + paragraphs $ decode-map-as option:unwrap-or get-in db $ [] :articles article-id :paragraphs {} @@ -2375,7 +2466,7 @@ 'update-content $ %{} 'CodeEntry (:doc |) :code $ quote $ defn update-content (db op-data sid op-id op-time) let - op-map $ assert-type op-data 'Map + op-map $ decode-map-as op-data $ :: 'Map 'Tag 'Dynamic article-id $ option:unwrap-or get-in db $ [] :sessions sid :article-id , nil @@ -2444,37 +2535,45 @@ :require $ app.schema :as schema 'app.updater.user $ %{} 'FileEntry :defs $ {} + 'LoginCredentials $ %{} 'CodeEntry (:doc |) + :code $ quote $ defstruct LoginCredentials (:username 'String) (:password 'String) + :examples $ [] + :schema $ :: 'StructDef 'log-in $ %{} 'CodeEntry (:doc |) :code $ quote $ defn log-in (db op-data sid op-id op-time) - let-sugar - [] username password - , op-data - users $ assert-type - option:unwrap-or (get db :users) ({}) + let + db-map $ decode-map-as db $ :: 'Map 'Tag 'Dynamic + credentials $ read-login-credentials op-data + username $ :username credentials + password $ :password credentials + users $ decode-map-as + option:unwrap-or (get db-map :users) ({}) :: 'Map 'String $ :: 'Map 'Tag 'Dynamic maybe-user $ -> users (vals) (.to-list) find $ fn (user) - = username $ &map:get (assert-type user 'Map) :name - update-in db ([] :sessions sid) + = username $ &map:get + decode-map-as user $ :: 'Map 'Tag 'Dynamic + , :name + update-in db-map ([] :sessions sid) fn (session) let - session-map $ assert-type + session-map $ decode-map-as option:unwrap-or session $ {} - , 'Map + :: 'Map 'Tag 'Dynamic if (option:some? maybe-user) let - user-map $ assert-type (option:unwrap maybe-user) 'Map + user-map $ decode-map-as (option:unwrap maybe-user) (:: 'Map 'Tag 'Dynamic) if = (md5 password) (&map:get user-map :password) assoc session-map :user-id $ &map:get user-map :id update session-map :messages $ fn (messages) assoc - assert-type messages $ :: 'Map 'String 'Dynamic + decode-map-as messages $ :: 'Map 'String 'Dynamic , op-id $ {} (:id op-id) :text $ str "|Wrong password for " username update session-map :messages $ fn (messages) assoc - assert-type messages $ :: 'Map 'String 'Dynamic + decode-map-as messages $ :: 'Map 'String 'Dynamic , op-id $ {} (:id op-id) :text $ str "|No user named: " username :examples $ [] @@ -2486,26 +2585,74 @@ :examples $ [] :schema $ :: 'Fn $ {} (:return 'Dynamic) :args $ [] 'Dynamic 'Dynamic 'Number 'String 'Number + 'read-login-credentials $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn read-login-credentials (value) + let + fields $ decode-map-as value $ :: 'List 'Dynamic + username $ decode-map-as (&list:nth fields 0) 'String + password $ decode-map-as (&list:nth fields 1) 'String + LoginCredentials :username username :password password + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'app.updater.user/LoginCredentials) + :args $ [] 'Dynamic + :tests $ [] + %{} 'TestEntry (:name |credentials-strings-and-extra-fields) + :code $ quote $ let + source $ [] "|用户🐈" |password 42 + value $ read-login-credentials source + assert= "|用户🐈" $ :username value + assert= |password $ :password value + assert= 42 $ &list:nth source 2 + :tags $ #{} :input-boundary :unit + %{} 'TestEntry (:name |credentials-empty-password-preserved) + :code $ quote $ let + value $ read-login-credentials $ [] |guest | + assert= |guest $ :username value + assert= | $ :password value + :tags $ #{} :input-boundary :unit + %{} 'TestEntry (:name |credentials-reject-invalid-fields) + :code $ quote $ let + rejected? $ fn (value) + hint-fn $ {} + :args $ [] 'Dynamic + :return 'Bool + try + do (read-login-credentials value) false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + assert= true $ rejected? $ [] 42 |password + assert= true $ rejected? $ [] |user nil + assert= true $ rejected? $ [] |user + assert= true $ rejected? $ {} + assert= true $ rejected? nil + :tags $ #{} :input-boundary :unit 'sign-up $ %{} 'CodeEntry (:doc |) :code $ quote $ defn sign-up (db op-data sid op-id op-time) - let-sugar - [] username password - , op-data - users $ assert-type - option:unwrap-or (get db :users) ({}) + let + db-map $ decode-map-as db $ :: 'Map 'Tag 'Dynamic + credentials $ read-login-credentials op-data + username $ :username credentials + password $ :password credentials + users $ decode-map-as + option:unwrap-or (get db-map :users) ({}) :: 'Map 'String $ :: 'Map 'Tag 'Dynamic maybe-user $ find .to-list $ vals users fn (user) - = username $ &map:get (assert-type user 'Map) :name + = username $ &map:get + decode-map-as user $ :: 'Map 'Tag 'Dynamic + , :name if (option:some? maybe-user) - update-in db ([] :sessions sid :messages) + update-in db-map ([] :sessions sid :messages) fn (messages) assoc option:unwrap-or messages $ {} , op-id $ {} (:id op-id) :text $ str "|Name is taken: " username - -> db + -> db-map assoc-in ([] :sessions sid :user-id) op-id assoc-in ([] :users op-id) {} (:id op-id) (:name username) (:nickname username) @@ -2538,7 +2685,7 @@ 'filter-kv $ %{} 'CodeEntry (:doc |) :code $ quote $ defn filter-kv (xs f) filter-map-kv xs $ fn (k v) - if (f k v) (%:: MapEntryDecision :keep k v) (%:: MapEntryDecision :drop) + if (f k v) (MapEntryDecision :keep k v) (MapEntryDecision :drop) :examples $ [] :schema $ :: 'Fn $ {} :args $ [] (:: 'Map 'K 'V) diff --git a/deps.cirru b/deps.cirru index 4ca7957..d944e3b 100644 --- a/deps.cirru +++ b/deps.cirru @@ -5,9 +5,9 @@ |Cumulo/cumulo-util.calcit |0.0.27 |Respo/alerts.calcit |0.10.51 |Respo/respo-feather.calcit |0.4.25 - |Respo/respo-markdown.calcit |0.4.51 + |Respo/respo-markdown.calcit |0.4.53 |Respo/respo-message.calcit |0.0.33 - |Respo/respo-ui.calcit |0.7.36 + |Respo/respo-ui.calcit |0.7.37 |Respo/respo.calcit |0.16.118 |calcit-lang/bisection-key |0.0.30 |calcit-lang/calcit-wss |0.2.34 From 5fde3a83f2eee205a801d2c53f5193fa8d638db8 Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 14 Oct 2026 12:40:14 +0800 Subject: [PATCH 11/18] fix: check session message removal boundaries --- README.md | 7 ++++- calcit.cirru | 72 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dceabef..4c5c99d 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ focus 分组保持 List,session key 保持 Number。登录输入派发一个 ` `:require` 规则和 `:refer` 名称列表使用 Cirru syntax list,不加多余 `[]`。 定义内的 `:tests` 验证空值、Unicode、0、访客投影、编辑时间选择、段落插入和消息顺序。 -当前候选双入口严格检查、42 项 native / 生成 JS 共享回放及前端构建通过。 +当前候选双入口严格检查、45 项 native / 生成 JS 共享回放及前端构建通过。 路由数据按实际用途解码:文章数据使用 Tag 键,profile 成员使用 Number session 键, 不会将所有路由都解释成同一种 Map。宿主 trait 使用完整类型引用,拖放 props 与回调有明确合同。 @@ -51,6 +51,10 @@ Cirru EDN patch,避免将传输中的 enum 直接当作已校验的名义值 Markdown 使用正式 0.4.53,高亮回调的输入与输出均有 String 合同。 隔离浏览器已验证实际代码高亮、Number session key 和刷新后自动登录。 +删除 session 消息时在使用边界验证 Tag 键 payload、String 消息 ID 和 +String 键消息集合;session key 保持 Number。定义测试覆盖仅删除目标消息、 +其他 session 和字段保持不变、目标缺失及非法 payload 拒绝。 + ## 部署 COS 只接收前端 `dist/` 资产。生产前缀为 `TopixIM/tabletwo/`; @@ -69,6 +73,7 @@ PR 前缀带 PR 编号、run 和 attempt,使用 COS Action 的内置公共 URL - 普通 Caps 的传递版本选择告警不等同严格依赖解析通过。 - Highlight.js 仍使用原有 legacy 调用方式,保留其弃用提示,未宣称已迁移该接口。 - 完整 `fix --workflow strict --verify` 被 core `update-in` 的回调泛型输入证明阻断;双入口检查通过不等同完整收尾。 +- 未发布的 core 修复候选已越过该阻塞,但应用 updater 的返回证明仍待处理;候选诊断不等同匹配正式版本的完整验收。 ## License diff --git a/calcit.cirru b/calcit.cirru index 09ced89..d4b8d46 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -2510,14 +2510,76 @@ :args $ [] 'Dynamic 'Dynamic 'Number 'String 'Number 'remove-message $ %{} 'CodeEntry (:doc |) :code $ quote $ defn remove-message (db op-data sid op-id op-time) - update-in db ([] :sessions sid :messages) - fn (messages) - dissoc - option:unwrap-or messages $ {} - &map:get (assert-type op-data 'Map) :id + let + payload $ decode-map-as op-data $ :: 'Map 'Tag 'Dynamic + message-id $ decode-map-as (&map:get payload :id) 'String + update-in db ([] :sessions sid :messages) + fn (messages) + dissoc + decode-map-as + option:unwrap-or messages $ {} + :: 'Map 'String 'Dynamic + , message-id :examples $ [] :schema $ :: 'Fn $ {} (:return 'Dynamic) :args $ [] 'Dynamic 'Dynamic 'Number 'String 'Number + :tests $ [] + %{} 'TestEntry + :name |removal-preserves-numeric-session-and-other-messages + :code $ quote $ let + removed-message $ {} (:id |m-1) (:text "|错误🐈") + second $ {} (:id |m-2) (:text "|保留") + other-session $ {} $ :messages + {} $ |m-3 second + current-session $ {} + :cursor $ [] :article 42 + :messages $ {} (|m-1 removed-message) (|m-2 second) + db $ {} + :users $ {} $ |u-1 + {} $ :name |Ada + :sessions $ {} (42 current-session) (7 other-session) + expected $ assoc-in db ([] :sessions 42 :messages) + {} $ |m-2 second + assert= expected $ remove-message db + {} $ :id |m-1 + , 42 |op 1 + :tags $ #{} :message :unit + %{} 'TestEntry (:name |missing-message-preserves-existing-data) + :code $ quote $ let + message $ {} (:id |m-1) (:text "|保留") + db $ {} $ :sessions + {} $ 42 $ {} + :messages $ {} $ |m-1 message + assert= db $ remove-message db + {} $ :id |missing + , 42 |op 1 + assert= + {} $ :sessions $ {} + 42 $ {} $ :messages ({}) + remove-message ({}) + {} $ :id |missing + , 42 |op 1 + :tags $ #{} :message :unit + %{} 'TestEntry (:name |removal-rejects-invalid-payload-and-message-key) + :code $ quote $ let + rejected? $ fn (payload) + hint-fn $ {} + :args $ [] 'Dynamic + :return 'Bool + try + do + remove-message ({}) payload 42 |op 1 + , false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + assert= true $ rejected? nil + assert= true $ rejected? $ {} + assert= true $ rejected? $ {} (:id 42) + assert= true $ rejected? $ {} (:id nil) + :tags $ #{} :message :unit 'view-article $ %{} 'CodeEntry (:doc |) :code $ quote $ defn view-article (db op-data sid op-id op-time) update-in db ([] :sessions sid) From 6d2f4778dbaace48ec6b416ab8a2b8c7cfb08c30 Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 14 Oct 2026 12:51:16 +0800 Subject: [PATCH 12/18] fix: validate updater result at the state boundary --- README.md | 4 +++- calcit.cirru | 65 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 4c5c99d..12bb042 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ focus 分组保持 List,session key 保持 Number。登录输入派发一个 ` `:require` 规则和 `:refer` 名称列表使用 Cirru syntax list,不加多余 `[]`。 定义内的 `:tests` 验证空值、Unicode、0、访客投影、编辑时间选择、段落插入和消息顺序。 -当前候选双入口严格检查、45 项 native / 生成 JS 共享回放及前端构建通过。 +当前候选双入口严格检查、47 项 native / 生成 JS 共享回放及前端构建通过。 路由数据按实际用途解码:文章数据使用 Tag 键,profile 成员使用 Number session 键, 不会将所有路由都解释成同一种 Map。宿主 trait 使用完整类型引用,拖放 props 与回调有明确合同。 @@ -54,6 +54,8 @@ Markdown 使用正式 0.4.53,高亮回调的输入与输出均有 String 合 删除 session 消息时在使用边界验证 Tag 键 payload、String 消息 ID 和 String 键消息集合;session key 保持 Number。定义测试覆盖仅删除目标消息、 其他 session 和字段保持不变、目标缺失及非法 payload 拒绝。 +根 updater 在返回协议边界验证 `Map`,保留原操作分派和未知操作 +返回原状态的行为;测试覆盖连接、断开及 Number session key。 ## 部署 diff --git a/calcit.cirru b/calcit.cirru index d4b8d46..61861f7 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -2208,30 +2208,55 @@ :defs $ {} $ 'updater %{} 'CodeEntry (:doc |) :code $ quote $ defn updater (db op sid op-id op-time) - match op - (:session/connect op-data) (session/connect db op-data sid op-id op-time) - (:session/disconnect op-data) (session/disconnect db op-data sid op-id op-time) - (:session/remove-message op-data) (session/remove-message db op-data sid op-id op-time) - (:user/log-in op-data) (user/log-in db op-data sid op-id op-time) - (:user/sign-up op-data) (user/sign-up db op-data sid op-id op-time) - (:user/log-out op-data) (user/log-out db op-data sid op-id op-time) - (:session/view-article op-data) (session/view-article db op-data sid op-id op-time) - (:router/change op-data) (router/change db op-data sid op-id op-time) - (:paragraph/prepend op-data) (paragraph/para-prepend db op-data sid op-id op-time) - (:paragraph/append-to op-data) (paragraph/append-to db op-data sid op-id op-time) - (:paragraph/content op-data) (paragraph/update-content db op-data sid op-id op-time) - (:paragraph/remove op-data) (paragraph/remove-one db op-data sid op-id op-time) - (:paragraph/move op-data) (paragraph/move db op-data sid op-id op-time) - (:paragraph/edit op-data) (paragraph/edit db op-data sid op-id op-time) - (:paragraph/finish-editing op-data) (paragraph/finish-editing db op-data sid op-id op-time) - (:article/create op-data) (article/create db op-data sid op-id op-time) - (:article/remove-one op-data) (article/remove-one db op-data sid op-id op-time) - (:article/title op-data) (article/change-title db op-data sid op-id op-time) - _ $ do (println "|Unknown op:" op) db + decode-map-as + match op + (:session/connect op-data) (session/connect db op-data sid op-id op-time) + (:session/disconnect op-data) (session/disconnect db op-data sid op-id op-time) + (:session/remove-message op-data) (session/remove-message db op-data sid op-id op-time) + (:user/log-in op-data) (user/log-in db op-data sid op-id op-time) + (:user/sign-up op-data) (user/sign-up db op-data sid op-id op-time) + (:user/log-out op-data) (user/log-out db op-data sid op-id op-time) + (:session/view-article op-data) (session/view-article db op-data sid op-id op-time) + (:router/change op-data) (router/change db op-data sid op-id op-time) + (:paragraph/prepend op-data) (paragraph/para-prepend db op-data sid op-id op-time) + (:paragraph/append-to op-data) (paragraph/append-to db op-data sid op-id op-time) + (:paragraph/content op-data) (paragraph/update-content db op-data sid op-id op-time) + (:paragraph/remove op-data) (paragraph/remove-one db op-data sid op-id op-time) + (:paragraph/move op-data) (paragraph/move db op-data sid op-id op-time) + (:paragraph/edit op-data) (paragraph/edit db op-data sid op-id op-time) + (:paragraph/finish-editing op-data) (paragraph/finish-editing db op-data sid op-id op-time) + (:article/create op-data) (article/create db op-data sid op-id op-time) + (:article/remove-one op-data) (article/remove-one db op-data sid op-id op-time) + (:article/title op-data) (article/change-title db op-data sid op-id op-time) + _ $ do (println "|Unknown op:" op) db + :: 'Map 'Tag 'Dynamic :examples $ [] :schema $ :: 'Fn $ {} :args $ [] (:: 'Map 'Tag 'Dynamic) 'Enum 'Number 'String 'Number :return $ :: 'Map 'Tag 'Dynamic + :tests $ [] + %{} 'TestEntry + :name |session-operation-preserves-root-and-numeric-key + :code $ quote $ let + db $ {} + :sessions $ {} + :users $ {} $ |u-1 + {} $ :name |Ada + connected $ updater db (:: :session/connect nil) 42 |op-1 1 + sessions $ decode-map-as (&map:get connected :sessions) (:: 'Map 'Number 'Dynamic) + session-value $ decode-map-as (&map:get sessions 42) (:: 'Map 'Tag 'Dynamic) + assert= 42 $ &map:get session-value :id + assert= (&map:get db :users) (&map:get connected :users) + assert= db $ updater connected (:: :session/disconnect nil) 42 |op-2 2 + :tags $ #{} :unit :updater + %{} 'TestEntry (:name |unknown-operation-preserves-root-map) + :code $ quote $ let + db $ {} + :sessions $ {} $ 42 + {} $ :id 42 + :users $ {} + assert= db $ updater db (:: :unknown nil) 42 |op 1 + :tags $ #{} :unit :updater :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns app.updater :require (app.updater.session :as session) (app.updater.user :as user) (app.updater.router :as router) (app.updater.paragraph :as paragraph) (app.updater.article :as article) From f77311fe2cae452dd81352ff958fea59b18d485a Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 14 Oct 2026 13:01:34 +0800 Subject: [PATCH 13/18] fix: return Unit from dispatch effects and test native transition --- .github/workflows/upload.yaml | 3 ++- README.md | 6 +++++- calcit.cirru | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/upload.yaml b/.github/workflows/upload.yaml index 419f62d..dbc70f1 100644 --- a/.github/workflows/upload.yaml +++ b/.github/workflows/upload.yaml @@ -56,7 +56,8 @@ jobs: git diff --exit-code -- calcit.cirru calcit calcit.cirru --check-only --warn-dyn-method calcit calcit.cirru --entry server --check-only --warn-dyn-method - calcit calcit.cirru test --require-match --summary-only + calcit calcit.cirru test --exclude-tag server-effect --require-match --summary-only + calcit calcit.cirru --entry server test --tag server-effect --require-match --summary-only - name: Select CDN asset path id: asset-path diff --git a/README.md b/README.md index 12bb042..03a27de 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ yarn install --immutable caps verify --toolchain calcit calcit.cirru --check-only calcit calcit.cirru --entry server --check-only -calcit calcit.cirru test --require-match --summary-only +calcit calcit.cirru test --exclude-tag server-effect --require-match --summary-only +calcit calcit.cirru --entry server test --tag server-effect --require-match --summary-only calcit calcit.cirru js yarn release-page ``` @@ -56,6 +57,9 @@ String 键消息集合;session key 保持 Number。定义测试覆盖仅删除 其他 session 和字段保持不变、目标缺失及非法 payload 拒绝。 根 updater 在返回协议边界验证 `Map`,保留原操作分派和未知操作 返回原状态的行为;测试覆盖连接、断开及 Number session key。 +服务端 `dispatch!` 完成持久化或 Reel 更新后明确返回 Unit,不把 `reset!` 的 +赋值结果当作 Unit。另有一项 `server-effect` 定义测试在 native server entry 下 +验证 Unit 返回、数据库保持和原记录行为,并恢复进程内 Reel;不执行持久化分支。 ## 部署 diff --git a/calcit.cirru b/calcit.cirru index 61861f7..7e55712 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -1807,9 +1807,24 @@ if config/dev? $ println |Dispatch! (str op) op-data sid if (= op :effect/persist) (persist-db!) reset! *reel $ reel-reducer @*reel updater (:: op op-data) sid op-id op-time config/dev? + , &unit :examples $ [] :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] 'Tag 'Dynamic 'Number + :tests $ [] $ %{} 'TestEntry (:name |unit-result-preserves-state-transition) + :code $ quote $ let + reel-before @*reel + dispatch-result $ dispatch! :unknown nil 42 + reel-after @*reel + reset! *reel reel-before + assert= &unit dispatch-result + assert= (:db reel-before) (:db reel-after) + assert= + if config/dev? + inc $ count $ :records reel-before + count $ :records reel-before + count $ :records reel-after + :tags $ #{} :server-effect :unit 'get-backup-path! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn get-backup-path! () let From 884301c864ab01b873ab191cd19087d429644376 Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 14 Oct 2026 13:15:30 +0800 Subject: [PATCH 14/18] fix: check wire Map and Tag operation boundaries --- README.md | 2 ++ calcit.cirru | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 03a27de..1890cbf 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ String 键消息集合;session key 保持 Number。定义测试覆盖仅删除 服务端 `dispatch!` 完成持久化或 Reel 更新后明确返回 Unit,不把 `reset!` 的 赋值结果当作 Unit。另有一项 `server-effect` 定义测试在 native server entry 下 验证 Unit 返回、数据库保持和原记录行为,并恢复进程内 Reel;不执行持久化分支。 +服务端在读取消息字段前验证解析结果为 `Map`,进入 dispatch 前 +校验操作名为 Tag;传输格式仍是原 Cirru EDN Map,未知 kind 的日志行为不变。 ## 部署 diff --git a/calcit.cirru b/calcit.cirru index 7e55712..8a0478f 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -1910,9 +1910,12 @@ do (dispatch! :session/connect nil sid) (println "|New client.") (:message sid msg) let - action $ parse-cirru-edn msg + action $ decode-map-as (parse-cirru-edn msg) (:: 'Map 'Tag 'Dynamic) case-default (&map:get action :kind) (println "|unknown action:" action) - :op $ dispatch! (&map:get action :op) (&map:get action :data) sid + :op $ dispatch! + decode-map-as (&map:get action :op) 'Tag + &map:get action :data + , sid (:disconnect sid) do (println "|Client closed!") (dispatch! :session/disconnect nil sid) _ $ println "|unknown data:" data From 43392f687ec8535aeb7c29e86f40d2cd4963318f Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 14 Oct 2026 20:07:12 +0800 Subject: [PATCH 15/18] fix: return Unit from unchanged render callbacks --- README.md | 4 +++- calcit.cirru | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1890cbf..69341f3 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ String 键消息集合;session key 保持 Number。定义测试覆盖仅删除 验证 Unit 返回、数据库保持和原记录行为,并恢复进程内 Reel;不执行持久化分支。 服务端在读取消息字段前验证解析结果为 `Map`,进入 dispatch 前 校验操作名为 Tag;传输格式仍是原 Cirru EDN Map,未知 kind 的日志行为不变。 +定时渲染回调明确返回 Unit,包括 Reel 未变化、不需要同步客户端的分支。 +两项 `server-effect` 测试使用正式 0.29.3 CLI 验证返回值和进程内状态;共同语义测试仍为 47 项。 ## 部署 @@ -81,7 +83,7 @@ PR 前缀带 PR 编号、run 和 attempt,使用 COS Action 的内置公共 URL - 普通 Caps 的传递版本选择告警不等同严格依赖解析通过。 - Highlight.js 仍使用原有 legacy 调用方式,保留其弃用提示,未宣称已迁移该接口。 - 完整 `fix --workflow strict --verify` 被 core `update-in` 的回调泛型输入证明阻断;双入口检查通过不等同完整收尾。 -- 未发布的 core 修复候选已越过该阻塞,但应用 updater 的返回证明仍待处理;候选诊断不等同匹配正式版本的完整验收。 +- 未发布的 core 修复候选通过当前完整 strict;这不等同正式版本的完整验收,也不替代剩余业务回放与依赖退场核对。 ## License diff --git a/calcit.cirru b/calcit.cirru index 8a0478f..4305ff5 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -1889,9 +1889,18 @@ not $ identical? @*reader-reel @*reel reset! *reader-reel @*reel sync-clients! @*reader-reel + , &unit :examples $ [] - :schema $ :: 'Fn $ {} (:return 'Dynamic) + :schema $ :: 'Fn $ {} (:return 'Unit) :args $ [] + :tests $ [] $ %{} 'TestEntry (:name |unchanged-reel-still-returns-unit) + :code $ quote $ let + original @*reader-reel + reset! *reader-reel @*reel + assert= &unit $ render-loop! + assert= true $ identical? @*reader-reel @*reel + reset! *reader-reel original + :tags $ #{} :server-effect :unit 'replay-updater $ %{} 'CodeEntry (:doc |) :code $ quote $ defn replay-updater (db op sid op-id op-time) if (enum? op) From dc31f999ed503731710b37d40979c99fad1f707c Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 15 Oct 2026 05:53:19 +0800 Subject: [PATCH 16/18] chore: validate Tabletwo with released Calcit alpha.3 --- .github/workflows/upload.yaml | 1 + README.md | 12 ++++++++---- deps.cirru | 2 +- package.json | 2 +- yarn.lock | 10 +++++----- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/upload.yaml b/.github/workflows/upload.yaml index dbc70f1..cef6a50 100644 --- a/.github/workflows/upload.yaml +++ b/.github/workflows/upload.yaml @@ -56,6 +56,7 @@ jobs: git diff --exit-code -- calcit.cirru calcit calcit.cirru --check-only --warn-dyn-method calcit calcit.cirru --entry server --check-only --warn-dyn-method + calcit calcit.cirru fix --workflow strict --verify --format edn calcit calcit.cirru test --exclude-tag server-effect --require-match --summary-only calcit calcit.cirru --entry server test --tag server-effect --require-match --summary-only diff --git a/README.md b/README.md index 69341f3..eb893d4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## 开发与构建 -候选迁移使用正式 Calcit / `@calcit/procs` 0.29.3、Node.js 24 和 Yarn 4.18.0。 +候选迁移使用已发布的 Calcit / `@calcit/procs` 0.30.0-alpha.3、Node.js 24 和 Yarn 4.18.0。 `deps.cirru` 记录精确模块版本。先安装匹配的 CLI,再运行: ```sh @@ -16,6 +16,7 @@ yarn install --immutable caps verify --toolchain calcit calcit.cirru --check-only calcit calcit.cirru --entry server --check-only +calcit calcit.cirru fix --workflow strict --verify --format edn calcit calcit.cirru test --exclude-tag server-effect --require-match --summary-only calcit calcit.cirru --entry server test --tag server-effect --require-match --summary-only calcit calcit.cirru js @@ -63,7 +64,11 @@ String 键消息集合;session key 保持 Number。定义测试覆盖仅删除 服务端在读取消息字段前验证解析结果为 `Map`,进入 dispatch 前 校验操作名为 Tag;传输格式仍是原 Cirru EDN Map,未知 kind 的日志行为不变。 定时渲染回调明确返回 Unit,包括 Reel 未变化、不需要同步客户端的分支。 -两项 `server-effect` 测试使用正式 0.29.3 CLI 验证返回值和进程内状态;共同语义测试仍为 47 项。 +两项 `server-effect` 测试使用发布版 CLI 验证返回值和进程内状态;共同语义测试仍为 47 项。 + +已发布的 alpha.3 配对工具链通过完整 strict workflow,47 项共同测试在 native / JS +各通过一次,另有两项 native server-effect 测试;前端生成和 Vite 构建通过。 +这些检查不修改应用源码、选择新的业务默认值或放宽类型。 ## 部署 @@ -82,8 +87,7 @@ PR 前缀带 PR 编号、run 和 attempt,使用 COS Action 的内置公共 URL - UI state 和投影 Map 的开放字段仍需在具体使用处验证,未宣称已全部静态化。 - 普通 Caps 的传递版本选择告警不等同严格依赖解析通过。 - Highlight.js 仍使用原有 legacy 调用方式,保留其弃用提示,未宣称已迁移该接口。 -- 完整 `fix --workflow strict --verify` 被 core `update-in` 的回调泛型输入证明阻断;双入口检查通过不等同完整收尾。 -- 未发布的 core 修复候选通过当前完整 strict;这不等同正式版本的完整验收,也不替代剩余业务回放与依赖退场核对。 +- 传递模块仍有选版和旧 require 写法告警;正式依赖迁移、完整浏览器业务和依赖退场核对尚未完成。 ## License diff --git a/deps.cirru b/deps.cirru index d944e3b..29f5755 100644 --- a/deps.cirru +++ b/deps.cirru @@ -1,5 +1,5 @@ -{} (:calcit-version |0.29.3) +{} (:calcit-version |0.30.0-alpha.3) :version |0.1.1 :dependencies $ {} (|Cumulo/cumulo-reel.calcit |0.0.52) |Cumulo/cumulo-util.calcit |0.0.27 diff --git a/package.json b/package.json index c83d651..2a5aa80 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "vite": "^8.2.2" }, "dependencies": { - "@calcit/procs": "0.29.3", + "@calcit/procs": "0.30.0-alpha.3", "escape-html": "^1.0.3", "nanoid": "^5.1.5" } diff --git a/yarn.lock b/yarn.lock index 11b7058..f351af8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,15 +12,15 @@ __metadata: languageName: node linkType: hard -"@calcit/procs@npm:0.29.3": - version: 0.29.3 - resolution: "@calcit/procs@npm:0.29.3" +"@calcit/procs@npm:0.30.0-alpha.3": + version: 0.30.0-alpha.3 + resolution: "@calcit/procs@npm:0.30.0-alpha.3" dependencies: "@calcit/finger-vec": "npm:0.1.1" "@calcit/ternary-tree": "npm:0.0.26" "@cirru/parser.ts": "npm:0.0.10" "@cirru/writer.ts": "npm:^0.1.9" - checksum: 10c0/88713901f7e522a42252302ae43c44aa7d68551bb4b15add8521a9f3ef553a923c2ef8d13ee1d9413d0bec934efd7b63d4e77a2cdc777a550607f3e778d086a3 + checksum: 10c0/02ab938f03a2e79b4bdac0e2b982b6cafed57795881c5c9d69d39559cc8624d412d7b9d863813d24d158bc4d922af789ef1b67ff7c2cf86c174279ff19040e71 languageName: node linkType: hard @@ -725,7 +725,7 @@ __metadata: version: 0.0.0-use.local resolution: "tabletwo@workspace:." dependencies: - "@calcit/procs": "npm:0.29.3" + "@calcit/procs": "npm:0.30.0-alpha.3" bottom-tip: "npm:^0.1.3" cirru-color: "npm:^0.2.4" copy-text-to-clipboard: "npm:^3.2.2" From cb096799ad046409fef8998c9039fdf17231065c Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 15 Oct 2026 06:02:52 +0800 Subject: [PATCH 17/18] fix: retain typed map entries in article text export --- README.md | 11 +++- calcit.cirru | 62 ++++++++++++++++--- .../202610150610-typed-article-text-export.md | 27 ++++++++ 3 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 history/202610150610-typed-article-text-export.md diff --git a/README.md b/README.md index eb893d4..6f1d5a4 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ focus 分组保持 List,session key 保持 Number。登录输入派发一个 ` `:require` 规则和 `:refer` 名称列表使用 Cirru syntax list,不加多余 `[]`。 定义内的 `:tests` 验证空值、Unicode、0、访客投影、编辑时间选择、段落插入和消息顺序。 -当前候选双入口严格检查、47 项 native / 生成 JS 共享回放及前端构建通过。 +当前候选双入口严格检查、50 项 native / 生成 JS 共享回放及前端构建通过。 路由数据按实际用途解码:文章数据使用 Tag 键,profile 成员使用 Number session 键, 不会将所有路由都解释成同一种 Map。宿主 trait 使用完整类型引用,拖放 props 与回调有明确合同。 @@ -64,12 +64,17 @@ String 键消息集合;session key 保持 Number。定义测试覆盖仅删除 服务端在读取消息字段前验证解析结果为 `Map`,进入 dispatch 前 校验操作名为 Tag;传输格式仍是原 Cirru EDN Map,未知 kind 的日志行为不变。 定时渲染回调明确返回 Unit,包括 Reel 未变化、不需要同步客户端的分支。 -两项 `server-effect` 测试使用发布版 CLI 验证返回值和进程内状态;共同语义测试仍为 47 项。 +两项 `server-effect` 测试使用发布版 CLI 验证返回值和进程内状态;共同语义测试为 50 项。 -已发布的 alpha.3 配对工具链通过完整 strict workflow,47 项共同测试在 native / JS +已发布的 alpha.3 配对工具链通过完整 strict workflow,50 项共同测试在 native / JS 各通过一次,另有两项 native server-effect 测试;前端生成和 Vite 构建通过。 这些检查不修改应用源码、选择新的业务默认值或放宽类型。 +Text 导出使用保留 key/value 类型的 MapEntry,按段落 key 排序并验证文本字段, +保留标题、分隔符、Unicode 和 HTML 转义。定义测试验证正常导出、空集合和非法字段拒绝。 +隔离浏览器已验证当前版本的双客户端编辑、追加、拖放顺序同步、高亮、自动登录、 +持久化重启与退出;Text 点击不再出现 Option 被当作 Map 的解码错误。 + ## 部署 COS 只接收前端 `dist/` 资产。生产前缀为 `TopixIM/tabletwo/`; diff --git a/calcit.cirru b/calcit.cirru index 4305ff5..c97b39b 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -1142,6 +1142,59 @@ :on-dragenter 'respo.schema/EventHandler :examples $ [] :schema $ :: 'StructDef + 'article-text $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn article-text (article) + let + title $ read-article-title article + paragraphs $ decode-map-as (&map:get article :paragraphs) + :: 'Map 'String $ :: 'Map 'Tag 'Dynamic + entries $ map-entries paragraphs + contents $ -> entries + .sort-by $ fn (entry) (:key entry) + .map $ fn (entry) + read-paragraph-content $ :value entry + str &newline "|# " title &newline &newline |---- &newline &newline $ .join-string contents $ str &newline &newline |---- &newline &newline + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] $ :: 'Map 'Tag 'Dynamic + :tests $ [] + %{} 'TestEntry (:name |export-orders-keys-and-preserves-unicode) + :code $ quote $ let + article $ {} (:title "|协作🐈") + :paragraphs $ {} + |b $ {} $ :content "|第二段" + |a $ {} $ :content | + expected $ str &newline "|# 协作🐈" &newline &newline |---- &newline &newline | &newline &newline |---- &newline &newline "|第二段" + assert= expected $ article-text article + :tags $ #{} :export :unit + %{} 'TestEntry (:name |export-empty-paragraphs-preserves-header) + :code $ quote $ let + article $ {} (:title |) + :paragraphs $ {} + assert= (str &newline "|# " &newline &newline |---- &newline &newline) (article-text article) + :tags $ #{} :export :unit + %{} 'TestEntry (:name |export-rejects-invalid-wire-fields) + :code $ quote $ let + rejected? $ fn (article) + hint-fn $ {} + :args $ [] $ :: 'Map 'Tag 'Dynamic + :return 'Bool + try + do (article-text article) false + fn (message) + hint-fn $ {} + :args $ [] 'String + :return 'Bool + , true + assert= true $ rejected? $ {} (:title 42) + :paragraphs $ {} + assert= true $ rejected? $ {} (:title |Title) + :paragraphs $ {} $ 1 + {} $ :content |body + assert= true $ rejected? $ {} (:title |Title) + :paragraphs $ {} $ |a + {} $ :content 42 + :tags $ #{} :export :unit 'comp-info-list $ %{} 'CodeEntry (:doc |) :code $ quote $ defcomp comp-info-list (focus-list) let @@ -1354,14 +1407,7 @@ :on-click $ fn (e d!) let child $ option:unwrap $ window-open | - paragraphs $ decode-map-as (&map:get article-map :paragraphs) - :: 'Map 'String $ :: 'Map 'Tag 'Dynamic - content $ str &newline "|# " (&map:get article-map :title) &newline &newline |---- &newline &newline $ -> paragraphs (.to-list) (.sort-by first) - map $ fn (pair) - let - paragraph-map $ decode-map-as (last pair) (:: 'Map 'Tag 'Dynamic) - &map:get paragraph-map :content - .join-string $ str &newline &newline |---- &newline &newline + content $ article-text article-map html $ str |
 (escape-html content) |
document $ unsafe-coerce (child :document) 'app.ffi.popup/DocumentWriterHost document .write! html diff --git a/history/202610150610-typed-article-text-export.md b/history/202610150610-typed-article-text-export.md new file mode 100644 index 0000000..a8b3b3d --- /dev/null +++ b/history/202610150610-typed-article-text-export.md @@ -0,0 +1,27 @@ +# 文章文本导出的 MapEntry 类型关系 + +## 问题与决策 + +正式 alpha.3 浏览器业务回放发现:点击 Text 后,旧代码把异构 Map pair 的 +`last` 结果交给 Map decoder,但 `last` 返回 Option,实际解码必然失败。 +可失败的显式 decoder 不代表其调用一定成功,完整 strict 通过不能代替业务回放。 + +将文本生成提取为纯函数 `article-text`,输入沿用受检文章的 +`Map` 边界,返回 String。段落集合检查为 +`Map>` 后,使用 `map-entries` 保留 key/value +关系,按 key 排序,用已有 `read-paragraph-content` 验证文本;不再拆解 +异构 pair,也不使用 Option unwrap、unsafe 或补充强制断言。 + +原文章标题、段落顺序、分隔符、空集合、Unicode 和 HTML 转义位置保持不变。 +组件仍负责打开窗口及转义 HTML,纯函数不新增宿主能力或公开工具入口。 + +## 验证 + +定义 `:tests` 覆盖 key 排序与 Unicode/原始 HTML 字符、空段落集合,以及错误 +标题、Number key 和非 String 段落内容的拒绝。使用已发布的 alpha.3 CLI/runtime +配对执行原 native/JS 回放:50 项共同测试在两侧通过,完整 strict 诊断与改写建议为空, +Vite 构建通过。实际浏览器重新加载最新生成 JS 后点击 Text,没有再产生原 Option→Map +异常;弹出窗口的完整内容展示仍须独立核对,不以控制台无新异常冒充该项完成。 + +完整消费者验收仍要求正式依赖更新、其余浏览器业务和 PR/main 门禁;本文不作为 +依赖退场或稳定 0.30.0 已完成的声明。 From 7248cb896e2de3e525ff3d592cd5a4b228a36c92 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 15 Oct 2026 12:42:56 +0800 Subject: [PATCH 18/18] chore: converge Tabletwo on released module graph --- .github/workflows/upload.yaml | 2 +- README.md | 135 +++++++++++++++++----------------- deps.cirru | 20 ++--- 3 files changed, 77 insertions(+), 80 deletions(-) diff --git a/.github/workflows/upload.yaml b/.github/workflows/upload.yaml index cef6a50..8c449c8 100644 --- a/.github/workflows/upload.yaml +++ b/.github/workflows/upload.yaml @@ -45,7 +45,7 @@ jobs: - name: Resolve immutable dependencies run: | set -euo pipefail - caps --ci + caps --strict --ci yarn install --immutable caps verify --toolchain diff --git a/README.md b/README.md index 6f1d5a4..2734335 100644 --- a/README.md +++ b/README.md @@ -7,92 +7,89 @@ ## 开发与构建 -候选迁移使用已发布的 Calcit / `@calcit/procs` 0.30.0-alpha.3、Node.js 24 和 Yarn 4.18.0。 -`deps.cirru` 记录精确模块版本。先安装匹配的 CLI,再运行: +使用 Node.js 24、Yarn 4.18.0 和 Caps 0.1.1。 +先安装 `deps.cirru :calcit-version` 指定的已发布 Calcit CLI, +`package.json` 中的 `@calcit/procs` 必须与它精确配对。 +模块使用 `deps.cirru` 中的正式 tag,npm 新包安装保留一小时隔离。 ```sh -caps --ci +caps --strict --ci yarn install --immutable caps verify --toolchain -calcit calcit.cirru --check-only -calcit calcit.cirru --entry server --check-only +calcit calcit.cirru --check-only --warn-dyn-method +calcit calcit.cirru --entry server --check-only --warn-dyn-method calcit calcit.cirru fix --workflow strict --verify --format edn -calcit calcit.cirru test --exclude-tag server-effect --require-match --summary-only -calcit calcit.cirru --entry server test --tag server-effect --require-match --summary-only +calcit calcit.cirru test --exclude-tag server-effect --require-match --summary-only --format edn +calcit calcit.cirru --entry server test --tag server-effect --require-match --summary-only --format edn calcit calcit.cirru js yarn release-page ``` -前端以单个 Enum 派发操作,服务端保留原 `op` / `data` envelope。 -投影保留 Map 与 Cirru EDN 传输格式;客户端保存 -`Ref>>`,离线时为 None,连接后为 Some。 -升级已运行的页面时完整刷新,避免热重载保留旧状态和 watcher。 - -导航在投影 Map 的使用边界验证 `logged-in?` 为 Bool,成员计数为 -`Option`:缺失与 nil 保留原来的空值显示,0 仍显示为 0。 -导航 props 使用只声明 style 与 EventHandler 的具名 `NavigationProps`, -通过 Respo 原有 record props 接口渲染,不改变投影或消息的 EDN 格式。 -离线分支先判断 store,再读取在线 session。 - -投影中的标题、段落正文、登录字段和编辑时间在实际使用边界解码; -focus 分组保持 List,session key 保持 Number。登录输入派发一个 `:states` Enum, -登录保存与退出清理使用 JS FFI 模块的类型化 storage 接口。 -`:require` 规则和 `:refer` 名称列表使用 Cirru syntax list,不加多余 `[]`。 - -定义内的 `:tests` 验证空值、Unicode、0、访客投影、编辑时间选择、段落插入和消息顺序。 -当前候选双入口严格检查、50 项 native / 生成 JS 共享回放及前端构建通过。 -路由数据按实际用途解码:文章数据使用 Tag 键,profile 成员使用 Number session 键, -不会将所有路由都解释成同一种 Map。宿主 trait 使用完整类型引用,拖放 props 与回调有明确合同。 - -验收以双入口严格类型检查、定义内 `:tests`、native / 生成 JS 共享语义测试和 -真实业务回放为准。已删除 CI 不再使用的 Dynamic / unsafe 数量基线; -这不放宽类型合同,也不以增加预算代替验证。 - -登录凭据在服务端解码为具名结构;客户端通过 Recollect 已发布的 codec 还原 -Cirru EDN patch,避免将传输中的 enum 直接当作已校验的名义值。 -Markdown 使用正式 0.4.53,高亮回调的输入与输出均有 String 合同。 -隔离浏览器已验证实际代码高亮、Number session key 和刷新后自动登录。 - -删除 session 消息时在使用边界验证 Tag 键 payload、String 消息 ID 和 -String 键消息集合;session key 保持 Number。定义测试覆盖仅删除目标消息、 -其他 session 和字段保持不变、目标缺失及非法 payload 拒绝。 -根 updater 在返回协议边界验证 `Map`,保留原操作分派和未知操作 -返回原状态的行为;测试覆盖连接、断开及 Number session key。 -服务端 `dispatch!` 完成持久化或 Reel 更新后明确返回 Unit,不把 `reset!` 的 -赋值结果当作 Unit。另有一项 `server-effect` 定义测试在 native server entry 下 -验证 Unit 返回、数据库保持和原记录行为,并恢复进程内 Reel;不执行持久化分支。 -服务端在读取消息字段前验证解析结果为 `Map`,进入 dispatch 前 -校验操作名为 Tag;传输格式仍是原 Cirru EDN Map,未知 kind 的日志行为不变。 -定时渲染回调明确返回 Unit,包括 Reel 未变化、不需要同步客户端的分支。 -两项 `server-effect` 测试使用发布版 CLI 验证返回值和进程内状态;共同语义测试为 50 项。 - -已发布的 alpha.3 配对工具链通过完整 strict workflow,50 项共同测试在 native / JS -各通过一次,另有两项 native server-effect 测试;前端生成和 Vite 构建通过。 -这些检查不修改应用源码、选择新的业务默认值或放宽类型。 - -Text 导出使用保留 key/value 类型的 MapEntry,按段落 key 排序并验证文本字段, -保留标题、分隔符、Unicode 和 HTML 转义。定义测试验证正常导出、空集合和非法字段拒绝。 -隔离浏览器已验证当前版本的双客户端编辑、追加、拖放顺序同步、高亮、自动登录、 -持久化重启与退出;Text 点击不再出现 Option 被当作 Map 的解码错误。 +`yarn watch-page` 显式监听源码变化。生成的 `js-out/`、`dist/`、 +`dist-server/` 和 `.calcit/` 缓存不提交。 + +`calcit.cirru` 是结构化程序,修改前先读取当前 CLI 的 mutation contract, +用 query 定位,再通过结构化命令和 revision 守卫写入: + +```sh +calcit docs agents --contract +calcit query context app.client/main! --format edn +``` + +`:require` 规则和 `:refer` 名称列表直接使用 Cirru syntax list,不加前导 `[]`。 + +## 类型与运行合同 + +前端以单个 Enum 派发操作,服务端保留原 `op` / `data` envelope; +消息和存储默认使用 Cirru EDN。Recollect 的发布版 codec 还原 patch 的名义值, +开放 wire Map 不直接断言成已验证的业务类型。 + +客户端 store 是 `Ref>>`:离线为 None,连接后为 Some。 +session key 保持 Number;路由、消息、标题、正文、凭据和时间在具体使用处验证。 +成员计数的缺失与 nil 保持空值显示,0 仍显示为 0。 + +组件 props、拖放事件和 storage/time/DOM 等宿主方法具有明确合同。 +渲染与服务端 effect callback 返回 Unit,Reel 未变化的分支同样遵守该合同; +不把 `reset!` 或连接函数的赋值结果当作 Unit,也不在尾 recur 后补 Unit。 +升级已运行的页面时完整刷新,避免保留旧状态或 watcher。 + +Markdown 高亮回调保持 String 输入和输出。Text 导出通过类型化 MapEntry +保留段落 key/value,按 key 排序后验证正文;输出保留标题、分隔符与 Unicode, +写入弹窗前转义 HTML。 + +## 验证 + +语义测试保存在定义的 `:tests` 中,涵盖空值、Unicode、0、访客、 +时间选择、段落和消息顺序、文本导出及错误输入拒绝。 +`server-effect` 测试在 native server entry 下验证 Unit 和进程内状态, +并恢复测试修改的 Reel;共同语义回放复用 Calcit 仓库已有的 +`scripts/run-core-tests.mjs`,比较 native / JS 的原断言与日志。 + +验收还包括双入口完整 strict、严格依赖图、生产构建、 +隔离 WebSocket 与持久化,以及真实浏览器交互。 +测试使用合成数据库和独立服务,不连接生产存储。 +Dynamic / unsafe 数量基线已退役,不以统计预算代替类型和业务语义门槛。 ## 部署 COS 只接收前端 `dist/` 资产。生产前缀为 `TopixIM/tabletwo/`; -PR 前缀带 PR 编号、run 和 attempt,使用 COS Action 的内置公共 URL 校验。 +PR 前缀包含编号、run 和 attempt,使用 COS Action 的公共 URL 校验。 上传按事件和分支排队,job / 上传分别限制为 15 / 10 分钟。 -原网页入口与 `/servers/tabletwo/` rsync 服务端部署路径保持不变。 -`dist-server/` 由构建生成,不提交产物。 -服务端包保存 `calcit.cirru` 和依赖清单;服务器安装匹配工具与模块后运行 -`calcit calcit.cirru --entry server`。诊断 IR 不再伪装成 `compact.cirru` 部署。 + +只有 main 触发原网页与 `/servers/tabletwo/` 的 rsync 部署。 +`dist-server/` 保存 Snapshot 与依赖清单;服务器安装匹配工具和模块后运行: + +```sh +calcit calcit.cirru --entry server +``` ## 限制 -- 完整业务、宿主交互与浏览器验收尚未完成,局部回放不代表整项迁移通过。 -- 旧 0.28 分支的隔离回放不能证明此候选或完整 UI 已通过。 -- UI state 和投影 Map 的开放字段仍需在具体使用处验证,未宣称已全部静态化。 -- 普通 Caps 的传递版本选择告警不等同严格依赖解析通过。 -- Highlight.js 仍使用原有 legacy 调用方式,保留其弃用提示,未宣称已迁移该接口。 -- 传递模块仍有选版和旧 require 写法告警;正式依赖迁移、完整浏览器业务和依赖退场核对尚未完成。 +- 完整交互及迁移验收尚未完成,局部测试不代表整个应用已可发布。 +- 删除 Confirm 与 Text 弹窗完整内容仍需独立验收。 +- 框架 state、投影和宿主的开放边界仍存在,不宣称全应用已静态化。 +- Highlight.js 保留原 legacy 调用及弃用提示,依赖中仍有旧 require 写法告警。 +- Vite 保留大 chunk 提示,未通过放宽阈值掩盖。 ## License diff --git a/deps.cirru b/deps.cirru index 29f5755..403028c 100644 --- a/deps.cirru +++ b/deps.cirru @@ -1,17 +1,17 @@ {} (:calcit-version |0.30.0-alpha.3) :version |0.1.1 - :dependencies $ {} (|Cumulo/cumulo-reel.calcit |0.0.52) - |Cumulo/cumulo-util.calcit |0.0.27 - |Respo/alerts.calcit |0.10.51 - |Respo/respo-feather.calcit |0.4.25 - |Respo/respo-markdown.calcit |0.4.53 - |Respo/respo-message.calcit |0.0.33 - |Respo/respo-ui.calcit |0.7.37 - |Respo/respo.calcit |0.16.118 + :dependencies $ {} (|Cumulo/cumulo-reel.calcit |0.0.53) + |Cumulo/cumulo-util.calcit |0.0.28 + |Respo/alerts.calcit |0.10.52 + |Respo/respo-feather.calcit |0.4.26 + |Respo/respo-markdown.calcit |0.4.54 + |Respo/respo-message.calcit |0.0.34 + |Respo/respo-ui.calcit |0.7.39 + |Respo/respo.calcit |0.16.120 |calcit-lang/bisection-key |0.0.30 |calcit-lang/calcit-wss |0.2.34 |calcit-lang/calcit.std |0.2.41 |calcit-lang/memof |0.0.38 - |calcit-lang/recollect |0.0.58 - |mvc-works/ws-edn.calcit |0.0.38 + |calcit-lang/recollect |0.0.60 + |mvc-works/ws-edn.calcit |0.0.39