raga-cli 0.0.0-stage → 0.8.0
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/CHANGELOG.md +103 -0
- package/LICENSE +21 -0
- package/README.md +139 -2
- package/bundled-skills/AGENTS.md +5 -0
- package/bundled-skills/README.md +25 -0
- package/bundled-skills/agents/comment-sicko.json +9 -0
- package/bundled-skills/agents/comment-sicko.md +28 -0
- package/bundled-skills/agents/model-roles.json +41 -0
- package/bundled-skills/agents/poteto-agent.json +9 -0
- package/bundled-skills/agents/poteto-agent.md +4 -0
- package/bundled-skills/agents/worker.json +8 -0
- package/bundled-skills/agents/worker.md +1 -0
- package/bundled-skills/bundle.json +1448 -0
- package/bundled-skills/dependencies.json +40 -0
- package/bundled-skills/docs/guide/01-setup.md +3 -0
- package/bundled-skills/docs/guide/02-peak.md +135 -0
- package/bundled-skills/docs/guide/03-understand.md +79 -0
- package/bundled-skills/docs/guide/04-design.md +133 -0
- package/bundled-skills/docs/guide/05-build-and-clean.md +83 -0
- package/bundled-skills/docs/guide/06-verify-and-ship.md +166 -0
- package/bundled-skills/docs/guide/07-overnight.md +118 -0
- package/bundled-skills/docs/guide/08-principles.md +72 -0
- package/bundled-skills/docs/guide/09-make-it-yours.md +96 -0
- package/bundled-skills/docs/guide/10-recipes-and-pitfalls.md +180 -0
- package/bundled-skills/docs/guide/README.md +38 -0
- package/bundled-skills/docs/guide/images/design.jpg +0 -0
- package/bundled-skills/docs/guide/images/overnight.jpg +0 -0
- package/bundled-skills/docs/guide/images/recipes.jpg +0 -0
- package/bundled-skills/docs/guide/images/router.jpg +0 -0
- package/bundled-skills/docs/guide/images/understanding.jpg +0 -0
- package/bundled-skills/docs/guide/images/verification.jpg +0 -0
- package/bundled-skills/licenses/commander +22 -0
- package/bundled-skills/licenses/cursor-team-kit +21 -0
- package/bundled-skills/licenses/no-mistakes +21 -0
- package/bundled-skills/licenses/pstack +21 -0
- package/bundled-skills/skills/peak/SKILL.md +184 -0
- package/bundled-skills/skills/peak/playbooks/authoring-a-skill.md +12 -0
- package/bundled-skills/skills/peak/playbooks/autonomous-run.md +13 -0
- package/bundled-skills/skills/peak/playbooks/autopilot-full.md +25 -0
- package/bundled-skills/skills/peak/playbooks/autopilot-stack.md +12 -0
- package/bundled-skills/skills/peak/playbooks/babysit.md +18 -0
- package/bundled-skills/skills/peak/playbooks/bug-fix.md +15 -0
- package/bundled-skills/skills/peak/playbooks/eval.md +25 -0
- package/bundled-skills/skills/peak/playbooks/feature.md +21 -0
- package/bundled-skills/skills/peak/playbooks/hillclimb.md +21 -0
- package/bundled-skills/skills/peak/playbooks/investigation.md +14 -0
- package/bundled-skills/skills/peak/playbooks/multi-phase-plan.md +155 -0
- package/bundled-skills/skills/peak/playbooks/opening-a-pr.md +34 -0
- package/bundled-skills/skills/peak/playbooks/orchestrate.md +112 -0
- package/bundled-skills/skills/peak/playbooks/pause-safely.md +10 -0
- package/bundled-skills/skills/peak/playbooks/perf-issue.md +25 -0
- package/bundled-skills/skills/peak/playbooks/prototype.md +14 -0
- package/bundled-skills/skills/peak/playbooks/refactoring.md +16 -0
- package/bundled-skills/skills/peak/playbooks/runtime-forensics.md +11 -0
- package/bundled-skills/skills/peak/playbooks/session-pickup.md +11 -0
- package/bundled-skills/skills/peak/playbooks/shipping.md +12 -0
- package/bundled-skills/skills/peak/playbooks/trace-forensics.md +14 -0
- package/bundled-skills/skills/peak/playbooks/visual-parity.md +11 -0
- package/bundled-skills/skills/peak/playbooks/worktree-cleanup.md +6 -0
- package/bundled-skills/skills/peak/references/architect/SKILL.md +81 -0
- package/bundled-skills/skills/peak/references/architect/references/design-red-flags.md +57 -0
- package/bundled-skills/skills/peak/references/architect/references/rationale-template.md +35 -0
- package/bundled-skills/skills/peak/references/architect/references/runner-prompt.md +20 -0
- package/bundled-skills/skills/peak/references/arena/SKILL.md +50 -0
- package/bundled-skills/skills/peak/references/automate-me/SKILL.md +106 -0
- package/bundled-skills/skills/peak/references/benchmark-checklist/SKILL.md +41 -0
- package/bundled-skills/skills/peak/references/blast-radius/SKILL.md +52 -0
- package/bundled-skills/skills/peak/references/bro/SKILL.md +9 -0
- package/bundled-skills/skills/peak/references/control-cli/SKILL.md +111 -0
- package/bundled-skills/skills/peak/references/control-ui/SKILL.md +111 -0
- package/bundled-skills/skills/peak/references/correct/SKILL.md +34 -0
- package/bundled-skills/skills/peak/references/create-formal-verification-skill/SKILL.md +43 -0
- package/bundled-skills/skills/peak/references/create-skill/SKILL.md +14 -0
- package/bundled-skills/skills/peak/references/create-verification-skill/SKILL.md +46 -0
- package/bundled-skills/skills/peak/references/create-verification-skill/references/feature-map-example/README.md +47 -0
- package/bundled-skills/skills/peak/references/create-verification-skill/references/feature-map-example/create-note.md +39 -0
- package/bundled-skills/skills/peak/references/create-verification-skill/references/feature-map-example/search.md +45 -0
- package/bundled-skills/skills/peak/references/deslop/SKILL.md +24 -0
- package/bundled-skills/skills/peak/references/figure-it-out/SKILL.md +54 -0
- package/bundled-skills/skills/peak/references/how/SKILL.md +38 -0
- package/bundled-skills/skills/peak/references/how/references/explainer-prompt.md +55 -0
- package/bundled-skills/skills/peak/references/how/references/explorer-prompt.md +35 -0
- package/bundled-skills/skills/peak/references/in-your-own-words/SKILL.md +9 -0
- package/bundled-skills/skills/peak/references/interrogate/SKILL.md +76 -0
- package/bundled-skills/skills/peak/references/interrogate/references/code-quality-review.md +47 -0
- package/bundled-skills/skills/peak/references/interrogate/references/lead-judgment.md +39 -0
- package/bundled-skills/skills/peak/references/interrogate/references/reviewer-prompt.md +62 -0
- package/bundled-skills/skills/peak/references/interrogate/references/rubric.md +77 -0
- package/bundled-skills/skills/peak/references/ketchup/SKILL.md +45 -0
- package/bundled-skills/skills/peak/references/maintain-formal-verification-skill/SKILL.md +60 -0
- package/bundled-skills/skills/peak/references/maintain-verification-skill/SKILL.md +41 -0
- package/bundled-skills/skills/peak/references/no-comments/SKILL.md +26 -0
- package/bundled-skills/skills/peak/references/no-mistakes/SKILL.md +20 -0
- package/bundled-skills/skills/peak/references/no-mistakes/upstream/SKILL.md +394 -0
- package/bundled-skills/skills/peak/references/peak-setup/SKILL.md +18 -0
- package/bundled-skills/skills/peak/references/poteto-help/SKILL.md +16 -0
- package/bundled-skills/skills/peak/references/poteto-help/references/prompting.md +51 -0
- package/bundled-skills/skills/peak/references/poteto-help/references/recipes.md +51 -0
- package/bundled-skills/skills/peak/references/principle-attack-the-premise/SKILL.md +23 -0
- package/bundled-skills/skills/peak/references/principle-boundary-discipline/SKILL.md +36 -0
- package/bundled-skills/skills/peak/references/principle-build-the-lever/SKILL.md +25 -0
- package/bundled-skills/skills/peak/references/principle-encode-lessons-in-structure/SKILL.md +13 -0
- package/bundled-skills/skills/peak/references/principle-exhaust-the-design-space/SKILL.md +11 -0
- package/bundled-skills/skills/peak/references/principle-experience-first/SKILL.md +21 -0
- package/bundled-skills/skills/peak/references/principle-explain-the-number/SKILL.md +19 -0
- package/bundled-skills/skills/peak/references/principle-fix-root-causes/SKILL.md +23 -0
- package/bundled-skills/skills/peak/references/principle-foundational-thinking/SKILL.md +23 -0
- package/bundled-skills/skills/peak/references/principle-guard-the-context-window/SKILL.md +14 -0
- package/bundled-skills/skills/peak/references/principle-laziness-protocol/SKILL.md +12 -0
- package/bundled-skills/skills/peak/references/principle-make-operations-idempotent/SKILL.md +24 -0
- package/bundled-skills/skills/peak/references/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +16 -0
- package/bundled-skills/skills/peak/references/principle-minimize-reader-load/SKILL.md +20 -0
- package/bundled-skills/skills/peak/references/principle-model-the-domain/SKILL.md +26 -0
- package/bundled-skills/skills/peak/references/principle-never-block-on-the-human/SKILL.md +17 -0
- package/bundled-skills/skills/peak/references/principle-outcome-oriented-execution/SKILL.md +23 -0
- package/bundled-skills/skills/peak/references/principle-prove-it-works/SKILL.md +24 -0
- package/bundled-skills/skills/peak/references/principle-redesign-from-first-principles/SKILL.md +18 -0
- package/bundled-skills/skills/peak/references/principle-separate-before-serializing-shared-state/SKILL.md +18 -0
- package/bundled-skills/skills/peak/references/principle-sequence-verifiable-units/SKILL.md +19 -0
- package/bundled-skills/skills/peak/references/principle-subtract-before-you-add/SKILL.md +20 -0
- package/bundled-skills/skills/peak/references/principle-test-behavior-not-implementation/SKILL.md +22 -0
- package/bundled-skills/skills/peak/references/principle-type-system-discipline/SKILL.md +15 -0
- package/bundled-skills/skills/peak/references/raga-runtime/SKILL.md +37 -0
- package/bundled-skills/skills/peak/references/raga-runtime/managed-tools.mjs +34 -0
- package/bundled-skills/skills/peak/references/raga-runtime/raga_runtime.py +44 -0
- package/bundled-skills/skills/peak/references/recall/SKILL.md +37 -0
- package/bundled-skills/skills/peak/references/reflect/SKILL.md +63 -0
- package/bundled-skills/skills/peak/references/reflect/references/divergent-reviewer.md +38 -0
- package/bundled-skills/skills/peak/references/reflect/references/judgment-reviewer.md +37 -0
- package/bundled-skills/skills/peak/references/reflect/references/synthesizer.md +56 -0
- package/bundled-skills/skills/peak/references/reflect/references/tooling-reviewer.md +48 -0
- package/bundled-skills/skills/peak/references/reviewer-triage.md +142 -0
- package/bundled-skills/skills/peak/references/sharpen/SKILL.md +18 -0
- package/bundled-skills/skills/peak/references/show-me-your-work/SKILL.md +84 -0
- package/bundled-skills/skills/peak/references/show-me-your-work/references/decision-log-template.tsv +1 -0
- package/bundled-skills/skills/peak/references/show-me-your-work/scripts/log.sh +42 -0
- package/bundled-skills/skills/peak/references/statechart/SKILL.md +38 -0
- package/bundled-skills/skills/peak/references/statechart/scripts/chart-to-tla.mjs +236 -0
- package/bundled-skills/skills/peak/references/statechart/scripts/compare-charts.mjs +104 -0
- package/bundled-skills/skills/peak/references/statechart/scripts/lib.mjs +725 -0
- package/bundled-skills/skills/peak/references/statechart/scripts/model/OutputFolder.cfg +6 -0
- package/bundled-skills/skills/peak/references/statechart/scripts/model/OutputFolder.map.mjs +106 -0
- package/bundled-skills/skills/peak/references/statechart/scripts/model/OutputFolder.tla +176 -0
- package/bundled-skills/skills/peak/references/statechart/scripts/statechart.mjs +749 -0
- package/bundled-skills/skills/peak/references/statechart/scripts/test/statechart.test.mjs +2790 -0
- package/bundled-skills/skills/peak/references/statechart/scripts/test/sync.mmd +19 -0
- package/bundled-skills/skills/peak/references/statechart/scripts/test/upload.mmd +16 -0
- package/bundled-skills/skills/peak/references/statechart/scripts/tlc-to-chart.mjs +1347 -0
- package/bundled-skills/skills/peak/references/swarm/SKILL.md +43 -0
- package/bundled-skills/skills/peak/references/tdd/SKILL.md +44 -0
- package/bundled-skills/skills/peak/references/teach/SKILL.md +23 -0
- package/bundled-skills/skills/peak/references/technical-writing/SKILL.md +108 -0
- package/bundled-skills/skills/peak/references/typescript-best-practices/SKILL.md +33 -0
- package/bundled-skills/skills/peak/references/typescript-best-practices/references/patterns.md +328 -0
- package/bundled-skills/skills/peak/references/unslop/SKILL.md +64 -0
- package/bundled-skills/skills/peak/references/verify-this/SKILL.md +76 -0
- package/bundled-skills/skills/peak/references/why/SKILL.md +100 -0
- package/bundled-skills/skills/peak/references/why/references/epistemics.md +71 -0
- package/bundled-skills/skills/peak/references/why/references/investigator-prompt.md +84 -0
- package/bundled-skills/skills/peak/references/why/references/source-playbook.md +17 -0
- package/bundled-skills/skills/peak/references/why/references/sources/code-archaeology.md +51 -0
- package/bundled-skills/skills/peak/references/why/references/sources/databricks.md +70 -0
- package/bundled-skills/skills/peak/references/why/references/sources/datadog.md +97 -0
- package/bundled-skills/skills/peak/references/why/references/sources/incident-postmortem.md +5 -0
- package/bundled-skills/skills/peak/references/why/references/sources/linear.md +48 -0
- package/bundled-skills/skills/peak/references/why/references/sources/notion.md +55 -0
- package/bundled-skills/skills/peak/references/why/references/sources/sentry.md +66 -0
- package/bundled-skills/skills/peak/references/why/references/sources/slack.md +17 -0
- package/bundled-skills/skills/peak/references/why/references/synthesizer-prompt.md +129 -0
- package/bundled-skills/skills/peak/scripts/check-plan.mjs +205 -0
- package/bundled-skills/skills/peak/scripts/check-plan.test.ts +96 -0
- package/bundled-skills/skills/peak/scripts/orch/orch.test.ts +761 -0
- package/bundled-skills/skills/peak/scripts/orch/orch.ts +588 -0
- package/bundled-skills/skills/peak/scripts/orch/store.ts +1676 -0
- package/bundled-skills/skills/peak/scripts/vendor/commander/LICENSE +22 -0
- package/bundled-skills/skills/peak/scripts/vendor/commander/esm.mjs +16 -0
- package/bundled-skills/skills/peak/scripts/vendor/commander/index.js +24 -0
- package/bundled-skills/skills/peak/scripts/vendor/commander/lib/argument.js +149 -0
- package/bundled-skills/skills/peak/scripts/vendor/commander/lib/command.js +2778 -0
- package/bundled-skills/skills/peak/scripts/vendor/commander/lib/error.js +39 -0
- package/bundled-skills/skills/peak/scripts/vendor/commander/lib/help.js +747 -0
- package/bundled-skills/skills/peak/scripts/vendor/commander/lib/option.js +379 -0
- package/bundled-skills/skills/peak/scripts/vendor/commander/lib/suggestSimilar.js +101 -0
- package/bundled-skills/skills/peak/scripts/vendor/commander/package.json +1 -0
- package/bundled-skills/skills/peak/scripts/vendor/commander/typings/esm.d.mts +3 -0
- package/bundled-skills/skills/peak/scripts/vendor/commander/typings/index.d.ts +1113 -0
- package/bundled-skills/skills/peak/scripts/watch-pr/cli.test.ts +224 -0
- package/bundled-skills/skills/peak/scripts/watch-pr/cli.ts +223 -0
- package/bundled-skills/skills/peak/scripts/watch-pr/fakes.test-helper.ts +129 -0
- package/bundled-skills/skills/peak/scripts/watch-pr/github.test.ts +436 -0
- package/bundled-skills/skills/peak/scripts/watch-pr/github.ts +718 -0
- package/bundled-skills/skills/peak/scripts/watch-pr/policy.test.ts +420 -0
- package/bundled-skills/skills/peak/scripts/watch-pr/policy.ts +836 -0
- package/bundled-skills/skills/peak/scripts/watch-pr/render.ts +169 -0
- package/bundled-skills/skills/peak/scripts/watch-pr/tsconfig.json +13 -0
- package/bundled-skills/skills/peak/scripts/watch-pr/types.compile.ts +93 -0
- package/bundled-skills/skills/peak/scripts/watch-pr/types.ts +413 -0
- package/bundled-skills/skills/peak/scripts/watch-pr/watch-pr +4 -0
- package/bundled-skills/skills/peak/scripts/worktree-audit.sh +4 -0
- package/dist/bundle/chunks/anthropic-messages-BMDYDGHW.js +6 -0
- package/dist/bundle/chunks/anthropic.js +75 -0
- package/dist/bundle/chunks/azure-openai-responses-Q7EZTEHK.js +2 -0
- package/dist/bundle/chunks/bedrock-converse-stream.js +86 -0
- package/dist/bundle/chunks/chunk-3EGDWOKT.js +2 -0
- package/dist/bundle/chunks/chunk-3V45WARH.js +7 -0
- package/dist/bundle/chunks/chunk-5AGIVGSV.js +6 -0
- package/dist/bundle/chunks/chunk-6QGV3IWD.js +6 -0
- package/dist/bundle/chunks/chunk-7CBJINYJ.js +20 -0
- package/dist/bundle/chunks/chunk-7DDKXS2Q.js +2 -0
- package/dist/bundle/chunks/chunk-7ZC4UXSL.js +2 -0
- package/dist/bundle/chunks/chunk-AI2NMHE7.js +1567 -0
- package/dist/bundle/chunks/chunk-C4IJNE7U.js +2 -0
- package/dist/bundle/chunks/chunk-CF5GWZHW.js +31 -0
- package/dist/bundle/chunks/chunk-G54UBZTF.js +44 -0
- package/dist/bundle/chunks/chunk-H76KZMOI.js +2 -0
- package/dist/bundle/chunks/chunk-IOPE2TJ4.js +131 -0
- package/dist/bundle/chunks/chunk-IXDQLPKS.js +11 -0
- package/dist/bundle/chunks/chunk-J22MF7YW.js +64 -0
- package/dist/bundle/chunks/chunk-JLV25LHE.js +2 -0
- package/dist/bundle/chunks/chunk-JXWJOIH4.js +2 -0
- package/dist/bundle/chunks/chunk-KNY3PXQI.js +2 -0
- package/dist/bundle/chunks/chunk-L2ZPNLYD.js +2 -0
- package/dist/bundle/chunks/chunk-LPXNMA2W.js +2 -0
- package/dist/bundle/chunks/chunk-LVVZS2NO.js +12 -0
- package/dist/bundle/chunks/chunk-LX24DVQ5.js +2 -0
- package/dist/bundle/chunks/chunk-P2QCWBFM.js +9 -0
- package/dist/bundle/chunks/chunk-P7FPUEYA.js +2 -0
- package/dist/bundle/chunks/chunk-PGXDEOVV.js +2 -0
- package/dist/bundle/chunks/chunk-PJACZCIR.js +2 -0
- package/dist/bundle/chunks/chunk-PMRZO46Y.js +2 -0
- package/dist/bundle/chunks/chunk-PRRNXB7S.js +2 -0
- package/dist/bundle/chunks/chunk-Q4U25BSY.js +2 -0
- package/dist/bundle/chunks/chunk-RDZVAGPR.js +2 -0
- package/dist/bundle/chunks/chunk-RFPFZTK2.js +7 -0
- package/dist/bundle/chunks/chunk-RP2WKNT7.js +4 -0
- package/dist/bundle/chunks/chunk-TEPMHNKQ.js +2 -0
- package/dist/bundle/chunks/chunk-TMTOQ7H7.js +2 -0
- package/dist/bundle/chunks/chunk-UDT647VB.js +82 -0
- package/dist/bundle/chunks/chunk-UKVNB6C4.js +2 -0
- package/dist/bundle/chunks/chunk-USJZMAIR.js +2 -0
- package/dist/bundle/chunks/chunk-UTB4DTYQ.js +2 -0
- package/dist/bundle/chunks/chunk-WJGLPWUE.js +16 -0
- package/dist/bundle/chunks/chunk-YMZUNB25.js +2 -0
- package/dist/bundle/chunks/cli-4P6FVVLT.js +51 -0
- package/dist/bundle/chunks/cloudflare-workers-ai-system-one-S6CT2FXS.js +2 -0
- package/dist/bundle/chunks/codemode-worker.js +478 -0
- package/dist/bundle/chunks/easter-egg-3d-65VIC4G2.js +2 -0
- package/dist/bundle/chunks/execute-3H5WKMA3.js +505 -0
- package/dist/bundle/chunks/github-copilot.js +2 -0
- package/dist/bundle/chunks/google-generative-ai-35NZKG6S.js +2 -0
- package/dist/bundle/chunks/google-vertex-P33AHYYX.js +2 -0
- package/dist/bundle/chunks/https-proxy-agent-LVOO5TPL.js +13 -0
- package/dist/bundle/chunks/image-resize-worker.js +2 -0
- package/dist/bundle/chunks/jiti-loader-GL4HJFAD.js +21 -0
- package/dist/bundle/chunks/jiti-static-loader-2RCDI6B4.js +2 -0
- package/dist/bundle/chunks/kimi-coding.js +2 -0
- package/dist/bundle/chunks/lib-4AVRLBHM.js +9 -0
- package/dist/bundle/chunks/llama-cpp-classify-U4EH6TAM.js +22 -0
- package/dist/bundle/chunks/meta.js +2 -0
- package/dist/bundle/chunks/mistral-conversations-J62DPS2P.js +5 -0
- package/dist/bundle/chunks/multipart-parser-FMGQ7342.js +3 -0
- package/dist/bundle/chunks/node-AUOGOBCN.js +2 -0
- package/dist/bundle/chunks/node-KCMANYC2.js +16 -0
- package/dist/bundle/chunks/openai-chatgpt.js +75 -0
- package/dist/bundle/chunks/openai-codex-responses-4HXATL2M.js +10 -0
- package/dist/bundle/chunks/openai-codex.js +75 -0
- package/dist/bundle/chunks/openai-completions-75YYY35N.js +7 -0
- package/dist/bundle/chunks/openai-decisions-HEBEVGA2.js +5 -0
- package/dist/bundle/chunks/openai-responses-UWNAKTBV.js +4 -0
- package/dist/bundle/chunks/openrouter-images-HVOPXKHS.js +2 -0
- package/dist/bundle/chunks/openrouter.js +75 -0
- package/dist/bundle/chunks/pi-messages-DG7CALOV.js +8 -0
- package/dist/bundle/chunks/radius.js +75 -0
- package/dist/bundle/chunks/runtime-435R6IKY.js +2 -0
- package/dist/bundle/chunks/src-QKGKDFSU.js +4 -0
- package/dist/bundle/chunks/typesafe-system-one-24HEGHNN.js +2 -0
- package/dist/bundle/chunks/virtual-modules-U3JBUDP7.js +2 -0
- package/dist/bundle/chunks/xai.js +2 -0
- package/dist/bundle/cli-runtime.js +3 -0
- package/dist/bundle/cli.js +5 -0
- package/dist/bundle/index.js +2 -0
- package/dist/bundle/raga-runtime.js +3 -0
- package/dist/bundle/raga.js +5 -0
- package/dist/bundle/rpc-entry.js +3 -0
- package/dist/core/export-html/template.css +1085 -0
- package/dist/core/export-html/template.html +56 -0
- package/dist/core/export-html/template.js +2044 -0
- package/dist/core/export-html/vendor/highlight.min.js +1213 -0
- package/dist/core/export-html/vendor/marked-cjk-friendly.min.js +6 -0
- package/dist/core/export-html/vendor/marked.min.js +79 -0
- package/dist/modes/interactive/assets/clankolas.png +0 -0
- package/dist/modes/interactive/theme/dark.json +78 -0
- package/dist/modes/interactive/theme/light.json +78 -0
- package/docs/cli-integration.md +106 -0
- package/docs/cli.md +337 -0
- package/docs/codemode.md +230 -0
- package/docs/compaction.md +463 -0
- package/docs/configuration.md +62 -0
- package/docs/containerization.md +183 -0
- package/docs/custom-provider.md +227 -0
- package/docs/docs.json +220 -0
- package/docs/environment-variables.md +101 -0
- package/docs/extensions.md +400 -0
- package/docs/how-pi-works.md +49 -0
- package/docs/images/doom-extension.png +0 -0
- package/docs/images/exy.png +0 -0
- package/docs/images/interactive-mode.png +0 -0
- package/docs/images/tree-view.png +0 -0
- package/docs/index.md +39 -0
- package/docs/json.md +226 -0
- package/docs/keybindings.md +195 -0
- package/docs/llama-cpp.md +121 -0
- package/docs/mcp.md +272 -0
- package/docs/message-types.md +279 -0
- package/docs/models-configuration.md +196 -0
- package/docs/models.md +225 -0
- package/docs/nvidia-rtx-llama-cpp.md +96 -0
- package/docs/packages.md +129 -0
- package/docs/prompt-templates.md +59 -0
- package/docs/providers.md +247 -0
- package/docs/quickstart.md +136 -0
- package/docs/rpc-commands.md +860 -0
- package/docs/rpc-extension-ui.md +200 -0
- package/docs/rpc.md +193 -0
- package/docs/sdk.md +148 -0
- package/docs/security.md +99 -0
- package/docs/session-format.md +293 -0
- package/docs/sessions.md +66 -0
- package/docs/settings.md +184 -0
- package/docs/shell-aliases.md +93 -0
- package/docs/skills.md +93 -0
- package/docs/slash-commands.md +61 -0
- package/docs/terminal-setup.md +233 -0
- package/docs/termux.md +117 -0
- package/docs/themes.md +138 -0
- package/docs/tmux.md +55 -0
- package/docs/tui.md +195 -0
- package/docs/usage.md +94 -0
- package/docs/virtual-models.md +114 -0
- package/docs/windows.md +81 -0
- package/examples/README.md +36 -0
- package/examples/extensions/README.md +214 -0
- package/examples/extensions/auto-commit-on-exit.ts +49 -0
- package/examples/extensions/bash-spawn-hook.ts +30 -0
- package/examples/extensions/bookmark.ts +50 -0
- package/examples/extensions/border-status-editor.ts +150 -0
- package/examples/extensions/built-in-tool-renderer.ts +225 -0
- package/examples/extensions/claude-rules.ts +86 -0
- package/examples/extensions/commands.ts +72 -0
- package/examples/extensions/confirm-destructive.ts +59 -0
- package/examples/extensions/custom-compaction.ts +117 -0
- package/examples/extensions/custom-footer.ts +64 -0
- package/examples/extensions/custom-header.ts +73 -0
- package/examples/extensions/custom-provider-anthropic/index.ts +617 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
- package/examples/extensions/custom-provider-anthropic/package.json +19 -0
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +405 -0
- package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -0
- package/examples/extensions/debug-provider.ts +90 -0
- package/examples/extensions/dirty-repo-guard.ts +56 -0
- package/examples/extensions/doom-overlay/README.md +46 -0
- package/examples/extensions/doom-overlay/doom/build/doom.js +21 -0
- package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
- package/examples/extensions/doom-overlay/doom/build.sh +152 -0
- package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
- package/examples/extensions/doom-overlay/doom-component.ts +132 -0
- package/examples/extensions/doom-overlay/doom-engine.ts +173 -0
- package/examples/extensions/doom-overlay/doom-keys.ts +104 -0
- package/examples/extensions/doom-overlay/index.ts +74 -0
- package/examples/extensions/doom-overlay/wad-finder.ts +55 -0
- package/examples/extensions/dynamic-resources/SKILL.md +8 -0
- package/examples/extensions/dynamic-resources/dynamic.json +81 -0
- package/examples/extensions/dynamic-resources/dynamic.md +5 -0
- package/examples/extensions/dynamic-resources/index.ts +15 -0
- package/examples/extensions/dynamic-tools.ts +74 -0
- package/examples/extensions/entry-renderer.ts +41 -0
- package/examples/extensions/event-bus.ts +43 -0
- package/examples/extensions/file-trigger.ts +41 -0
- package/examples/extensions/git-checkpoint.ts +53 -0
- package/examples/extensions/git-merge-and-resolve.ts +115 -0
- package/examples/extensions/github-issue-autocomplete.ts +185 -0
- package/examples/extensions/gondolin/index.ts +531 -0
- package/examples/extensions/gondolin/package-lock.json +175 -0
- package/examples/extensions/gondolin/package.json +19 -0
- package/examples/extensions/handoff.ts +190 -0
- package/examples/extensions/hello.ts +26 -0
- package/examples/extensions/hidden-thinking-label.ts +53 -0
- package/examples/extensions/inline-bash.ts +94 -0
- package/examples/extensions/input-transform-streaming.ts +39 -0
- package/examples/extensions/input-transform.ts +43 -0
- package/examples/extensions/interactive-shell.ts +196 -0
- package/examples/extensions/jev-router.ts +113 -0
- package/examples/extensions/mac-system-theme.ts +47 -0
- package/examples/extensions/message-renderer.ts +59 -0
- package/examples/extensions/minimal-mode.ts +353 -0
- package/examples/extensions/modal-editor.ts +85 -0
- package/examples/extensions/model-status.ts +31 -0
- package/examples/extensions/notify.ts +57 -0
- package/examples/extensions/overlay-qa-tests.ts +1450 -0
- package/examples/extensions/overlay-test.ts +153 -0
- package/examples/extensions/permission-gate.ts +34 -0
- package/examples/extensions/pirate.ts +47 -0
- package/examples/extensions/plan-mode/README.md +66 -0
- package/examples/extensions/plan-mode/index.ts +394 -0
- package/examples/extensions/plan-mode/utils.ts +168 -0
- package/examples/extensions/preset.ts +436 -0
- package/examples/extensions/project-trust.ts +64 -0
- package/examples/extensions/prompt-customizer.ts +49 -0
- package/examples/extensions/protected-paths.ts +30 -0
- package/examples/extensions/provider-payload.ts +18 -0
- package/examples/extensions/qna.ts +118 -0
- package/examples/extensions/question.ts +286 -0
- package/examples/extensions/questionnaire.ts +448 -0
- package/examples/extensions/rainbow-editor.ts +88 -0
- package/examples/extensions/reload-runtime.ts +37 -0
- package/examples/extensions/rpc-demo.ts +118 -0
- package/examples/extensions/sandbox/index.ts +326 -0
- package/examples/extensions/sandbox/package-lock.json +92 -0
- package/examples/extensions/sandbox/package.json +19 -0
- package/examples/extensions/send-user-message.ts +97 -0
- package/examples/extensions/session-name.ts +27 -0
- package/examples/extensions/shutdown-command.ts +63 -0
- package/examples/extensions/snake.ts +343 -0
- package/examples/extensions/space-invaders.ts +560 -0
- package/examples/extensions/ssh.ts +220 -0
- package/examples/extensions/status-line.ts +32 -0
- package/examples/extensions/structured-output.ts +65 -0
- package/examples/extensions/subagent/README.md +177 -0
- package/examples/extensions/subagent/agents/planner.md +37 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/subagent/agents/scout.md +50 -0
- package/examples/extensions/subagent/agents/worker.md +24 -0
- package/examples/extensions/subagent/agents.ts +157 -0
- package/examples/extensions/subagent/index.ts +1038 -0
- package/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/examples/extensions/subagent/prompts/implement.md +10 -0
- package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/examples/extensions/summarize.ts +199 -0
- package/examples/extensions/system-prompt-header.ts +17 -0
- package/examples/extensions/tic-tac-toe.ts +1008 -0
- package/examples/extensions/timed-confirm.ts +70 -0
- package/examples/extensions/titlebar-spinner.ts +58 -0
- package/examples/extensions/todo.ts +297 -0
- package/examples/extensions/tool-override.ts +144 -0
- package/examples/extensions/tools.ts +146 -0
- package/examples/extensions/trigger-compact.ts +50 -0
- package/examples/extensions/truncated-tool.ts +195 -0
- package/examples/extensions/widget-placement.ts +9 -0
- package/examples/extensions/with-deps/index.ts +32 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +22 -0
- package/examples/extensions/working-indicator.ts +123 -0
- package/examples/extensions/working-message-test.ts +25 -0
- package/examples/rpc-client.ts +35 -0
- package/examples/rpc-extension-ui.ts +661 -0
- package/examples/sdk/01-minimal.ts +26 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +75 -0
- package/examples/sdk/04-skills.ts +55 -0
- package/examples/sdk/05-tools.ts +48 -0
- package/examples/sdk/06-extensions.ts +99 -0
- package/examples/sdk/07-context-files.ts +47 -0
- package/examples/sdk/08-prompt-templates.ts +51 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +34 -0
- package/examples/sdk/10-settings.ts +53 -0
- package/examples/sdk/11-sessions.ts +52 -0
- package/examples/sdk/12-full-control.ts +74 -0
- package/examples/sdk/13-session-runtime.ts +67 -0
- package/examples/sdk/14-codemode-mcp.ts +61 -0
- package/examples/sdk/README.md +141 -0
- package/package.json +21 -5
- package/python/raga_rlm/__init__.py +336 -0
- package/python/raga_rlm/repl.py +920 -0
- package/schemas/keybindings.schema.json +393 -0
- package/schemas/models.schema.json +1211 -0
- package/schemas/settings.schema.json +815 -0
- package/schemas/theme.schema.json +365 -0
|
@@ -0,0 +1,1347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
FULL_LABELS,
|
|
8
|
+
GROUP_PALETTE,
|
|
9
|
+
LEGEND_ID,
|
|
10
|
+
PLAIN_FILL,
|
|
11
|
+
checkTlcOutput,
|
|
12
|
+
compareCharts,
|
|
13
|
+
groupClassDef,
|
|
14
|
+
groupColorProblem,
|
|
15
|
+
isRecord,
|
|
16
|
+
joinLabels,
|
|
17
|
+
parseDot,
|
|
18
|
+
parseMermaid,
|
|
19
|
+
parseTraces,
|
|
20
|
+
productOf,
|
|
21
|
+
quote,
|
|
22
|
+
ruleSentences,
|
|
23
|
+
stateKey,
|
|
24
|
+
wrap,
|
|
25
|
+
STATUS_CLASSES,
|
|
26
|
+
} from "./lib.mjs";
|
|
27
|
+
|
|
28
|
+
const USAGE = "Usage: node tlc-to-chart.mjs --dot FILE --map FILE [options]";
|
|
29
|
+
const HELP = `${USAGE}
|
|
30
|
+
|
|
31
|
+
Collapses a TLC state graph onto the reader's states and writes a Mermaid stateDiagram-v2.
|
|
32
|
+
statechart.mjs runs it once for the overview and once for each bug chart.
|
|
33
|
+
|
|
34
|
+
--dot FILE The state graph from TLC -dump dot,actionlabels FILE, for the current code.
|
|
35
|
+
Required.
|
|
36
|
+
--map FILE The mapping module. Required. It exports state(vars, facts), and
|
|
37
|
+
optionally detail(vars, facts), step(action, before, after),
|
|
38
|
+
guard(action, before, after), timer(action, before, after),
|
|
39
|
+
cause(vars, facts, invariant), group(vars, facts), groupColors,
|
|
40
|
+
regionNames, readerActions, and legend. guard is when the step
|
|
41
|
+
can happen, in plain words. An edge reads "step [words]" when every
|
|
42
|
+
model step behind it has words. timer is how many seconds the code waits
|
|
43
|
+
before a timer step, and the step reads "after N s, step".
|
|
44
|
+
cause is the hidden reason a bad state is bad: counterexamples with the
|
|
45
|
+
same invariant and cause are one bug. When cause returns nothing, the
|
|
46
|
+
end state stands in for it. readerActions are the actions the
|
|
47
|
+
reader takes. group is the group of a state, below. legend, such as
|
|
48
|
+
{ works: "Works on main" }, gives the legend's words for the works,
|
|
49
|
+
wrong, and fixed statuses.
|
|
50
|
+
facts.enabled is the set of actions the state enables. facts.settled is
|
|
51
|
+
true when only reader actions are enabled, so nothing else happens until
|
|
52
|
+
the reader acts.
|
|
53
|
+
--traces FILE TLC output that holds counterexamples. Repeat it. TLC -continue gives one
|
|
54
|
+
per bad state. It refuses a file from a TLC run that stopped early or hit
|
|
55
|
+
an error other than a broken rule.
|
|
56
|
+
--fixed-dot FILE The state graph of the model with the open fix. Each bug's steps replay
|
|
57
|
+
on it.
|
|
58
|
+
--fixed-traces FILE TLC output for the same invariants on the fixed model. Repeat it. It marks
|
|
59
|
+
states fixed or still wrong, and adds the bugs that only the fixed model
|
|
60
|
+
has after the current model's bugs.
|
|
61
|
+
--against-dot FILE The state graph of the spec that the current code must follow.
|
|
62
|
+
Each bug's steps replay on it. Do not use it with --fixed-dot.
|
|
63
|
+
--against-traces FILE TLC output for the spec. Repeat it. Its bugs are drawn wrong, and the
|
|
64
|
+
bugs that only the spec has number after the code's bugs.
|
|
65
|
+
--label TEXT The name of the second model in the chart and the stories. Default:
|
|
66
|
+
"the fix" with --fixed-dot, "the spec" with --against-dot.
|
|
67
|
+
--spec FILE A TLA+ file whose \\* comment lines above a rule are the rule's sentence in
|
|
68
|
+
the stories. Repeat it. The first file that has a sentence wins.
|
|
69
|
+
--view NAME customer (state labels, the default), developer (detail labels), or
|
|
70
|
+
layered (detail inside state).
|
|
71
|
+
--cover Keep only the shortest path to each settled reader state, plus the
|
|
72
|
+
counterexample paths.
|
|
73
|
+
--highlight N Draw bug N as a numbered path. Default 1. 0 draws none.
|
|
74
|
+
--path-only Draw only the highlighted path.
|
|
75
|
+
--full-labels Write each label on an edge in full, joined by " or ", and mark the chart
|
|
76
|
+
"${FULL_LABELS}". Default: write the leading words that the labels
|
|
77
|
+
share once, as in "you tap Pay or Back".
|
|
78
|
+
--flat Draw every state on its own, with no boxes.
|
|
79
|
+
--regions When every state reads "context: progress", draw two parallel regions,
|
|
80
|
+
context and progress, when they are smaller and proved. They draw the
|
|
81
|
+
steps of the shortest way to each region state, of each bug path, and
|
|
82
|
+
the nearest step with each label.
|
|
83
|
+
Otherwise draw the chart the other flags ask for, and print why.
|
|
84
|
+
Customer view, --highlight 0, no --flat or --path-only.
|
|
85
|
+
--proof FILE With --regions, write the flat chart that the proof compares with.
|
|
86
|
+
--regions-all FILE With --regions, also write the regions with every step.
|
|
87
|
+
--title TEXT The chart title.
|
|
88
|
+
--out FILE The Mermaid chart. Default: stdout.
|
|
89
|
+
--story FILE The reader stories, one per bug.
|
|
90
|
+
--summary FILE The counts for the chart and the bugs, as JSON.
|
|
91
|
+
-h, --help Print this help, and exit 0.
|
|
92
|
+
|
|
93
|
+
Each bug replays on the second model by its screens and step words, which are what the reader
|
|
94
|
+
sees. The replay takes the second model's steps that keep the screen and that the reader does not
|
|
95
|
+
take. A bug step that the reader does not see must keep the screen on the second model too, unless
|
|
96
|
+
the second model cannot take it. The replay stops at the first step that the second model shows
|
|
97
|
+
the reader differently. There the chart draws "with LABEL, STEP" (or "in LABEL, STEP" for
|
|
98
|
+
--against-dot) to where the second model goes, or a loop "... STEP cannot happen" when it cannot
|
|
99
|
+
take a step that the reader sees.
|
|
100
|
+
|
|
101
|
+
The second model may have other variables and actions than the current model. Variables and
|
|
102
|
+
record fields that one start state has and the other does not are left out when the two models
|
|
103
|
+
are compared. The second model's states take the current model's start values for the variables
|
|
104
|
+
it does not have, so one mapping draws both. A step of an action that the second model does not
|
|
105
|
+
have is skipped when it changes no shared variable, and matches any step to the same shared state
|
|
106
|
+
when it does.
|
|
107
|
+
|
|
108
|
+
In the customer view, states whose labels read "context: progress" with the same context sit in
|
|
109
|
+
one box named context, and each shows only its progress. A context with one state stays a plain
|
|
110
|
+
state. An arrow from a box's border stands for the same step from every state in the box: it is
|
|
111
|
+
drawn only when every state in the box takes that step, with the same labels, to the same state.
|
|
112
|
+
The layered view puts each detail state in a box named by its state label.
|
|
113
|
+
|
|
114
|
+
With --regions, each "context: progress" label is one state in the context region and one in the
|
|
115
|
+
progress region. A step that happens beside only some states of the other region gets a guard:
|
|
116
|
+
"[when A or B]" or "[unless A or B]", whichever is shorter, and a "%% when [not] ids" line under it.
|
|
117
|
+
The mapping's guard words, when the step has them, replace "when A or B" in the label.
|
|
118
|
+
The proof combines the regions with every step as compare-charts.mjs does, and needs exactly the
|
|
119
|
+
flat chart's states, start states, and labeled steps. Each state's color must be the worse color
|
|
120
|
+
of its two region states. Then the drawn arrows, combined, must make only steps of the flat chart:
|
|
121
|
+
a step that they make wrongly gets back every arrow that the full regions take there.
|
|
122
|
+
compare-charts.mjs on the --regions-all and --proof files repeats the first proof.
|
|
123
|
+
mapping.regionNames = [context, progress] names the two regions.
|
|
124
|
+
|
|
125
|
+
Outside regions, two states have at most one arrow. Its lines are the numbered steps, then the
|
|
126
|
+
current model's steps, then the second model's steps, with a blank line between each group.
|
|
127
|
+
|
|
128
|
+
Without group(), the legend shows only the statuses the chart uses. A chart with a second model
|
|
129
|
+
has the line "%% second model (fix|against): LABEL" right after stateDiagram-v2. Each chart
|
|
130
|
+
starts with front matter that keeps Mermaid from shrinking it to the page or rewrapping its labels.
|
|
131
|
+
|
|
132
|
+
Groups: group(vars, facts) returns the name of the group that a model state is in, such as
|
|
133
|
+
"Toast shown", or nothing. A chart state is in a group when all its model states are in it. A
|
|
134
|
+
region state is in a group when every state that it is part of is in it. Each group fills its
|
|
135
|
+
states with one color, and the legend lists only the groups that the chart draws: first the
|
|
136
|
+
groups in groupColors, in that order, then the others from A to Z. groupColors, such as
|
|
137
|
+
{ "Toast shown": "#d6eaf8" }, gives a group a light "#rgb" or "#rrggbb" color. A group with no
|
|
138
|
+
color, or null, takes the next free palette color: light blue, orange, purple, yellow, teal, tan,
|
|
139
|
+
then pink. A state in no group, or in more than one, is gray. summary.json counts the drawn
|
|
140
|
+
states of each group in chartGroups, and lists the chart states with more than one in mixedGroups.
|
|
141
|
+
TLC decides the status, and a group cannot change it. On top of the group color, a state that
|
|
142
|
+
breaks a rule has a thick red border, and a fixed state or a state only in the second model a
|
|
143
|
+
thick green border. The legend then shows that border. A thick blue border marks the bug path on
|
|
144
|
+
states that work. On other states of the path, the numbered arrows show it. A group cannot have
|
|
145
|
+
the name of a status, such as "Breaks a rule".
|
|
146
|
+
|
|
147
|
+
Writes only the files that --out, --story, and --summary name.
|
|
148
|
+
|
|
149
|
+
Exit codes
|
|
150
|
+
0 It wrote the chart.
|
|
151
|
+
1 It cannot use an input: a mapping without state(), TLC output that it refuses, or a
|
|
152
|
+
--highlight with no such bug. The error says why.
|
|
153
|
+
2 A bad argument.`;
|
|
154
|
+
const more = "Run node tlc-to-chart.mjs --help for the flags.";
|
|
155
|
+
|
|
156
|
+
const args = {
|
|
157
|
+
traces: [],
|
|
158
|
+
fixedTraces: [],
|
|
159
|
+
againstTraces: [],
|
|
160
|
+
specs: [],
|
|
161
|
+
view: "customer",
|
|
162
|
+
highlight: 1,
|
|
163
|
+
};
|
|
164
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
165
|
+
const a = process.argv[i];
|
|
166
|
+
const next = () => process.argv[++i];
|
|
167
|
+
if (a === "-h" || a === "--help") {
|
|
168
|
+
console.log(HELP);
|
|
169
|
+
process.exit(0);
|
|
170
|
+
} else if (a === "--dot") args.dot = next();
|
|
171
|
+
else if (a === "--map") args.map = next();
|
|
172
|
+
else if (a === "--traces") args.traces.push(next());
|
|
173
|
+
else if (a === "--fixed-dot") args.fixedDot = next();
|
|
174
|
+
else if (a === "--fixed-traces") args.fixedTraces.push(next());
|
|
175
|
+
else if (a === "--against-dot") args.againstDot = next();
|
|
176
|
+
else if (a === "--against-traces") args.againstTraces.push(next());
|
|
177
|
+
else if (a === "--label") args.label = next();
|
|
178
|
+
else if (a === "--spec") args.specs.push(next());
|
|
179
|
+
else if (a === "--view") args.view = next();
|
|
180
|
+
else if (a === "--highlight") args.highlight = Number(next());
|
|
181
|
+
else if (a === "--path-only") args.pathOnly = true;
|
|
182
|
+
else if (a === "--full-labels") args.fullLabels = true;
|
|
183
|
+
else if (a === "--flat") args.flat = true;
|
|
184
|
+
else if (a === "--regions") args.regions = true;
|
|
185
|
+
else if (a === "--proof") args.proof = next();
|
|
186
|
+
else if (a === "--regions-all") args.regionsAll = next();
|
|
187
|
+
else if (a === "--cover") args.cover = true;
|
|
188
|
+
else if (a === "--title") args.title = next();
|
|
189
|
+
else if (a === "--out") args.out = next();
|
|
190
|
+
else if (a === "--story") args.story = next();
|
|
191
|
+
else if (a === "--summary") args.summary = next();
|
|
192
|
+
else {
|
|
193
|
+
console.error(`unknown argument ${a}. ${more}`);
|
|
194
|
+
process.exit(2);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const fail = (message) => {
|
|
198
|
+
console.error(`${message} ${more}`);
|
|
199
|
+
process.exit(2);
|
|
200
|
+
};
|
|
201
|
+
if (!args.dot || !args.map) fail(USAGE);
|
|
202
|
+
if (!["customer", "developer", "layered"].includes(args.view))
|
|
203
|
+
fail(`--view must be customer, developer, or layered, not ${args.view}.`);
|
|
204
|
+
if (args.fixedDot && args.againstDot) fail("use --fixed-dot or --against-dot, not both.");
|
|
205
|
+
if (args.fixedTraces.length && !args.fixedDot) fail("--fixed-traces needs --fixed-dot.");
|
|
206
|
+
if (args.againstTraces.length && !args.againstDot) fail("--against-traces needs --against-dot.");
|
|
207
|
+
if (args.label !== undefined && !args.fixedDot && !args.againstDot)
|
|
208
|
+
fail("--label names the second model; it needs --fixed-dot or --against-dot.");
|
|
209
|
+
if (args.proof && !args.regions) fail("--proof writes the regions proof; it needs --regions.");
|
|
210
|
+
if (args.regionsAll && !args.regions)
|
|
211
|
+
fail("--regions-all writes every regions arrow; it needs --regions.");
|
|
212
|
+
if (
|
|
213
|
+
args.regions &&
|
|
214
|
+
(args.view !== "customer" || args.flat || args.pathOnly || args.highlight !== 0)
|
|
215
|
+
)
|
|
216
|
+
fail(
|
|
217
|
+
"--regions draws the whole customer chart: use --highlight 0, without --flat or --path-only.",
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const role = args.fixedDot ? "fix" : args.againstDot ? "against" : null;
|
|
221
|
+
const L = args.label ?? (role === "fix" ? "the fix" : "the spec");
|
|
222
|
+
const within = role === "fix" ? "with" : "in";
|
|
223
|
+
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
224
|
+
|
|
225
|
+
const mapping = await import(pathToFileURL(path.resolve(args.map)).href);
|
|
226
|
+
if (typeof mapping.state !== "function")
|
|
227
|
+
throw new Error("the mapping module must export state(vars)");
|
|
228
|
+
if (args.view !== "customer" && typeof mapping.detail !== "function")
|
|
229
|
+
throw new Error(`--view ${args.view} needs detail(vars) in the mapping module`);
|
|
230
|
+
const SEP = "\u0000";
|
|
231
|
+
const edgeId = (from, to) => `${from}${SEP}${SEP}${to}`;
|
|
232
|
+
|
|
233
|
+
const rules = new Map();
|
|
234
|
+
for (const f of args.specs)
|
|
235
|
+
for (const [name, sentence] of ruleSentences(fs.readFileSync(f, "utf8")))
|
|
236
|
+
if (!rules.has(name)) rules.set(name, sentence);
|
|
237
|
+
|
|
238
|
+
const readerActions = new Set(mapping.readerActions ?? []);
|
|
239
|
+
const factsFrom = (enabled) => ({
|
|
240
|
+
enabled,
|
|
241
|
+
settled: [...enabled].every((a) => readerActions.has(a)),
|
|
242
|
+
});
|
|
243
|
+
function keyOf(vars, facts) {
|
|
244
|
+
if (args.view === "developer") return String(mapping.detail(vars, facts));
|
|
245
|
+
const outer = mapping.state(vars, facts);
|
|
246
|
+
if (typeof outer !== "string")
|
|
247
|
+
throw new Error(`state() must return a string; got ${JSON.stringify(outer)}`);
|
|
248
|
+
return args.view === "layered" ? `${outer}${SEP}${mapping.detail(vars, facts)}` : outer;
|
|
249
|
+
}
|
|
250
|
+
function stepLabel(action, before, after, view = args.view) {
|
|
251
|
+
const said = typeof mapping.step === "function" ? mapping.step(action, before, after) : action;
|
|
252
|
+
const named = view === "customer" || said === action ? said : `${said} [${action}]`;
|
|
253
|
+
const seconds = typeof mapping.timer === "function" ? mapping.timer(action, before, after) : null;
|
|
254
|
+
if (seconds === undefined || seconds === null || seconds === "" || !named) return named;
|
|
255
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0)
|
|
256
|
+
throw new Error(
|
|
257
|
+
`timer() must return a number of seconds above 0, or nothing; got ${JSON.stringify(seconds)} for ${action}`,
|
|
258
|
+
);
|
|
259
|
+
return `after ${seconds} s, ${named}`;
|
|
260
|
+
}
|
|
261
|
+
function stepGuard(action, before, after) {
|
|
262
|
+
if (typeof mapping.guard !== "function") return "";
|
|
263
|
+
return String(mapping.guard(action, before, after) ?? "")
|
|
264
|
+
.replace(/[[\]]/g, "")
|
|
265
|
+
.trim();
|
|
266
|
+
}
|
|
267
|
+
function guarded(guards, label) {
|
|
268
|
+
const words = guards?.get(label);
|
|
269
|
+
return words && !words.has("") ? `${label} [${[...words].sort().join(" or ")}]` : label;
|
|
270
|
+
}
|
|
271
|
+
const addGuard = (guards, label, words) => {
|
|
272
|
+
if (!guards.has(label)) guards.set(label, new Set());
|
|
273
|
+
guards.get(label).add(words);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
function lacking(a, b, prefix = []) {
|
|
277
|
+
return Object.keys(a).flatMap((k) =>
|
|
278
|
+
!(k in b)
|
|
279
|
+
? [[...prefix, k]]
|
|
280
|
+
: isRecord(a[k]) && isRecord(b[k])
|
|
281
|
+
? lacking(a[k], b[k], [...prefix, k])
|
|
282
|
+
: [],
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const holder = (o, p) => p.slice(0, -1).reduce((x, k) => (isRecord(x?.[k]) ? x[k] : null), o);
|
|
286
|
+
function omit(vars, paths) {
|
|
287
|
+
if (!paths.length) return vars;
|
|
288
|
+
const out = structuredClone(vars);
|
|
289
|
+
for (const p of paths) {
|
|
290
|
+
const o = holder(out, p);
|
|
291
|
+
if (o) delete o[p.at(-1)];
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
function fill(vars, paths, from) {
|
|
296
|
+
if (!paths.length) return vars;
|
|
297
|
+
const out = structuredClone(vars);
|
|
298
|
+
for (const p of paths) {
|
|
299
|
+
const [o, f] = [holder(out, p), holder(from, p)];
|
|
300
|
+
if (o && f && p.at(-1) in f) o[p.at(-1)] = structuredClone(f[p.at(-1)]);
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function modelOf(graph, lookup = (vars) => vars) {
|
|
306
|
+
const byId = new Map([...graph.nodes.keys()].map((id) => [id, new Set()]));
|
|
307
|
+
for (const e of [...graph.edges, ...graph.loops]) byId.get(e.from).add(e.action);
|
|
308
|
+
const index = new Map(
|
|
309
|
+
[...graph.nodes.values()].map((n) => [stateKey(n.raw ?? n.vars), byId.get(n.id)]),
|
|
310
|
+
);
|
|
311
|
+
const facts = (vars) => factsFrom(index.get(stateKey(lookup(vars))) ?? new Set());
|
|
312
|
+
return {
|
|
313
|
+
graph,
|
|
314
|
+
facts,
|
|
315
|
+
key: (vars) => keyOf(vars, facts(vars)),
|
|
316
|
+
customer: (vars) => String(mapping.state(vars, facts(vars))),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function quotient(m) {
|
|
321
|
+
const { graph } = m;
|
|
322
|
+
const keys = new Map();
|
|
323
|
+
for (const n of graph.nodes.values()) keys.set(n.id, m.key(n.vars));
|
|
324
|
+
const nodes = new Set(keys.values());
|
|
325
|
+
const initial = new Set(
|
|
326
|
+
[...graph.nodes.values()].filter((n) => n.init).map((n) => keys.get(n.id)),
|
|
327
|
+
);
|
|
328
|
+
const edges = new Map();
|
|
329
|
+
for (const e of graph.edges) {
|
|
330
|
+
const from = keys.get(e.from);
|
|
331
|
+
const to = keys.get(e.to);
|
|
332
|
+
if (from === to) continue;
|
|
333
|
+
const id = edgeId(from, to);
|
|
334
|
+
if (!edges.has(id)) edges.set(id, { from, to, counts: new Map(), guards: new Map() });
|
|
335
|
+
const [before, after] = [graph.nodes.get(e.from).vars, graph.nodes.get(e.to).vars];
|
|
336
|
+
const label = stepLabel(e.action, before, after);
|
|
337
|
+
if (!label) continue;
|
|
338
|
+
edges.get(id).counts.set(label, (edges.get(id).counts.get(label) ?? 0) + 1);
|
|
339
|
+
addGuard(edges.get(id).guards, label, stepGuard(e.action, before, after));
|
|
340
|
+
}
|
|
341
|
+
for (const e of edges.values())
|
|
342
|
+
e.labels = new Set([...e.counts].sort((a, b) => b[1] - a[1]).map(([l]) => l));
|
|
343
|
+
const settled = new Set(
|
|
344
|
+
[...graph.nodes.values()].filter((n) => m.facts(n.vars).settled).map((n) => keys.get(n.id)),
|
|
345
|
+
);
|
|
346
|
+
return { nodes, initial, edges, keys, settled };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const graph = parseDot(fs.readFileSync(args.dot, "utf8"));
|
|
350
|
+
const main = modelOf(graph);
|
|
351
|
+
const now = quotient(main);
|
|
352
|
+
const mainInit = [...graph.nodes.values()].find((n) => n.init).vars;
|
|
353
|
+
|
|
354
|
+
const secondDot = args.fixedDot ?? args.againstDot;
|
|
355
|
+
const secondGraph = secondDot ? parseDot(fs.readFileSync(secondDot, "utf8")) : null;
|
|
356
|
+
const secondInit = secondGraph && [...secondGraph.nodes.values()].find((n) => n.init).vars;
|
|
357
|
+
const lacks = secondGraph ? lacking(mainInit, secondInit) : [];
|
|
358
|
+
const extra = secondGraph ? lacking(secondInit, mainInit) : [];
|
|
359
|
+
const secondFilled = secondGraph && {
|
|
360
|
+
...secondGraph,
|
|
361
|
+
nodes: new Map(
|
|
362
|
+
[...secondGraph.nodes].map(([id, n]) => [
|
|
363
|
+
id,
|
|
364
|
+
{ ...n, raw: n.vars, vars: fill(n.vars, lacks, mainInit) },
|
|
365
|
+
]),
|
|
366
|
+
),
|
|
367
|
+
};
|
|
368
|
+
const second = secondFilled && modelOf(secondFilled, (vars) => omit(vars, lacks));
|
|
369
|
+
const secondQ = second && quotient(second);
|
|
370
|
+
|
|
371
|
+
function readerPath(trace, key, labelOf = stepLabel) {
|
|
372
|
+
const out = [{ key: key(trace.steps[0].vars), label: null, modelStep: 0 }];
|
|
373
|
+
for (let i = 1; i < trace.steps.length; i++) {
|
|
374
|
+
const { action } = trace.steps[i];
|
|
375
|
+
const k = key(trace.steps[i].vars);
|
|
376
|
+
if (k === out[out.length - 1].key && !readerActions.has(action)) continue;
|
|
377
|
+
const [before, after] = [trace.steps[i - 1].vars, trace.steps[i].vars];
|
|
378
|
+
const label = labelOf(action, before, after) || action;
|
|
379
|
+
const guard = stepGuard(action, before, after);
|
|
380
|
+
out.push({ key: k, label, guard, action, modelStep: i });
|
|
381
|
+
}
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const readerLabel = (action, before, after) => stepLabel(action, before, after, "customer");
|
|
386
|
+
function replaySecond(trace) {
|
|
387
|
+
if (!second) return null;
|
|
388
|
+
const shared = (vars) => stateKey(omit(vars, lacks));
|
|
389
|
+
const sharedOf = (n) => stateKey(omit(n.raw, extra));
|
|
390
|
+
const out = new Map();
|
|
391
|
+
for (const e of [...second.graph.edges, ...second.graph.loops]) {
|
|
392
|
+
if (!out.has(e.from)) out.set(e.from, []);
|
|
393
|
+
out.get(e.from).push(e);
|
|
394
|
+
}
|
|
395
|
+
const actions = new Set([...second.graph.edges, ...second.graph.loops].map((e) => e.action));
|
|
396
|
+
const nodeOf = (id) => second.graph.nodes.get(id);
|
|
397
|
+
const screen = new Map(
|
|
398
|
+
[...second.graph.nodes.values()].map((n) => [n.id, second.customer(n.vars)]),
|
|
399
|
+
);
|
|
400
|
+
const words = (e) => readerLabel(e.action, nodeOf(e.from).vars, nodeOf(e.to).vars) || e.action;
|
|
401
|
+
const settle = (ids) => {
|
|
402
|
+
const all = new Set(ids);
|
|
403
|
+
for (const id of all)
|
|
404
|
+
for (const e of out.get(id) ?? [])
|
|
405
|
+
if (!readerActions.has(e.action) && screen.get(e.to) === screen.get(id)) all.add(e.to);
|
|
406
|
+
return all;
|
|
407
|
+
};
|
|
408
|
+
const seen = readerPath(trace, main.customer, readerLabel);
|
|
409
|
+
const shownAt = new Map(seen.map((p, j) => [p.modelStep, j]));
|
|
410
|
+
let here = settle(
|
|
411
|
+
[...second.graph.nodes.values()]
|
|
412
|
+
.filter((n) => n.init && screen.get(n.id) === seen[0].key)
|
|
413
|
+
.map((n) => n.id),
|
|
414
|
+
);
|
|
415
|
+
if (here.size === 0) return { reason: "starts on another screen" };
|
|
416
|
+
const passed = new Set(here);
|
|
417
|
+
let j = 0;
|
|
418
|
+
for (let i = 1; i < trace.steps.length; i++) {
|
|
419
|
+
const { action } = trace.steps[i];
|
|
420
|
+
const hidden = !shownAt.has(i);
|
|
421
|
+
if (!hidden) j = shownAt.get(i);
|
|
422
|
+
const want = seen[j];
|
|
423
|
+
const [before, after] = [trace.steps[i - 1].vars, trace.steps[i].vars];
|
|
424
|
+
const known = actions.has(action);
|
|
425
|
+
if (!known && (hidden || shared(before) === shared(after))) continue;
|
|
426
|
+
const tries = [...here].flatMap((id) =>
|
|
427
|
+
(out.get(id) ?? []).filter((e) =>
|
|
428
|
+
known ? e.action === action : sharedOf(nodeOf(e.to)) === shared(after),
|
|
429
|
+
),
|
|
430
|
+
);
|
|
431
|
+
if (hidden && tries.length === 0) continue;
|
|
432
|
+
const same = known
|
|
433
|
+
? tries.filter((e) => screen.get(e.to) === want.key && (hidden || words(e) === want.label))
|
|
434
|
+
: tries;
|
|
435
|
+
if (same.length) {
|
|
436
|
+
here = settle(same.map((e) => e.to));
|
|
437
|
+
for (const id of here) passed.add(id);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const outcomes = new Map();
|
|
441
|
+
for (const e of tries) {
|
|
442
|
+
const id = `${screen.get(e.to)}${SEP}${words(e)}`;
|
|
443
|
+
outcomes.set(id, [...(outcomes.get(id) ?? []), e]);
|
|
444
|
+
}
|
|
445
|
+
const taken = [...outcomes.values()].sort((a, b) => b.length - a.length)[0]?.[0];
|
|
446
|
+
const from = main.key(before);
|
|
447
|
+
const said = hidden ? readerLabel(action, before, after) || action : want.label;
|
|
448
|
+
const at = { step: j, said, from, hidden };
|
|
449
|
+
if (!taken)
|
|
450
|
+
return {
|
|
451
|
+
...at,
|
|
452
|
+
key: from,
|
|
453
|
+
label: stepLabel(action, before, after) || action,
|
|
454
|
+
disabled: true,
|
|
455
|
+
};
|
|
456
|
+
const [b, a] = [nodeOf(taken.from).vars, nodeOf(taken.to).vars];
|
|
457
|
+
return { ...at, key: second.key(a), label: stepLabel(taken.action, b, a) || taken.action };
|
|
458
|
+
}
|
|
459
|
+
const bad = new Set(
|
|
460
|
+
rawSecondTraces
|
|
461
|
+
.filter((t) => t.invariant === trace.invariant)
|
|
462
|
+
.map((t) => stateKey(t.steps[t.steps.length - 1].vars)),
|
|
463
|
+
);
|
|
464
|
+
const ruleHolds =
|
|
465
|
+
secondTraceFiles.length > 0 && ![...passed].some((id) => bad.has(stateKey(nodeOf(id).vars)));
|
|
466
|
+
return { sameEnd: true, ruleHolds };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function readTraces(files, paths = []) {
|
|
470
|
+
return files.flatMap((f) => {
|
|
471
|
+
const text = fs.readFileSync(f, "utf8");
|
|
472
|
+
checkTlcOutput(text, f);
|
|
473
|
+
return parseTraces(text).map((t) => ({
|
|
474
|
+
...t,
|
|
475
|
+
steps: t.steps.map((s) => ({ ...s, vars: fill(s.vars, paths, mainInit) })),
|
|
476
|
+
}));
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
// With -continue, TLC reports every bad state, including states after the first bad one.
|
|
480
|
+
function firstBad(raw) {
|
|
481
|
+
const badStates = new Map();
|
|
482
|
+
for (const t of raw) {
|
|
483
|
+
if (!badStates.has(t.invariant)) badStates.set(t.invariant, new Set());
|
|
484
|
+
badStates.get(t.invariant).add(stateKey(t.steps[t.steps.length - 1].vars));
|
|
485
|
+
}
|
|
486
|
+
const cuts = [];
|
|
487
|
+
const seen = new Set();
|
|
488
|
+
for (const t of raw) {
|
|
489
|
+
const first = t.steps.findIndex((s) => badStates.get(t.invariant).has(stateKey(s.vars)));
|
|
490
|
+
const cut = { invariant: t.invariant, steps: t.steps.slice(0, first + 1) };
|
|
491
|
+
const id = `${t.invariant}${SEP}${stateKey(cut.steps[cut.steps.length - 1].vars)}`;
|
|
492
|
+
if (seen.has(id)) continue;
|
|
493
|
+
seen.add(id);
|
|
494
|
+
cuts.push(cut);
|
|
495
|
+
}
|
|
496
|
+
return cuts;
|
|
497
|
+
}
|
|
498
|
+
const rawTraces = readTraces(args.traces);
|
|
499
|
+
const secondTraceFiles = role === "fix" ? args.fixedTraces : args.againstTraces;
|
|
500
|
+
const rawSecondTraces = readTraces(secondTraceFiles, lacks);
|
|
501
|
+
const traces = firstBad(rawTraces);
|
|
502
|
+
const secondTraces = firstBad(rawSecondTraces);
|
|
503
|
+
function group(cuts, m) {
|
|
504
|
+
const byId = new Map();
|
|
505
|
+
for (const trace of cuts) {
|
|
506
|
+
const rp = readerPath(trace, m.customer);
|
|
507
|
+
const last = trace.steps[trace.steps.length - 1].vars;
|
|
508
|
+
const said =
|
|
509
|
+
typeof mapping.cause === "function"
|
|
510
|
+
? mapping.cause(last, m.facts(last), trace.invariant)
|
|
511
|
+
: null;
|
|
512
|
+
const cause = said === undefined || said === null ? null : String(said);
|
|
513
|
+
const id = `${trace.invariant}${SEP}${cause ?? rp[rp.length - 1].key}`;
|
|
514
|
+
const prev = byId.get(id);
|
|
515
|
+
if (
|
|
516
|
+
!prev ||
|
|
517
|
+
rp.length < prev.steps ||
|
|
518
|
+
(rp.length === prev.steps && trace.steps.length < prev.trace.steps.length)
|
|
519
|
+
) {
|
|
520
|
+
byId.set(id, {
|
|
521
|
+
id,
|
|
522
|
+
cause,
|
|
523
|
+
steps: rp.length,
|
|
524
|
+
trace,
|
|
525
|
+
invariant: trace.invariant,
|
|
526
|
+
count: (prev?.count ?? 0) + 1,
|
|
527
|
+
});
|
|
528
|
+
} else prev.count++;
|
|
529
|
+
}
|
|
530
|
+
return byId;
|
|
531
|
+
}
|
|
532
|
+
const byOrder = (a, b) =>
|
|
533
|
+
a.steps - b.steps ||
|
|
534
|
+
a.trace.steps.length - b.trace.steps.length ||
|
|
535
|
+
(a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
536
|
+
const inSecond = (l) => `${within} ${L}, ${l}`;
|
|
537
|
+
const modelEdges = new Map(now.edges);
|
|
538
|
+
if (secondQ)
|
|
539
|
+
for (const [id, e] of secondQ.edges)
|
|
540
|
+
if (!modelEdges.has(id))
|
|
541
|
+
modelEdges.set(id, {
|
|
542
|
+
...e,
|
|
543
|
+
labels: new Set([...e.labels].map(inSecond)),
|
|
544
|
+
guards: new Map([...e.guards].map(([l, words]) => [inSecond(l), words])),
|
|
545
|
+
secondOnly: true,
|
|
546
|
+
});
|
|
547
|
+
function divergence(s) {
|
|
548
|
+
if (s?.key === undefined) return s;
|
|
549
|
+
const plain = edgeId(s.from, s.key);
|
|
550
|
+
const text = inSecond(`${s.label}${s.disabled ? " cannot happen" : ""}`);
|
|
551
|
+
const drawn = !s.disabled && [s.label, text].some((l) => modelEdges.get(plain)?.labels.has(l));
|
|
552
|
+
return { ...s, text, edge: drawn ? plain : `${plain}${SEP}second` };
|
|
553
|
+
}
|
|
554
|
+
const withPath = (c, m) => {
|
|
555
|
+
const path = readerPath(c.trace, m.key);
|
|
556
|
+
return { ...c, path, end: path[path.length - 1].key };
|
|
557
|
+
};
|
|
558
|
+
const classesById = group(traces, main);
|
|
559
|
+
const mainClasses = [...classesById.values()]
|
|
560
|
+
.sort(byOrder)
|
|
561
|
+
.map((c) => ({ ...withPath(c, main), second: divergence(replaySecond(c.trace)) }));
|
|
562
|
+
const introduced = second
|
|
563
|
+
? [...group(secondTraces, second).values()]
|
|
564
|
+
.filter((c) => !classesById.has(c.id))
|
|
565
|
+
.sort(byOrder)
|
|
566
|
+
.map((c) => ({ ...withPath(c, second), second: null, introduced: true }))
|
|
567
|
+
: [];
|
|
568
|
+
const classes = [...mainClasses, ...introduced];
|
|
569
|
+
|
|
570
|
+
const diverges = (c) => c.second?.key !== undefined;
|
|
571
|
+
function badKeys(m, keys, raw) {
|
|
572
|
+
const g = m.graph;
|
|
573
|
+
const bad = new Set(raw.map((t) => stateKey(t.steps[t.steps.length - 1].vars)));
|
|
574
|
+
const out = new Set(raw.map((t) => m.key(t.steps[t.steps.length - 1].vars)));
|
|
575
|
+
const ok = (id) => !bad.has(stateKey(g.nodes.get(id).vars));
|
|
576
|
+
const next = new Map();
|
|
577
|
+
for (const e of g.edges) {
|
|
578
|
+
if (!next.has(e.from)) next.set(e.from, []);
|
|
579
|
+
next.get(e.from).push(e.to);
|
|
580
|
+
}
|
|
581
|
+
const clean = new Set([...g.nodes.values()].filter((n) => n.init && ok(n.id)).map((n) => n.id));
|
|
582
|
+
const queue = [...clean];
|
|
583
|
+
while (queue.length) {
|
|
584
|
+
for (const to of next.get(queue.shift()) ?? []) {
|
|
585
|
+
if (clean.has(to) || !ok(to)) continue;
|
|
586
|
+
clean.add(to);
|
|
587
|
+
queue.push(to);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const reached = new Set([...clean].map((id) => keys.get(id)));
|
|
591
|
+
for (const k of keys.values()) if (!reached.has(k)) out.add(k);
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
594
|
+
const badNow = badKeys(main, now.keys, rawTraces);
|
|
595
|
+
const badSecond =
|
|
596
|
+
second && secondTraceFiles.length > 0
|
|
597
|
+
? badKeys(second, secondQ.keys, rawSecondTraces)
|
|
598
|
+
: role === "fix"
|
|
599
|
+
? badNow
|
|
600
|
+
: new Set();
|
|
601
|
+
const allNodes = new Set([...now.nodes, ...(secondQ?.nodes ?? [])]);
|
|
602
|
+
const initial = new Set([...now.initial, ...(secondQ?.initial ?? [])]);
|
|
603
|
+
const onlySecond = (k) => second && !now.nodes.has(k);
|
|
604
|
+
const status = (k) => {
|
|
605
|
+
if (role === "against")
|
|
606
|
+
return badNow.has(k) || badSecond.has(k) ? "wrong" : onlySecond(k) ? "fixed" : "works";
|
|
607
|
+
if (role === "fix")
|
|
608
|
+
return badSecond.has(k) ? "wrong" : badNow.has(k) || onlySecond(k) ? "fixed" : "works";
|
|
609
|
+
return badNow.has(k) ? "wrong" : "works";
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
const chosen = args.highlight > 0 ? classes[args.highlight - 1] : null;
|
|
613
|
+
if (args.highlight > 0 && classes.length > 0 && !chosen)
|
|
614
|
+
throw new Error(`--highlight ${args.highlight}: only ${classes.length} classes`);
|
|
615
|
+
const onPath = new Set(chosen ? chosen.path.map((p) => p.key) : []);
|
|
616
|
+
if (chosen && diverges(chosen)) onPath.add(chosen.second.key);
|
|
617
|
+
|
|
618
|
+
const edgesAlong = (c) =>
|
|
619
|
+
c.path.slice(1).map((p, i) => ({
|
|
620
|
+
id: edgeId(c.path[i].key, p.key),
|
|
621
|
+
from: c.path[i].key,
|
|
622
|
+
to: p.key,
|
|
623
|
+
label: p.label,
|
|
624
|
+
guard: p.guard,
|
|
625
|
+
}));
|
|
626
|
+
function chartEdges() {
|
|
627
|
+
const edges = new Map(modelEdges);
|
|
628
|
+
for (const { second: s } of classes.filter(diverges)) {
|
|
629
|
+
if (modelEdges.has(s.edge)) continue;
|
|
630
|
+
if (!edges.has(s.edge))
|
|
631
|
+
edges.set(s.edge, { from: s.from, to: s.key, labels: new Set(), secondOnly: true });
|
|
632
|
+
edges.get(s.edge).labels.add(s.text);
|
|
633
|
+
}
|
|
634
|
+
const pathNumbers = new Map();
|
|
635
|
+
(chosen ? edgesAlong(chosen) : []).forEach((step, i) => {
|
|
636
|
+
if (!pathNumbers.has(step.id)) pathNumbers.set(step.id, []);
|
|
637
|
+
const shown = step.guard ? `${step.label} [${step.guard}]` : step.label;
|
|
638
|
+
pathNumbers.get(step.id).push({ n: i + 1, label: step.label, shown });
|
|
639
|
+
if (!edges.has(step.id))
|
|
640
|
+
edges.set(step.id, { from: step.from, to: step.to, labels: new Set([step.label]) });
|
|
641
|
+
});
|
|
642
|
+
if (!args.cover) return { edges, pathNumbers, coverEdges: null };
|
|
643
|
+
|
|
644
|
+
const coverEdges = new Set();
|
|
645
|
+
const bfs = (q, targets) => {
|
|
646
|
+
const parent = new Map([...q.initial].map((k) => [k, null]));
|
|
647
|
+
const queue = [...q.initial];
|
|
648
|
+
const outEdges = new Map();
|
|
649
|
+
for (const [id, e] of q.edges) {
|
|
650
|
+
if (!outEdges.has(e.from)) outEdges.set(e.from, []);
|
|
651
|
+
outEdges.get(e.from).push([id, e]);
|
|
652
|
+
}
|
|
653
|
+
while (queue.length) {
|
|
654
|
+
const k = queue.shift();
|
|
655
|
+
for (const [id, e] of outEdges.get(k) ?? []) {
|
|
656
|
+
if (parent.has(e.to)) continue;
|
|
657
|
+
parent.set(e.to, id);
|
|
658
|
+
queue.push(e.to);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
for (const t of targets) {
|
|
662
|
+
for (let id = parent.get(t); id; id = parent.get(q.edges.get(id).from)) coverEdges.add(id);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
bfs(now, now.settled);
|
|
666
|
+
if (secondQ)
|
|
667
|
+
bfs(
|
|
668
|
+
secondQ,
|
|
669
|
+
[...secondQ.settled].filter((k) => !now.nodes.has(k)),
|
|
670
|
+
);
|
|
671
|
+
const pathLabels = new Map();
|
|
672
|
+
for (const c of classes) {
|
|
673
|
+
for (const step of edgesAlong(c)) {
|
|
674
|
+
coverEdges.add(step.id);
|
|
675
|
+
pathLabels.set(step.id, (pathLabels.get(step.id) ?? new Set()).add(step.label));
|
|
676
|
+
}
|
|
677
|
+
if (diverges(c)) coverEdges.add(c.second.edge);
|
|
678
|
+
}
|
|
679
|
+
for (const id of coverEdges) {
|
|
680
|
+
if (edges.has(id)) continue;
|
|
681
|
+
const [from, to] = id.split(`${SEP}${SEP}`);
|
|
682
|
+
edges.set(id, { from, to, labels: pathLabels.get(id) });
|
|
683
|
+
}
|
|
684
|
+
return { edges, pathNumbers, coverEdges };
|
|
685
|
+
}
|
|
686
|
+
const { edges, pathNumbers, coverEdges } = chartEdges();
|
|
687
|
+
|
|
688
|
+
const LEGEND = {
|
|
689
|
+
works: ["legendWorks", mapping.legend?.works ?? (role === "fix" ? "Works today" : "Works")],
|
|
690
|
+
wrong: [
|
|
691
|
+
"legendWrong",
|
|
692
|
+
mapping.legend?.wrong ?? (role === "fix" ? `Still wrong with ${L}` : "Breaks a rule"),
|
|
693
|
+
],
|
|
694
|
+
fixed: [
|
|
695
|
+
"legendFixed",
|
|
696
|
+
mapping.legend?.fixed ?? (role === "fix" ? `Fixed or new with ${L}` : `Only in ${L}`),
|
|
697
|
+
],
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const grouping = typeof mapping.group === "function";
|
|
701
|
+
const groupsOf = new Map();
|
|
702
|
+
const groupOf = (k) => (groupsOf.get(k)?.size === 1 ? [...groupsOf.get(k)][0] : "");
|
|
703
|
+
const groupNames = [];
|
|
704
|
+
const fills = new Map();
|
|
705
|
+
if (grouping) {
|
|
706
|
+
const add = (k, vars, m) => {
|
|
707
|
+
const g = mapping.group(vars, m.facts(vars));
|
|
708
|
+
if (g !== undefined && g !== null && typeof g !== "string")
|
|
709
|
+
throw new Error(`group() must return a group name or nothing; got ${JSON.stringify(g)}`);
|
|
710
|
+
if (!groupsOf.has(k)) groupsOf.set(k, new Set());
|
|
711
|
+
groupsOf.get(k).add((g ?? "").replace(/\s+/g, " ").trim());
|
|
712
|
+
};
|
|
713
|
+
for (const n of graph.nodes.values()) add(now.keys.get(n.id), n.vars, main);
|
|
714
|
+
if (second)
|
|
715
|
+
for (const n of second.graph.nodes.values()) add(secondQ.keys.get(n.id), n.vars, second);
|
|
716
|
+
|
|
717
|
+
const given = mapping.groupColors ?? {};
|
|
718
|
+
if (!isRecord(given))
|
|
719
|
+
throw new Error("groupColors must be an object from a group name to a color such as #d6eaf8");
|
|
720
|
+
const statusNames = new Set(
|
|
721
|
+
[
|
|
722
|
+
"Works",
|
|
723
|
+
"Works today",
|
|
724
|
+
"Breaks a rule",
|
|
725
|
+
"Still wrong",
|
|
726
|
+
"Fixed",
|
|
727
|
+
...Object.values(LEGEND).map(([, t]) => t),
|
|
728
|
+
].map((t) => t.toLowerCase()),
|
|
729
|
+
);
|
|
730
|
+
const found = new Set([...groupsOf.values()].flatMap((gs) => [...gs]).filter(Boolean));
|
|
731
|
+
for (const g of found)
|
|
732
|
+
if (statusNames.has(g.toLowerCase()))
|
|
733
|
+
throw new Error(
|
|
734
|
+
`group "${g}" has the name of a status; TLC decides the status, so name the group for what the reader sees`,
|
|
735
|
+
);
|
|
736
|
+
groupNames.push(
|
|
737
|
+
...Object.keys(given).filter((g) => found.has(g)),
|
|
738
|
+
...[...found].filter((g) => !Object.hasOwn(given, g)).sort(),
|
|
739
|
+
);
|
|
740
|
+
for (const [g, color] of Object.entries(given)) {
|
|
741
|
+
if (color === undefined || color === null || color === "") continue;
|
|
742
|
+
const why = groupColorProblem(color);
|
|
743
|
+
if (why) throw new Error(`groupColors["${g}"] ${JSON.stringify(color)} ${why}`);
|
|
744
|
+
if (found.has(g)) fills.set(g, color.toLowerCase());
|
|
745
|
+
}
|
|
746
|
+
const free = GROUP_PALETTE.filter((c) => ![...fills.values()].includes(c));
|
|
747
|
+
for (const g of groupNames) {
|
|
748
|
+
if (fills.has(g)) continue;
|
|
749
|
+
if (!free.length)
|
|
750
|
+
throw new Error(
|
|
751
|
+
`the mapping has ${groupNames.length} groups, more than the ${GROUP_PALETTE.length} colors of the palette; give the others a color in groupColors`,
|
|
752
|
+
);
|
|
753
|
+
fills.set(g, free.shift());
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const mixedGroups = [...groupsOf].filter(([, gs]) => gs.size > 1).map(([k]) => k);
|
|
757
|
+
|
|
758
|
+
const BORDER = { works: "", wrong: "Wrong", fixed: "Fixed" };
|
|
759
|
+
const classFor = (g, s, path) =>
|
|
760
|
+
grouping
|
|
761
|
+
? `${g ? `group${groupNames.indexOf(g)}` : "plain"}${BORDER[s] || (path ? "Path" : "")}`
|
|
762
|
+
: `${s}${path ? "Path" : ""}`;
|
|
763
|
+
const drawnState = (g, s, path = false) => ({ group: g, status: s, cls: classFor(g, s, path) });
|
|
764
|
+
const groupCounts = (drawn) =>
|
|
765
|
+
groupNames
|
|
766
|
+
.map((g) => ({
|
|
767
|
+
name: g,
|
|
768
|
+
color: fills.get(g),
|
|
769
|
+
states: drawn.filter((d) => d.group === g).length,
|
|
770
|
+
}))
|
|
771
|
+
.filter((g) => g.states > 0);
|
|
772
|
+
|
|
773
|
+
// Mermaid shrinks a chart to the page width unless useMaxWidth is false, and rewraps labels that
|
|
774
|
+
// wrap() already wrapped unless markdownAutoWrap is false.
|
|
775
|
+
const frontMatter = (title) => [
|
|
776
|
+
"---",
|
|
777
|
+
...(title ? [`title: ${JSON.stringify(title)}`] : []),
|
|
778
|
+
"config:",
|
|
779
|
+
" markdownAutoWrap: false",
|
|
780
|
+
" state:",
|
|
781
|
+
" useMaxWidth: false",
|
|
782
|
+
"---",
|
|
783
|
+
];
|
|
784
|
+
function head(drawn) {
|
|
785
|
+
const out = [...frontMatter(args.title), "stateDiagram-v2"];
|
|
786
|
+
if (role) out.push(` %% second model (${role}): ${L}`);
|
|
787
|
+
if (args.fullLabels) out.push(` ${FULL_LABELS}`);
|
|
788
|
+
const used = Object.keys(LEGEND).filter((s) => drawn.some((d) => d.status === s));
|
|
789
|
+
const legend = grouping
|
|
790
|
+
? [
|
|
791
|
+
...groupCounts(drawn).map(({ name }) => {
|
|
792
|
+
const i = groupNames.indexOf(name);
|
|
793
|
+
return [`legendGroup${i}`, name, `group${i}`];
|
|
794
|
+
}),
|
|
795
|
+
...used.filter((s) => BORDER[s]).map((s) => [...LEGEND[s], `plain${BORDER[s]}`]),
|
|
796
|
+
]
|
|
797
|
+
: used.map((s) => [...LEGEND[s], s]);
|
|
798
|
+
if (grouping) {
|
|
799
|
+
const styleClasses = new Set([...drawn.map((d) => d.cls), ...legend.map(([, , c]) => c)]);
|
|
800
|
+
const bases = [["plain", PLAIN_FILL], ...groupNames.map((g, i) => [`group${i}`, fills.get(g)])];
|
|
801
|
+
for (const [base, fill] of bases)
|
|
802
|
+
for (const border of ["", "Wrong", "Fixed", "Path"])
|
|
803
|
+
if (styleClasses.has(base + border)) out.push(` ${groupClassDef(base, fill, border)}`);
|
|
804
|
+
} else for (const c of STATUS_CLASSES) out.push(` ${c}`);
|
|
805
|
+
if (legend.length) {
|
|
806
|
+
out.push(` state "Legend" as ${LEGEND_ID} {`);
|
|
807
|
+
for (const [id, text] of legend) out.push(` state "${quote(text)}" as ${id}`);
|
|
808
|
+
out.push(" }");
|
|
809
|
+
for (const [id, , c] of legend) out.push(` class ${id} ${c}`);
|
|
810
|
+
}
|
|
811
|
+
return out;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function regionsChart({ states, starts, edges, classes, proofPath, allPath }) {
|
|
815
|
+
const norm = (s) => s.replaceAll(";", ",").replace(/\s+/g, " ").trim();
|
|
816
|
+
const split = (k) => {
|
|
817
|
+
const at = k.indexOf(": ");
|
|
818
|
+
return at > 0 && at + 2 < k.length ? [norm(k.slice(0, at)), norm(k.slice(at + 2))] : null;
|
|
819
|
+
};
|
|
820
|
+
const nodes = [...states].sort();
|
|
821
|
+
if (!nodes.every(split)) return null;
|
|
822
|
+
const steps = [];
|
|
823
|
+
for (const e of edges.values()) {
|
|
824
|
+
if (e.labels.size === 0) return { why: "a step has no label" };
|
|
825
|
+
for (const l of e.labels)
|
|
826
|
+
steps.push({
|
|
827
|
+
from: split(e.from),
|
|
828
|
+
to: split(e.to),
|
|
829
|
+
label: norm(l),
|
|
830
|
+
words: e.guards?.get(l) ?? new Set([""]),
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
const bar = (p) => p.join(" | ");
|
|
834
|
+
const flat = {
|
|
835
|
+
Main: {
|
|
836
|
+
states: new Set(nodes.map((k) => bar(split(k)))),
|
|
837
|
+
initial: new Set(starts.map((k) => bar(split(k)))),
|
|
838
|
+
transitions: steps.map((s) => ({ from: bar(s.from), to: bar(s.to), label: s.label })),
|
|
839
|
+
},
|
|
840
|
+
};
|
|
841
|
+
const ids = new Map([...flat.Main.states].map((s, i) => [s, `p${i}`]));
|
|
842
|
+
const proof = [
|
|
843
|
+
...frontMatter(),
|
|
844
|
+
"stateDiagram-v2",
|
|
845
|
+
...[...ids].map(([s, id]) => ` state "${quote(s)}" as ${id}`),
|
|
846
|
+
...[...flat.Main.initial].map((s) => ` [*] --> ${ids.get(s)}`),
|
|
847
|
+
...steps.map((s) => ` ${ids.get(bar(s.from))} --> ${ids.get(bar(s.to))} : ${s.label}`),
|
|
848
|
+
];
|
|
849
|
+
if (proofPath) fs.writeFileSync(proofPath, proof.join("\n") + "\n");
|
|
850
|
+
|
|
851
|
+
const beside = [new Map(), new Map()];
|
|
852
|
+
for (const k of nodes) {
|
|
853
|
+
const p = split(k);
|
|
854
|
+
for (const i of [0, 1]) {
|
|
855
|
+
if (!beside[i].has(p[i])) beside[i].set(p[i], new Set());
|
|
856
|
+
beside[i].get(p[i]).add(p[1 - i]);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const size = beside[0].size + beside[1].size;
|
|
860
|
+
if (size >= nodes.length)
|
|
861
|
+
return { why: `${size} region states are not fewer than the ${nodes.length} flat states` };
|
|
862
|
+
|
|
863
|
+
const rank = ["works", "fixed", "wrong"];
|
|
864
|
+
const regionStatus = [new Map(), new Map()];
|
|
865
|
+
for (const k of nodes) {
|
|
866
|
+
const p = split(k);
|
|
867
|
+
for (const i of [0, 1]) {
|
|
868
|
+
const prev = regionStatus[i].get(p[i]);
|
|
869
|
+
if (!prev || rank.indexOf(status(k)) < rank.indexOf(prev))
|
|
870
|
+
regionStatus[i].set(p[i], status(k));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
for (const k of nodes) {
|
|
874
|
+
const p = split(k);
|
|
875
|
+
const shown = rank[Math.max(...[0, 1].map((i) => rank.indexOf(regionStatus[i].get(p[i]))))];
|
|
876
|
+
if (shown !== status(k))
|
|
877
|
+
return {
|
|
878
|
+
why: `"${k}" is "${LEGEND[status(k)][1]}", and its two region states would show "${LEGEND[shown][1]}"`,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const moves = [new Map(), new Map()];
|
|
883
|
+
for (const s of steps)
|
|
884
|
+
for (const i of [0, 1]) {
|
|
885
|
+
const id = [s.from[i], s.label, s.to[i]].join(SEP);
|
|
886
|
+
if (!moves[i].has(id))
|
|
887
|
+
moves[i].set(id, {
|
|
888
|
+
from: s.from[i],
|
|
889
|
+
label: s.label,
|
|
890
|
+
to: s.to[i],
|
|
891
|
+
beside: new Set(),
|
|
892
|
+
guards: new Map(),
|
|
893
|
+
});
|
|
894
|
+
moves[i].get(id).beside.add(s.from[1 - i]);
|
|
895
|
+
for (const w of s.words) addGuard(moves[i].get(id).guards, s.label, norm(w));
|
|
896
|
+
}
|
|
897
|
+
for (const m of moves)
|
|
898
|
+
for (const [id, x] of m)
|
|
899
|
+
if (
|
|
900
|
+
x.from === x.to &&
|
|
901
|
+
![...m.values()].some((y) => y.from === x.from && y.label === x.label && y.to !== x.from)
|
|
902
|
+
)
|
|
903
|
+
m.delete(id);
|
|
904
|
+
const rid = beside.map((m, i) => new Map([...m.keys()].sort().map((s, j) => [s, `r${i}s${j}`])));
|
|
905
|
+
const regionGroup = [new Map(), new Map()];
|
|
906
|
+
for (const k of nodes) {
|
|
907
|
+
const p = split(k);
|
|
908
|
+
for (const i of [0, 1]) {
|
|
909
|
+
const prev = regionGroup[i].get(p[i]);
|
|
910
|
+
regionGroup[i].set(p[i], prev === undefined || prev === groupOf(k) ? groupOf(k) : "");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
const drawn = rid.map((m, i) =>
|
|
914
|
+
[...m.keys()].map((s) => drawnState(regionGroup[i].get(s), regionStatus[i].get(s))),
|
|
915
|
+
);
|
|
916
|
+
const draw = (only) => {
|
|
917
|
+
const out = head(drawn.flat());
|
|
918
|
+
out.push(` state "At the same time" as regions {`);
|
|
919
|
+
let count = 0;
|
|
920
|
+
for (const i of [0, 1]) {
|
|
921
|
+
if (i) out.push(" --");
|
|
922
|
+
const name = mapping.regionNames?.[i];
|
|
923
|
+
const pad = name ? " " : " ";
|
|
924
|
+
if (name) out.push(` state "${quote(wrap(name))}" as region${i} {`);
|
|
925
|
+
for (const s of new Set(starts.map((k) => split(k)[i])))
|
|
926
|
+
out.push(`${pad}[*] --> ${rid[i].get(s)}`);
|
|
927
|
+
for (const [s, id] of rid[i]) out.push(`${pad}state "${quote(wrap(s))}" as ${id}`);
|
|
928
|
+
const shownMoves = [...moves[i]]
|
|
929
|
+
.filter(([id]) => !only || only[i].has(id))
|
|
930
|
+
.map(([, m]) => m)
|
|
931
|
+
.sort((a, b) => a.from.localeCompare(b.from) || a.label.localeCompare(b.label));
|
|
932
|
+
for (const m of shownMoves) {
|
|
933
|
+
const all = beside[i].get(m.from);
|
|
934
|
+
const not = [...all].filter((s) => !m.beside.has(s)).sort();
|
|
935
|
+
const yes = [...m.beside].sort();
|
|
936
|
+
const unless = not.length > 0 && not.length < yes.length;
|
|
937
|
+
const guard = not.length === 0 ? null : unless ? not : yes;
|
|
938
|
+
const worded = guarded(m.guards, m.label);
|
|
939
|
+
const text =
|
|
940
|
+
worded !== m.label || !guard
|
|
941
|
+
? worded
|
|
942
|
+
: `${m.label} [${unless ? "unless" : "when"} ${guard.join(" or ")}]`;
|
|
943
|
+
out.push(`${pad}${rid[i].get(m.from)} --> ${rid[i].get(m.to)} : ${wrap(text, 30)}`);
|
|
944
|
+
if (guard)
|
|
945
|
+
out.push(
|
|
946
|
+
`${pad}%% when ${unless ? "not " : ""}${guard.map((s) => rid[1 - i].get(s)).join(", ")}`,
|
|
947
|
+
);
|
|
948
|
+
count++;
|
|
949
|
+
}
|
|
950
|
+
if (name) out.push(" }");
|
|
951
|
+
}
|
|
952
|
+
out.push(" }");
|
|
953
|
+
for (const i of [0, 1])
|
|
954
|
+
[...rid[i].values()].forEach((id, j) => out.push(` class ${id} ${drawn[i][j].cls}`));
|
|
955
|
+
return { mermaid: out.join("\n") + "\n", count };
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const all = draw(null);
|
|
959
|
+
const differences = compareCharts(parseMermaid(all.mermaid).regions, flat);
|
|
960
|
+
if (differences.length) {
|
|
961
|
+
const n = differences.reduce((sum, [, xs]) => sum + xs.length, 0);
|
|
962
|
+
return {
|
|
963
|
+
why: `the regions do not combine into the flat chart, with ${n} difference(s), such as ${differences[0][0]}: ${differences[0][1][0]}`,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
if (allPath) fs.writeFileSync(allPath, all.mermaid);
|
|
967
|
+
|
|
968
|
+
const parts = new Map(nodes.map((k) => [bar(split(k)), split(k)]));
|
|
969
|
+
const key = (from, label, to) => `${from} --> ${to} : ${label}`;
|
|
970
|
+
const real = new Set(steps.map((s) => key(bar(s.from), s.label, bar(s.to))));
|
|
971
|
+
const next = new Map();
|
|
972
|
+
for (const s of steps) {
|
|
973
|
+
if (!next.has(bar(s.from))) next.set(bar(s.from), []);
|
|
974
|
+
next.get(bar(s.from)).push(s);
|
|
975
|
+
}
|
|
976
|
+
const keep = [new Set(), new Set()];
|
|
977
|
+
const movesOf = (s) =>
|
|
978
|
+
[0, 1]
|
|
979
|
+
.filter((i) => s.from[i] !== s.to[i])
|
|
980
|
+
.map((i) => [i, [s.from[i], s.label, s.to[i]].join(SEP)]);
|
|
981
|
+
const price = (s) => movesOf(s).filter(([i, id]) => !keep[i].has(id)).length * nodes.length + 1;
|
|
982
|
+
const wanted = [];
|
|
983
|
+
const want = (s) => {
|
|
984
|
+
wanted.push(s);
|
|
985
|
+
for (const [i, id] of movesOf(s)) keep[i].add(id);
|
|
986
|
+
};
|
|
987
|
+
const stepsOf = (e, said) => {
|
|
988
|
+
const labels = [...e.labels].map(norm);
|
|
989
|
+
return (labels.includes(said) ? [said] : labels).map((label) => ({
|
|
990
|
+
from: split(e.from),
|
|
991
|
+
to: split(e.to),
|
|
992
|
+
label,
|
|
993
|
+
}));
|
|
994
|
+
};
|
|
995
|
+
for (const c of classes) {
|
|
996
|
+
for (const step of edgesAlong(c)) {
|
|
997
|
+
const e = edges.get(step.id);
|
|
998
|
+
if (e) stepsOf(e, norm(step.label)).forEach(want);
|
|
999
|
+
}
|
|
1000
|
+
if (diverges(c)) stepsOf(edges.get(c.second.edge), "").forEach(want);
|
|
1001
|
+
}
|
|
1002
|
+
const cheapest = () => {
|
|
1003
|
+
const cost = new Map(starts.map((k) => [bar(split(k)), 0]));
|
|
1004
|
+
const parent = new Map();
|
|
1005
|
+
const open = new Set(cost.keys());
|
|
1006
|
+
while (open.size) {
|
|
1007
|
+
const k = [...open].reduce((a, b) => (cost.get(b) < cost.get(a) ? b : a));
|
|
1008
|
+
open.delete(k);
|
|
1009
|
+
for (const s of next.get(k) ?? []) {
|
|
1010
|
+
const c = cost.get(k) + price(s);
|
|
1011
|
+
if (c < (cost.get(bar(s.to)) ?? Infinity)) {
|
|
1012
|
+
cost.set(bar(s.to), c);
|
|
1013
|
+
parent.set(bar(s.to), s);
|
|
1014
|
+
open.add(bar(s.to));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return { cost, parent };
|
|
1019
|
+
};
|
|
1020
|
+
const targets = [
|
|
1021
|
+
...[0, 1].flatMap((i) =>
|
|
1022
|
+
[...beside[i].keys()].sort().map((r) => ({ at: (k) => parts.get(k)[i] === r })),
|
|
1023
|
+
),
|
|
1024
|
+
...[...new Set(steps.map((s) => s.label))]
|
|
1025
|
+
.filter((l) => !wanted.some((s) => s.label === l))
|
|
1026
|
+
.sort()
|
|
1027
|
+
.map((l) => ({ ends: (s) => s.label === l })),
|
|
1028
|
+
];
|
|
1029
|
+
while (targets.length) {
|
|
1030
|
+
const { cost, parent } = cheapest();
|
|
1031
|
+
let best = null;
|
|
1032
|
+
targets.forEach((t, n) => {
|
|
1033
|
+
const ways = t.at
|
|
1034
|
+
? [...cost.keys()].filter(t.at).map((k) => ({ k, c: cost.get(k), last: null }))
|
|
1035
|
+
: steps
|
|
1036
|
+
.filter(t.ends)
|
|
1037
|
+
.filter((s) => cost.has(bar(s.from)))
|
|
1038
|
+
.map((s) => ({ k: bar(s.from), c: cost.get(bar(s.from)) + price(s), last: s }));
|
|
1039
|
+
for (const w of ways) if (!best || w.c < best.c) best = { ...w, n };
|
|
1040
|
+
});
|
|
1041
|
+
if (!best) break;
|
|
1042
|
+
targets.splice(best.n, 1);
|
|
1043
|
+
const path = best.last ? [best.last] : [];
|
|
1044
|
+
for (let s = parent.get(best.k); s; s = parent.get(bar(s.from))) path.push(s);
|
|
1045
|
+
path.forEach(want);
|
|
1046
|
+
}
|
|
1047
|
+
const takes = (x, label) =>
|
|
1048
|
+
[0, 1].flatMap((i) =>
|
|
1049
|
+
[...moves[i]]
|
|
1050
|
+
.filter(([, m]) => m.from === x[i] && m.label === label && m.beside.has(x[1 - i]))
|
|
1051
|
+
.map(([id]) => [i, id]),
|
|
1052
|
+
);
|
|
1053
|
+
let scoped;
|
|
1054
|
+
let made;
|
|
1055
|
+
for (;;) {
|
|
1056
|
+
scoped = draw(keep);
|
|
1057
|
+
made = productOf(parseMermaid(scoped.mermaid).regions);
|
|
1058
|
+
const madeKeys = new Set(made.transitions.map((t) => key(t.from, t.label, t.to)));
|
|
1059
|
+
const redo = [
|
|
1060
|
+
...made.transitions
|
|
1061
|
+
.filter((t) => parts.has(t.from) && !real.has(key(t.from, t.label, t.to)))
|
|
1062
|
+
.map((t) => [parts.get(t.from), t.label]),
|
|
1063
|
+
...wanted
|
|
1064
|
+
.filter((s) => made.states.has(bar(s.from)))
|
|
1065
|
+
.filter((s) => !madeKeys.has(key(bar(s.from), s.label, bar(s.to))))
|
|
1066
|
+
.map((s) => [s.from, s.label]),
|
|
1067
|
+
];
|
|
1068
|
+
const before = keep[0].size + keep[1].size;
|
|
1069
|
+
for (const [x, label] of redo) for (const [i, id] of takes(x, label)) keep[i].add(id);
|
|
1070
|
+
if (keep[0].size + keep[1].size === before) break;
|
|
1071
|
+
}
|
|
1072
|
+
const sound =
|
|
1073
|
+
[...made.states].every((s) => parts.has(s)) &&
|
|
1074
|
+
made.transitions.every((t) => real.has(key(t.from, t.label, t.to)));
|
|
1075
|
+
if (!sound) scoped = all;
|
|
1076
|
+
|
|
1077
|
+
return {
|
|
1078
|
+
mermaid: scoped.mermaid,
|
|
1079
|
+
states: [beside[0].size, beside[1].size],
|
|
1080
|
+
transitions: scoped.count,
|
|
1081
|
+
all: all.count,
|
|
1082
|
+
flat: { states: nodes.length, transitions: steps.length },
|
|
1083
|
+
status: Object.fromEntries(
|
|
1084
|
+
rank.map((s) => [
|
|
1085
|
+
s,
|
|
1086
|
+
regionStatus.reduce((n, m) => n + [...m.values()].filter((x) => x === s).length, 0),
|
|
1087
|
+
]),
|
|
1088
|
+
),
|
|
1089
|
+
groups: groupCounts(drawn.flat()),
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
const regions = args.regions
|
|
1093
|
+
? regionsChart({
|
|
1094
|
+
states: allNodes,
|
|
1095
|
+
starts: [...initial],
|
|
1096
|
+
edges,
|
|
1097
|
+
classes,
|
|
1098
|
+
proofPath: args.proof,
|
|
1099
|
+
allPath: args.regionsAll,
|
|
1100
|
+
})
|
|
1101
|
+
: null;
|
|
1102
|
+
if (regions?.why) console.error(`regions: not drawn, because ${regions.why}`);
|
|
1103
|
+
|
|
1104
|
+
const flatStates =
|
|
1105
|
+
args.pathOnly && chosen
|
|
1106
|
+
? onPath
|
|
1107
|
+
: coverEdges
|
|
1108
|
+
? new Set([
|
|
1109
|
+
...initial,
|
|
1110
|
+
...[...coverEdges].flatMap((id) => [edges.get(id).from, edges.get(id).to]),
|
|
1111
|
+
])
|
|
1112
|
+
: allNodes;
|
|
1113
|
+
const sorted = [...flatStates].sort();
|
|
1114
|
+
const drawn = sorted.map((k) => drawnState(groupOf(k), status(k), onPath.has(k)));
|
|
1115
|
+
|
|
1116
|
+
const boxes = new Map();
|
|
1117
|
+
const boxOf = new Map();
|
|
1118
|
+
const inBox = (k, box) => {
|
|
1119
|
+
if (!boxes.has(box)) boxes.set(box, []);
|
|
1120
|
+
boxes.get(box).push(k);
|
|
1121
|
+
};
|
|
1122
|
+
if (args.view === "layered") for (const k of sorted) inBox(k, k.split(SEP)[0]);
|
|
1123
|
+
else if (args.view === "customer" && !args.flat) {
|
|
1124
|
+
for (const k of sorted) {
|
|
1125
|
+
const at = k.indexOf(": ");
|
|
1126
|
+
if (at > 0 && at + 2 < k.length) inBox(k, k.slice(0, at));
|
|
1127
|
+
}
|
|
1128
|
+
for (const [box, members] of boxes) if (members.length < 2) boxes.delete(box);
|
|
1129
|
+
}
|
|
1130
|
+
const boxIds = new Map([...boxes.keys()].map((box, i) => [box, `g${i}`]));
|
|
1131
|
+
for (const [box, members] of boxes) for (const k of members) boxOf.set(k, box);
|
|
1132
|
+
const declared = [...[...boxes.values()].flat(), ...sorted.filter((k) => !boxOf.has(k))];
|
|
1133
|
+
const stateIds = new Map(declared.map((k, i) => [k, `s${i}`]));
|
|
1134
|
+
const idOf = (k) => stateIds.get(k);
|
|
1135
|
+
const caption = (k) =>
|
|
1136
|
+
args.view === "layered" ? k.split(SEP)[1] : boxOf.has(k) ? k.slice(boxOf.get(k).length + 2) : k;
|
|
1137
|
+
|
|
1138
|
+
function numbered(steps) {
|
|
1139
|
+
const runs = [];
|
|
1140
|
+
for (const { n, shown } of steps) {
|
|
1141
|
+
const last = runs.at(-1);
|
|
1142
|
+
if (last?.shown === shown && last.n.at(-1) === n - 1) last.n.push(n);
|
|
1143
|
+
else runs.push({ shown, n: [n] });
|
|
1144
|
+
}
|
|
1145
|
+
return runs.map(({ shown, n }) => `${n.join(", ")}. ${shown}`);
|
|
1146
|
+
}
|
|
1147
|
+
const drawnLabels = new Set();
|
|
1148
|
+
const arrows = new Map();
|
|
1149
|
+
for (const [id, e] of edges) {
|
|
1150
|
+
if (!flatStates.has(e.from) || !flatStates.has(e.to)) continue;
|
|
1151
|
+
if (args.pathOnly && chosen && !pathNumbers.has(id) && id !== chosen.second?.edge) continue;
|
|
1152
|
+
if (coverEdges && !coverEdges.has(id)) continue;
|
|
1153
|
+
const pair = edgeId(e.from, e.to);
|
|
1154
|
+
if (!arrows.has(pair))
|
|
1155
|
+
arrows.set(pair, { from: e.from, to: e.to, groups: [[], [], []], plain: true, labels: [] });
|
|
1156
|
+
const a = arrows.get(pair);
|
|
1157
|
+
const [numbers, current, other] = a.groups;
|
|
1158
|
+
const all = [...e.labels];
|
|
1159
|
+
const shown = all.map((l) => guarded(e.guards, l));
|
|
1160
|
+
const said = args.fullLabels ? shown.join(" or ") : joinLabels(shown);
|
|
1161
|
+
const p = pathNumbers.get(id);
|
|
1162
|
+
const chosenStep = chosen?.second && id === chosen.second.edge;
|
|
1163
|
+
if (p) numbers.push(...numbered(p));
|
|
1164
|
+
else if (!e.secondOnly) current.push(said);
|
|
1165
|
+
else other.push(args.pathOnly && chosenStep ? chosen.second.text : said);
|
|
1166
|
+
if (chosenStep && !e.secondOnly) other.push(chosen.second.text);
|
|
1167
|
+
if (p || chosenStep || id.endsWith(`${SEP}second`)) a.plain = false;
|
|
1168
|
+
a.labels.push(...all);
|
|
1169
|
+
for (const l of p ? p.map((s) => s.label) : all) drawnLabels.add(l);
|
|
1170
|
+
}
|
|
1171
|
+
const transitions = [...arrows.values()].map(({ from, to, groups, plain, labels }) => ({
|
|
1172
|
+
from,
|
|
1173
|
+
to,
|
|
1174
|
+
// ";" ends a Mermaid statement, even inside a transition label.
|
|
1175
|
+
text: groups
|
|
1176
|
+
.map((g) =>
|
|
1177
|
+
g
|
|
1178
|
+
.filter(Boolean)
|
|
1179
|
+
.map((l) => wrap(l.replaceAll(";", ","), 30))
|
|
1180
|
+
.join("<br/>"),
|
|
1181
|
+
)
|
|
1182
|
+
.filter(Boolean)
|
|
1183
|
+
.join("<br/>#160;<br/>"),
|
|
1184
|
+
plain,
|
|
1185
|
+
labels: labels.sort(),
|
|
1186
|
+
}));
|
|
1187
|
+
const fromBox = [];
|
|
1188
|
+
const folded = new Set();
|
|
1189
|
+
for (const [box, members] of boxes) {
|
|
1190
|
+
const mine = new Set(members);
|
|
1191
|
+
const byStep = new Map();
|
|
1192
|
+
for (const l of transitions) {
|
|
1193
|
+
if (!l.plain || !mine.has(l.from) || mine.has(l.to)) continue;
|
|
1194
|
+
const step = `${l.to}${SEP}${l.text}${SEP}${l.labels.join(SEP)}`;
|
|
1195
|
+
if (!byStep.has(step)) byStep.set(step, []);
|
|
1196
|
+
byStep.get(step).push(l);
|
|
1197
|
+
}
|
|
1198
|
+
for (const same of byStep.values()) {
|
|
1199
|
+
if (new Set(same.map((l) => l.from)).size !== members.length) continue;
|
|
1200
|
+
for (const l of same) folded.add(l);
|
|
1201
|
+
fromBox.push({ ...same[0], box });
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const out = head(drawn);
|
|
1206
|
+
for (const [box, members] of boxes) {
|
|
1207
|
+
out.push(` state "${quote(wrap(box))}" as ${boxIds.get(box)} {`);
|
|
1208
|
+
for (const k of members) out.push(` state "${quote(wrap(caption(k)))}" as ${idOf(k)}`);
|
|
1209
|
+
out.push(" }");
|
|
1210
|
+
}
|
|
1211
|
+
for (const k of sorted)
|
|
1212
|
+
if (!boxOf.has(k)) out.push(` state "${quote(wrap(caption(k)))}" as ${idOf(k)}`);
|
|
1213
|
+
for (const k of sorted)
|
|
1214
|
+
if (initial.has(k) && (!args.pathOnly || k === chosen?.path[0].key))
|
|
1215
|
+
out.push(` [*] --> ${idOf(k)}`);
|
|
1216
|
+
const arrow = (from, l) => ` ${from} --> ${idOf(l.to)}${l.text ? ` : ${l.text}` : ""}`;
|
|
1217
|
+
for (const l of fromBox) out.push(arrow(boxIds.get(l.box), l));
|
|
1218
|
+
for (const l of transitions) if (!folded.has(l)) out.push(arrow(idOf(l.from), l));
|
|
1219
|
+
sorted.forEach((k, i) => out.push(` class ${idOf(k)} ${drawn[i].cls}`));
|
|
1220
|
+
const mermaid = regions?.mermaid ?? out.join("\n") + "\n";
|
|
1221
|
+
if (args.out) fs.writeFileSync(args.out, mermaid);
|
|
1222
|
+
else process.stdout.write(mermaid);
|
|
1223
|
+
|
|
1224
|
+
const show = (k) => (args.view === "layered" ? k.replace(SEP, ": ") : k);
|
|
1225
|
+
const ruleOf = (c) => rules.get(c.invariant) ?? `\`${c.invariant}\``;
|
|
1226
|
+
function secondLine(c) {
|
|
1227
|
+
const W = cap(within);
|
|
1228
|
+
if (c.introduced) return `Only ${L} has this bug. The steps above happen ${within} ${L}.`;
|
|
1229
|
+
const s = c.second;
|
|
1230
|
+
if (!s) return null;
|
|
1231
|
+
if (s.sameEnd)
|
|
1232
|
+
return s.ruleHolds
|
|
1233
|
+
? `${W} ${L} you see the same screens and steps, yet the rule holds, so the state labels do not show what ${L} changes.`
|
|
1234
|
+
: `${W} ${L} you see the same screens and steps, so ${L} does not change this path.`;
|
|
1235
|
+
if (s.reason) return `${cap(L)} cannot take these steps: it ${s.reason}.`;
|
|
1236
|
+
const outcome = s.disabled
|
|
1237
|
+
? "cannot happen, so you stay at"
|
|
1238
|
+
: s.key === s.from
|
|
1239
|
+
? "keeps you at"
|
|
1240
|
+
: "takes you to";
|
|
1241
|
+
if (s.hidden) {
|
|
1242
|
+
const when = s.step ? `after step ${s.step}` : "before your first step";
|
|
1243
|
+
return `${W} ${L}, ${when}, "${s.said}" happens and ${outcome}: ${show(s.key)}.`;
|
|
1244
|
+
}
|
|
1245
|
+
return `${W} ${L}, step ${s.step} (${s.said}) ${outcome}: ${show(s.key)}.`;
|
|
1246
|
+
}
|
|
1247
|
+
if (args.story) {
|
|
1248
|
+
const lines = [];
|
|
1249
|
+
const header = role === "fix" ? `New with ${L}: ` : `Only in ${L}: `;
|
|
1250
|
+
classes.forEach((c, i) => {
|
|
1251
|
+
lines.push(`## ${i + 1}. ${c.introduced ? header : ""}${show(c.end)}`, "");
|
|
1252
|
+
lines.push(`Rule broken: ${ruleOf(c)}`);
|
|
1253
|
+
if (c.cause) lines.push(`Why: ${c.cause.replace(/\.$/, "")}.`);
|
|
1254
|
+
lines.push(
|
|
1255
|
+
c.count === 1
|
|
1256
|
+
? "This is the only way to get here in the model."
|
|
1257
|
+
: `There are ${c.count} ways to get here in the model. This is the shortest.`,
|
|
1258
|
+
"",
|
|
1259
|
+
);
|
|
1260
|
+
c.path.forEach((p, j) =>
|
|
1261
|
+
lines.push(
|
|
1262
|
+
j === 0 ? `0. Start: ${show(p.key)}` : `${j}. ${cap(p.label)}. Now: ${show(p.key)}`,
|
|
1263
|
+
),
|
|
1264
|
+
);
|
|
1265
|
+
lines.push("");
|
|
1266
|
+
const said = secondLine(c);
|
|
1267
|
+
if (said) lines.push(said, "");
|
|
1268
|
+
});
|
|
1269
|
+
fs.writeFileSync(args.story, lines.join("\n"));
|
|
1270
|
+
}
|
|
1271
|
+
const CODE_LIKE = /\b[a-z]{2,}[A-Z][a-z]|\b\w+_\w+\b|\b[A-Za-z_]\w*\(|\b[A-Z][a-z]+[A-Z][a-z]/;
|
|
1272
|
+
const codeLike = [
|
|
1273
|
+
...new Set([
|
|
1274
|
+
...(args.view === "customer" ? [...sorted, ...drawnLabels] : []),
|
|
1275
|
+
...classes.map((c) => c.cause).filter(Boolean),
|
|
1276
|
+
]),
|
|
1277
|
+
].filter((t) => CODE_LIKE.test(t));
|
|
1278
|
+
const secondOf = (c) =>
|
|
1279
|
+
c.introduced
|
|
1280
|
+
? role === "fix"
|
|
1281
|
+
? `new with ${L}`
|
|
1282
|
+
: `only in ${L}`
|
|
1283
|
+
: c.second
|
|
1284
|
+
? c.second.sameEnd
|
|
1285
|
+
? c.second.ruleHolds
|
|
1286
|
+
? "same screens, but the rule holds"
|
|
1287
|
+
: "unchanged"
|
|
1288
|
+
: (c.second.reason ?? show(c.second.key))
|
|
1289
|
+
: null;
|
|
1290
|
+
const summary = {
|
|
1291
|
+
view: args.view,
|
|
1292
|
+
...(role ? { second: { role, label: L, lacks: lacks.map((p) => p.join(".")) } } : {}),
|
|
1293
|
+
modelStates: graph.nodes.size,
|
|
1294
|
+
modelTransitions: graph.edges.length,
|
|
1295
|
+
chartStates: regions?.mermaid ? regions.states[0] + regions.states[1] : flatStates.size,
|
|
1296
|
+
chartTransitions: regions?.mermaid ? regions.transitions : transitions.length,
|
|
1297
|
+
chartStatus: regions?.mermaid
|
|
1298
|
+
? regions.status
|
|
1299
|
+
: Object.fromEntries(
|
|
1300
|
+
["works", "wrong", "fixed"].map((s) => [
|
|
1301
|
+
s,
|
|
1302
|
+
[...flatStates].filter((k) => status(k) === s).length,
|
|
1303
|
+
]),
|
|
1304
|
+
),
|
|
1305
|
+
...(grouping
|
|
1306
|
+
? {
|
|
1307
|
+
chartGroups: regions?.mermaid ? regions.groups : groupCounts(drawn),
|
|
1308
|
+
mixedGroups: mixedGroups.map(show).sort(),
|
|
1309
|
+
}
|
|
1310
|
+
: {}),
|
|
1311
|
+
...(regions
|
|
1312
|
+
? {
|
|
1313
|
+
regions: regions.mermaid
|
|
1314
|
+
? {
|
|
1315
|
+
drawn: true,
|
|
1316
|
+
states: regions.states,
|
|
1317
|
+
transitions: regions.transitions,
|
|
1318
|
+
all: regions.all,
|
|
1319
|
+
flat: regions.flat,
|
|
1320
|
+
}
|
|
1321
|
+
: { drawn: false, why: regions.why },
|
|
1322
|
+
}
|
|
1323
|
+
: {}),
|
|
1324
|
+
classes: classes.map((c, i) => ({
|
|
1325
|
+
n: i + 1,
|
|
1326
|
+
end: show(c.end),
|
|
1327
|
+
invariant: c.invariant,
|
|
1328
|
+
rule: rules.get(c.invariant) ?? null,
|
|
1329
|
+
cause: c.cause,
|
|
1330
|
+
readerSteps: c.path.length - 1,
|
|
1331
|
+
modelSteps: c.trace.steps.length,
|
|
1332
|
+
counterexamples: c.count,
|
|
1333
|
+
[role ?? "fix"]: secondOf(c),
|
|
1334
|
+
})),
|
|
1335
|
+
codeLike,
|
|
1336
|
+
};
|
|
1337
|
+
if (args.summary) fs.writeFileSync(args.summary, JSON.stringify(summary, null, 2) + "\n");
|
|
1338
|
+
const extraBugs = introduced.length
|
|
1339
|
+
? `, ${introduced.length} ${role === "fix" ? `new with ${L}` : `only in ${L}`}`
|
|
1340
|
+
: "";
|
|
1341
|
+
const drawnAs = regions?.mermaid
|
|
1342
|
+
? ` of ${regions.all} in 2 regions (proved against ${regions.flat.states} states, ${regions.flat.transitions} transitions)`
|
|
1343
|
+
: "";
|
|
1344
|
+
console.error(
|
|
1345
|
+
`chart: ${summary.chartStates} states, ${summary.chartTransitions} transitions${drawnAs} from ${summary.modelStates} model states; ` +
|
|
1346
|
+
`${mainClasses.length} bug(s)${extraBugs}`,
|
|
1347
|
+
);
|