scrum-skills 0.3.2 → 0.3.3
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/.agents/skills/diagram-design/LICENSE +4 -0
- package/.agents/skills/diagram-design/SKILL.md +413 -0
- package/.agents/skills/diagram-design/UPSTREAM.json +76 -0
- package/.agents/skills/diagram-design/UPSTREAM.md +23 -0
- package/.agents/skills/diagram-design/assets/example-architecture-containment.html +205 -0
- package/.agents/skills/diagram-design/assets/example-architecture-delta.html +193 -0
- package/.agents/skills/diagram-design/assets/example-architecture.html +183 -0
- package/.agents/skills/diagram-design/assets/example-axonometric-plan-campus.html +108 -0
- package/.agents/skills/diagram-design/assets/example-axonometric-plan-coffee-shop.html +121 -0
- package/.agents/skills/diagram-design/assets/example-axonometric-plan-warehouse.html +127 -0
- package/.agents/skills/diagram-design/assets/example-axonometric-plan.html +114 -0
- package/.agents/skills/diagram-design/assets/example-bar.html +129 -0
- package/.agents/skills/diagram-design/assets/example-beeswarm.html +384 -0
- package/.agents/skills/diagram-design/assets/example-box-plot.html +127 -0
- package/.agents/skills/diagram-design/assets/example-bubble.html +147 -0
- package/.agents/skills/diagram-design/assets/example-bump.html +142 -0
- package/.agents/skills/diagram-design/assets/example-data-flow.html +143 -0
- package/.agents/skills/diagram-design/assets/example-datalake.html +269 -0
- package/.agents/skills/diagram-design/assets/example-db-schema.html +302 -0
- package/.agents/skills/diagram-design/assets/example-dependency.html +224 -0
- package/.agents/skills/diagram-design/assets/example-deployment.html +190 -0
- package/.agents/skills/diagram-design/assets/example-dp-integration.html +81 -0
- package/.agents/skills/diagram-design/assets/example-dp-security-matrix.html +88 -0
- package/.agents/skills/diagram-design/assets/example-er.html +202 -0
- package/.agents/skills/diagram-design/assets/example-exploded-ai-stack.html +85 -0
- package/.agents/skills/diagram-design/assets/example-exploded-keyboard.html +84 -0
- package/.agents/skills/diagram-design/assets/example-exploded-phone.html +83 -0
- package/.agents/skills/diagram-design/assets/example-exploded-unboxing.html +84 -0
- package/.agents/skills/diagram-design/assets/example-exploded.html +83 -0
- package/.agents/skills/diagram-design/assets/example-fishbone.html +152 -0
- package/.agents/skills/diagram-design/assets/example-flowchart.html +163 -0
- package/.agents/skills/diagram-design/assets/example-gantt.html +137 -0
- package/.agents/skills/diagram-design/assets/example-heatmap.html +224 -0
- package/.agents/skills/diagram-design/assets/example-high-level-vertical.html +325 -0
- package/.agents/skills/diagram-design/assets/example-high-level.html +301 -0
- package/.agents/skills/diagram-design/assets/example-import-drawio.html +185 -0
- package/.agents/skills/diagram-design/assets/example-import-excalidraw.html +70 -0
- package/.agents/skills/diagram-design/assets/example-import-mermaid.html +70 -0
- package/.agents/skills/diagram-design/assets/example-it-state.html +128 -0
- package/.agents/skills/diagram-design/assets/example-journey.html +148 -0
- package/.agents/skills/diagram-design/assets/example-kanban.html +174 -0
- package/.agents/skills/diagram-design/assets/example-layers.html +123 -0
- package/.agents/skills/diagram-design/assets/example-line.html +111 -0
- package/.agents/skills/diagram-design/assets/example-loop.html +100 -0
- package/.agents/skills/diagram-design/assets/example-marimekko.html +148 -0
- package/.agents/skills/diagram-design/assets/example-medallion.html +159 -0
- package/.agents/skills/diagram-design/assets/example-nested.html +136 -0
- package/.agents/skills/diagram-design/assets/example-org-chart.html +45 -0
- package/.agents/skills/diagram-design/assets/example-polar.html +116 -0
- package/.agents/skills/diagram-design/assets/example-process.html +64 -0
- package/.agents/skills/diagram-design/assets/example-pyramid.html +119 -0
- package/.agents/skills/diagram-design/assets/example-quadrant-consultant.html +166 -0
- package/.agents/skills/diagram-design/assets/example-quadrant.html +135 -0
- package/.agents/skills/diagram-design/assets/example-radar.html +139 -0
- package/.agents/skills/diagram-design/assets/example-ridgeline.html +104 -0
- package/.agents/skills/diagram-design/assets/example-sankey.html +150 -0
- package/.agents/skills/diagram-design/assets/example-scatter.html +141 -0
- package/.agents/skills/diagram-design/assets/example-sequence-oauth.html +227 -0
- package/.agents/skills/diagram-design/assets/example-sequence.html +222 -0
- package/.agents/skills/diagram-design/assets/example-service-blueprint.html +185 -0
- package/.agents/skills/diagram-design/assets/example-slopegraph.html +119 -0
- package/.agents/skills/diagram-design/assets/example-state-lifecycle.html +92 -0
- package/.agents/skills/diagram-design/assets/example-state.html +147 -0
- package/.agents/skills/diagram-design/assets/example-story-map.html +184 -0
- package/.agents/skills/diagram-design/assets/example-streamgraph.html +99 -0
- package/.agents/skills/diagram-design/assets/example-swimlane.html +172 -0
- package/.agents/skills/diagram-design/assets/example-timeline.html +135 -0
- package/.agents/skills/diagram-design/assets/example-tree-block-decomposition.html +151 -0
- package/.agents/skills/diagram-design/assets/example-tree.html +173 -0
- package/.agents/skills/diagram-design/assets/example-treemap.html +107 -0
- package/.agents/skills/diagram-design/assets/example-uml-class.html +188 -0
- package/.agents/skills/diagram-design/assets/example-venn.html +110 -0
- package/.agents/skills/diagram-design/assets/example-wardley.html +153 -0
- package/.agents/skills/diagram-design/assets/example-waterfall.html +129 -0
- package/.agents/skills/diagram-design/assets/template-dark.html +99 -0
- package/.agents/skills/diagram-design/assets/template-full.html +421 -0
- package/.agents/skills/diagram-design/assets/template-motion.html +448 -0
- package/.agents/skills/diagram-design/assets/template-terminal.html +188 -0
- package/.agents/skills/diagram-design/assets/template.html +100 -0
- package/.agents/skills/diagram-design/references/animation.md +143 -0
- package/.agents/skills/diagram-design/references/export-registry.md +78 -0
- package/.agents/skills/diagram-design/references/export.md +376 -0
- package/.agents/skills/diagram-design/references/import-drawio.md +181 -0
- package/.agents/skills/diagram-design/references/import-excalidraw.md +130 -0
- package/.agents/skills/diagram-design/references/import-mermaid.md +137 -0
- package/.agents/skills/diagram-design/references/layout-budget.md +97 -0
- package/.agents/skills/diagram-design/references/onboarding.md +368 -0
- package/.agents/skills/diagram-design/references/output-spec.md +277 -0
- package/.agents/skills/diagram-design/references/primitive-annotation.md +36 -0
- package/.agents/skills/diagram-design/references/primitive-icons.md +827 -0
- package/.agents/skills/diagram-design/references/primitive-sketchy.md +43 -0
- package/.agents/skills/diagram-design/references/primitive-terminal.md +76 -0
- package/.agents/skills/diagram-design/references/primitives-core.md +146 -0
- package/.agents/skills/diagram-design/references/profiles.md +183 -0
- package/.agents/skills/diagram-design/references/semantic-patterns.md +154 -0
- package/.agents/skills/diagram-design/references/style-guide.md +236 -0
- package/.agents/skills/diagram-design/references/style-vivid.md +148 -0
- package/.agents/skills/diagram-design/references/type-architecture-delta.md +143 -0
- package/.agents/skills/diagram-design/references/type-architecture.md +117 -0
- package/.agents/skills/diagram-design/references/type-axonometric-plan.md +83 -0
- package/.agents/skills/diagram-design/references/type-bar.md +108 -0
- package/.agents/skills/diagram-design/references/type-data-flow.md +371 -0
- package/.agents/skills/diagram-design/references/type-db-schema.md +48 -0
- package/.agents/skills/diagram-design/references/type-dependency.md +44 -0
- package/.agents/skills/diagram-design/references/type-deployment.md +45 -0
- package/.agents/skills/diagram-design/references/type-dp-integration.md +407 -0
- package/.agents/skills/diagram-design/references/type-dp-security-matrix.md +379 -0
- package/.agents/skills/diagram-design/references/type-er.md +25 -0
- package/.agents/skills/diagram-design/references/type-exploded.md +115 -0
- package/.agents/skills/diagram-design/references/type-fishbone.md +75 -0
- package/.agents/skills/diagram-design/references/type-flowchart.md +23 -0
- package/.agents/skills/diagram-design/references/type-gantt.md +45 -0
- package/.agents/skills/diagram-design/references/type-heatmap.md +80 -0
- package/.agents/skills/diagram-design/references/type-high-level.md +470 -0
- package/.agents/skills/diagram-design/references/type-it-state.md +468 -0
- package/.agents/skills/diagram-design/references/type-journey.md +55 -0
- package/.agents/skills/diagram-design/references/type-kanban.md +49 -0
- package/.agents/skills/diagram-design/references/type-layers.md +26 -0
- package/.agents/skills/diagram-design/references/type-line.md +337 -0
- package/.agents/skills/diagram-design/references/type-loop.md +223 -0
- package/.agents/skills/diagram-design/references/type-medallion.md +356 -0
- package/.agents/skills/diagram-design/references/type-nested.md +22 -0
- package/.agents/skills/diagram-design/references/type-org-chart.md +44 -0
- package/.agents/skills/diagram-design/references/type-polar.md +136 -0
- package/.agents/skills/diagram-design/references/type-process.md +490 -0
- package/.agents/skills/diagram-design/references/type-pyramid.md +33 -0
- package/.agents/skills/diagram-design/references/type-quadrant.md +81 -0
- package/.agents/skills/diagram-design/references/type-radar.md +82 -0
- package/.agents/skills/diagram-design/references/type-sankey.md +75 -0
- package/.agents/skills/diagram-design/references/type-scatter.md +276 -0
- package/.agents/skills/diagram-design/references/type-sequence.md +130 -0
- package/.agents/skills/diagram-design/references/type-service-blueprint.md +103 -0
- package/.agents/skills/diagram-design/references/type-state.md +30 -0
- package/.agents/skills/diagram-design/references/type-story-map.md +46 -0
- package/.agents/skills/diagram-design/references/type-swimlane.md +22 -0
- package/.agents/skills/diagram-design/references/type-timeline.md +20 -0
- package/.agents/skills/diagram-design/references/type-tree.md +24 -0
- package/.agents/skills/diagram-design/references/type-treemap.md +137 -0
- package/.agents/skills/diagram-design/references/type-uml-class.md +65 -0
- package/.agents/skills/diagram-design/references/type-venn.md +26 -0
- package/.agents/skills/diagram-design/references/type-wardley.md +39 -0
- package/.agents/skills/diagram-design/references/type-waterfall.md +75 -0
- package/.agents/skills/scrum-meta/SKILL.md +36 -36
- package/.agents/skills/scrum-planning/SKILL.md +61 -61
- package/.agents/skills/scrum-planning/references/schema.md +384 -336
- package/.agents/skills/scrum-report/SKILL.md +42 -42
- package/.agents/skills/scrum-sprint/SKILL.md +49 -49
- package/.agents/skills/scrum-tdd/SKILL.md +140 -127
- package/README.md +224 -201
- package/assets/board/200.html +1 -1
- package/assets/board/404.html +1 -1
- package/assets/board/_nuxt/1XlYaCp9.js +1 -0
- package/assets/board/_nuxt/4gQF_HS3.js +1 -0
- package/assets/board/_nuxt/5okfNy5D.js +1 -0
- package/assets/board/_nuxt/{Bb3Limos.js → B0C2jsdk.js} +1 -1
- package/assets/board/_nuxt/B1j6UC4U.js +1 -0
- package/assets/board/_nuxt/B3LKy6o2.js +1 -0
- package/assets/board/_nuxt/BAQQngAq.js +13 -0
- package/assets/board/_nuxt/BE6t7Q2m.js +1 -0
- package/assets/board/_nuxt/BLvbupFS.js +1 -0
- package/assets/board/_nuxt/BQgzP7ur.js +1 -0
- package/assets/board/_nuxt/BRZIF_wo.js +1 -0
- package/assets/board/_nuxt/BW3xJdEx.js +1 -0
- package/assets/board/_nuxt/{Cm9lkUvP.js → BY36DFyj.js} +1 -1
- package/assets/board/_nuxt/BZlZD94i.js +1 -0
- package/assets/board/_nuxt/BZvXNwev.js +3 -0
- package/assets/board/_nuxt/BbxmvdIF.js +1 -0
- package/assets/board/_nuxt/BiWrMOsL.js +1 -0
- package/assets/board/_nuxt/BizDeOK5.js +1 -0
- package/assets/board/_nuxt/BkcaOYQ_.js +95 -0
- package/assets/board/_nuxt/BkoKCNUY.js +1 -0
- package/assets/board/_nuxt/BlKmZhhF.js +1 -0
- package/assets/board/_nuxt/BmBpmU73.js +1 -0
- package/assets/board/_nuxt/BrWE0up6.js +1 -0
- package/assets/board/_nuxt/Bs52qkgk.js +1 -0
- package/assets/board/_nuxt/BtzUbHO_.js +1 -0
- package/assets/board/_nuxt/BuBzMkFZ.js +1 -0
- package/assets/board/_nuxt/Bubk0xn-.js +1 -0
- package/assets/board/_nuxt/BxuBMtk_.js +3 -0
- package/assets/board/_nuxt/By1KXXLR.js +1 -0
- package/assets/board/_nuxt/BygIGL69.js +2 -0
- package/assets/board/_nuxt/C1iO5KH3.js +1 -0
- package/assets/board/_nuxt/C2YPmTzt.js +1 -0
- package/assets/board/_nuxt/C3SpAhCW.js +1 -0
- package/assets/board/_nuxt/C3ZRWz5L.js +1 -0
- package/assets/board/_nuxt/C4WdPP6K.js +1 -0
- package/assets/board/_nuxt/C6o03JHe.js +1 -0
- package/assets/board/_nuxt/C8qB5llI.js +1 -0
- package/assets/board/_nuxt/C9MgRdhu.js +1 -0
- package/assets/board/_nuxt/CCA81-4P.js +1 -0
- package/assets/board/_nuxt/{D2O8Q8x9.js → CFCM18fF.js} +1 -1
- package/assets/board/_nuxt/CGXljzd_.js +1 -0
- package/assets/board/_nuxt/CI0scS-N.js +1 -0
- package/assets/board/_nuxt/CJs9B42o.js +1 -0
- package/assets/board/_nuxt/COecDWbb.js +1 -0
- package/assets/board/_nuxt/COmtTtfB.js +1 -0
- package/assets/board/_nuxt/CPYm4J_K.js +1 -0
- package/assets/board/_nuxt/CReA4TEB.js +3 -0
- package/assets/board/_nuxt/CXG7GZes.js +1 -0
- package/assets/board/_nuxt/CYGiZn-X.js +1 -0
- package/assets/board/_nuxt/CaE8VlUp.js +1 -0
- package/assets/board/_nuxt/CcGiNUgA.js +1 -0
- package/assets/board/_nuxt/Ccgv3lDj.js +1 -0
- package/assets/board/_nuxt/CdPGFZyy.js +1 -0
- package/assets/board/_nuxt/{DfR3IZN_.js → Cdf_T8Xa.js} +1 -1
- package/assets/board/_nuxt/CgQ30j_6.js +1 -0
- package/assets/board/_nuxt/ChUHb39z.js +1 -0
- package/assets/board/_nuxt/Ck0-FqVI.js +1 -0
- package/assets/board/_nuxt/CkWKLtjO.js +1 -0
- package/assets/board/_nuxt/Cq7hsCKB.js +1 -0
- package/assets/board/_nuxt/CtFknnHZ.js +1 -0
- package/assets/board/_nuxt/D166WVUG.js +1 -0
- package/assets/board/_nuxt/D4Dpzo9e.js +1 -0
- package/assets/board/_nuxt/D5i92OZO.js +1 -0
- package/assets/board/_nuxt/D6dUp4Nm.js +9 -0
- package/assets/board/_nuxt/DFQDFspC.js +1 -0
- package/assets/board/_nuxt/DHX2tw9J.js +1 -0
- package/assets/board/_nuxt/DJM7u8P3.js +1 -0
- package/assets/board/_nuxt/{BPTrRhcw.js → DLr85ebo.js} +1 -1
- package/assets/board/_nuxt/DOMM1wfl.js +1 -0
- package/assets/board/_nuxt/DU6ArlUN.js +1 -0
- package/assets/board/_nuxt/{B_SlFcWp.js → DUgGsde6.js} +1 -1
- package/assets/board/_nuxt/DV4ti-Pq.js +1 -0
- package/assets/board/_nuxt/DZ--fXuU.js +1 -0
- package/assets/board/_nuxt/D_ct-Stz.js +2 -0
- package/assets/board/_nuxt/DbWAComO.js +1 -0
- package/assets/board/_nuxt/DbwlcHAt.js +1 -0
- package/assets/board/_nuxt/DjzLR9Bl.js +1 -0
- package/assets/board/_nuxt/Dki9VkAL.js +3 -0
- package/assets/board/_nuxt/DlNJhOoc.js +1 -0
- package/assets/board/_nuxt/DqG7CtD4.js +1 -0
- package/assets/board/_nuxt/DqUSsCJ2.js +1 -0
- package/assets/board/_nuxt/Du2F4WXh.js +1 -0
- package/assets/board/_nuxt/DuRiEXn4.js +1 -0
- package/assets/board/_nuxt/DxBwVNyP.js +2 -0
- package/assets/board/_nuxt/DzmVW5-D.js +1 -0
- package/assets/board/_nuxt/I-sGtDIB.js +6 -0
- package/assets/board/_nuxt/JU9-x68N.js +1 -0
- package/assets/board/_nuxt/Lozm7hhE.js +1 -0
- package/assets/board/_nuxt/MMsTMkcB.js +1 -0
- package/assets/board/_nuxt/MS7CGmCB.js +1 -0
- package/assets/board/_nuxt/OKKaKILz.js +1 -0
- package/assets/board/_nuxt/{Csqgy925.js → P6qlvBHA.js} +1 -1
- package/assets/board/_nuxt/SrNUvNwO.js +1 -0
- package/assets/board/_nuxt/{DSaWoFHS.js → SywdkA98.js} +1 -1
- package/assets/board/_nuxt/TBIRpVLG.js +1 -0
- package/assets/board/_nuxt/TlTrMIMm.js +1 -0
- package/assets/board/_nuxt/Zv9-SOQF.js +1 -0
- package/assets/board/_nuxt/_id_.ItzoSaBL.css +1 -0
- package/assets/board/_nuxt/builds/latest.json +1 -1
- package/assets/board/_nuxt/builds/meta/2218461a-f45b-41d6-ae44-b0d2332a9511.json +1 -0
- package/assets/board/_nuxt/burndown.CT8fx1Ge.css +1 -0
- package/assets/board/_nuxt/diagrams.D1ybqBem.css +1 -0
- package/assets/board/_nuxt/{entry.CKHePurH.css → entry.BCuHiFgF.css} +1 -1
- package/assets/board/_nuxt/epics.CUeDIEa0.css +1 -0
- package/assets/board/_nuxt/error-404.Y9imKiDC.css +1 -0
- package/assets/board/_nuxt/error-500.DufSBGVC.css +1 -0
- package/assets/board/_nuxt/gantt.CiC2Ts1t.css +1 -0
- package/assets/board/_nuxt/jbri6q1p.js +1 -0
- package/assets/board/_nuxt/last-change.C05Vc3BF.css +1 -0
- package/assets/board/_nuxt/milestones.Civ3sMiQ.css +1 -0
- package/assets/board/_nuxt/oSJF4yHw.js +1 -0
- package/assets/board/_nuxt/pages.BshWbOyo.css +1 -0
- package/assets/board/_nuxt/q_8bxRMt.js +1 -0
- package/assets/board/_nuxt/tests.Ci9fBCka.css +1 -0
- package/assets/board/burndown/index.html +1 -1
- package/assets/board/diagrams/index.html +1 -0
- package/assets/board/epics/index.html +1 -1
- package/assets/board/favicon.svg +16 -16
- package/assets/board/gantt/index.html +1 -1
- package/assets/board/index.html +1 -1
- package/assets/board/last-change/index.html +1 -1
- package/assets/board/milestones/index.html +1 -1
- package/assets/board/tests/index.html +1 -1
- package/bin/scrum-skills.mjs +204 -191
- package/docs/ai-assistant-guide.md +1008 -1008
- package/package.json +42 -41
- package/templates/AGENTS.md +84 -74
- package/tools/agile-tools.mjs +510 -510
- package/tools/ai.mjs +464 -464
- package/tools/diagram-import.mjs +238 -0
- package/tools/diagram-lint.mjs +518 -0
- package/tools/diagram.mjs +299 -0
- package/tools/git-commit.mjs +216 -216
- package/tools/last-change.mjs +326 -326
- package/tools/projects.mjs +240 -215
- package/tools/serve.mjs +180 -154
- package/tools/validate.mjs +781 -664
- package/assets/board/_nuxt/1eqboJxR.js +0 -1
- package/assets/board/_nuxt/2K7ag6ES.js +0 -1
- package/assets/board/_nuxt/51OWHj8c.js +0 -2
- package/assets/board/_nuxt/6gFtTBwm.js +0 -1
- package/assets/board/_nuxt/B3jkABw9.js +0 -1
- package/assets/board/_nuxt/B5n2_jZN.js +0 -1
- package/assets/board/_nuxt/BDK2Hj7B.js +0 -1
- package/assets/board/_nuxt/BDcnzXHq.js +0 -1
- package/assets/board/_nuxt/BaG8y0Wt.js +0 -1
- package/assets/board/_nuxt/BbwJq5pb.js +0 -1
- package/assets/board/_nuxt/BgLeKwx0.js +0 -1
- package/assets/board/_nuxt/Bi9Axj3V.js +0 -1
- package/assets/board/_nuxt/BmxvtaMq.js +0 -1
- package/assets/board/_nuxt/BodFvto3.js +0 -1
- package/assets/board/_nuxt/BpWeHLuN.js +0 -1
- package/assets/board/_nuxt/BuOKDFiJ.js +0 -1
- package/assets/board/_nuxt/C40iTe-x.js +0 -1
- package/assets/board/_nuxt/C7K0R7F-.js +0 -2
- package/assets/board/_nuxt/C8vL692X.js +0 -1
- package/assets/board/_nuxt/C9StZL3D.js +0 -3
- package/assets/board/_nuxt/CGI_ZV79.js +0 -1
- package/assets/board/_nuxt/CJ2mz0WY.js +0 -1
- package/assets/board/_nuxt/CJMBJQuf.js +0 -1
- package/assets/board/_nuxt/CK-lOYG9.js +0 -1
- package/assets/board/_nuxt/CK9_veoz.js +0 -1
- package/assets/board/_nuxt/CLBcBrYf.js +0 -1
- package/assets/board/_nuxt/CMUi2-Lj.js +0 -1
- package/assets/board/_nuxt/COf6E6h4.js +0 -1
- package/assets/board/_nuxt/CPCUMJnF.js +0 -1
- package/assets/board/_nuxt/CPcKlmSm.js +0 -1
- package/assets/board/_nuxt/CQTvWh4i.js +0 -1
- package/assets/board/_nuxt/CSEEUF8S.js +0 -1
- package/assets/board/_nuxt/CSSb154I.js +0 -13
- package/assets/board/_nuxt/CgDTd7jS.js +0 -1
- package/assets/board/_nuxt/ChG76uh9.js +0 -1
- package/assets/board/_nuxt/Ci0qbkgh.js +0 -1
- package/assets/board/_nuxt/CqoQ__I_.js +0 -1
- package/assets/board/_nuxt/CuJUHLl8.js +0 -1
- package/assets/board/_nuxt/CvLiA4_O.js +0 -1
- package/assets/board/_nuxt/Cvs12oA0.js +0 -3
- package/assets/board/_nuxt/CyEXFe3V.js +0 -1
- package/assets/board/_nuxt/Cz6rZcLK.js +0 -1
- package/assets/board/_nuxt/D2FhHl1o.js +0 -1
- package/assets/board/_nuxt/D3YCAmfu.js +0 -1
- package/assets/board/_nuxt/D5CE81jK.js +0 -1
- package/assets/board/_nuxt/D75vbfSb.js +0 -1
- package/assets/board/_nuxt/D99LbGfb.js +0 -9
- package/assets/board/_nuxt/DEfLc7p3.js +0 -6
- package/assets/board/_nuxt/DF-nsa1Z.js +0 -1
- package/assets/board/_nuxt/DIc8yx98.js +0 -1
- package/assets/board/_nuxt/DLkAKotw.js +0 -1
- package/assets/board/_nuxt/DRPlYvb3.js +0 -1
- package/assets/board/_nuxt/DRlQgul3.js +0 -1
- package/assets/board/_nuxt/DSLbmoWa.js +0 -1
- package/assets/board/_nuxt/DWO9c9yp.js +0 -1
- package/assets/board/_nuxt/DZo3dYLJ.js +0 -3
- package/assets/board/_nuxt/D_S6N7_b.js +0 -1
- package/assets/board/_nuxt/DajA-Tkt.js +0 -1
- package/assets/board/_nuxt/DbHiyab-.js +0 -1
- package/assets/board/_nuxt/Dc7Ql6eE.js +0 -1
- package/assets/board/_nuxt/Dd9ZukPU.js +0 -1
- package/assets/board/_nuxt/Dg2LiqmN.js +0 -1
- package/assets/board/_nuxt/DhuleNnm.js +0 -1
- package/assets/board/_nuxt/DkI1Qxpp.js +0 -95
- package/assets/board/_nuxt/DnDHGSYI.js +0 -1
- package/assets/board/_nuxt/DpXJ_tCU.js +0 -1
- package/assets/board/_nuxt/DqZgF3hU.js +0 -1
- package/assets/board/_nuxt/DqcxvfbD.js +0 -1
- package/assets/board/_nuxt/DsA2lVci.js +0 -1
- package/assets/board/_nuxt/Dtsa-1NS.js +0 -1
- package/assets/board/_nuxt/Du91iq0p.js +0 -1
- package/assets/board/_nuxt/Dx1wcYFo.js +0 -1
- package/assets/board/_nuxt/EYTShbt4.js +0 -1
- package/assets/board/_nuxt/MaXJprwN.js +0 -1
- package/assets/board/_nuxt/P9x67aq2.js +0 -1
- package/assets/board/_nuxt/S08nFESZ.js +0 -3
- package/assets/board/_nuxt/V0uynWE2.js +0 -1
- package/assets/board/_nuxt/Vo4wMbnS.js +0 -1
- package/assets/board/_nuxt/ZVTqot-_.js +0 -1
- package/assets/board/_nuxt/ZngEd3HB.js +0 -1
- package/assets/board/_nuxt/_id_.B2NDKSlX.css +0 -1
- package/assets/board/_nuxt/assMu_Wn.js +0 -1
- package/assets/board/_nuxt/builds/meta/8e23aec9-5b67-4569-b8bf-d4f07e50ffdf.json +0 -1
- package/assets/board/_nuxt/burndown.Do6IbM6o.css +0 -1
- package/assets/board/_nuxt/epics.Dmessplu.css +0 -1
- package/assets/board/_nuxt/error-404.BFmXOnCT.css +0 -1
- package/assets/board/_nuxt/error-500.BZ-DdjQ6.css +0 -1
- package/assets/board/_nuxt/gantt.Dmp00GUo.css +0 -1
- package/assets/board/_nuxt/last-change.1Su4GrCN.css +0 -1
- package/assets/board/_nuxt/milestones.U0xKCsEo.css +0 -1
- package/assets/board/_nuxt/pages.Dbj2IVk8.css +0 -1
- package/assets/board/_nuxt/rlePnzpc.js +0 -1
- package/assets/board/_nuxt/tests.dXlUX16A.css +0 -1
- package/assets/board/_nuxt/uKzCKCTq.js +0 -1
- package/assets/board/_nuxt/vwxaNGfc.js +0 -1
- package/assets/board/_nuxt/yh6sf_D5.js +0 -1
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 图例产物质检(零依赖,node >= 18)
|
|
4
|
+
*
|
|
5
|
+
* 这是 diagram-design 上游 `scripts/self_check.py` 的 Node 移植版。
|
|
6
|
+
* 上游那份是纯标准库、纯静态分析、不启浏览器的检查器,为了不让用户装 Python,
|
|
7
|
+
* 我们按它的判定口径重写一遍(同一套七组检查、同样「拿不准就跳过、绝不误报」的策略)。
|
|
8
|
+
* 上游是 MIT(© cathrynlavery),见 .agents/skills/diagram-design/LICENSE。
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* node tools/diagram-lint.mjs <file...> [--offline] [--skill <dir>]
|
|
12
|
+
* 退出码: 0 通过, 1 有错误
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
15
|
+
import { dirname, join, resolve } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const DEFAULT_SKILL = resolve(HERE, '..', '.agents/skills/diagram-design');
|
|
20
|
+
|
|
21
|
+
const UNSAFE_TAGS = new Set(['base', 'embed', 'object', 'iframe']);
|
|
22
|
+
const VOID_TAGS = new Set([
|
|
23
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
24
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
25
|
+
]);
|
|
26
|
+
// 内容按原文处理、不当标签解析:style / script 里的 `>` 是合法的比较符号
|
|
27
|
+
const RAW_TEXT = new Set(['style', 'script', 'textarea', 'title']);
|
|
28
|
+
const UNPAINTED = new Set(['defs', 'symbol', 'marker', 'pattern', 'clippath', 'mask', 'foreignobject']);
|
|
29
|
+
const PAINTED = new Set(['rect', 'line', 'circle', 'ellipse', 'polygon', 'polyline', 'text']);
|
|
30
|
+
const MOTION_MODES = new Set(['none', 'reveal', 'step', 'loop']);
|
|
31
|
+
const MOTION_ACTIONS = ['play', 'pause', 'replay', 'prev', 'next'];
|
|
32
|
+
const REF_ATTRS = ['src', 'href', 'xlink:href', 'poster', 'srcset', 'action', 'formaction'];
|
|
33
|
+
const CSS_REF_ATTRS = ['style', 'fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', 'marker-mid', 'marker-end', 'cursor'];
|
|
34
|
+
/** viewBox 容差:和上游一致,抗浮点误差与描边外扩 */
|
|
35
|
+
const VIEWBOX_TOLERANCE = 2.0;
|
|
36
|
+
|
|
37
|
+
// ---------- 极简 HTML 解析 ----------
|
|
38
|
+
|
|
39
|
+
/** 找开标签的 `>`,跳过引号里的内容 */
|
|
40
|
+
function tagEnd(src, from) {
|
|
41
|
+
let quote = '';
|
|
42
|
+
for (let i = from; i < src.length; i++) {
|
|
43
|
+
const ch = src[i];
|
|
44
|
+
if (quote) { if (ch === quote) quote = ''; continue; }
|
|
45
|
+
if (ch === '"' || ch === "'") { quote = ch; continue; }
|
|
46
|
+
if (ch === '>') return i;
|
|
47
|
+
}
|
|
48
|
+
return -1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const decodeEntities = s => s
|
|
52
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
53
|
+
.replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ');
|
|
54
|
+
|
|
55
|
+
function parseAttrs(raw) {
|
|
56
|
+
const attrs = {};
|
|
57
|
+
const order = [];
|
|
58
|
+
const re = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>`]+)))?/g;
|
|
59
|
+
let m;
|
|
60
|
+
while ((m = re.exec(raw))) {
|
|
61
|
+
const name = m[1].toLowerCase();
|
|
62
|
+
if (name === '/' || name === '') continue;
|
|
63
|
+
attrs[name] = decodeEntities(m[2] ?? m[3] ?? m[4] ?? '');
|
|
64
|
+
order.push(name);
|
|
65
|
+
}
|
|
66
|
+
return { attrs, order };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 解析成树。HTML 那套容错语义只覆盖我们需要的部分:
|
|
71
|
+
* 注释 / CDATA / 声明直接跳过,raw-text 元素整体吞掉,其余按栈配对。
|
|
72
|
+
* 末尾仍留在栈上的就是未闭合元素(对应上游的标签平衡检查)。
|
|
73
|
+
*/
|
|
74
|
+
function parse(src) {
|
|
75
|
+
const root = { name: '#root', attrs: {}, order: [], children: [], parent: null, text: '' };
|
|
76
|
+
const stack = [root];
|
|
77
|
+
const unclosed = [];
|
|
78
|
+
let i = 0;
|
|
79
|
+
const lower = src.toLowerCase();
|
|
80
|
+
|
|
81
|
+
const pushText = t => { if (t.trim()) stack[stack.length - 1].text += t; };
|
|
82
|
+
|
|
83
|
+
while (i < src.length) {
|
|
84
|
+
const lt = src.indexOf('<', i);
|
|
85
|
+
if (lt === -1) { pushText(src.slice(i)); break; }
|
|
86
|
+
if (lt > i) pushText(src.slice(i, lt));
|
|
87
|
+
|
|
88
|
+
if (src.startsWith('<!--', lt)) {
|
|
89
|
+
const end = src.indexOf('-->', lt + 4);
|
|
90
|
+
i = end === -1 ? src.length : end + 3;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (src.startsWith('<![CDATA[', lt)) {
|
|
94
|
+
const end = src.indexOf(']]>', lt + 9);
|
|
95
|
+
i = end === -1 ? src.length : end + 3;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (src.startsWith('<!', lt)) {
|
|
99
|
+
const end = src.indexOf('>', lt + 2);
|
|
100
|
+
i = end === -1 ? src.length : end + 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (src.startsWith('</', lt)) {
|
|
104
|
+
const end = src.indexOf('>', lt + 2);
|
|
105
|
+
if (end === -1) { i = src.length; break; }
|
|
106
|
+
const name = src.slice(lt + 2, end).trim().toLowerCase();
|
|
107
|
+
// 找到最近一个同名开标签再一起弹栈;中间没闭合的记下来
|
|
108
|
+
let hit = -1;
|
|
109
|
+
for (let k = stack.length - 1; k > 0; k--) if (stack[k].name === name) { hit = k; break; }
|
|
110
|
+
if (hit > 0) {
|
|
111
|
+
for (let k = stack.length - 1; k > hit; k--) unclosed.push(stack[k]);
|
|
112
|
+
stack.length = hit;
|
|
113
|
+
}
|
|
114
|
+
i = end + 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const end = tagEnd(src, lt + 1);
|
|
119
|
+
if (end === -1) { pushText(src.slice(lt)); break; }
|
|
120
|
+
const inner = src.slice(lt + 1, end);
|
|
121
|
+
const m = /^([a-zA-Z][^\s/>]*)/.exec(inner);
|
|
122
|
+
if (!m) { i = end + 1; continue; }
|
|
123
|
+
const name = m[1].toLowerCase();
|
|
124
|
+
const selfClose = /\/\s*$/.test(inner);
|
|
125
|
+
const { attrs, order } = parseAttrs(inner.slice(m[1].length));
|
|
126
|
+
const node = { name, attrs, order, children: [], parent: stack[stack.length - 1], text: '', selfClose };
|
|
127
|
+
stack[stack.length - 1].children.push(node);
|
|
128
|
+
i = end + 1;
|
|
129
|
+
|
|
130
|
+
if (selfClose || VOID_TAGS.has(name)) continue;
|
|
131
|
+
if (RAW_TEXT.has(name)) {
|
|
132
|
+
const close = lower.indexOf(`</${name}`, i);
|
|
133
|
+
if (close === -1) { node.text = src.slice(i); unclosed.push(node); i = src.length; break; }
|
|
134
|
+
node.text = src.slice(i, close);
|
|
135
|
+
const gt = src.indexOf('>', close);
|
|
136
|
+
i = gt === -1 ? src.length : gt + 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
stack.push(node);
|
|
140
|
+
}
|
|
141
|
+
if (stack.length > 1) unclosed.push(...stack.slice(1));
|
|
142
|
+
return { root, unclosed };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const walk = (node, fn, parents = []) => {
|
|
146
|
+
for (const c of node.children) {
|
|
147
|
+
fn(c, parents);
|
|
148
|
+
walk(c, fn, [...parents, c]);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const elements = root => {
|
|
153
|
+
const out = [];
|
|
154
|
+
walk(root, n => out.push(n));
|
|
155
|
+
return out;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** 解码 CSS 十六进制转义(`\75 rl(` → `url(`),防止绕过引用检查 */
|
|
159
|
+
const decodeCss = css => css
|
|
160
|
+
.replace(/\\([0-9a-fA-F]{1,6})\s?/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
|
|
161
|
+
.replace(/\\(.)/g, '$1');
|
|
162
|
+
|
|
163
|
+
const stripComments = s => s.replace(/<!--[\s\S]*?-->/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
164
|
+
|
|
165
|
+
const isRemote = ref => /^(?:https?:)?\/\//i.test(ref) || /^[a-z][a-z0-9+.-]*:\/\//i.test(ref);
|
|
166
|
+
|
|
167
|
+
// ---------- 单文件校验 ----------
|
|
168
|
+
|
|
169
|
+
function checkFile(file, { offline, skillDir }) {
|
|
170
|
+
const errors = [];
|
|
171
|
+
const warns = [];
|
|
172
|
+
const err = m => errors.push(m);
|
|
173
|
+
const warn = m => warns.push(m);
|
|
174
|
+
|
|
175
|
+
const src = readFileSync(file, 'utf8');
|
|
176
|
+
const { root, unclosed } = parse(src);
|
|
177
|
+
const all = elements(root);
|
|
178
|
+
|
|
179
|
+
// ① 安全边界
|
|
180
|
+
for (const el of all) {
|
|
181
|
+
if (UNSAFE_TAGS.has(el.name)) err(`[unsafe] 禁用标签 <${el.name}>:产物必须是单文件,不许嵌外部文档`);
|
|
182
|
+
for (const key of Object.keys(el.attrs)) {
|
|
183
|
+
if (key.startsWith('on')) err(`[unsafe] <${el.name}> 上不许有事件属性 ${key}`);
|
|
184
|
+
if (key === 'srcdoc') err(`[unsafe] <${el.name}> 上不许有 srcdoc`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ② 文档完整性(截断 / 乱码 / 未闭合)
|
|
189
|
+
const noComments = stripComments(src).trim();
|
|
190
|
+
if (!/<\/html>\s*$/i.test(noComments)) err('[doc] 文档没有以 </html> 结尾,疑似被截断');
|
|
191
|
+
const bad = src.indexOf('\ufffd');
|
|
192
|
+
if (bad !== -1) {
|
|
193
|
+
const line = src.slice(0, bad).split('\n').length;
|
|
194
|
+
err(`[doc] 第 ${line} 行出现 U+FFFD 替换字符:多字节内容被截断了`);
|
|
195
|
+
}
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
for (const el of unclosed) {
|
|
198
|
+
if (seen.has(el.name)) continue;
|
|
199
|
+
seen.add(el.name);
|
|
200
|
+
err(`[doc] <${el.name}> 未闭合(容器标签必须成对)`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ③ CSS 引用 + ④ 资源引用
|
|
204
|
+
const styleText = all.filter(e => e.name === 'style').map(e => e.text).join('\n');
|
|
205
|
+
const css = decodeCss(styleText);
|
|
206
|
+
if (/@import/i.test(css)) err('[css] 不许用 @import 引外部样式');
|
|
207
|
+
if (/image-set\s*\(/i.test(css)) err('[css] 不许用 image-set() 引外部图片');
|
|
208
|
+
if (/(?:https?:)?\/\//i.test(stripComments(css))) err('[css] 样式里出现远程地址:产物必须自包含');
|
|
209
|
+
|
|
210
|
+
const checkRef = (ref, where) => {
|
|
211
|
+
const v = ref.trim();
|
|
212
|
+
if (!v) return;
|
|
213
|
+
if (/^javascript:/i.test(v)) return err(`[ref] ${where} 使用了 javascript: 伪协议`);
|
|
214
|
+
if (/^data:text\/html/i.test(v)) return err(`[ref] ${where} 内嵌了 data:text/html`);
|
|
215
|
+
if (/^data:/i.test(v) && !/^data:image\//i.test(v)) return err(`[ref] ${where} 的 data: 只允许图片类型`);
|
|
216
|
+
if (isRemote(v)) return err(`[ref] ${where} 引用了远程地址 ${v.slice(0, 60)}:看板里跑在 sandbox iframe 中,外链会直接白屏`);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
for (const el of all) {
|
|
220
|
+
for (const key of REF_ATTRS) {
|
|
221
|
+
const v = el.attrs[key];
|
|
222
|
+
if (!v) continue;
|
|
223
|
+
// Google Fonts 是唯一白名单;--offline 下连它也不许
|
|
224
|
+
if (el.name === 'link' && key === 'href') {
|
|
225
|
+
let u;
|
|
226
|
+
try { u = new URL(v); } catch { err(`[ref] <link> 的 href ${v} 不是合法地址`); continue; }
|
|
227
|
+
const ok = u.protocol === 'https:' && u.host === 'fonts.googleapis.com' && u.pathname.startsWith('/css2');
|
|
228
|
+
if (!ok) err(`[ref] <link rel=stylesheet> 只允许 https://fonts.googleapis.com/css2,实际 ${v}`);
|
|
229
|
+
else if (offline) err('[ref] --offline 模式下不许有任何网络请求(包括 Google Fonts)');
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (key === 'srcset') { v.split(',').forEach(part => checkRef(part.trim().split(/\s+/)[0], '<srcset>')); continue; }
|
|
233
|
+
checkRef(v, `<${el.name} ${key}>`);
|
|
234
|
+
}
|
|
235
|
+
for (const key of CSS_REF_ATTRS) {
|
|
236
|
+
const v = el.attrs[key];
|
|
237
|
+
if (!v) continue;
|
|
238
|
+
const decoded = decodeCss(v);
|
|
239
|
+
if (/@import/i.test(decoded)) err(`[css] ${key} 里出现 @import`);
|
|
240
|
+
for (const m of decoded.matchAll(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi)) {
|
|
241
|
+
const ref = m[2].trim();
|
|
242
|
+
if (!ref.startsWith('#')) checkRef(ref, `<${el.name} ${key}> 的 url()`);
|
|
243
|
+
}
|
|
244
|
+
if (key !== 'style' && !/^url\(/i.test(decoded) && isRemote(decoded)) checkRef(decoded, `<${el.name} ${key}>`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ⑤ SVG 无障碍契约
|
|
249
|
+
const svgs = all.filter(e => e.name === 'svg');
|
|
250
|
+
const accessible = svgs.filter(e => e.attrs['aria-hidden'] !== 'true');
|
|
251
|
+
if (!accessible.length) err('[svg] 至少需要一个可访问的 <svg>(aria-hidden 的不算)');
|
|
252
|
+
const ids = new Map();
|
|
253
|
+
for (const el of all) {
|
|
254
|
+
const id = el.attrs.id;
|
|
255
|
+
if (!id) continue;
|
|
256
|
+
if (ids.has(id)) err(`[svg] id "${id}" 重复:同一页多张图会互相串台`);
|
|
257
|
+
ids.set(id, el);
|
|
258
|
+
}
|
|
259
|
+
for (const svg of accessible) {
|
|
260
|
+
if (svg.attrs.role !== 'img') err('[svg] 可访问的 <svg> 必须带 role="img"');
|
|
261
|
+
const first = svg.children.find(c => c.name !== '#text');
|
|
262
|
+
if (!first || first.name !== 'title') err('[svg] <title> 必须是 <svg> 的第一个子元素');
|
|
263
|
+
const title = svg.children.find(c => c.name === 'title');
|
|
264
|
+
const desc = svg.children.find(c => c.name === 'desc');
|
|
265
|
+
if (!title) err('[svg] 缺少 <title>');
|
|
266
|
+
else {
|
|
267
|
+
if (!title.text.trim()) err('[svg] <title> 不能为空');
|
|
268
|
+
if (!title.attrs.id || ['title', 'desc'].includes(title.attrs.id)) err('[svg] <title> 的 id 必须带图表前缀(不能是裸的 title)');
|
|
269
|
+
}
|
|
270
|
+
if (desc) {
|
|
271
|
+
if (!desc.text.trim()) err('[svg] <desc> 不能为空');
|
|
272
|
+
if (!desc.attrs.id || ['title', 'desc'].includes(desc.attrs.id)) err('[svg] <desc> 的 id 必须带图表前缀(不能是裸的 desc)');
|
|
273
|
+
}
|
|
274
|
+
const labelled = svg.attrs['aria-labelledby'];
|
|
275
|
+
if (title && desc) {
|
|
276
|
+
const want = `${title.attrs.id} ${desc.attrs.id}`;
|
|
277
|
+
if (labelled !== want) err(`[svg] aria-labelledby 必须是「title 的 id 然后 desc 的 id」:期望 "${want}",实际 "${labelled ?? ''}"`);
|
|
278
|
+
} else if (labelled && title && labelled !== title.attrs.id) {
|
|
279
|
+
err('[svg] aria-labelledby 与 <title> 的 id 不一致');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ⑥ viewBox 越界(静态近似:只认 translate,其余一律跳过)
|
|
284
|
+
const dynamicSelectors = [];
|
|
285
|
+
for (const m of stripComments(styleText).matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
286
|
+
const decl = m[2];
|
|
287
|
+
if (!/(transform|translate|rotate|scale|display|visibility|clip-path|mask|animation|offset)/i.test(decl)) continue;
|
|
288
|
+
const sel = m[1].split(',').map(s => s.trim().split(/\s+/).pop()).filter(Boolean);
|
|
289
|
+
dynamicSelectors.push(...sel);
|
|
290
|
+
}
|
|
291
|
+
const matchesDynamic = el => dynamicSelectors.some(sel => {
|
|
292
|
+
if (sel.startsWith('.')) return (el.attrs.class ?? '').split(/\s+/).includes(sel.slice(1));
|
|
293
|
+
if (sel.startsWith('#')) return el.attrs.id === sel.slice(1);
|
|
294
|
+
return el.name === sel.toLowerCase();
|
|
295
|
+
});
|
|
296
|
+
const isStatic = el => {
|
|
297
|
+
if (el.attrs.transform && !/^\s*translate\s*\(/i.test(el.attrs.transform)) return false;
|
|
298
|
+
const style = el.attrs.style ?? '';
|
|
299
|
+
if (/(transform|translate|rotate|scale|clip-path|mask|animation|offset)/i.test(style)) return false;
|
|
300
|
+
if (/display\s*:\s*none/i.test(style) || /visibility\s*:\s*hidden/i.test(style)) return false;
|
|
301
|
+
if (el.attrs['clip-path'] || el.attrs.mask) return false;
|
|
302
|
+
if (matchesDynamic(el)) return false;
|
|
303
|
+
return true;
|
|
304
|
+
};
|
|
305
|
+
const num = v => {
|
|
306
|
+
const n = Number.parseFloat(v);
|
|
307
|
+
return Number.isFinite(n) ? n : null;
|
|
308
|
+
};
|
|
309
|
+
const boxOf = (el, tx, ty) => {
|
|
310
|
+
const a = el.attrs;
|
|
311
|
+
const rect = (x, y, w, h) => ({ minX: tx + x, minY: ty + y, maxX: tx + x + w, maxY: ty + y + h });
|
|
312
|
+
switch (el.name) {
|
|
313
|
+
case 'rect': {
|
|
314
|
+
const x = num(a.x) ?? 0, y = num(a.y) ?? 0, w = num(a.width), h = num(a.height);
|
|
315
|
+
return w === null || h === null ? null : rect(x, y, w, h);
|
|
316
|
+
}
|
|
317
|
+
case 'line': {
|
|
318
|
+
const x1 = num(a.x1), y1 = num(a.y1), x2 = num(a.x2), y2 = num(a.y2);
|
|
319
|
+
if ([x1, y1, x2, y2].some(v => v === null)) return null;
|
|
320
|
+
return { minX: tx + Math.min(x1, x2), minY: ty + Math.min(y1, y2), maxX: tx + Math.max(x1, x2), maxY: ty + Math.max(y1, y2) };
|
|
321
|
+
}
|
|
322
|
+
case 'circle': {
|
|
323
|
+
const cx = num(a.cx), cy = num(a.cy), r = num(a.r);
|
|
324
|
+
if ([cx, cy, r].some(v => v === null)) return null;
|
|
325
|
+
return { minX: tx + cx - r, minY: ty + cy - r, maxX: tx + cx + r, maxY: ty + cy + r };
|
|
326
|
+
}
|
|
327
|
+
case 'ellipse': {
|
|
328
|
+
const cx = num(a.cx), cy = num(a.cy), rx = num(a.rx), ry = num(a.ry);
|
|
329
|
+
if ([cx, cy, rx, ry].some(v => v === null)) return null;
|
|
330
|
+
return { minX: tx + cx - rx, minY: ty + cy - ry, maxX: tx + cx + rx, maxY: ty + cy + ry };
|
|
331
|
+
}
|
|
332
|
+
case 'polygon': case 'polyline': {
|
|
333
|
+
const pts = (a.points ?? '').trim().split(/[\s,]+/).map(Number);
|
|
334
|
+
if (pts.length < 2 || pts.some(v => !Number.isFinite(v))) return null;
|
|
335
|
+
const xs = pts.filter((_, i) => i % 2 === 0), ys = pts.filter((_, i) => i % 2 === 1);
|
|
336
|
+
return { minX: tx + Math.min(...xs), minY: ty + Math.min(...ys), maxX: tx + Math.max(...xs), maxY: ty + Math.max(...ys) };
|
|
337
|
+
}
|
|
338
|
+
case 'text': {
|
|
339
|
+
// 文本不排版,只按锚点算(上游同样不算字体实际宽度)
|
|
340
|
+
const x = num(a.x), y = num(a.y);
|
|
341
|
+
if (x === null || y === null) return null;
|
|
342
|
+
return { minX: tx + x, minY: ty + y, maxX: tx + x, maxY: ty + y };
|
|
343
|
+
}
|
|
344
|
+
default: return null;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
for (const svg of svgs) {
|
|
349
|
+
const vb = (svg.attrs.viewbox ?? '').trim().split(/[\s,]+/).map(Number);
|
|
350
|
+
if (vb.length !== 4 || vb.some(v => !Number.isFinite(v))) {
|
|
351
|
+
if (svg.attrs.viewbox !== undefined) err('[svg] viewBox 必须是 4 个数字');
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const [minX, minY, w, h] = vb;
|
|
355
|
+
const maxX = minX + w, maxY = minY + h;
|
|
356
|
+
|
|
357
|
+
const visit = (el, tx, ty, skip) => {
|
|
358
|
+
const skipped = skip || !isStatic(el) || UNPAINTED.has(el.name);
|
|
359
|
+
let ntx = tx, nty = ty;
|
|
360
|
+
const tr = el.attrs.transform;
|
|
361
|
+
if (tr && /^\s*translate\s*\(/i.test(tr)) {
|
|
362
|
+
const nums = tr.replace(/^\s*translate\s*\(/i, '').replace(/\)\s*$/, '').split(/[\s,]+/).map(Number);
|
|
363
|
+
if (Number.isFinite(nums[0])) ntx += nums[0];
|
|
364
|
+
if (Number.isFinite(nums[1])) nty += nums[1];
|
|
365
|
+
}
|
|
366
|
+
if (!skipped && PAINTED.has(el.name)) {
|
|
367
|
+
const box = boxOf(el, ntx, nty);
|
|
368
|
+
if (box) {
|
|
369
|
+
const over = [
|
|
370
|
+
['左', minX - box.minX], ['上', minY - box.minY],
|
|
371
|
+
['右', box.maxX - maxX], ['下', box.maxY - maxY],
|
|
372
|
+
].filter(([, v]) => v > VIEWBOX_TOLERANCE).sort((a, b) => b[1] - a[1]);
|
|
373
|
+
if (over.length) {
|
|
374
|
+
const [edge, amount] = over[0];
|
|
375
|
+
err(`[viewBox] <${el.name}> 超出 ${edge}边界 ${amount.toFixed(1)}px(容差 ${VIEWBOX_TOLERANCE}):渲染时会被裁掉`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
for (const child of el.children) visit(child, ntx, nty, skipped || (el.name === 'svg' && el !== svg));
|
|
380
|
+
};
|
|
381
|
+
for (const child of svg.children) visit(child, 0, 0, false);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ⑦ 脚本与动画契约
|
|
385
|
+
const scripts = all.filter(e => e.name === 'script');
|
|
386
|
+
if (scripts.length > 1) err(`[script] 最多允许一个 <script>,实际 ${scripts.length} 个`);
|
|
387
|
+
let canonical = null;
|
|
388
|
+
const templateFile = join(skillDir, 'assets/template-motion.html');
|
|
389
|
+
if (existsSync(templateFile)) {
|
|
390
|
+
const tpl = readFileSync(templateFile, 'utf8');
|
|
391
|
+
const found = [...tpl.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi)];
|
|
392
|
+
if (found.length === 1) canonical = found[0][2].replace(/\r\n/g, '\n').trim();
|
|
393
|
+
else warn('[script] 模板 template-motion.html 里的 controller 不唯一,跳过逐字比对');
|
|
394
|
+
} else {
|
|
395
|
+
warn(`[script] 找不到 ${templateFile},跳过 controller 逐字比对(--skill 可指定 skill 目录)`);
|
|
396
|
+
}
|
|
397
|
+
for (const s of scripts) {
|
|
398
|
+
const keys = s.order.filter(k => k !== '/');
|
|
399
|
+
if (keys.length !== 1 || keys[0] !== 'data-diagram-controls' || (s.attrs[keys[0]] ?? '') !== '') {
|
|
400
|
+
err(`[script] <script> 的属性只允许一个 data-diagram-controls="",实际 ${keys.join(' ') || '(无)'}`);
|
|
401
|
+
}
|
|
402
|
+
if (canonical !== null && s.text.replace(/\r\n/g, '\n').trim() !== canonical) {
|
|
403
|
+
err('[script] 控制器脚本体与 template-motion.html 不一致:动画图必须逐字复制规范控制器');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const motionRoots = all.filter(e => 'data-motion-root' in e.attrs);
|
|
408
|
+
const motionItems = all.filter(e => 'data-motion-item' in e.attrs);
|
|
409
|
+
const controls = all.filter(e => 'data-motion-controls' in e.attrs);
|
|
410
|
+
const status = all.filter(e => 'data-motion-status' in e.attrs);
|
|
411
|
+
|
|
412
|
+
if (motionRoots.length || motionItems.length || scripts.length) {
|
|
413
|
+
if (motionRoots.length > 1) err(`[motion] data-motion-root 只能有一个,实际 ${motionRoots.length}`);
|
|
414
|
+
for (const rootEl of motionRoots) {
|
|
415
|
+
const mode = rootEl.attrs['data-motion-mode'] ?? '';
|
|
416
|
+
if (!MOTION_MODES.has(mode)) err(`[motion] data-motion-mode 必须是 ${[...MOTION_MODES].join('/')},实际 "${mode}"`);
|
|
417
|
+
const rawCount = rootEl.attrs['data-step-count'];
|
|
418
|
+
const minStep = mode === 'none' ? 0 : 1;
|
|
419
|
+
if (rawCount !== undefined && !/^\d+$/.test(rawCount)) {
|
|
420
|
+
err(`[motion] data-step-count 必须是十进制整数,实际 "${rawCount}"`);
|
|
421
|
+
} else if (rawCount !== undefined) {
|
|
422
|
+
const count = Number(rawCount);
|
|
423
|
+
if (count < minStep || count > 8) err(`[motion] data-step-count ${count} 越界:${mode} 模式允许 ${minStep}~8`);
|
|
424
|
+
}
|
|
425
|
+
if (motionItems.length > 12) err(`[motion] 动画分步元素最多 12 个,实际 ${motionItems.length}`);
|
|
426
|
+
const steps = new Map();
|
|
427
|
+
for (const item of motionItems) {
|
|
428
|
+
const step = item.attrs['data-step'];
|
|
429
|
+
if (step === undefined || !/^\d+$/.test(step)) err(`[motion] 每个 data-motion-item 都必须有十进制的 data-step`);
|
|
430
|
+
const decorative = item.attrs['aria-hidden'] === 'true';
|
|
431
|
+
if (decorative) {
|
|
432
|
+
if (item.attrs.focusable !== 'false') err('[motion] 装饰性分步元素必须同时带 aria-hidden="true" 与 focusable="false"');
|
|
433
|
+
} else {
|
|
434
|
+
const label = (item.attrs['aria-label'] ?? '').trim();
|
|
435
|
+
if (!label) err('[motion] 语义分步元素必须有非空的 aria-label(读屏要知道这一步是什么)');
|
|
436
|
+
else if (/^#[0-9a-f]{3,8}$/i.test(label)) err('[motion] aria-label 不能只有一个颜色值');
|
|
437
|
+
}
|
|
438
|
+
const style = item.attrs.style ?? '';
|
|
439
|
+
if (/display\s*:\s*none/i.test(style) || /visibility\s*:\s*hidden/i.test(style) || /opacity\s*:\s*0(?:[^\d.]|$)/i.test(style)) {
|
|
440
|
+
err('[motion] 分步元素不许在源码里隐藏:静态回退必须可见');
|
|
441
|
+
}
|
|
442
|
+
if (!decorative && step !== undefined) {
|
|
443
|
+
const n = Number(step);
|
|
444
|
+
if (!steps.has(n)) steps.set(n, 0);
|
|
445
|
+
steps.set(n, steps.get(n) + 1);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
for (const [n, count] of steps) if (count > 2) err(`[motion] 第 ${n} 步最多 2 个语义元素,实际 ${count}`);
|
|
449
|
+
|
|
450
|
+
const controlled = mode === 'step' || scripts.length > 0;
|
|
451
|
+
if (mode === 'none' || mode === 'loop') {
|
|
452
|
+
if (scripts.length) err(`[motion] ${mode} 模式不许带脚本`);
|
|
453
|
+
if (controls.length) err(`[motion] ${mode} 模式不许带播放控件`);
|
|
454
|
+
if (status.length) err(`[motion] ${mode} 模式不许带 live status`);
|
|
455
|
+
}
|
|
456
|
+
if (controlled) {
|
|
457
|
+
if (controls.length !== 1) err(`[motion] 受控动画必须有且只有一个 data-motion-controls,实际 ${controls.length}`);
|
|
458
|
+
const actions = all.filter(e => e.attrs['data-motion-action']).map(e => e.attrs['data-motion-action']);
|
|
459
|
+
const missing = MOTION_ACTIONS.filter(a => !actions.includes(a));
|
|
460
|
+
if (missing.length) err(`[motion] 播放控件缺少 action:${missing.join('、')}`);
|
|
461
|
+
if (status.length !== 1) err(`[motion] 受控动画必须有且只有一个 data-motion-status`);
|
|
462
|
+
for (const st of status) {
|
|
463
|
+
if (st.attrs.role !== 'status' || st.attrs['aria-live'] !== 'polite' || st.attrs['aria-atomic'] !== 'true')
|
|
464
|
+
err('[motion] status 必须带 role="status" aria-live="polite" aria-atomic="true"');
|
|
465
|
+
let p = st.parent, inside = false;
|
|
466
|
+
while (p) { if ('data-motion-controls' in (p.attrs ?? {})) { inside = true; break; } p = p.parent; }
|
|
467
|
+
if (inside) err('[motion] status 必须放在播放控件之外');
|
|
468
|
+
}
|
|
469
|
+
if (!scripts.length) err('[motion] 受控动画必须有脚本(否则按钮点了没反应)');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (scripts.length) {
|
|
473
|
+
if (!/prefers-reduced-motion\s*:\s*reduce/i.test(css)) err('[motion] 有脚本就必须有 prefers-reduced-motion: reduce 回退');
|
|
474
|
+
if (!/@media\s+print/i.test(css)) err('[motion] 有脚本就必须有 @media print 打印回退');
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return { errors, warns };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ---------- CLI ----------
|
|
482
|
+
|
|
483
|
+
const argv = process.argv.slice(2);
|
|
484
|
+
const files = [];
|
|
485
|
+
let offline = false;
|
|
486
|
+
let skillDir = DEFAULT_SKILL;
|
|
487
|
+
for (let i = 0; i < argv.length; i++) {
|
|
488
|
+
const a = argv[i];
|
|
489
|
+
if (a === '--offline') offline = true;
|
|
490
|
+
else if (a === '--skill') { skillDir = resolve(argv[++i] ?? DEFAULT_SKILL); }
|
|
491
|
+
else if (a.startsWith('--')) { console.error(`✘ 未知参数: ${a}`); process.exit(1); }
|
|
492
|
+
else files.push(a);
|
|
493
|
+
}
|
|
494
|
+
if (!files.length) {
|
|
495
|
+
console.error('用法: node tools/diagram-lint.mjs <file...> [--offline] [--skill <dir>]');
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let failed = false;
|
|
500
|
+
for (const file of files) {
|
|
501
|
+
let out;
|
|
502
|
+
try {
|
|
503
|
+
out = checkFile(file, { offline, skillDir });
|
|
504
|
+
} catch (e) {
|
|
505
|
+
failed = true;
|
|
506
|
+
console.error(`✘ ${file}\n - [io] 读不了文件: ${e.message}`);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
for (const w of out.warns) console.warn(`WARN ${file}: ${w}`);
|
|
510
|
+
if (out.errors.length) {
|
|
511
|
+
failed = true;
|
|
512
|
+
console.error(`✘ ${file} 未通过质检(${out.errors.length} 处):`);
|
|
513
|
+
for (const e of out.errors) console.error(` - ${e}`);
|
|
514
|
+
} else {
|
|
515
|
+
console.log(`OK ${file}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
process.exit(failed ? 1 : 0);
|