makersclaw 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/package.json +1 -1
- package/r/activity-log.json +52 -0
- package/r/agent-changes.json +51 -0
- package/r/agent-chat.json +53 -0
- package/r/agent-column.json +85 -0
- package/r/agent-dock.json +57 -0
- package/r/agent-kit.json +49 -0
- package/r/agent-marks.json +49 -0
- package/r/agent-pane.json +93 -0
- package/r/ai-action.json +56 -0
- package/r/annotate.json +4 -4
- package/r/approval-request.json +2 -2
- package/r/approval.json +54 -0
- package/r/ask-bar.json +50 -0
- package/r/audio-player.json +2 -2
- package/r/auth-form.json +2 -2
- package/r/automation-editor.json +66 -0
- package/r/automation-list.json +57 -0
- package/r/automation-model.json +118 -0
- package/r/automations-screen.json +71 -0
- package/r/board.json +2 -2
- package/r/calendar-view.json +3 -3
- package/r/cart.json +2 -2
- package/r/chart.json +5 -5
- package/r/chat-pane.json +84 -0
- package/r/chat.json +3 -3
- package/r/code-editor.json +2 -2
- package/r/comment-box.json +3 -3
- package/r/connect-card.json +51 -0
- package/r/contact-section.json +2 -2
- package/r/core.json +18 -6
- package/r/cost.json +51 -0
- package/r/csv-import.json +3 -3
- package/r/data-picker.json +60 -0
- package/r/data-table.json +2 -2
- package/r/diagram.json +2 -2
- package/r/doc-editor.json +2 -2
- package/r/doc-page.json +3 -3
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +2 -2
- package/r/first-run.json +63 -0
- package/r/fix-note.json +57 -0
- package/r/flow-canvas.json +3 -3
- package/r/flow-editor.json +79 -0
- package/r/footer.json +3 -3
- package/r/gantt.json +3 -3
- package/r/job-panel.json +2 -2
- package/r/json-editor.json +3 -3
- package/r/library-shelf.json +56 -0
- package/r/map.json +2 -2
- package/r/margin-notes.json +67 -0
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/needs-you.json +55 -0
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/pace-bar.json +69 -0
- package/r/page-builder.json +3 -3
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +3 -3
- package/r/product-detail.json +2 -2
- package/r/product-grid.json +3 -3
- package/r/record-form.json +2 -2
- package/r/record-grid.json +52 -0
- package/r/record-notes.json +4 -4
- package/r/registry.json +4089 -1510
- package/r/run-history.json +52 -0
- package/r/run-view.json +80 -0
- package/r/schedule-card.json +54 -0
- package/r/schedule-engine.json +79 -0
- package/r/schedule-gantt.json +83 -0
- package/r/schedule-ruler.json +64 -0
- package/r/search-dialog.json +2 -2
- package/r/settings-list.json +2 -2
- package/r/setup-checklist.json +2 -2
- package/r/share-dialog.json +2 -2
- package/r/sidebar.json +3 -3
- package/r/signature-pad.json +2 -2
- package/r/sketch-canvas.json +3 -3
- package/r/source-receipt.json +52 -0
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +3 -3
- package/r/step-picker.json +53 -0
- package/r/step-settings.json +72 -0
- package/r/suggestion.json +58 -0
- package/r/sum-bar.json +64 -0
- package/r/task-list.json +3 -3
- package/r/task-model.json +2 -2
- package/r/testimonials.json +2 -2
- package/r/tier-map.json +67 -0
- package/r/time-grid.json +73 -0
- package/r/time-strip.json +64 -0
- package/r/top-bar.json +66 -0
- package/r/transaction-list.json +2 -2
- package/r/trigger-picker.json +61 -0
- package/r/whiteboard.json +4 -4
- package/r/workflow-view.json +55 -0
- package/registry/blocks/activity-log/README.md +112 -0
- package/registry/blocks/activity-log/activity-log.jsx +351 -0
- package/registry/blocks/activity-log/block.json +33 -0
- package/registry/blocks/activity-log/demo.jsx +48 -0
- package/registry/blocks/activity-log/index.js +4 -0
- package/registry/blocks/agent-changes/README.md +94 -0
- package/registry/blocks/agent-changes/agent-changes.jsx +118 -0
- package/registry/blocks/agent-changes/block.json +28 -0
- package/registry/blocks/agent-changes/demo.jsx +39 -0
- package/registry/blocks/agent-changes/index.js +3 -0
- package/registry/blocks/agent-chat/README.md +145 -0
- package/registry/blocks/agent-chat/agent-chat.jsx +415 -0
- package/registry/blocks/agent-chat/block.json +30 -0
- package/registry/blocks/agent-chat/demo.jsx +165 -0
- package/registry/blocks/agent-chat/index.js +3 -0
- package/registry/blocks/agent-column/README.md +197 -0
- package/registry/blocks/agent-column/agent-column.jsx +492 -0
- package/registry/blocks/agent-column/block.json +67 -0
- package/registry/blocks/agent-column/column-picker.jsx +163 -0
- package/registry/blocks/agent-column/demo.jsx +50 -0
- package/registry/blocks/agent-column/index.js +7 -0
- package/registry/blocks/agent-column/model.js +164 -0
- package/registry/blocks/agent-column/receipt.jsx +483 -0
- package/registry/blocks/agent-column/spreadsheet-type.jsx +24 -0
- package/registry/blocks/agent-dock/README.md +210 -0
- package/registry/blocks/agent-dock/agent-dock.jsx +356 -0
- package/registry/blocks/agent-dock/block.json +47 -0
- package/registry/blocks/agent-dock/demo.jsx +153 -0
- package/registry/blocks/agent-dock/dock-model.js +118 -0
- package/registry/blocks/agent-dock/index.js +5 -0
- package/registry/blocks/agent-kit/README.md +73 -0
- package/registry/blocks/agent-kit/agent-kit.jsx +86 -0
- package/registry/blocks/agent-kit/block.json +10 -0
- package/registry/blocks/agent-kit/demo.jsx +35 -0
- package/registry/blocks/agent-kit/index.js +3 -0
- package/registry/blocks/agent-marks/README.md +80 -0
- package/registry/blocks/agent-marks/agent-marks.jsx +133 -0
- package/registry/blocks/agent-marks/block.json +27 -0
- package/registry/blocks/agent-marks/demo.jsx +49 -0
- package/registry/blocks/agent-marks/index.js +3 -0
- package/registry/blocks/agent-pane/README.md +225 -0
- package/registry/blocks/agent-pane/agent-pane.jsx +244 -0
- package/registry/blocks/agent-pane/block.json +41 -0
- package/registry/blocks/agent-pane/composer.jsx +6 -0
- package/registry/blocks/agent-pane/demo.jsx +233 -0
- package/registry/blocks/agent-pane/dock-context.js +10 -0
- package/registry/blocks/agent-pane/index.js +4 -0
- package/registry/blocks/agent-pane/prose.jsx +84 -0
- package/registry/blocks/agent-pane/questions.jsx +134 -0
- package/registry/blocks/agent-pane/transcript.jsx +268 -0
- package/registry/blocks/agent-pane/work.jsx +368 -0
- package/registry/blocks/ai-action/README.md +94 -0
- package/registry/blocks/ai-action/ai-action.jsx +188 -0
- package/registry/blocks/ai-action/block.json +34 -0
- package/registry/blocks/ai-action/demo.jsx +93 -0
- package/registry/blocks/ai-action/index.js +3 -0
- package/registry/blocks/annotate/README.md +14 -11
- package/registry/blocks/approval/README.md +145 -0
- package/registry/blocks/approval/approval.jsx +163 -0
- package/registry/blocks/approval/block.json +31 -0
- package/registry/blocks/approval/demo.jsx +121 -0
- package/registry/blocks/approval/index.js +3 -0
- package/registry/blocks/ask-bar/README.md +105 -0
- package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
- package/registry/blocks/ask-bar/block.json +26 -0
- package/registry/blocks/ask-bar/demo.jsx +78 -0
- package/registry/blocks/ask-bar/index.js +3 -0
- package/registry/blocks/automation-editor/README.md +291 -0
- package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
- package/registry/blocks/automation-editor/block.json +39 -0
- package/registry/blocks/automation-editor/demo.jsx +284 -0
- package/registry/blocks/automation-editor/index.js +4 -0
- package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
- package/registry/blocks/automation-list/README.md +129 -0
- package/registry/blocks/automation-list/automation-list.jsx +476 -0
- package/registry/blocks/automation-list/block.json +35 -0
- package/registry/blocks/automation-list/demo.jsx +67 -0
- package/registry/blocks/automation-list/index.js +3 -0
- package/registry/blocks/automation-model/README.md +109 -0
- package/registry/blocks/automation-model/block.json +135 -0
- package/registry/blocks/automation-model/catalog.js +256 -0
- package/registry/blocks/automation-model/check.js +161 -0
- package/registry/blocks/automation-model/demo.jsx +42 -0
- package/registry/blocks/automation-model/edit.js +262 -0
- package/registry/blocks/automation-model/fixes.js +287 -0
- package/registry/blocks/automation-model/glyph.jsx +13 -0
- package/registry/blocks/automation-model/graph.js +410 -0
- package/registry/blocks/automation-model/index.js +33 -0
- package/registry/blocks/automation-model/overlay.js +175 -0
- package/registry/blocks/automation-model/parts.jsx +94 -0
- package/registry/blocks/automation-model/schedule.js +53 -0
- package/registry/blocks/automation-model/templates.js +128 -0
- package/registry/blocks/automation-model/values.js +29 -0
- package/registry/blocks/automation-model/words.js +439 -0
- package/registry/blocks/automations-screen/README.md +207 -0
- package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
- package/registry/blocks/automations-screen/block.json +47 -0
- package/registry/blocks/automations-screen/demo.jsx +364 -0
- package/registry/blocks/automations-screen/index.js +4 -0
- package/registry/blocks/automations-screen/screen-model.js +129 -0
- package/registry/blocks/chart/cartesian.jsx +1 -1
- package/registry/blocks/chart/core.jsx +33 -23
- package/registry/blocks/chart/pie.jsx +1 -1
- package/registry/blocks/chat/README.md +4 -1
- package/registry/blocks/chat/demo.jsx +2 -2
- package/registry/blocks/chat-pane/README.md +143 -0
- package/registry/blocks/chat-pane/block.json +48 -0
- package/registry/blocks/chat-pane/chat-attach.js +166 -0
- package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
- package/registry/blocks/chat-pane/chat-context.js +17 -0
- package/registry/blocks/chat-pane/chat-model.js +43 -0
- package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
- package/registry/blocks/chat-pane/chats-list.jsx +52 -0
- package/registry/blocks/chat-pane/demo.jsx +103 -0
- package/registry/blocks/chat-pane/index.js +9 -0
- package/registry/blocks/connect-card/README.md +99 -0
- package/registry/blocks/connect-card/block.json +10 -0
- package/registry/blocks/connect-card/connect-card.jsx +103 -0
- package/registry/blocks/connect-card/demo.jsx +49 -0
- package/registry/blocks/connect-card/index.js +3 -0
- package/registry/blocks/cost/README.md +115 -0
- package/registry/blocks/cost/block.json +10 -0
- package/registry/blocks/cost/cost.jsx +176 -0
- package/registry/blocks/cost/demo.jsx +66 -0
- package/registry/blocks/cost/index.js +3 -0
- package/registry/blocks/data-picker/README.md +81 -0
- package/registry/blocks/data-picker/block.json +38 -0
- package/registry/blocks/data-picker/data-picker.jsx +210 -0
- package/registry/blocks/data-picker/demo.jsx +34 -0
- package/registry/blocks/data-picker/fields.js +89 -0
- package/registry/blocks/data-picker/index.js +4 -0
- package/registry/blocks/data-table/demo.jsx +1 -1
- package/registry/blocks/first-run/README.md +118 -0
- package/registry/blocks/first-run/block.json +11 -0
- package/registry/blocks/first-run/demo.jsx +88 -0
- package/registry/blocks/first-run/first-run-model.js +51 -0
- package/registry/blocks/first-run/first-run.jsx +156 -0
- package/registry/blocks/first-run/index.js +5 -0
- package/registry/blocks/first-run/probe-rules.js +26 -0
- package/registry/blocks/fix-note/README.md +79 -0
- package/registry/blocks/fix-note/block.json +35 -0
- package/registry/blocks/fix-note/demo.jsx +30 -0
- package/registry/blocks/fix-note/fix-note.jsx +235 -0
- package/registry/blocks/fix-note/index.js +3 -0
- package/registry/blocks/flow-canvas/README.md +15 -8
- package/registry/blocks/flow-editor/README.md +108 -0
- package/registry/blocks/flow-editor/block.json +44 -0
- package/registry/blocks/flow-editor/demo.jsx +115 -0
- package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
- package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
- package/registry/blocks/flow-editor/index.js +6 -0
- package/registry/blocks/flow-editor/minimap.jsx +78 -0
- package/registry/blocks/flow-editor/outline.jsx +134 -0
- package/registry/blocks/footer/README.md +3 -2
- package/registry/blocks/library-shelf/README.md +117 -0
- package/registry/blocks/library-shelf/block.json +28 -0
- package/registry/blocks/library-shelf/demo.jsx +68 -0
- package/registry/blocks/library-shelf/index.js +3 -0
- package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
- package/registry/blocks/library-shelf/probe-rules.js +15 -0
- package/registry/blocks/margin-notes/README.md +222 -0
- package/registry/blocks/margin-notes/block.json +10 -0
- package/registry/blocks/margin-notes/demo.jsx +144 -0
- package/registry/blocks/margin-notes/index.js +20 -0
- package/registry/blocks/margin-notes/margin-model.js +224 -0
- package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
- package/registry/blocks/mind-map/README.md +4 -2
- package/registry/blocks/needs-you/README.md +177 -0
- package/registry/blocks/needs-you/block.json +34 -0
- package/registry/blocks/needs-you/demo.jsx +143 -0
- package/registry/blocks/needs-you/index.js +3 -0
- package/registry/blocks/needs-you/needs-you.jsx +441 -0
- package/registry/blocks/org-chart/README.md +4 -2
- package/registry/blocks/pace-bar/README.md +230 -0
- package/registry/blocks/pace-bar/block.json +10 -0
- package/registry/blocks/pace-bar/demo.jsx +255 -0
- package/registry/blocks/pace-bar/index.js +22 -0
- package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
- package/registry/blocks/pace-bar/pace-model.js +288 -0
- package/registry/blocks/page-builder/README.md +1 -1
- package/registry/blocks/page-builder/demo.jsx +3 -3
- package/registry/blocks/pdf-viewer/README.md +4 -1
- package/registry/blocks/record-grid/README.md +100 -0
- package/registry/blocks/record-grid/block.json +28 -0
- package/registry/blocks/record-grid/demo.jsx +43 -0
- package/registry/blocks/record-grid/index.js +3 -0
- package/registry/blocks/record-grid/record-grid.jsx +251 -0
- package/registry/blocks/run-history/README.md +138 -0
- package/registry/blocks/run-history/block.json +10 -0
- package/registry/blocks/run-history/demo.jsx +74 -0
- package/registry/blocks/run-history/index.js +3 -0
- package/registry/blocks/run-history/run-history.jsx +218 -0
- package/registry/blocks/run-view/README.md +174 -0
- package/registry/blocks/run-view/block.json +50 -0
- package/registry/blocks/run-view/demo.jsx +286 -0
- package/registry/blocks/run-view/index.js +5 -0
- package/registry/blocks/run-view/run-controls.jsx +266 -0
- package/registry/blocks/run-view/run-view.jsx +236 -0
- package/registry/blocks/run-view/runs-list.jsx +99 -0
- package/registry/blocks/schedule-card/README.md +121 -0
- package/registry/blocks/schedule-card/block.json +10 -0
- package/registry/blocks/schedule-card/demo.jsx +82 -0
- package/registry/blocks/schedule-card/index.js +3 -0
- package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
- package/registry/blocks/schedule-engine/README.md +137 -0
- package/registry/blocks/schedule-engine/block.json +10 -0
- package/registry/blocks/schedule-engine/demo.jsx +82 -0
- package/registry/blocks/schedule-engine/engine.js +769 -0
- package/registry/blocks/schedule-engine/index.js +9 -0
- package/registry/blocks/schedule-engine/ops.js +71 -0
- package/registry/blocks/schedule-engine/place.js +294 -0
- package/registry/blocks/schedule-engine/tree.js +79 -0
- package/registry/blocks/schedule-engine/waits.js +80 -0
- package/registry/blocks/schedule-gantt/README.md +159 -0
- package/registry/blocks/schedule-gantt/block.json +10 -0
- package/registry/blocks/schedule-gantt/demo.jsx +223 -0
- package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
- package/registry/blocks/schedule-gantt/geometry.js +176 -0
- package/registry/blocks/schedule-gantt/index.js +26 -0
- package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
- package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
- package/registry/blocks/schedule-gantt/track.jsx +190 -0
- package/registry/blocks/schedule-ruler/README.md +148 -0
- package/registry/blocks/schedule-ruler/block.json +10 -0
- package/registry/blocks/schedule-ruler/demo.jsx +181 -0
- package/registry/blocks/schedule-ruler/index.js +21 -0
- package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
- package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
- package/registry/blocks/sidebar/README.md +23 -13
- package/registry/blocks/sketch-canvas/README.md +15 -9
- package/registry/blocks/source-receipt/README.md +97 -0
- package/registry/blocks/source-receipt/block.json +30 -0
- package/registry/blocks/source-receipt/demo.jsx +71 -0
- package/registry/blocks/source-receipt/index.js +3 -0
- package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
- package/registry/blocks/step-picker/README.md +78 -0
- package/registry/blocks/step-picker/block.json +31 -0
- package/registry/blocks/step-picker/demo.jsx +23 -0
- package/registry/blocks/step-picker/index.js +3 -0
- package/registry/blocks/step-picker/step-picker.jsx +144 -0
- package/registry/blocks/step-settings/README.md +100 -0
- package/registry/blocks/step-settings/block.json +44 -0
- package/registry/blocks/step-settings/demo.jsx +93 -0
- package/registry/blocks/step-settings/fields.jsx +58 -0
- package/registry/blocks/step-settings/flow-settings.jsx +115 -0
- package/registry/blocks/step-settings/index.js +5 -0
- package/registry/blocks/step-settings/step-settings.jsx +290 -0
- package/registry/blocks/suggestion/README.md +171 -0
- package/registry/blocks/suggestion/block.json +40 -0
- package/registry/blocks/suggestion/demo.jsx +91 -0
- package/registry/blocks/suggestion/index.js +3 -0
- package/registry/blocks/suggestion/suggestion.jsx +436 -0
- package/registry/blocks/sum-bar/README.md +122 -0
- package/registry/blocks/sum-bar/block.json +10 -0
- package/registry/blocks/sum-bar/demo.jsx +110 -0
- package/registry/blocks/sum-bar/index.js +5 -0
- package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
- package/registry/blocks/sum-bar/sum-model.js +126 -0
- package/registry/blocks/tier-map/README.md +217 -0
- package/registry/blocks/tier-map/block.json +10 -0
- package/registry/blocks/tier-map/demo.jsx +222 -0
- package/registry/blocks/tier-map/index.js +24 -0
- package/registry/blocks/tier-map/tier-map.jsx +574 -0
- package/registry/blocks/tier-map/tier-model.js +318 -0
- package/registry/blocks/time-grid/README.md +305 -0
- package/registry/blocks/time-grid/block.json +10 -0
- package/registry/blocks/time-grid/demo.jsx +279 -0
- package/registry/blocks/time-grid/grid-model.js +322 -0
- package/registry/blocks/time-grid/index.js +25 -0
- package/registry/blocks/time-grid/time-grid.jsx +1325 -0
- package/registry/blocks/time-strip/README.md +141 -0
- package/registry/blocks/time-strip/block.json +10 -0
- package/registry/blocks/time-strip/demo.jsx +129 -0
- package/registry/blocks/time-strip/index.js +19 -0
- package/registry/blocks/time-strip/strip-model.js +178 -0
- package/registry/blocks/time-strip/time-strip.jsx +260 -0
- package/registry/blocks/top-bar/README.md +149 -0
- package/registry/blocks/top-bar/block.json +35 -0
- package/registry/blocks/top-bar/demo.jsx +78 -0
- package/registry/blocks/top-bar/index.js +5 -0
- package/registry/blocks/top-bar/probe-rules.js +74 -0
- package/registry/blocks/top-bar/top-bar-model.js +152 -0
- package/registry/blocks/top-bar/top-bar.jsx +244 -0
- package/registry/blocks/trigger-picker/README.md +98 -0
- package/registry/blocks/trigger-picker/block.json +38 -0
- package/registry/blocks/trigger-picker/demo.jsx +59 -0
- package/registry/blocks/trigger-picker/index.js +3 -0
- package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
- package/registry/blocks/whiteboard/README.md +20 -14
- package/registry/blocks/workflow-view/README.md +123 -0
- package/registry/blocks/workflow-view/block.json +10 -0
- package/registry/blocks/workflow-view/demo.jsx +90 -0
- package/registry/blocks/workflow-view/index.js +3 -0
- package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
- package/src/cli.mjs +2 -2
- package/src/install.mjs +100 -22
- package/src/registry.mjs +37 -3
- package/templates/next/browser/lib/data.js +6 -1
- package/templates/next/files/README.md +909 -26
- package/templates/next/files/app/shell.jsx +54 -37
- package/templates/next/files/check/automation-format.ts +2103 -0
- package/templates/next/files/check/automation-package.ts +142 -0
- package/templates/next/files/check/run.mjs +1043 -12
- package/templates/next/files/components/ui/field.jsx +17 -4
- package/templates/next/files/components/ui/icon.jsx +13 -0
- package/templates/next/files/components/ui/index.js +3 -1
- package/templates/next/files/components/ui/layout.jsx +382 -0
- package/templates/next/files/components/ui/notice.jsx +70 -0
- package/templates/next/files/components/ui/page.jsx +124 -0
- package/templates/next/files/components/ui/stat.jsx +22 -7
- package/templates/next/files/components/ui/toast.jsx +6 -3
- package/templates/next/files/lib/automations.js +660 -0
- package/templates/next/files/lib/claw.js +244 -5
- package/templates/next/files/lib/door.js +92 -0
- package/templates/next/files/lib/hooks.js +1694 -6
- package/templates/next/files/lib/platform.js +611 -14
- package/templates/next/template.json +22 -3
|
@@ -18,6 +18,33 @@
|
|
|
18
18
|
// screen, long after the build (brain). Number 10 is browser mode's rule
|
|
19
19
|
// (claw's STATIC-SPEC §3): a browser app is files and a door, and the
|
|
20
20
|
// build refuses its server code late and unclearly, or not at all.
|
|
21
|
+
// Number 12 is the shell that grew with its content: a footer and the
|
|
22
|
+
// buttons after a long article sat past the window's end (viewport).
|
|
23
|
+
// Number 13 (llm, 0.8.0) keeps a model call on llm(): a screen that
|
|
24
|
+
// fetched the model door by its path got none of what the function does.
|
|
25
|
+
// Number 14 (library, 0.8.1) does the same for the Library's door.
|
|
26
|
+
// Number 15 (actions, 0.8.1) keeps every write a screen makes a declared
|
|
27
|
+
// action, the one its agent calls too: the CRM's agent saved fifty people
|
|
28
|
+
// as one note, because it had no other way to make them.
|
|
29
|
+
// Number 16 (squeeze, 0.8.1) refuses a wrapping flex row that holds a
|
|
30
|
+
// growing text cell (min-w-0 flex-1): it squeezed to a word a line in the
|
|
31
|
+
// CRM's list header and its connect card.
|
|
32
|
+
// Number 17 (resize, 0.8.1) keeps a ResizeObserver from changing what it
|
|
33
|
+
// watches inside its own delivery: the CRM's People screen reported "loop
|
|
34
|
+
// completed with undelivered notifications" and every report began a repair.
|
|
35
|
+
// Number 18 (automations, 0.8.1) asks the platform's own validator about the
|
|
36
|
+
// automations an app ships (makersclaw.json `automations`), so a flow the
|
|
37
|
+
// platform would refuse at install is refused here, at build.
|
|
38
|
+
// Number 19 (stretch, 0.8.1) keeps a row of ActionCards one height, their
|
|
39
|
+
// buttons on one line (UI review X6). Number 20 (meter, 0.8.1) wants
|
|
40
|
+
// `compact` on a labelled Meter in a box under 8rem (X1). Number 21
|
|
41
|
+
// (landed, 0.8.1) wants a page that opens a row it just made to wait for
|
|
42
|
+
// the write (X11). Number 25 (agentfirst, 0.8.1) keeps a "have the agent do it"
|
|
43
|
+
// button opening the agent with its line, never a form first (X9). Number 26
|
|
44
|
+
// (barlabel, 0.8.1) wants an icon on a top-bar action whose words are a
|
|
45
|
+
// BarText: under 32rem the words give way, and an icon-less button is empty.
|
|
46
|
+
// Number 27 (sidelong, 0.8.1) keeps a side column short: a PageLayout `side`
|
|
47
|
+
// or an <aside> that holds a form or a long list is refused (X13).
|
|
21
48
|
//
|
|
22
49
|
// npm run check source rules, then smoke
|
|
23
50
|
// npm run check -- --no-build source rules only: no build, no smoke
|
|
@@ -73,7 +100,7 @@ import { createServer as createHttpServer } from "node:http";
|
|
|
73
100
|
import { createServer } from "node:net";
|
|
74
101
|
import { basename, dirname, join, posix, relative, sep } from "node:path";
|
|
75
102
|
import { homedir, tmpdir } from "node:os";
|
|
76
|
-
import { fileURLToPath } from "node:url";
|
|
103
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
77
104
|
|
|
78
105
|
// The app dir: this file's parent, unless the platform runs a canonical
|
|
79
106
|
// copy of it from elsewhere against an app (CLAW_APP_DIR).
|
|
@@ -94,7 +121,7 @@ const ACTIONS_ONLY = process.argv.includes("--actions");
|
|
|
94
121
|
// .jsx (a components/ui/depiction.js beside depiction.jsx won); Next 16's
|
|
95
122
|
// Turbopack tries .jsx before .js (seen on 16.3.7), so the kit's own file
|
|
96
123
|
// wins there, but the rule holds for every bundler.
|
|
97
|
-
const KIT = /^components\/(?:ui\/(?:cx|index)\.js|ui\/(?:radio|toggle|button-group|avatar|breadcrumb|pagination|button|collapsible|item|table|depiction|feedback|stat|distribution|swatch|steps|separator|scroll-area|field|icon|menu|context-menu|popover|calendar|date-picker|tooltip|notice|sheet|tabs|slider|input-group|image|file-input|input-otp|command|combobox|rating|toast|carousel|resizable)\.jsx|parts\/(?:index|sample)\.js|parts\/(?:fixture|record-switcher|live-work)\.jsx)$/;
|
|
124
|
+
const KIT = /^components\/(?:ui\/(?:cx|index)\.js|ui\/(?:radio|toggle|button-group|avatar|breadcrumb|pagination|button|collapsible|item|table|depiction|feedback|stat|distribution|swatch|steps|separator|scroll-area|layout|page|field|icon|menu|context-menu|popover|calendar|date-picker|tooltip|notice|sheet|tabs|slider|input-group|image|file-input|input-otp|command|combobox|rating|toast|carousel|resizable)\.jsx|parts\/(?:index|sample)\.js|parts\/(?:fixture|record-switcher|live-work)\.jsx)$/;
|
|
98
125
|
|
|
99
126
|
// The pins: sha256 of every fixed file, written by `makersclaw init` and
|
|
100
127
|
// `makersclaw add` (check/pins.json). Read first, because a BLOCK is
|
|
@@ -228,13 +255,20 @@ const COLOUR_PREFIXES = "bg|text|border(?:-[xytrbles])?|ring(?:-offset)?|inset-r
|
|
|
228
255
|
|
|
229
256
|
/** Source without its comments, newlines kept so line numbers hold: a
|
|
230
257
|
* rule reads code, not the prose beside it (a header that says
|
|
231
|
-
* "window." or names <a> is not browser access or a link).
|
|
232
|
-
|
|
258
|
+
* "window." or names <a> is not browser access or a link). A line comment
|
|
259
|
+
* is a `//` that follows anything but a `:` or a backslash, which also
|
|
260
|
+
* blanks a string that opens with two slashes ("//llm") or holds a
|
|
261
|
+
* doubled one ("/_claw//llm") to the end of its line. `slashStrings`
|
|
262
|
+
* reads those as strings: a `//` is a comment only at the start of a line
|
|
263
|
+
* or after a space or one of ; , ( ) { } [ ]. For a rule that reads a
|
|
264
|
+
* URL's slashes (12, llm). */
|
|
265
|
+
function stripComments(text, { slashStrings = false } = {}) {
|
|
233
266
|
const blank = (m) => m.replace(/[^\n]/g, " ");
|
|
267
|
+
const line = slashStrings ? /(^|[\s;,(){}[\]])\/\/[^\n]*/g : /(^|[^:\\])\/\/[^\n]*/g;
|
|
234
268
|
return text
|
|
235
269
|
.replace(/\{\/\*[\s\S]*?\*\/\}/g, blank)
|
|
236
270
|
.replace(/\/\*[\s\S]*?\*\//g, blank)
|
|
237
|
-
.replace(
|
|
271
|
+
.replace(line, (m, lead) => lead + blank(m.slice(lead.length)));
|
|
238
272
|
}
|
|
239
273
|
/** JSX attributes up to the tag's `>`: strings and balanced braces may hold a `>` or `=>`. */
|
|
240
274
|
const ATTRS = String.raw`(?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}|[^>"'{])*`;
|
|
@@ -251,6 +285,204 @@ const MANIFEST = (() => {
|
|
|
251
285
|
}
|
|
252
286
|
})();
|
|
253
287
|
|
|
288
|
+
// The app's declared actions (makersclaw.json `actions`, 0.8.1; claw's
|
|
289
|
+
// docs/APP-TOOLS-PLAN.md D2): the writes its screens make, which its agent
|
|
290
|
+
// calls as tools through the platform's one runner. Read once for
|
|
291
|
+
// `--actions`, rule 8 and rule 15. Once an app declares any, its agent can
|
|
292
|
+
// write no other way, so every write a screen makes must be one (rule 15).
|
|
293
|
+
const DECLARED = (() => {
|
|
294
|
+
const list = MANIFEST.manifest?.actions;
|
|
295
|
+
return Array.isArray(list) ? list.filter((a) => a && typeof a === "object" && !Array.isArray(a) && typeof a.name === "string") : [];
|
|
296
|
+
})();
|
|
297
|
+
const DECLARED_NAMES = new Set(DECLARED.map((a) => a.name));
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The refusals of a declaration, each naming its place: [] when it holds.
|
|
301
|
+
* The same rules as claw's shared/src/app-actions.ts validateActions, so
|
|
302
|
+
* what passes here passes the platform at ship (keep the two together).
|
|
303
|
+
*/
|
|
304
|
+
function actionRefusals(actions, brainDecl, mode) {
|
|
305
|
+
if (actions === undefined || actions === null) return [];
|
|
306
|
+
if (!Array.isArray(actions)) return ["actions must be a list"];
|
|
307
|
+
const NAME = /^[a-z][a-z0-9_]{0,39}$/;
|
|
308
|
+
const FN = /^[a-z_][a-z0-9_]{0,62}$/;
|
|
309
|
+
const PROP = /^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/;
|
|
310
|
+
const KEYWORDS = new Set(["type", "description", "title", "properties", "required", "items", "enum", "minItems", "maxItems", "minLength", "maxLength", "minimum", "maximum", "additionalProperties", "format", "default", "examples"]);
|
|
311
|
+
const TYPES = new Set(["object", "array", "string", "number", "integer", "boolean", "null"]);
|
|
312
|
+
const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
313
|
+
const out = [];
|
|
314
|
+
if (actions.length > 60) out.push(`actions: at most 60; this app declares ${actions.length}`);
|
|
315
|
+
const b = isObj(brainDecl) ? brainDecl : {};
|
|
316
|
+
const writes = new Set(Array.isArray(b.writes) ? b.writes.filter((w) => typeof w === "string") : []);
|
|
317
|
+
const defs = new Map([["note", { fields: ["title", "body", "imported_from"], link_types: [] }]]);
|
|
318
|
+
for (const d of Array.isArray(b.kinds) ? b.kinds : [])
|
|
319
|
+
if (isObj(d) && typeof d.kind === "string" && Array.isArray(d.fields))
|
|
320
|
+
defs.set(d.kind, { fields: d.fields.filter((f) => isObj(f) && typeof f.name === "string" && f.retired !== true).map((f) => f.name), link_types: Array.isArray(d.link_types) ? d.link_types.filter(isObj) : [] });
|
|
321
|
+
const schema = (sch, at, depth = 0) => {
|
|
322
|
+
if (!isObj(sch)) return [`${at}: a schema is an object`];
|
|
323
|
+
if (depth > 6) return [`${at}: nested deeper than six levels`];
|
|
324
|
+
const r = [];
|
|
325
|
+
for (const k of Object.keys(sch)) if (!KEYWORDS.has(k)) r.push(`${at}: "${k}" is not a schema keyword the platform checks`);
|
|
326
|
+
if (!TYPES.has(sch.type)) return [...r, `${at}: type must be one of ${[...TYPES].join(", ")}`];
|
|
327
|
+
if (sch.type === "object" && sch.properties !== undefined) {
|
|
328
|
+
if (!isObj(sch.properties)) r.push(`${at}: properties must be an object`);
|
|
329
|
+
else for (const [n, sub] of Object.entries(sch.properties)) {
|
|
330
|
+
if (!PROP.test(n)) r.push(`${at}.${n}: a property is a plain name`);
|
|
331
|
+
r.push(...schema(sub, `${at}.${n}`, depth + 1));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (sch.type === "object" && sch.required !== undefined)
|
|
335
|
+
for (const q of Array.isArray(sch.required) ? sch.required : [null])
|
|
336
|
+
if (typeof q !== "string" || !isObj(sch.properties) || !(q in sch.properties)) r.push(`${at}: required ${q} is not one of its properties`);
|
|
337
|
+
if (sch.type === "array") r.push(...(sch.items === undefined ? [`${at}: an array says its items`] : schema(sch.items, `${at}[]`, depth + 1)));
|
|
338
|
+
return r;
|
|
339
|
+
};
|
|
340
|
+
const prop = (sch, n) => (isObj(sch) && isObj(sch.properties) && isObj(sch.properties[n]) ? sch.properties[n] : null);
|
|
341
|
+
const WORDS = `a source is "key", "$.key", { "value": … } or { "now": true }`;
|
|
342
|
+
// `edit`: the write names its record (id), so { "value": null } clears.
|
|
343
|
+
const source = (src, at, item, input, edit = false) => {
|
|
344
|
+
if (isObj(src)) {
|
|
345
|
+
const keys = Object.keys(src);
|
|
346
|
+
if (keys.length === 1 && keys[0] === "now") return src.now === true ? null : `${at}: { "now": true } is the time of the call`;
|
|
347
|
+
if (keys.length !== 1 || keys[0] !== "value") return `${at}: ${WORDS}`;
|
|
348
|
+
if (src.value === null) return edit ? null : `${at}: { "value": null } clears a field, only in a write that names its record (id)`;
|
|
349
|
+
return ["string", "number", "boolean"].includes(typeof src.value) ? null : `${at}: a constant is text, a number, true/false, or null to clear`;
|
|
350
|
+
}
|
|
351
|
+
if (typeof src !== "string" || !src) return `${at}: ${WORDS}`;
|
|
352
|
+
const top = src.startsWith("$.");
|
|
353
|
+
return prop(top ? input : item, top ? src.slice(2) : src) ? null : `${at}: ${src} is not a property of the ${top || item === input ? "input" : "item"}`;
|
|
354
|
+
};
|
|
355
|
+
// An arg a write reads by name (id, if_version, clear): its schema and whether it is required.
|
|
356
|
+
const arg = (src, at, item, input) => {
|
|
357
|
+
if (typeof src !== "string" || !src) return `${at}: names an arg, "key" or "$.key"`;
|
|
358
|
+
const top = src.startsWith("$.");
|
|
359
|
+
const from = top ? input : item;
|
|
360
|
+
const key = top ? src.slice(2) : src;
|
|
361
|
+
const sch = prop(from, key);
|
|
362
|
+
if (!sch) return `${at}: ${src} is not a property of the ${top || item === input ? "input" : "item"}`;
|
|
363
|
+
return { sch, required: isObj(from) && Array.isArray(from.required) && from.required.includes(key) };
|
|
364
|
+
};
|
|
365
|
+
const seen = new Set();
|
|
366
|
+
actions.forEach((a, i) => {
|
|
367
|
+
const at = `actions[${i}]${isObj(a) && typeof a.name === "string" ? ` (${a.name})` : ""}`;
|
|
368
|
+
if (!isObj(a)) return out.push(`${at}: an action is an object`);
|
|
369
|
+
for (const k of Object.keys(a)) if (!["name", "description", "input", "brain", "sql", "agent"].includes(k)) out.push(`${at}: unknown key "${k}"`);
|
|
370
|
+
if (!NAME.test(a.name ?? "")) out.push(`${at}: name is lower case, a-z, 0-9 and _, starting with a letter, at most 40`);
|
|
371
|
+
else if (seen.has(a.name)) out.push(`${at}: ${a.name} is declared twice`);
|
|
372
|
+
seen.add(a.name);
|
|
373
|
+
if (typeof a.description !== "string" || !a.description.trim()) out.push(`${at}: needs a description, one line of what it does`);
|
|
374
|
+
else if (a.description.length > 300 || a.description.includes("\n")) out.push(`${at}: the description is one line of at most 300 characters`);
|
|
375
|
+
if (a.agent !== undefined && typeof a.agent !== "boolean") out.push(`${at}: agent is true or false`);
|
|
376
|
+
const input = a.input;
|
|
377
|
+
if (!isObj(input) || input.type !== "object") out.push(`${at}: input is a JSON Schema of type object`);
|
|
378
|
+
else {
|
|
379
|
+
out.push(...schema(input, `${at}.input`));
|
|
380
|
+
if (JSON.stringify(input).length > 12000) out.push(`${at}: input is over 12000 characters of JSON; shorten its descriptions`);
|
|
381
|
+
}
|
|
382
|
+
if (a.brain === undefined && a.sql === undefined) out.push(`${at}: needs brain, sql, or both: an action does something`);
|
|
383
|
+
if (a.sql !== undefined && (typeof a.sql !== "string" || !FN.test(a.sql))) out.push(`${at}: sql names one function of the app's schema, lower case`);
|
|
384
|
+
if (a.brain === undefined) return;
|
|
385
|
+
const br = a.brain;
|
|
386
|
+
if (!isObj(br)) return out.push(`${at}.brain: an object with writes, and each and links when needed`);
|
|
387
|
+
if (mode === "server") out.push(`${at}.brain: company brain writes run only in a browser app ("mode": "browser"), through the platform's door`);
|
|
388
|
+
for (const k of Object.keys(br)) if (!["each", "writes", "links"].includes(k)) out.push(`${at}.brain: unknown key "${k}"`);
|
|
389
|
+
if (prop(input, "records")) out.push(`${at}.input: records is the platform's: it hands the written records to the sql function under that name`);
|
|
390
|
+
let item = input;
|
|
391
|
+
let per = 1;
|
|
392
|
+
if (br.each !== undefined) {
|
|
393
|
+
const each = typeof br.each === "string" ? prop(input, br.each) : null;
|
|
394
|
+
if (!each || each.type !== "array" || !isObj(each.items) || each.items.type !== "object") out.push(`${at}.brain.each: names an input property that is an array of objects`);
|
|
395
|
+
else {
|
|
396
|
+
item = each.items;
|
|
397
|
+
if (typeof each.maxItems !== "number" || each.maxItems > 100) out.push(`${at}.brain.each: ${br.each} needs a maxItems of at most 100`);
|
|
398
|
+
else per = each.maxItems;
|
|
399
|
+
if (!(Array.isArray(input.required) && input.required.includes(br.each))) out.push(`${at}.brain.each: ${br.each} must be required`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const steps = br.writes === undefined ? [] : br.writes;
|
|
403
|
+
if (!Array.isArray(steps) || steps.length > 10 || (steps.length === 0 && !(Array.isArray(br.links) && br.links.length)))
|
|
404
|
+
return out.push(`${at}.brain.writes: a list of 1 to 10 writes (none only when the action has links)`);
|
|
405
|
+
if (steps.length * per > 500) out.push(`${at}.brain: up to ${steps.length * per} writes a call; at most 500`);
|
|
406
|
+
const kindOf = new Map();
|
|
407
|
+
steps.forEach((w, j) => {
|
|
408
|
+
const wat = `${at}.brain.writes[${j}]`;
|
|
409
|
+
if (!isObj(w)) return out.push(`${wat}: a write is an object with as, kind and fields`);
|
|
410
|
+
for (const k of Object.keys(w)) if (!["as", "kind", "fields", "when", "id", "if_version", "clear"].includes(k)) out.push(`${wat}: unknown key "${k}"`);
|
|
411
|
+
if (typeof w.as !== "string" || !NAME.test(w.as)) out.push(`${wat}: as is a lower-case name the links and records use`);
|
|
412
|
+
else if (kindOf.has(w.as)) out.push(`${wat}: as ${w.as} is used twice`);
|
|
413
|
+
if (typeof w.kind !== "string" || !w.kind) return out.push(`${wat}: kind names a company brain kind`);
|
|
414
|
+
if (typeof w.as === "string") kindOf.set(w.as, w.kind);
|
|
415
|
+
if (!writes.has("*") && !writes.has(w.kind)) out.push(`${wat}: the app does not declare ${w.kind} in brain.writes`);
|
|
416
|
+
const def = defs.get(w.kind);
|
|
417
|
+
if (!def) out.push(`${wat}: ${w.kind} has no def in brain.kinds`);
|
|
418
|
+
const edit = w.id !== undefined;
|
|
419
|
+
if (!isObj(w.fields) || !Object.keys(w.fields).length) out.push(`${wat}: fields maps the kind's fields to their sources`);
|
|
420
|
+
else for (const [f, src] of Object.entries(w.fields)) {
|
|
421
|
+
if (def && !def.fields.includes(f)) out.push(`${wat}.fields.${f}: not a field of ${w.kind}`);
|
|
422
|
+
const bad = source(src, `${wat}.fields.${f}`, item, input, edit);
|
|
423
|
+
if (bad) out.push(bad);
|
|
424
|
+
}
|
|
425
|
+
if (w.when !== undefined) {
|
|
426
|
+
const bad = source(w.when, `${wat}.when`, item, input);
|
|
427
|
+
if (bad) out.push(bad);
|
|
428
|
+
}
|
|
429
|
+
if (edit) {
|
|
430
|
+
const id = arg(w.id, `${wat}.id`, item, input);
|
|
431
|
+
if (typeof id === "string") out.push(id);
|
|
432
|
+
else {
|
|
433
|
+
if (id.sch.type !== "string") out.push(`${wat}.id: ${w.id} is a string, the record's id`);
|
|
434
|
+
if (!id.required && w.when !== w.id) out.push(`${wat}.id: ${w.id} must be required, or the write's when`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (w.if_version !== undefined) {
|
|
438
|
+
if (!edit) out.push(`${wat}.if_version: only in a write that names its record (id)`);
|
|
439
|
+
const v = arg(w.if_version, `${wat}.if_version`, item, input);
|
|
440
|
+
if (typeof v === "string") out.push(v);
|
|
441
|
+
else if (v.sch.type !== "integer" && v.sch.type !== "number") out.push(`${wat}.if_version: ${w.if_version} is a whole number, the version read`);
|
|
442
|
+
}
|
|
443
|
+
if (w.clear !== undefined) {
|
|
444
|
+
if (!edit) out.push(`${wat}.clear: only in a write that names its record (id)`);
|
|
445
|
+
const c = arg(w.clear, `${wat}.clear`, item, input);
|
|
446
|
+
if (typeof c === "string") out.push(c);
|
|
447
|
+
else {
|
|
448
|
+
const its = isObj(c.sch.items) ? c.sch.items : null;
|
|
449
|
+
const names = its && Array.isArray(its.enum) ? its.enum : null;
|
|
450
|
+
const mapped = isObj(w.fields) ? Object.keys(w.fields) : [];
|
|
451
|
+
if (c.sch.type !== "array" || !its || its.type !== "string" || !names) out.push(`${wat}.clear: ${w.clear} is an array of strings with an enum of the fields it may clear`);
|
|
452
|
+
else for (const n of names) if (typeof n !== "string" || !mapped.includes(n)) out.push(`${wat}.clear: ${JSON.stringify(n)} is not one of this write's fields`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
if (br.links === undefined) return;
|
|
457
|
+
if (!Array.isArray(br.links)) return out.push(`${at}.brain.links: a list`);
|
|
458
|
+
br.links.forEach((l, j) => {
|
|
459
|
+
const lat = `${at}.brain.links[${j}]`;
|
|
460
|
+
if (!isObj(l)) return out.push(`${lat}: a link is { from, to, type }`);
|
|
461
|
+
const end = (e, which) => {
|
|
462
|
+
if (typeof e === "string") {
|
|
463
|
+
if (!kindOf.has(e)) out.push(`${lat}.${which}: ${e} is no write's as`);
|
|
464
|
+
return kindOf.get(e) ?? null;
|
|
465
|
+
}
|
|
466
|
+
if (isObj(e) && Object.keys(e).length === 1 && "id" in e) {
|
|
467
|
+
const bad = source(e.id, `${lat}.${which}.id`, item, input);
|
|
468
|
+
if (bad) out.push(bad);
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
out.push(`${lat}.${which}: a write's as, or { "id": <source> }`);
|
|
472
|
+
return null;
|
|
473
|
+
};
|
|
474
|
+
for (const k of Object.keys(l)) if (!["from", "to", "type", "op"].includes(k)) out.push(`${lat}: unknown key "${k}"`);
|
|
475
|
+
const from = end(l.from, "from");
|
|
476
|
+
const to = end(l.to, "to");
|
|
477
|
+
if (l.op !== undefined && l.op !== "add" && l.op !== "remove") out.push(`${lat}: op is "add" or "remove"`);
|
|
478
|
+
if (typeof l.type !== "string" || !NAME.test(l.type)) out.push(`${lat}: type is a link type's name`);
|
|
479
|
+
else if (l.type !== "related" && from && !(defs.get(from)?.link_types ?? []).some((t) => t.type === l.type && t.from === from && (to === null || t.to === to)))
|
|
480
|
+
out.push(`${lat}: ${from} declares no link type ${l.type}${to ? ` to ${to}` : ""}`);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
|
|
254
486
|
/**
|
|
255
487
|
* The tables and views the app's own schema makes, for rule 9's last
|
|
256
488
|
* clause: { names, from }, or null where no list is here. The first
|
|
@@ -525,6 +757,10 @@ function objectProps(text) {
|
|
|
525
757
|
// list.
|
|
526
758
|
const ROW_WRITES = ["insertRow", "updateRow", "moveRow", "deleteRow", "saveSetting"];
|
|
527
759
|
const DOOR_MODULES = ["lib/actions", "lib/door"];
|
|
760
|
+
/** The company brain's writes a screen can make (lib/brain.js), for rule
|
|
761
|
+
* 15: once an app declares actions, each is one of them. Erase, confirm
|
|
762
|
+
* and merge stay a person's press, and are not listed. */
|
|
763
|
+
const BRAIN_WRITES = ["writeRecord", "writeRecords", "linkRecords", "unlinkRecords", "createList", "changeList"];
|
|
528
764
|
/** The one bare, lower-case name the data door calls (ACTION_NAME in lib/actions.js). */
|
|
529
765
|
const ACTION_NAME = /^[a-z_][a-z0-9_]{0,62}$/;
|
|
530
766
|
/** A name, escaped for a RegExp (`$` is legal in one). */
|
|
@@ -573,6 +809,7 @@ function writeCalls() {
|
|
|
573
809
|
// them (`export { runAction as go } from "../lib/actions"`, `export *
|
|
574
810
|
// from`), until no file adds one.
|
|
575
811
|
const doors = new Map(DOOR_MODULES.map((m) => [m, all()]));
|
|
812
|
+
doors.set("lib/brain", new Map(BRAIN_WRITES.map((n) => [n, n])));
|
|
576
813
|
for (let grew = true; grew; ) {
|
|
577
814
|
grew = false;
|
|
578
815
|
for (const f of jsFiles) {
|
|
@@ -601,6 +838,7 @@ function writeCalls() {
|
|
|
601
838
|
const actions = [];
|
|
602
839
|
const tables = [];
|
|
603
840
|
const unread = [];
|
|
841
|
+
const brainWrites = [];
|
|
604
842
|
for (const f of jsFiles) {
|
|
605
843
|
const code = f.code;
|
|
606
844
|
const local = all(); // a name in this file → the function it calls
|
|
@@ -659,6 +897,10 @@ function writeCalls() {
|
|
|
659
897
|
};
|
|
660
898
|
const entries = mounted.get(moduleOf(f.path));
|
|
661
899
|
for (const { fn, open } of found.sort((a, b) => a.open - b.open)) {
|
|
900
|
+
if (BRAIN_WRITES.includes(fn)) {
|
|
901
|
+
brainWrites.push({ f, line: lineOf(code, open), fn });
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
662
904
|
const start = open + 1 + /^\s*/.exec(code.slice(open + 1))[0].length;
|
|
663
905
|
if (fn === "runAction" && /["'`]/.test(code[start] ?? "")) {
|
|
664
906
|
const m = new RegExp(`^${lit(1)}`).exec(code.slice(start));
|
|
@@ -677,7 +919,7 @@ function writeCalls() {
|
|
|
677
919
|
} else unread.push({ f, line, fn });
|
|
678
920
|
}
|
|
679
921
|
}
|
|
680
|
-
return { actions, tables, unread };
|
|
922
|
+
return { actions, tables, unread, brainWrites };
|
|
681
923
|
}
|
|
682
924
|
|
|
683
925
|
// `--actions`: what the screens run and write, as JSON, and nothing else,
|
|
@@ -688,7 +930,20 @@ function writeCalls() {
|
|
|
688
930
|
if (ACTIONS_ONLY) {
|
|
689
931
|
const { actions, tables } = writeCalls();
|
|
690
932
|
const names = (xs) => [...new Set(xs)].sort();
|
|
691
|
-
|
|
933
|
+
// A declared action's name runs its `sql` function (none for one that
|
|
934
|
+
// only writes the company brain), and every declared function is listed,
|
|
935
|
+
// a screen's or the agent's alone: the ship checks each exists and keeps
|
|
936
|
+
// the live app's from a change build (0.8.1).
|
|
937
|
+
const fnsOf = (name) => {
|
|
938
|
+
const a = DECLARED.find((x) => x.name === name);
|
|
939
|
+
return a ? (typeof a.sql === "string" ? [a.sql] : []) : [name];
|
|
940
|
+
};
|
|
941
|
+
const declaredFns = DECLARED.map((a) => a.sql).filter((x) => typeof x === "string");
|
|
942
|
+
const listed = {
|
|
943
|
+
actions: names([...actions.filter((a) => a.name !== null).flatMap((a) => fnsOf(a.name)), ...declaredFns]),
|
|
944
|
+
tables: names(tables.map((t) => t.table)),
|
|
945
|
+
...(DECLARED.length ? { declared: names([...DECLARED_NAMES]) } : {}),
|
|
946
|
+
};
|
|
692
947
|
await new Promise((done) => process.stdout.write(`${JSON.stringify(listed)}\n`, done));
|
|
693
948
|
process.exit(0);
|
|
694
949
|
}
|
|
@@ -851,11 +1106,13 @@ const surfaceKeys = [];
|
|
|
851
1106
|
// A browser app has no proxy.js either; rule 10 (browser) names both files, and why.
|
|
852
1107
|
if (MANIFEST.manifest?.mode !== "browser" && /^(?:middleware|proxy)\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts)$/.test(e) && e !== "proxy.js") L.hit(e, "proxy", `Next 16 runs proxy.js, the template's fixed token check; delete ${e}`);
|
|
853
1108
|
}
|
|
854
|
-
// check/ is this script
|
|
855
|
-
//
|
|
1109
|
+
// check/ is this script, its pins and the two copies of the automation
|
|
1110
|
+
// format rule 18 runs (pinned like any fixed file); a module hidden there
|
|
1111
|
+
// is bundled by Next and read by no rule.
|
|
856
1112
|
if (existsSync(join(ROOT, "check"))) {
|
|
857
1113
|
for (const e of readdirSync(join(ROOT, "check"))) {
|
|
858
|
-
|
|
1114
|
+
const copy = (e === "automation-format.ts" || e === "automation-package.ts") && PINS !== null && Object.prototype.hasOwnProperty.call(PINS, `check/${e}`);
|
|
1115
|
+
if (e !== "run.mjs" && e !== "pins.json" && !copy) L.hit(`check/${e}`, "extra", "check/ holds run.mjs, pins.json and the automation format's two pinned copies only; a module goes in components/ or lib/");
|
|
859
1116
|
}
|
|
860
1117
|
}
|
|
861
1118
|
for (const f of files) {
|
|
@@ -1250,12 +1507,17 @@ const surfaceKeys = [];
|
|
|
1250
1507
|
if (a.name === null) detail = 'runAction\'s name is decided at run time: name the action in the code, so the brief can (runAction({ name: "close_cycle", args }))';
|
|
1251
1508
|
else if (a.form === "string") detail = `runAction("${a.name}") runs nothing: it takes one object, runAction({ name: "${a.name}", args })`;
|
|
1252
1509
|
else if (!ACTION_NAME.test(a.name)) detail = `runAction runs "${a.name}": an action is one bare, lower-case name (close_cycle), a function of the app's own schema, the one the door calls`;
|
|
1510
|
+
// A declared action is the agent's own tool (rule 15): its brief
|
|
1511
|
+
// says when, never the SQL.
|
|
1512
|
+
else if (DECLARED_NAMES.has(a.name)) continue;
|
|
1253
1513
|
else if (!hasBrief) continue;
|
|
1254
1514
|
else if (!brief.includes(`\`${a.name}\``)) detail = `runAction runs ${a.name}, which the agent's brief does not name: add \`${a.name}\` to ../references/actions.md with its call, \`select ${a.name}($1::jsonb)\` and its params, so the app's agent runs the same function`;
|
|
1255
1515
|
else if (!selects(a.name)) detail = `the agent's brief names \`${a.name}\` but not its call: write \`select ${a.name}($1::jsonb)\` and its params there, so the app's agent runs the same function the screen does`;
|
|
1256
1516
|
if (detail) hits.push({ f: a.f, line: a.line, what: `${a.form} action ${a.name}`, detail });
|
|
1257
1517
|
}
|
|
1258
|
-
|
|
1518
|
+
// An app that declares actions has no one-row writes left to name:
|
|
1519
|
+
// rule 15 makes each an action.
|
|
1520
|
+
if (hasBrief && !DECLARED.length) {
|
|
1259
1521
|
for (const t of calls.tables) {
|
|
1260
1522
|
if (!brief.includes(`\`${t.table}\``)) hits.push({ f: t.f, line: t.line, what: `table ${t.table}`, detail: `${t.fn} writes ${t.table}, which the agent's brief does not name: add \`${t.table}\` to ../references/actions.md with the one statement that does the same on it` });
|
|
1261
1523
|
}
|
|
@@ -1871,6 +2133,770 @@ function columnFrom(code, at) {
|
|
|
1871
2133
|
}
|
|
1872
2134
|
}
|
|
1873
2135
|
|
|
2136
|
+
// 12. One viewport owner: the kit's AppLayout. Every design-first build
|
|
2137
|
+
// of 2026-10-07 shipped a shell of min-h-dvh that grew with its
|
|
2138
|
+
// content: the page scrolled, no pane scrolled on its own, and a
|
|
2139
|
+
// footer or a button after a 30-page article sat past the end of the
|
|
2140
|
+
// window, where nothing could reach it. So the app's code never sizes
|
|
2141
|
+
// anything by the window: no min-h-dvh, min-h-screen, h-screen, h-dvh
|
|
2142
|
+
// (nor their svh, lvh and max-h- kin), and no vh unit at all
|
|
2143
|
+
// (h-[80vh], max-h-[60vh], h-[calc(100vh-4rem)], style={{ height:
|
|
2144
|
+
// "100vh" }}). The kit is skipped: AppLayout in components/ui is the
|
|
2145
|
+
// one place the window's height is written. Depictions are read too:
|
|
2146
|
+
// a picture is drawn in px, never by the window.
|
|
2147
|
+
{
|
|
2148
|
+
const L = limiter("viewport");
|
|
2149
|
+
const DO =
|
|
2150
|
+
'the window has one owner, AppLayout from components/ui (the shell\'s frame: the content scrolls inside it, contentMode="fill" for a canvas, an inspector beside it), and a page for print is DocumentLayout; inside it bound a box by its parent (h-full, flex-1 min-h-0) or by a size (max-h-96)';
|
|
2151
|
+
scan(/(?<![\w-])(?:min-|max-)?h-(?:screen|dvh|svh|lvh)(?![\w-])/, (f, m, line) => L.hit(`${f.path}:${line}`, m[0], `${m[0]} sizes by the window, so what follows a long page cannot be reached; ${DO}`), files);
|
|
2152
|
+
scan(/(?<![\w.])\d+(?:\.\d+)?[dsl]?vh(?![A-Za-z0-9_])/, (f, m, line) => {
|
|
2153
|
+
// The whole class or value it sits in: h-[calc(100vh-4rem)], "100vh".
|
|
2154
|
+
const from = f.code.slice(0, m.index).search(/[^\s"'`{}]*$/);
|
|
2155
|
+
const token = f.code.slice(from).match(/^[^\s"'`{}]*/)[0];
|
|
2156
|
+
L.hit(`${f.path}:${line}`, token, `${token} is a fraction of the window, so what follows it cannot be reached; ${DO}`);
|
|
2157
|
+
}, files);
|
|
2158
|
+
if (L.count > 4) fail("viewport", `${L.count - 4} more`, "more of the same: sizes taken from the window");
|
|
2159
|
+
if (!L.count) ok("viewport", "nothing in the app sizes itself by the window");
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// 13. A model is called through llm() (0.8.0, the platform's _claw/llm
|
|
2163
|
+
// door). A screen that wants one answer from a model (sort, draft,
|
|
2164
|
+
// translate) calls llm() from lib/platform.js, or useLlm() from
|
|
2165
|
+
// lib/hooks.js, and never reaches the door by its URL: the path is the
|
|
2166
|
+
// platform's to move, the two functions are the contract, and an
|
|
2167
|
+
// app that fetches its own copy skips what they do (nothing throws, a
|
|
2168
|
+
// claw from before the door comes back `unavailable`, an abort
|
|
2169
|
+
// answers `aborted`). So no string in the app's code names `_claw/llm`,
|
|
2170
|
+
// and none handed to claw(), clawUrl() or clawStream() starts `llm`.
|
|
2171
|
+
// "Names" is as the browser reads it: clawUrl() drops every leading
|
|
2172
|
+
// slash, and a `./` or a doubled slash in a URL is dropped on the way
|
|
2173
|
+
// (`claw("//llm")`, `claw("./llm")`, `/_claw/./llm` all reach the door;
|
|
2174
|
+
// it reads the code with `slashStrings` so a `//` inside a string is
|
|
2175
|
+
// not taken for a comment). It reads literals, so a path built from
|
|
2176
|
+
// pieces is not seen, as in rule 8. Every mode and every tables
|
|
2177
|
+
// setting: the door is the same for all.
|
|
2178
|
+
{
|
|
2179
|
+
const L = limiter("llm");
|
|
2180
|
+
const LLM_PATH = /_claw\/(?:\.?\/)*llm(?![\w-])/;
|
|
2181
|
+
const LLM_NAME = /^(?:\.?\/)*llm(?![\w-])/;
|
|
2182
|
+
for (const f of jsFiles) {
|
|
2183
|
+
const code = stripComments(f.text, { slashStrings: true });
|
|
2184
|
+
const toClaw = new Set();
|
|
2185
|
+
for (const m of code.matchAll(/(?<![\w$.])claw(?:Url|Stream)?\s*(?:\?\.\s*)?\(\s*(?=["'`])/g)) toClaw.add(m.index + m[0].length);
|
|
2186
|
+
for (const { start, end } of literalsOf(code)) {
|
|
2187
|
+
const inner = code.slice(start + 1, end);
|
|
2188
|
+
if (!(LLM_PATH.test(inner) || (toClaw.has(start) && LLM_NAME.test(inner)))) continue;
|
|
2189
|
+
const path = inner.replace(/\s+/g, " ").slice(0, 60);
|
|
2190
|
+
L.hit(`${f.path}:${lineOf(code, start)}`, "path", `"${path}": the model door is reached through llm(input, { system, schema }) in lib/platform.js, or useLlm() in lib/hooks.js, never by its path (README §5, Calling a model)`);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
if (L.count > 4) fail("llm", `${L.count - 4} more`, "strings that name the model door's path instead of calling llm()");
|
|
2194
|
+
if (!L.count) ok("llm", "no screen names _claw/llm: a model is called through llm() or useLlm() (README §5)");
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
// 14. The Library is read through library() (0.8.1, the platform's
|
|
2198
|
+
// _claw/library door). A screen that shows what Makersclaw keeps
|
|
2199
|
+
// (decks, templates, a dataset) calls library.collections(),
|
|
2200
|
+
// library.items() and the rest from lib/platform.js, or the hooks
|
|
2201
|
+
// useLibraryItems() and kin from lib/hooks.js, and never reaches the
|
|
2202
|
+
// door by its URL: the path is the platform's to move, the functions
|
|
2203
|
+
// are the contract, and an app that fetches its own copy skips what
|
|
2204
|
+
// they do (nothing throws, a claw from before the door comes back
|
|
2205
|
+
// `unavailable`, an id that names nothing asks nothing, an abort
|
|
2206
|
+
// answers `aborted`). So no string in the app's code names
|
|
2207
|
+
// `_claw/library`, and none handed to claw(), clawUrl() or clawStream()
|
|
2208
|
+
// starts `library`, however the browser would read it (rule 13 says
|
|
2209
|
+
// how: `claw("//library")`, `claw("./library/items")` and
|
|
2210
|
+
// `/_claw/./library` are all the door). It reads literals, so a path
|
|
2211
|
+
// built from pieces is not seen, as in rules 8 and 13. Every mode and
|
|
2212
|
+
// every tables setting: the door is the same for all.
|
|
2213
|
+
{
|
|
2214
|
+
const L = limiter("library");
|
|
2215
|
+
const LIBRARY_PATH = /_claw\/(?:\.?\/)*library(?![\w-])/;
|
|
2216
|
+
const LIBRARY_NAME = /^(?:\.?\/)*library(?![\w-])/;
|
|
2217
|
+
for (const f of jsFiles) {
|
|
2218
|
+
const code = stripComments(f.text, { slashStrings: true });
|
|
2219
|
+
const toClaw = new Set();
|
|
2220
|
+
for (const m of code.matchAll(/(?<![\w$.])claw(?:Url|Stream)?\s*(?:\?\.\s*)?\(\s*(?=["'`])/g)) toClaw.add(m.index + m[0].length);
|
|
2221
|
+
for (const { start, end } of literalsOf(code)) {
|
|
2222
|
+
const inner = code.slice(start + 1, end);
|
|
2223
|
+
if (!(LIBRARY_PATH.test(inner) || (toClaw.has(start) && LIBRARY_NAME.test(inner)))) continue;
|
|
2224
|
+
const path = inner.replace(/\s+/g, " ").slice(0, 60);
|
|
2225
|
+
L.hit(`${f.path}:${lineOf(code, start)}`, "path", `"${path}": the Library door is reached through library.collections(), library.items(id) and kin in lib/platform.js, or useLibraryItems() and kin in lib/hooks.js, never by its path (README §5, Showing the Library)`);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
if (L.count > 4) fail("library", `${L.count - 4} more`, "strings that name the Library door's path instead of calling library()");
|
|
2229
|
+
if (!L.count) ok("library", "no screen names _claw/library: the Library is read through library() or its hooks (README §5)");
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// 15. Every write a screen makes is a declared action (0.8.1; claw's
|
|
2233
|
+
// docs/APP-TOOLS-PLAN.md D2, D6). makersclaw.json `actions` names
|
|
2234
|
+
// them: `name`, a one-line `description`, `input` (a JSON Schema
|
|
2235
|
+
// object), then `sql` (its function) and `brain` (the company brain's
|
|
2236
|
+
// writes the platform runs). The screens call each by its name with
|
|
2237
|
+
// runAction, and the app's agent calls the same name as a tool. Once
|
|
2238
|
+
// an app declares any, its agent can write no other way, so a write a
|
|
2239
|
+
// screen makes that is not one would be a thing only a person can do:
|
|
2240
|
+
// an undeclared runAction name, a one-row write (insertRow, updateRow,
|
|
2241
|
+
// moveRow, deleteRow, saveSetting) and a company brain write from
|
|
2242
|
+
// lib/brain.js (writeRecord(s), linkRecords, unlinkRecords, createList,
|
|
2243
|
+
// changeList) each fail. Erase, confirm and merge stay a person's
|
|
2244
|
+
// press. An app that declares none keeps rule 8 as it was.
|
|
2245
|
+
{
|
|
2246
|
+
const L = limiter("actions");
|
|
2247
|
+
const { manifest } = MANIFEST;
|
|
2248
|
+
const has = manifest && Object.prototype.hasOwnProperty.call(manifest, "actions");
|
|
2249
|
+
if (!has) ok("actions", "no actions declared in makersclaw.json: the app's agent writes as references/actions.md says");
|
|
2250
|
+
else {
|
|
2251
|
+
const refusals = actionRefusals(manifest.actions, manifest.brain, manifest.mode);
|
|
2252
|
+
for (const r of refusals.slice(0, 4)) fail("actions", "makersclaw.json", r);
|
|
2253
|
+
if (refusals.length > 4) fail("actions", "makersclaw.json", `${refusals.length - 4} more`);
|
|
2254
|
+
if (DECLARED.length) {
|
|
2255
|
+
const calls = writeCalls();
|
|
2256
|
+
const DECLARE = "declare it in makersclaw.json actions (its name, description, input, then sql or brain) and call it with runAction({ name, args }): once an app declares actions its agent writes no other way (README §5)";
|
|
2257
|
+
for (const a of calls.actions)
|
|
2258
|
+
if (a.name !== null && !DECLARED_NAMES.has(a.name)) L.hit(`${a.f.path}:${a.line}`, `action ${a.name}`, `runAction runs ${a.name}, which makersclaw.json does not declare: ${DECLARE}`);
|
|
2259
|
+
for (const t of [...calls.tables, ...calls.unread])
|
|
2260
|
+
L.hit(`${t.f.path}:${t.line}`, `${t.fn} ${t.table ?? ""}`, `${t.fn}${t.table ? ` writes ${t.table}` : ""}, a write the app's agent cannot make: make it a function of the app's schema and ${DECLARE}`);
|
|
2261
|
+
for (const b of calls.brainWrites)
|
|
2262
|
+
L.hit(`${b.f.path}:${b.line}`, b.fn, `${b.fn} writes the company brain from the screen, which the app's agent cannot do: give an action brain steps (writes, each, links) and ${DECLARE}`);
|
|
2263
|
+
if (L.count > 4) fail("actions", `${L.count - 4} more`, "writes the screens make that are not declared actions");
|
|
2264
|
+
}
|
|
2265
|
+
if (!refusals.length && !L.count)
|
|
2266
|
+
ok("actions", `${DECLARED.length} declared${DECLARED.length ? "; every write the screens make is one of them" : ""}`);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// 16. A row that wraps does not hold a text cell that grows (0.8.1; the
|
|
2271
|
+
// CRM's list header and its "Connect your mailbox" card, 2026-10-11).
|
|
2272
|
+
// `flex flex-wrap` with a child of `min-w-0 flex-1` looks right at a
|
|
2273
|
+
// wide window and is wrong at the width a real window has: flex-1 has
|
|
2274
|
+
// a basis of zero, so the text never wraps to a line of its own, it
|
|
2275
|
+
// squeezes to what the fixed pieces beside it leave, a word or two a
|
|
2276
|
+
// line, and the row grows tall around it. The founder saw it twice in
|
|
2277
|
+
// one app. So no direct child of a wrapping flex row both grows
|
|
2278
|
+
// (flex-1, grow) and may shrink to nothing (min-w-0) without a basis
|
|
2279
|
+
// of its own (basis-*, min-w-[…], w-full, w-N): stack the pieces
|
|
2280
|
+
// (grid gap-2, flex-col), or give the text a basis so it takes its own
|
|
2281
|
+
// line when it does not fit. It reads the JSX the way prettier lays it
|
|
2282
|
+
// out (a child is a line one indent in from its parent's opening tag,
|
|
2283
|
+
// className a string on that line), so a tag split across lines is not
|
|
2284
|
+
// seen; a cell that truncates (one line, cut short) is fine; the kit
|
|
2285
|
+
// and the blocks are skipped (pinned).
|
|
2286
|
+
{
|
|
2287
|
+
const L = limiter("squeeze");
|
|
2288
|
+
const classesOf = (line) => [...line.matchAll(/"([^"]*)"|`([^`$]*)`|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? m[3]).join(" ");
|
|
2289
|
+
const has = (cls, re) => new RegExp(`(?:^|\\s)(?:[\\w@\\[\\]/-]+:)*${re}(?![\\w-])`).test(cls);
|
|
2290
|
+
for (const f of appFiles.filter((x) => /\.(?:jsx|tsx)$/.test(x.path))) {
|
|
2291
|
+
const rows = f.code.split("\n");
|
|
2292
|
+
for (let i = 0; i < rows.length; i++) {
|
|
2293
|
+
const open = rows[i].match(/^(\s*)<(\w[\w.]*)\b[^>]*\bclassName=/);
|
|
2294
|
+
if (!open || /\/>\s*$/.test(rows[i])) continue;
|
|
2295
|
+
const cls = classesOf(rows[i]);
|
|
2296
|
+
if (!has(cls, "flex-wrap") || !(has(cls, "flex") || has(cls, "inline-flex")) || has(cls, "flex-col")) continue;
|
|
2297
|
+
const ind = open[1].length;
|
|
2298
|
+
for (let j = i + 1; j < rows.length; j++) {
|
|
2299
|
+
const lead = rows[j].match(/^(\s*)\S/);
|
|
2300
|
+
if (!lead) continue;
|
|
2301
|
+
if (lead[1].length <= ind) break;
|
|
2302
|
+
if (lead[1].length !== ind + 2 || !/^\s*<\w/.test(rows[j])) continue;
|
|
2303
|
+
const kid = classesOf(rows[j].slice(0, rows[j].indexOf(">") > 0 ? rows[j].indexOf(">") : undefined));
|
|
2304
|
+
if (!(has(kid, "flex-1") || has(kid, "grow")) || !has(kid, "min-w-0")) continue;
|
|
2305
|
+
if (has(kid, "truncate") || has(kid, "line-clamp-\\d")) continue; // one line, cut short: it never squeezes to a word a line
|
|
2306
|
+
if (has(kid, "basis-[^\\s]+") || has(kid, "min-w-\\[[^\\s]+") || has(kid, "w-full") || has(kid, "w-\\d[^\\s]*")) continue;
|
|
2307
|
+
L.hit(`${f.path}:${j + 1}`, "squeeze", `a text cell that grows (min-w-0 flex-1) sits in a flex-wrap row (line ${i + 1}) beside fixed pieces: flex-1 never wraps to a line of its own, it squeezes to a word or two a line. Stack them (grid gap-2, or flex-col), or give the text a basis so it wraps below (basis-64, min-w-[16rem])`);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
if (L.count > 4) fail("squeeze", `${L.count - 4} more`, "wrapping rows with a growing text cell");
|
|
2312
|
+
if (!L.count) ok("squeeze", "no wrapping flex row holds a text cell that grows (min-w-0 flex-1)");
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// 17. A ResizeObserver hands its work to the next frame (0.8.1; the CRM's
|
|
2316
|
+
// People screen, 2026-10-11). A browser says "ResizeObserver loop
|
|
2317
|
+
// completed with undelivered notifications" when a callback changes the
|
|
2318
|
+
// size of something an observer watches before the frame is over:
|
|
2319
|
+
// setting a height, a width, or state that a synchronous render turns
|
|
2320
|
+
// into one. The page still works, and nobody can say which line did it,
|
|
2321
|
+
// which is why a repair of the app found nothing to change every time.
|
|
2322
|
+
// So in the app's own files a `new ResizeObserver(cb)` takes its
|
|
2323
|
+
// measurements in the callback and acts on them in a
|
|
2324
|
+
// requestAnimationFrame: the callback (or the function it names, in the
|
|
2325
|
+
// same file) must contain one. The kit and the blocks are pinned and
|
|
2326
|
+
// are not read; the template reports no such notice any more
|
|
2327
|
+
// (lib/claw.js drops it), so this rule is what keeps the cause out.
|
|
2328
|
+
{
|
|
2329
|
+
const L = limiter("resize");
|
|
2330
|
+
scan(/new\s+ResizeObserver\s*\(/, (file, m, at) => {
|
|
2331
|
+
// The argument: from the opening bracket to its match.
|
|
2332
|
+
const open = m.index + m[0].length;
|
|
2333
|
+
let depth = 1;
|
|
2334
|
+
let end = open;
|
|
2335
|
+
while (end < file.code.length && depth > 0) {
|
|
2336
|
+
const ch = file.code[end++];
|
|
2337
|
+
if (ch === "(") depth++;
|
|
2338
|
+
else if (ch === ")") depth--;
|
|
2339
|
+
}
|
|
2340
|
+
const arg = file.code.slice(open, end - 1);
|
|
2341
|
+
if (/requestAnimationFrame/.test(arg)) return;
|
|
2342
|
+
// A function named in the same file: read its body.
|
|
2343
|
+
const named = /^\s*([A-Za-z_$][\w$]*)\s*$/.exec(arg);
|
|
2344
|
+
if (named) {
|
|
2345
|
+
const def = new RegExp(`(?:const|let|function)\\s+${named[1].replace(/\$/g, "\\$")}\\b`).exec(file.code);
|
|
2346
|
+
if (def && /requestAnimationFrame/.test(file.code.slice(def.index, def.index + 900))) return;
|
|
2347
|
+
}
|
|
2348
|
+
L.hit(`${file.path}:${at}`, "resize", "a ResizeObserver whose callback acts at once: whatever it sets (a height, a width, state) can change what it watches inside its own delivery, and the browser reports a loop. Measure in the callback and act in requestAnimationFrame: `let raf = 0; new ResizeObserver(() => { cancelAnimationFrame(raf); raf = requestAnimationFrame(apply); })`");
|
|
2349
|
+
});
|
|
2350
|
+
if (L.count > 4) fail("resize", `${L.count - 4} more`, "ResizeObservers that act inside their own delivery");
|
|
2351
|
+
if (!L.count) ok("resize", "every ResizeObserver acts in the next frame");
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
// 18. The automations an app ships hold to the platform's format (0.8.1;
|
|
2355
|
+
// claw's docs/AUTOMATIONS-PLAN.md phase 9). makersclaw.json `automations`
|
|
2356
|
+
// is a list of definitions (format 1), each with an optional
|
|
2357
|
+
// `replaces_wake`; the store ships the same list as seeds.automations.
|
|
2358
|
+
// The one validator is claw's validatePackageAutomations, which calls
|
|
2359
|
+
// validateAutomation: both are copied beside this file
|
|
2360
|
+
// (check/automation-format.ts, check/automation-package.ts; never
|
|
2361
|
+
// edited, refreshed by scripts/sync-automation-format.mjs in the
|
|
2362
|
+
// template repo) and run here as they are, so what passes here passes
|
|
2363
|
+
// the platform. An action step must name a declared action. They are
|
|
2364
|
+
// TypeScript with no runtime import, so node loads them as they are
|
|
2365
|
+
// from 22.18; on an older node the rule says so and is skipped, never
|
|
2366
|
+
// failed (the platform's own door still refuses the flow at install).
|
|
2367
|
+
// Wakes and kinds of account are not known to an app's own check: they
|
|
2368
|
+
// are the store's to check (it knows seeds.wakes and the connections).
|
|
2369
|
+
{
|
|
2370
|
+
const { manifest } = MANIFEST;
|
|
2371
|
+
const has = manifest && Object.prototype.hasOwnProperty.call(manifest, "automations") && manifest.automations !== undefined && manifest.automations !== null;
|
|
2372
|
+
if (!has) ok("automations", "no automations declared in makersclaw.json");
|
|
2373
|
+
else {
|
|
2374
|
+
const here = join(ROOT, "check");
|
|
2375
|
+
// Node says a .ts file with no "type" in package.json is reparsed as a
|
|
2376
|
+
// module, and (before 22.18) that type stripping is experimental. The
|
|
2377
|
+
// copies are ours and fixed: say nothing about how they are read.
|
|
2378
|
+
const emit = process.emitWarning;
|
|
2379
|
+
process.emitWarning = function (warning, ...rest) {
|
|
2380
|
+
const said = [warning, ...rest].map((x) => (typeof x === "string" ? x : (x?.code ?? x?.message ?? ""))).join(" ");
|
|
2381
|
+
if (/MODULE_TYPELESS_PACKAGE_JSON|Module type of file|Type Stripping/i.test(said)) return;
|
|
2382
|
+
return emit.call(this, warning, ...rest);
|
|
2383
|
+
};
|
|
2384
|
+
let loaded = null;
|
|
2385
|
+
let why = null;
|
|
2386
|
+
try {
|
|
2387
|
+
const fmt = await import(pathToFileURL(join(here, "automation-format.ts")).href);
|
|
2388
|
+
const pack = await import(pathToFileURL(join(here, "automation-package.ts")).href);
|
|
2389
|
+
if (typeof pack.validatePackageAutomations !== "function") throw new Error("check/automation-package.ts has no validatePackageAutomations");
|
|
2390
|
+
loaded = { fmt, pack };
|
|
2391
|
+
} catch (e) {
|
|
2392
|
+
why = String(e?.code ?? e?.message ?? e).split("\n")[0];
|
|
2393
|
+
}
|
|
2394
|
+
await new Promise((done) => setImmediate(done));
|
|
2395
|
+
process.emitWarning = emit;
|
|
2396
|
+
if (!loaded) {
|
|
2397
|
+
lines.push(`skip ${"automations".padEnd(10)} not checked: this node (${process.versions.node}) cannot read the format's TypeScript files (it needs 22.18 or newer): ${why}`);
|
|
2398
|
+
} else {
|
|
2399
|
+
const list = manifest.automations;
|
|
2400
|
+
const result = Array.isArray(list)
|
|
2401
|
+
? loaded.pack.validatePackageAutomations(list, { actionNames: new Set(DECLARED_NAMES), wakes: null, categories: null }, loaded.fmt)
|
|
2402
|
+
: { ok: false, problems: [{ path: "automations", message: "automations is a list" }] };
|
|
2403
|
+
if (result.ok) {
|
|
2404
|
+
const from = /@ (\w+)\./.exec(readFileSync(join(here, "automation-format.ts"), "utf8").split("\n", 1)[0])?.[1];
|
|
2405
|
+
ok("automations", `${result.entries.length} shipped; each holds to the platform's format${from ? ` (claw ${from})` : ""}`);
|
|
2406
|
+
} else {
|
|
2407
|
+
const said = loaded.pack.packageProblemLines(result.problems, 8);
|
|
2408
|
+
for (const line of said) fail("automations", "makersclaw.json", line);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// 19. Cards in a row are not set to their own height (0.8.1; UI review X6,
|
|
2415
|
+
// Project Roadmap's four start cards, 2026-10-12). ActionCard stretches
|
|
2416
|
+
// to the height of its row and pins its status and its button to the
|
|
2417
|
+
// bottom, so a row of them ends on one line and the buttons line up.
|
|
2418
|
+
// `self-start` (or self-center, self-end, h-fit, h-auto) on a card, or
|
|
2419
|
+
// `items-start` (items-center, items-end) on the grid or flex row that
|
|
2420
|
+
// holds two or more of them, takes the stretch away again: each card is
|
|
2421
|
+
// then only as tall as its own words, the cards end at different
|
|
2422
|
+
// heights and their buttons sit at different heights. The CRM's fix of
|
|
2423
|
+
// 2026-10-11 had put `self-start` into the kit to stop an empty block
|
|
2424
|
+
// under a card; that block came from a squeezed row (rule 16), not from
|
|
2425
|
+
// the stretch. A card on its own beside something unrelated takes the
|
|
2426
|
+
// `fit` prop instead (`fit` beside other ActionCards is refused too). It
|
|
2427
|
+
// reads the JSX the way rule 16 does (a child is a line one indent in
|
|
2428
|
+
// from its parent's opening tag).
|
|
2429
|
+
{
|
|
2430
|
+
const L = limiter("stretch");
|
|
2431
|
+
const classesOf = (text) => [...text.matchAll(/"([^"]*)"|`([^`$]*)`|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? m[3]).join(" ");
|
|
2432
|
+
const has = (cls, re) => new RegExp(`(?:^|\\s)(?:[\\w@\\[\\]/-]+:)*!?${re}(?![\\w-])`).test(cls);
|
|
2433
|
+
const card = new RegExp(`<ActionCard\\b(${ATTRS})`, "g");
|
|
2434
|
+
for (const f of appFiles.filter((x) => /\.(?:jsx|tsx)$/.test(x.path))) {
|
|
2435
|
+
if (!/<ActionCard\b/.test(f.code)) continue;
|
|
2436
|
+
for (const m of f.code.matchAll(card)) {
|
|
2437
|
+
const own = classesOf(m[1]);
|
|
2438
|
+
if (has(own, "self-(?:start|center|end)") || has(own, "h-(?:fit|auto|min)"))
|
|
2439
|
+
L.hit(`${f.path}:${lineOf(f.code, m.index)}`, "self", "an ActionCard sized to its own words (self-start, h-fit…): in a row it ends shorter than its neighbours and its button sits higher. A card stretches to its row and pins its button to the bottom; take the class off. One card alone beside something unrelated takes the `fit` prop");
|
|
2440
|
+
}
|
|
2441
|
+
const rows = f.code.split("\n");
|
|
2442
|
+
for (let i = 0; i < rows.length; i++) {
|
|
2443
|
+
const open = rows[i].match(/^(\s*)<(\w[\w.]*)\b[^>]*\bclassName=/);
|
|
2444
|
+
if (!open || /\/>\s*$/.test(rows[i]) || /^\s*<ActionCard\b/.test(rows[i])) continue;
|
|
2445
|
+
const cls = classesOf(rows[i]);
|
|
2446
|
+
if (!(has(cls, "grid") || has(cls, "flex") || has(cls, "inline-flex")) || !has(cls, "items-(?:start|center|end)")) continue;
|
|
2447
|
+
const ind = open[1].length;
|
|
2448
|
+
let cards = 0;
|
|
2449
|
+
let mapped = false;
|
|
2450
|
+
for (let j = i + 1; j < rows.length; j++) {
|
|
2451
|
+
const lead = rows[j].match(/^(\s*)\S/);
|
|
2452
|
+
if (!lead) continue;
|
|
2453
|
+
if (lead[1].length <= ind) break;
|
|
2454
|
+
if (/\.map\(/.test(rows[j])) mapped = true;
|
|
2455
|
+
if (/<ActionCard\b/.test(rows[j])) cards += 1;
|
|
2456
|
+
}
|
|
2457
|
+
if (cards >= 2 || (mapped && cards >= 1)) {
|
|
2458
|
+
L.hit(`${f.path}:${i + 1}`, "items", "a row of ActionCards whose row sets items-start (items-center, items-end): the cards then end at different heights and their buttons do not line up. Leave the row's alignment alone: the cards stretch to the row and pin the button to the bottom");
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
// `fit` on a card that has neighbours in the same parent.
|
|
2462
|
+
for (let i = 0; i < rows.length; i++) {
|
|
2463
|
+
if (!/<ActionCard\b[^>]*\sfit(?:[\s/>=]|$)/.test(rows[i])) continue;
|
|
2464
|
+
const ind = rows[i].match(/^(\s*)/)[1].length;
|
|
2465
|
+
let siblings = 0;
|
|
2466
|
+
for (const dir of [-1, 1]) {
|
|
2467
|
+
for (let j = i + dir; j >= 0 && j < rows.length; j += dir) {
|
|
2468
|
+
const lead = rows[j].match(/^(\s*)\S/);
|
|
2469
|
+
if (!lead) continue;
|
|
2470
|
+
if (lead[1].length < ind) break;
|
|
2471
|
+
if (lead[1].length === ind && /^\s*<ActionCard\b/.test(rows[j])) siblings += 1;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
if (siblings) L.hit(`${f.path}:${i + 1}`, "fit", "`fit` on an ActionCard that has ActionCard neighbours: in a row the cards stretch to one height. `fit` is for a card on its own");
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
if (L.count > 4) fail("stretch", `${L.count - 4} more`, "ActionCards sized to their own words");
|
|
2478
|
+
if (!L.count) ok("stretch", "no ActionCard is sized to its own words, and no row of them sets items-start");
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
// 20. A Meter in a narrow box is a compact Meter (0.8.1; UI review X1,
|
|
2482
|
+
// Newsletter Engine's top bar, 2026-10-12). A Meter draws its label and
|
|
2483
|
+
// its numbers as visible text above the bar. In a box under about 8rem
|
|
2484
|
+
// (`w-14`, `w-24`…) the label wraps to a second line and runs into the
|
|
2485
|
+
// button beside it ("15,000 cr left" over Find). `compact` draws the bar
|
|
2486
|
+
// alone; the label stays for a screen reader and as the tooltip. So a
|
|
2487
|
+
// `<Meter` with a `label` and a width of 8rem or less in its className
|
|
2488
|
+
// (w-N, w-[Nrem|Npx], max-w-N, size-N) and no `compact` is refused.
|
|
2489
|
+
{
|
|
2490
|
+
const L = limiter("meter");
|
|
2491
|
+
const meter = new RegExp(`<Meter\\b(${ATTRS})`, "g");
|
|
2492
|
+
const narrow = (cls) => {
|
|
2493
|
+
for (const m of cls.matchAll(/(?:^|\s)(?:[\w@[\]/-]+:)*!?(?:max-w|w|size)-(?:(\d+(?:\.\d+)?)|\[(\d+(?:\.\d+)?)(rem|px|em)\])(?![\w%/-])/g)) {
|
|
2494
|
+
const px = m[1] !== undefined ? Number(m[1]) * 4 : Number(m[2]) * (m[3] === "px" ? 1 : 16);
|
|
2495
|
+
if (px <= 128) return m[0].trim();
|
|
2496
|
+
}
|
|
2497
|
+
return null;
|
|
2498
|
+
};
|
|
2499
|
+
for (const f of appFiles.filter((x) => /\.(?:jsx|tsx)$/.test(x.path))) {
|
|
2500
|
+
for (const m of f.code.matchAll(meter)) {
|
|
2501
|
+
const attrs = m[1];
|
|
2502
|
+
if (!/(?:^|\s)label\s*=/.test(attrs) || /(?:^|\s)compact(?:[\s=/]|$)/.test(attrs)) continue;
|
|
2503
|
+
const cls = [...attrs.matchAll(/className\s*=\s*(?:"([^"]*)"|\{\s*(?:"([^"]*)"|`([^`$]*)`)\s*\})/g)].map((c) => c[1] ?? c[2] ?? c[3]).join(" ");
|
|
2504
|
+
const w = narrow(cls);
|
|
2505
|
+
if (w)
|
|
2506
|
+
L.hit(`${f.path}:${lineOf(f.code, m.index)}`, "meter", `a Meter with a label in a box ${w} wide: the label draws as visible text and spills into what is beside it. Add \`compact\` (the bar alone; the label stays as the tooltip and for a screen reader)`);
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
if (L.count > 4) fail("meter", `${L.count - 4} more`, "labelled Meters in narrow boxes");
|
|
2510
|
+
if (!L.count) ok("meter", "no labelled Meter sits in a box under 8rem without compact");
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// 21. A page that opens what it just made waits for the write (0.8.1; UI
|
|
2514
|
+
// review X11, Project Health's "That project isn't here", 2026-10-12).
|
|
2515
|
+
// Apps read their whole world once and share it. A write answers, THEN
|
|
2516
|
+
// the world is read again; `router.push(?row=<new id>)` straight after
|
|
2517
|
+
// the write opens a record the world does not hold yet, and the page
|
|
2518
|
+
// says "not found" until the re-read lands. So a router.push/replace
|
|
2519
|
+
// whose address is built from the answer of an awaited call
|
|
2520
|
+
// (`const out = await addProject(…)` … `${out.id}`) needs the write
|
|
2521
|
+
// wrapped in afterWrite() (lib/data), or an `await whenLanded()` between
|
|
2522
|
+
// them. A read (loadRow, fetch…) is not a write and is not asked. The
|
|
2523
|
+
// other half is the record page's: it says "not found" through
|
|
2524
|
+
// useNotFound() (lib/data), which stays false while a write or a
|
|
2525
|
+
// re-read is out; the template's tests hold that half.
|
|
2526
|
+
{
|
|
2527
|
+
const L = limiter("landed");
|
|
2528
|
+
const READS = /\b(?:loadRows?|countRows|countBy|loadActivity|fetch|claw|loadOne|read[A-Z]\w*)\s*\(/;
|
|
2529
|
+
for (const f of appFiles.filter((x) => /\.(?:jsx?|tsx?)$/.test(x.path))) {
|
|
2530
|
+
const rows = f.code.split("\n");
|
|
2531
|
+
for (let i = 0; i < rows.length; i++) {
|
|
2532
|
+
const m = /\b(?:router|history)\.(?:push|replace)\s*\(|\bnavigate\s*\(/.exec(rows[i]);
|
|
2533
|
+
if (!m) continue;
|
|
2534
|
+
// The address: this line to the call's closing bracket (or 4 lines).
|
|
2535
|
+
const arg = rows.slice(i, i + 4).join(" ").slice(m.index);
|
|
2536
|
+
const vars = new Set([...arg.matchAll(/\b([A-Za-z_$][\w$]*)\??\.[A-Za-z_$]/g)].map((x) => x[1]));
|
|
2537
|
+
for (const v of vars) {
|
|
2538
|
+
if (/^(?:window|location|document|Math|JSON|Object|Number|String|Array|process|props|this)$/.test(v)) continue;
|
|
2539
|
+
const name = v.replace(/\$/g, "\\$");
|
|
2540
|
+
const decl = new RegExp(`(?:\\b(?:const|let|var)\\s+${name}\\s*=|^\\s*${name}\\s*=)\\s*await\\b`);
|
|
2541
|
+
for (let k = i; k >= Math.max(0, i - 40); k--) {
|
|
2542
|
+
if (!decl.test(rows[k])) continue;
|
|
2543
|
+
const between = rows.slice(k, i + 1).join("\n");
|
|
2544
|
+
if (/\bafterWrite\s*\(|\bwhenLanded\s*\(/.test(between) || READS.test(rows[k])) break;
|
|
2545
|
+
L.hit(`${f.path}:${i + 1}`, v, `${m[0].replace(/\s*\($/, "")}() builds its address from \`${v}\`, the answer of a write awaited on line ${k + 1}, before the page holds what was written: the record page then says "not found" until the re-read lands. Wait for it: \`const ${v} = await afterWrite(<the write>)\` (lib/data), or \`await whenLanded()\` first`);
|
|
2546
|
+
break;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
if (L.count > 4) fail("landed", `${L.count - 4} more`, "navigations built from a write that has not landed");
|
|
2552
|
+
if (!L.count) ok("landed", "no page opens a row from a write it did not wait for");
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
// 22. A mounted agent can be opened (0.8.1; UI review X8, Gantt Planner,
|
|
2556
|
+
// 2026-10-12). Gantt mounted the agent's dock and defined a function to
|
|
2557
|
+
// toggle it, and no button anywhere called it: the agent could not be
|
|
2558
|
+
// opened unless a screen happened to ask it something. So an app that
|
|
2559
|
+
// mounts an AgentDock needs a way to open it: the TopBar block (which
|
|
2560
|
+
// always draws Ask when it is given the dock, or sits under an
|
|
2561
|
+
// AskProvider), its AskButton, a button of the app's own whose
|
|
2562
|
+
// onClick/onAsk calls the dock's toggle() or setOpen(), or a window event
|
|
2563
|
+
// the shell hears (and opens the dock from) that a bar dispatches. A
|
|
2564
|
+
// toggle that is defined and never wired to a control is what this
|
|
2565
|
+
// refuses.
|
|
2566
|
+
{
|
|
2567
|
+
const L = limiter("ask");
|
|
2568
|
+
const mounts = [];
|
|
2569
|
+
const names = new Set();
|
|
2570
|
+
scan(/<AgentDock\b/, (f, m, at) => {
|
|
2571
|
+
mounts.push(`${f.path}:${at}`);
|
|
2572
|
+
const tag = new RegExp(`^<AgentDock\\b${ATTRS}`).exec(f.code.slice(m.index))?.[0] ?? "";
|
|
2573
|
+
const named = /\bdock=\{\s*([A-Za-z_$][\w$]*)\s*\}/.exec(tag);
|
|
2574
|
+
if (named) names.add(named[1].replace(/\$/g, "\\$"));
|
|
2575
|
+
});
|
|
2576
|
+
if (mounts.length) {
|
|
2577
|
+
const BAR = /from\s*["'][^"']*\bblocks\/top-bar["']/;
|
|
2578
|
+
const bars = appFiles.filter((f) => BAR.test(f.code));
|
|
2579
|
+
const drawn = bars.some((f) => /<(?:TopBar|AskButton)\b/.test(f.code));
|
|
2580
|
+
const fed = bars.some((f) => /<AskProvider\b/.test(f.code) || new RegExp(`<(?:TopBar|AskButton)\\b${ATTRS}\\b(?:dock|onAsk)=`).test(f.code));
|
|
2581
|
+
const who = names.size ? [...names].join("|") : "[A-Za-z_$][\\w$]*";
|
|
2582
|
+
const own = appFiles.some((f) => new RegExp(`\\bon(?:Click|Ask)=\\{[^}]*\\b(?:${who})\\.(?:toggle|setOpen)\\b`).test(f.code));
|
|
2583
|
+
// The older way: a window event the shell listens for (and calls the
|
|
2584
|
+
// dock's toggle/setOpen from) that some bar dispatches by name.
|
|
2585
|
+
const opens = new RegExp(`\\b(?:${who})\\.(?:toggle|setOpen)\\b`);
|
|
2586
|
+
const heard = new Set();
|
|
2587
|
+
scan(/addEventListener\(\s*["']([^"']+)["']/, (f, m) => {
|
|
2588
|
+
if (opens.test(f.code.slice(Math.max(0, m.index - 400), m.index + 400))) heard.add(m[1]);
|
|
2589
|
+
});
|
|
2590
|
+
const sent = [...heard].some((name) => appFiles.some((f) => new RegExp(`new\\s+(?:Custom)?Event\\(\\s*["']${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`).test(f.code)));
|
|
2591
|
+
if (!(drawn && fed) && !own && !sent) {
|
|
2592
|
+
L.hit(mounts[0], "ask", "an AgentDock is mounted and nothing opens it: no button calls the dock's toggle() or setOpen(), so the agent cannot be reached unless a screen asks it something. Draw the top bar with the TopBar block (blocks/top-bar) and give it the dock (`<TopBar dock={dock} …/>`, or wrap the shell in `<AskProvider dock={dock}>`): it always draws Ask and Ask opens the dock. In a bar that cannot be a TopBar, use `<AskButton dock={dock}/>`");
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
if (!mounts.length) ok("ask", "no agent dock is mounted");
|
|
2596
|
+
else if (!L.count) ok("ask", "the agent dock is mounted and Ask opens it");
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// 23. A page with a side column uses PageLayout (0.8.1; UI review X12, Project
|
|
2600
|
+
// Roadmap's Today, 2026-10-12). A screen that draws its own main + side
|
|
2601
|
+
// (`grid-cols-[minmax(0,1fr)_19rem]` and an `<aside>` with `border-l`) is
|
|
2602
|
+
// as tall as its words: in a tall window the divider stops halfway, the
|
|
2603
|
+
// bottom half is empty, and the side cannot scroll on its own. PageLayout
|
|
2604
|
+
// (components/ui) takes the full height of the window's content, runs the
|
|
2605
|
+
// divider top to bottom and scrolls each column by itself.
|
|
2606
|
+
{
|
|
2607
|
+
const L = limiter("page");
|
|
2608
|
+
const SIDE = /(?:^|[\s"'`{])(?:[\w@[\]/-]+:)*grid-cols-\[(?:minmax\(0,\s*1fr\)|1fr)_(?:\d+(?:\.\d+)?(?:rem|px)|var\([^)]*\))\]/;
|
|
2609
|
+
const RULED = /(?:^|[\s"'`{])(?:[\w@[\]/-]+:)*border-l(?![\w-])/;
|
|
2610
|
+
for (const f of appFiles.filter((x) => /\.(?:jsx|tsx)$/.test(x.path))) {
|
|
2611
|
+
const grid = SIDE.exec(f.code);
|
|
2612
|
+
if (!grid) continue;
|
|
2613
|
+
const ruled = [...f.code.matchAll(new RegExp(`<aside\\b(${ATTRS})`, "g"))].some((a) => RULED.test(a[1]));
|
|
2614
|
+
if (!ruled) continue;
|
|
2615
|
+
L.hit(`${f.path}:${lineOf(f.code, grid.index)}`, "page", "a page drawn as a main column and a side column of its own (a fixed-width grid column and an <aside> with a left border) is only as tall as its words: in a tall window the divider stops halfway and the side cannot scroll by itself. Use PageLayout from components/ui: `<PageLayout side={<Side/>}>{main}</PageLayout>` in an AppLayout with contentMode=\"fill\"; it fills the height, runs the divider top to bottom and scrolls each column on its own");
|
|
2616
|
+
}
|
|
2617
|
+
if (L.count > 4) fail("page", `${L.count - 4} more`, "pages with a hand-drawn side column");
|
|
2618
|
+
if (!L.count) ok("page", "no screen draws its own main + side columns: PageLayout does");
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// 24. A screen's heading is a tool's, not a landing page's (0.8.1; UI review
|
|
2622
|
+
// X10, Gantt Planner's home, 2026-10-12: "Plan a project that lands on its
|
|
2623
|
+
// date." at 52px with four stacked cards and a big chart under it). An
|
|
2624
|
+
// <h1> at text-4xl (36px) or more, or a pixel size from 36 up, is a hero.
|
|
2625
|
+
// The first run of an app is the FirstRun block: one plain heading line
|
|
2626
|
+
// at the kit's size, what the app does in a line, and the real first
|
|
2627
|
+
// steps. A picture of someone else's page (components/depictions) is
|
|
2628
|
+
// skipped.
|
|
2629
|
+
{
|
|
2630
|
+
const L = limiter("hero");
|
|
2631
|
+
const BIG = /(?:^|[\s"'`{])(?:[\w@[\]/-]+:)*text-(?:[4-9]xl|\[(?:3[6-9]|[4-9]\d|\d{3,})(?:\.\d+)?px\]|\[(?:2\.(?:2[5-9]|[3-9])\d*|[3-9](?:\.\d+)?)rem\])(?![\w-])/;
|
|
2632
|
+
for (const f of appFiles.filter((x) => /\.(?:jsx|tsx)$/.test(x.path))) {
|
|
2633
|
+
const g = new RegExp(`<h1\\b(${ATTRS})`, "g");
|
|
2634
|
+
let m;
|
|
2635
|
+
while ((m = g.exec(f.code))) {
|
|
2636
|
+
if (!BIG.test(m[1])) continue;
|
|
2637
|
+
L.hit(`${f.path}:${lineOf(f.code, m.index)}`, "hero", "an <h1> at 36px or more reads as a website's hero, not an app's page. Use the kit's heading sizes (text-xl to text-2xl), and for a first run the FirstRun block (blocks/first-run): one heading line, what the app does in a line, then the real first steps");
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
if (L.count > 4) fail("hero", `${L.count - 4} more`, "oversized page headings");
|
|
2641
|
+
if (!L.count) ok("hero", "no page heading is set at a landing page's size");
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
// 25. "Have the agent do it" opens the agent, not a form (0.8.1; UI review
|
|
2645
|
+
// X9, Gantt Planner, 2026-10-12: "Have the agent draft it" still made
|
|
2646
|
+
// the member fill in a name, a code and two dates first; the founder:
|
|
2647
|
+
// "the agent should be able to create these"). A button whose words say
|
|
2648
|
+
// the agent will do the work ("Have the agent draft it", "Let the agent
|
|
2649
|
+
// plan it", "Draft it with the agent") calls openAgent("…")
|
|
2650
|
+
// (blocks/agent-dock) or sends to the agent (ask, chat.send): the dock
|
|
2651
|
+
// opens with the line already in hand and the agent asks only for what
|
|
2652
|
+
// it is missing, in the chat. A button that submits a form, follows a
|
|
2653
|
+
// link or only changes a step is what this refuses. A form stays only
|
|
2654
|
+
// for "type it yourself". It reads the buttons' own words (the handler
|
|
2655
|
+
// may be named in the same file), and a short quoted label ("Have the
|
|
2656
|
+
// agent draft it" as a choice's label): that file must call openAgent,
|
|
2657
|
+
// or send to the agent and draw no form of its own. The kit and the
|
|
2658
|
+
// blocks are pinned and are not read.
|
|
2659
|
+
{
|
|
2660
|
+
const L = limiter("agentfirst");
|
|
2661
|
+
const WHO = String.raw`(?:the\s+|your\s+)?(?:agent|ai|assistant)`;
|
|
2662
|
+
const LABEL = String.raw`(?:(?:have|let|get|ask)\s+${WHO}\s+(?:to\s+)?(?:draft|do|create|build|plan|write|make|set\s*up|fill|prepare|handle|generate)\b|(?:draft|plan|create|build|write|generate)\s+(?:it\s+|this\s+|them\s+)?with\s+${WHO}\b)`;
|
|
2663
|
+
const OPENS = /\bopenAgent\s*\(/;
|
|
2664
|
+
const SENDS = /\b(?:openAgent|askAgent|askTheAgent|ask|send)\s*\(/;
|
|
2665
|
+
const FORM = /<(?:form|Field|Input|Textarea|Select|DateInput|DatePicker)\b/;
|
|
2666
|
+
const NOT_FORMS = String.raw`openAgent("Draft a project plan for me") (blocks/agent-dock) opens the agent with that line already sent in a new chat, and the agent asks only for what it is missing, in the chat; a form stays only for "type it yourself"`;
|
|
2667
|
+
/** Does the handler an onClick names (in the same file) open the agent? */
|
|
2668
|
+
const sends = (code, click) => {
|
|
2669
|
+
if (SENDS.test(click)) return true;
|
|
2670
|
+
for (const id of new Set(click.match(/[A-Za-z_$][\w$]*/g) ?? [])) {
|
|
2671
|
+
const def = new RegExp(`(?:const|let|function)\\s+${id.replace(/\$/g, "\\$")}\\b`).exec(code);
|
|
2672
|
+
if (def && SENDS.test(code.slice(def.index, def.index + 900))) return true;
|
|
2673
|
+
}
|
|
2674
|
+
return false;
|
|
2675
|
+
};
|
|
2676
|
+
for (const f of appFiles.filter((x) => /\.(?:jsx|tsx)$/.test(x.path))) {
|
|
2677
|
+
const el = new RegExp(`<(Button|button|Link|a|MenuItem)\\b(${ATTRS})>\\s*(?:<Icon\\b[^>]*/>\\s*)?(${LABEL}[^<{]{0,40})`, "gi");
|
|
2678
|
+
let m;
|
|
2679
|
+
while ((m = el.exec(f.code))) {
|
|
2680
|
+
const click = /\bon(?:Click|Press|Select)=\{((?:[^{}]|\{[^{}]*\})*)\}/.exec(m[2])?.[1] ?? "";
|
|
2681
|
+
if (sends(f.code, click)) continue;
|
|
2682
|
+
L.hit(`${f.path}:${lineOf(f.code, m.index)}`, "agentfirst", `"${m[3].trim()}" opens ${click ? "something other than the agent" : "a form or a route"}: a button that says the agent will do the work must open the agent. ${NOT_FORMS}`);
|
|
2683
|
+
}
|
|
2684
|
+
if (OPENS.test(f.code)) continue;
|
|
2685
|
+
const drawsForm = FORM.test(f.code);
|
|
2686
|
+
if (SENDS.test(f.code) && !drawsForm) continue;
|
|
2687
|
+
const quoted = new RegExp(`(["'\`])\\s*${LABEL}[^"'\`\\n]{0,40}\\1`, "gi");
|
|
2688
|
+
while ((m = quoted.exec(f.code))) {
|
|
2689
|
+
L.hit(`${f.path}:${lineOf(f.code, m.index)}`, "agentfirst", `${m[0]} is a choice that says the agent will do the work, and ${SENDS.test(f.code) ? "this file draws a form around it" : "nothing in this file asks the agent"}: ${NOT_FORMS}`);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
if (L.count > 4) fail("agentfirst", `${L.count - 4} more`, 'buttons that say "have the agent do it" and open a form');
|
|
2693
|
+
if (!L.count) ok("agentfirst", 'every "have the agent do it" button opens the agent');
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
// 26. A bar action with words has an icon (0.8.1; the Newsletter Engine pilot,
|
|
2697
|
+
// 2026-10-12). The top bar's `BarText` hides its words under 32rem so the
|
|
2698
|
+
// action is its icon, and leaves them to a screen reader. A button whose
|
|
2699
|
+
// only content is `<BarText>` and which has no `icon` is, under 32rem,
|
|
2700
|
+
// an empty button: nothing to see, nothing to press. Give the Button an
|
|
2701
|
+
// `icon` (or an <Icon/> beside the BarText).
|
|
2702
|
+
{
|
|
2703
|
+
const L = limiter("barlabel");
|
|
2704
|
+
for (const f of appFiles.filter((x) => /\.(?:jsx|tsx)$/.test(x.path))) {
|
|
2705
|
+
if (!/<BarText\b/.test(f.code)) continue;
|
|
2706
|
+
const g = new RegExp(`<(Button|button|IconButton)\\b(${ATTRS})>`, "g");
|
|
2707
|
+
let m;
|
|
2708
|
+
while ((m = g.exec(f.code))) {
|
|
2709
|
+
if (/\/\s*$/.test(m[2])) continue;
|
|
2710
|
+
const end = f.code.indexOf(`</${m[1]}>`, m.index);
|
|
2711
|
+
if (end < 0) continue;
|
|
2712
|
+
const inner = f.code.slice(m.index + m[0].length, end);
|
|
2713
|
+
if (!/<BarText\b/.test(inner)) continue;
|
|
2714
|
+
if (/(?:^|\s)icon=/.test(m[2]) || /<(?:Icon|svg)\b/.test(inner)) continue;
|
|
2715
|
+
L.hit(`${f.path}:${lineOf(f.code, m.index)}`, "barlabel", "a button whose words are a <BarText> and that has no icon is an empty button under 32rem of bar (BarText gives its words up there, to a screen reader only). Give the Button an icon: `<Button icon=\"plus\"><BarText>New issue</BarText></Button>`");
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
if (L.count > 4) fail("barlabel", `${L.count - 4} more`, "bar actions with no icon");
|
|
2719
|
+
if (!L.count) ok("barlabel", "every bar action with words has an icon");
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// 27. A side column is the fixed one, so it is short (0.8.1; UI review X13,
|
|
2723
|
+
// Newsletter Engine's first run, 2026-10-12: a short intro on the left
|
|
2724
|
+
// and a long "Name it" form in the right column, "one small column and
|
|
2725
|
+
// then all the card long scrolling in other column"). In a main + side
|
|
2726
|
+
// page the main scrolls and the side holds its place; a form, a paste
|
|
2727
|
+
// box or a list that grows with the data does not belong there. Reads a
|
|
2728
|
+
// PageLayout `side` and an <aside>: their markup, and the components the
|
|
2729
|
+
// app defines that they draw (a name resolved in any app file, three
|
|
2730
|
+
// levels; a called function only when it returns JSX). Refused: a <form>, a <Textarea>, three or more of Field ·
|
|
2731
|
+
// Input · Textarea · Select, and a `.map(` of data with no `.slice(` in
|
|
2732
|
+
// the same piece of code (a constant in CAPITALS, an array literal and
|
|
2733
|
+
// Object.entries(CAPITALS) are fixed lists). It cannot see inside a block
|
|
2734
|
+
// or a kit component, content that comes in by props or context, or how
|
|
2735
|
+
// long a list is at run time.
|
|
2736
|
+
{
|
|
2737
|
+
const L = limiter("sidelong");
|
|
2738
|
+
const jsxFiles = appFiles.filter((x) => /\.(?:jsx|tsx)$/.test(x.path));
|
|
2739
|
+
const OPEN = { "{": "}", "(": ")", "[": "]" };
|
|
2740
|
+
/** The index just past the bracket that closes the one at `i`. Double-quoted
|
|
2741
|
+
* and template strings are skipped; single quotes are not (JSX text has
|
|
2742
|
+
* apostrophes). */
|
|
2743
|
+
const closeAt = (code, i) => {
|
|
2744
|
+
let depth = 0;
|
|
2745
|
+
for (let k = i; k < code.length; k++) {
|
|
2746
|
+
const c = code[k];
|
|
2747
|
+
if (c === '"' || c === "`") {
|
|
2748
|
+
const e = code.indexOf(c, k + 1);
|
|
2749
|
+
if (e < 0) return code.length;
|
|
2750
|
+
k = e;
|
|
2751
|
+
} else if (OPEN[c]) depth++;
|
|
2752
|
+
else if (c === "}" || c === ")" || c === "]") {
|
|
2753
|
+
depth--;
|
|
2754
|
+
if (depth === 0) return k + 1;
|
|
2755
|
+
if (depth < 0) return k;
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
return code.length;
|
|
2759
|
+
};
|
|
2760
|
+
/** From `i` to the `;` that ends the statement (brackets balanced). */
|
|
2761
|
+
const untilSemi = (code, i) => {
|
|
2762
|
+
let k = i;
|
|
2763
|
+
while (k < code.length) {
|
|
2764
|
+
const c = code[k];
|
|
2765
|
+
if (c === '"' || c === "`") {
|
|
2766
|
+
const e = code.indexOf(c, k + 1);
|
|
2767
|
+
k = e < 0 ? code.length : e + 1;
|
|
2768
|
+
} else if (OPEN[c]) k = closeAt(code, k);
|
|
2769
|
+
else if (c === ";" || c === "}" || c === ")" || c === "]") break;
|
|
2770
|
+
else k++;
|
|
2771
|
+
}
|
|
2772
|
+
return code.slice(i, k);
|
|
2773
|
+
};
|
|
2774
|
+
/** The body of the component or function `name` an app file defines. */
|
|
2775
|
+
const defined = (name) => {
|
|
2776
|
+
const out = [];
|
|
2777
|
+
for (const f of jsxFiles.concat(appFiles.filter((x) => /\.(?:js|ts)$/.test(x.path)))) {
|
|
2778
|
+
const fn = new RegExp(`\\bfunction\\s+${name}\\s*\\(`).exec(f.code);
|
|
2779
|
+
if (fn) {
|
|
2780
|
+
const p0 = fn.index + fn[0].length - 1;
|
|
2781
|
+
const b = f.code.indexOf("{", closeAt(f.code, p0));
|
|
2782
|
+
if (b >= 0) out.push(f.code.slice(b, closeAt(f.code, b)));
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
const cn = new RegExp(`\\b(?:const|let)\\s+${name}\\s*=`).exec(f.code);
|
|
2786
|
+
if (cn) out.push(untilSemi(f.code, cn.index + cn[0].length));
|
|
2787
|
+
}
|
|
2788
|
+
return out;
|
|
2789
|
+
};
|
|
2790
|
+
/** What a piece of side code holds: a reason, or null. */
|
|
2791
|
+
const holds = (code) => {
|
|
2792
|
+
if (/<form\b/.test(code)) return "a <form>";
|
|
2793
|
+
if (/<Textarea\b/.test(code)) return "a <Textarea> (a paste or text box)";
|
|
2794
|
+
const fields = new Set([...code.matchAll(/<(Field|Input|Select)\b/g)].map((m) => m[1]));
|
|
2795
|
+
const n = [...code.matchAll(/<(?:Field|Input|Select)\b/g)].length;
|
|
2796
|
+
if (n >= 3) return `${n} fields (${[...fields].join(", ")})`;
|
|
2797
|
+
if (!/\.slice\(/.test(code)) {
|
|
2798
|
+
for (const m of code.matchAll(/([A-Za-z_$][\w$.]*|\)|\])\s*\.map\(/g)) {
|
|
2799
|
+
const recv = m[1];
|
|
2800
|
+
if (recv === "]") continue;
|
|
2801
|
+
if (recv === ")") {
|
|
2802
|
+
if (/Object\.(?:entries|keys|values)\(\s*[A-Z][A-Z0-9_]*\s*\)\s*$/.test(code.slice(Math.max(0, m.index - 60), m.index + 1))) continue;
|
|
2803
|
+
if (/\bArray\.from\(/.test(code.slice(Math.max(0, m.index - 60), m.index + 1))) continue;
|
|
2804
|
+
} else if (/^(?:[A-Za-z_$][\w$]*\.)*[A-Z][A-Z0-9_]*$/.test(recv)) continue;
|
|
2805
|
+
return `a list from data (${recv === ")" ? "…)" : recv}.map) with no .slice(...) to bound it`;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
return null;
|
|
2809
|
+
};
|
|
2810
|
+
/** The code without what opens over the page (a Sheet, a Popover, a
|
|
2811
|
+
* Dialog, a menu): its form is not in the column until someone opens it. */
|
|
2812
|
+
const withoutOverlays = (code) => {
|
|
2813
|
+
let out = code;
|
|
2814
|
+
for (const tag of ["Sheet", "Popover", "Dialog", "Modal", "Drawer", "Menu", "DropdownMenu", "Tooltip", "AlertDialog"]) {
|
|
2815
|
+
for (;;) {
|
|
2816
|
+
const open = new RegExp(`<${tag}\\b`).exec(out);
|
|
2817
|
+
if (!open) break;
|
|
2818
|
+
const close = out.indexOf(`</${tag}>`, open.index);
|
|
2819
|
+
out = out.slice(0, open.index) + (close < 0 ? "" : out.slice(close + tag.length + 3));
|
|
2820
|
+
if (close < 0) break;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
return out;
|
|
2824
|
+
};
|
|
2825
|
+
/** The first reason in `code`, or in a component or function it draws. */
|
|
2826
|
+
const reason = (whole, seen = new Set(), depth = 0, via = []) => {
|
|
2827
|
+
const code = withoutOverlays(whole);
|
|
2828
|
+
const own = holds(code);
|
|
2829
|
+
if (own) return { why: own, via };
|
|
2830
|
+
if (depth >= 3) return null;
|
|
2831
|
+
const names = new Set([...code.matchAll(/<([A-Z][\w$]*)\b/g)].map((m) => m[1]));
|
|
2832
|
+
for (const m of code.matchAll(/(?<![\w$.])([a-z][\w$]*)\(/g)) names.add(m[1]);
|
|
2833
|
+
for (const name of names) {
|
|
2834
|
+
if (seen.has(name)) continue;
|
|
2835
|
+
seen.add(name);
|
|
2836
|
+
for (const body of defined(name)) {
|
|
2837
|
+
// A function the side calls draws something only if it returns JSX
|
|
2838
|
+
// (a formatter that maps a list of strings draws nothing).
|
|
2839
|
+
if (/^[a-z]/.test(name) && !/<[A-Za-z]/.test(body)) continue;
|
|
2840
|
+
const r = reason(body, seen, depth + 1, [...via, name]);
|
|
2841
|
+
if (r) return r;
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
return null;
|
|
2845
|
+
};
|
|
2846
|
+
/** The value of the `side` attribute of the <PageLayout> that opens at `at`:
|
|
2847
|
+
* JSX or an expression, or a name that stands for one in the same file. */
|
|
2848
|
+
const sideOf = (code, at) => {
|
|
2849
|
+
let k = at + "<PageLayout".length;
|
|
2850
|
+
while (k < code.length) {
|
|
2851
|
+
const c = code[k];
|
|
2852
|
+
if (c === ">") return null;
|
|
2853
|
+
if (c === '"' || c === "'") {
|
|
2854
|
+
const e = code.indexOf(c, k + 1);
|
|
2855
|
+
k = e < 0 ? code.length : e + 1;
|
|
2856
|
+
} else if (c === "{") k = closeAt(code, k);
|
|
2857
|
+
else if (/\s/.test(c) && /^\s+side=\{/.test(code.slice(k, k + 12))) {
|
|
2858
|
+
const b = code.indexOf("{", k);
|
|
2859
|
+
const expr = code.slice(b + 1, closeAt(code, b) - 1);
|
|
2860
|
+
const id = /^\s*([A-Za-z_$][\w$]*)\s*$/.exec(expr);
|
|
2861
|
+
if (!id) return expr;
|
|
2862
|
+
const decl = new RegExp(`\\b(?:const|let)\\s+${id[1]}\\s*=`).exec(code);
|
|
2863
|
+
return decl ? untilSemi(code, decl.index + decl[0].length) : expr;
|
|
2864
|
+
} else k++;
|
|
2865
|
+
}
|
|
2866
|
+
return null;
|
|
2867
|
+
};
|
|
2868
|
+
const SAY = "a side column is the fixed one: it stays put while the main scrolls, so it is short and needs no scroll of its own. Put the form or the long list in the main (a step that opens reveals its own card there) and keep the side to a summary, the next step or a small panel; bound a short list with .slice(0, 5)";
|
|
2869
|
+
for (const f of jsxFiles) {
|
|
2870
|
+
for (const m of f.code.matchAll(/<PageLayout\b/g)) {
|
|
2871
|
+
const side = sideOf(f.code, m.index);
|
|
2872
|
+
const r = side ? reason(side) : null;
|
|
2873
|
+
if (r) L.hit(`${f.path}:${lineOf(f.code, m.index)}`, "side", `a PageLayout side holds ${r.why}${r.via.length ? ` (through ${r.via.join(" → ")})` : ""}: ${SAY}`);
|
|
2874
|
+
}
|
|
2875
|
+
const beside = /(?:^|[\s"'`{])(?:[\w@[\]/-]+:)*grid-cols-\[(?:minmax\(0,\s*1fr\)|1fr)_(?:\d+(?:\.\d+)?(?:rem|px)|var\([^)]*\))\]/.test(f.code);
|
|
2876
|
+
for (const m of f.code.matchAll(new RegExp(`<aside\\b(${ATTRS})>`, "g"))) {
|
|
2877
|
+
if (/\/\s*$/.test(m[1])) continue;
|
|
2878
|
+
// An <aside> is a side column when it is ruled (border-l or border-r) or
|
|
2879
|
+
// sits in a file that draws a fixed-width column beside a main: a note
|
|
2880
|
+
// or a callout that happens to be an <aside> is not.
|
|
2881
|
+
if (!beside && !/(?:^|[\s"'`{])(?:[\w@[\]/-]+:)*border-[lr](?![\w-])/.test(m[1])) continue;
|
|
2882
|
+
const from = m.index + m[0].length;
|
|
2883
|
+
let depth = 1;
|
|
2884
|
+
let end = f.code.length;
|
|
2885
|
+
for (const t of f.code.slice(from).matchAll(/<(\/?)aside\b/g)) {
|
|
2886
|
+
depth += t[1] ? -1 : 1;
|
|
2887
|
+
if (!depth) {
|
|
2888
|
+
end = from + t.index;
|
|
2889
|
+
break;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
const r = reason(f.code.slice(from, end));
|
|
2893
|
+
if (r) L.hit(`${f.path}:${lineOf(f.code, m.index)}`, "aside", `an <aside> holds ${r.why}${r.via.length ? ` (through ${r.via.join(" → ")})` : ""}: ${SAY}`);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
if (L.count > 4) fail("sidelong", `${L.count - 4} more`, "side columns that hold a form or a long list");
|
|
2897
|
+
if (!L.count) ok("sidelong", "no side column holds a form or a long list");
|
|
2898
|
+
}
|
|
2899
|
+
|
|
1874
2900
|
// 6. Smoke: the app, built and started, answers every surface.
|
|
1875
2901
|
|
|
1876
2902
|
/** Newest mtime among the app's own sources (what a build reads): walk
|
|
@@ -1900,7 +2926,12 @@ function writable(dir) {
|
|
|
1900
2926
|
* the build goes to `.next` all the same. */
|
|
1901
2927
|
function buildSite(exported = false) {
|
|
1902
2928
|
if (!exported && writable(ROOT)) return { dir: ROOT, dist: ".next-check", copied: false };
|
|
1903
|
-
|
|
2929
|
+
// plays/<name>/app names the copy <name>; the store's packages/<key>/files/app
|
|
2930
|
+
// would all be "files", one folder shared by every package's check, so the
|
|
2931
|
+
// package's key names it there (2026-10-12, every builder of the wave).
|
|
2932
|
+
const parent = dirname(ROOT);
|
|
2933
|
+
let name = basename(parent) || "app";
|
|
2934
|
+
if (name === "files") name = basename(dirname(parent)) || name;
|
|
1904
2935
|
const home = writable(homedir()) ? homedir() : tmpdir();
|
|
1905
2936
|
return { dir: join(home, ".claw-check", name), dist: ".next", copied: true };
|
|
1906
2937
|
}
|