tinybeeai 0.0.0-stage → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.railway/railway.ts +83 -0
- package/.railway/tsconfig.json +13 -0
- package/AGENTS.md +102 -0
- package/Dockerfile +108 -0
- package/LICENSE +21 -0
- package/README.md +275 -2
- package/bin/localdost.js +29 -0
- package/bin/tinybeeai.js +28 -0
- package/compose.yml +63 -0
- package/demo/payloads/ci_failure.json +46 -0
- package/demo/payloads/outage.json +18 -0
- package/demo/payloads/pr_comment_edited.json +50 -0
- package/demo/payloads/pr_merged.json +80 -0
- package/demo/payloads/pr_opened.json +69 -0
- package/demo/payloads/pr_synchronize.json +69 -0
- package/demo/payloads/ticket_created.json +138 -0
- package/demo/sample-app/playwright.config.ts +20 -0
- package/demo/sample-app/server.ts +163 -0
- package/demo/sample-app/tests/checkout.spec.ts +13 -0
- package/demo/sample-app/tests/smoke.spec.ts +8 -0
- package/dist/agent/approval-retries.js +59 -0
- package/dist/agent/approval-retries.js.map +1 -0
- package/dist/agent/approvals.js +390 -0
- package/dist/agent/approvals.js.map +1 -0
- package/dist/agent/chat-bridge.js +154 -0
- package/dist/agent/chat-bridge.js.map +1 -0
- package/dist/agent/chat-lead.js +475 -0
- package/dist/agent/chat-lead.js.map +1 -0
- package/dist/agent/chat-loop.js +106 -0
- package/dist/agent/chat-loop.js.map +1 -0
- package/dist/agent/chat-mcp.js +49 -0
- package/dist/agent/chat-mcp.js.map +1 -0
- package/dist/agent/chat-tool-blocks.js +453 -0
- package/dist/agent/chat-tool-blocks.js.map +1 -0
- package/dist/agent/chat-view.js +94 -0
- package/dist/agent/chat-view.js.map +1 -0
- package/dist/agent/chat-views.js +781 -0
- package/dist/agent/chat-views.js.map +1 -0
- package/dist/agent/chat.js +1703 -0
- package/dist/agent/chat.js.map +1 -0
- package/dist/agent/destinations.js +150 -0
- package/dist/agent/destinations.js.map +1 -0
- package/dist/agent/llm-cli.js +451 -0
- package/dist/agent/llm-cli.js.map +1 -0
- package/dist/agent/llm-codex.js +493 -0
- package/dist/agent/llm-codex.js.map +1 -0
- package/dist/agent/llm-openai.js +211 -0
- package/dist/agent/llm-openai.js.map +1 -0
- package/dist/agent/llm.js +1464 -0
- package/dist/agent/llm.js.map +1 -0
- package/dist/agent/memory.js +33 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/mock-views.js +93 -0
- package/dist/agent/mock-views.js.map +1 -0
- package/dist/agent/model-availability.js +207 -0
- package/dist/agent/model-availability.js.map +1 -0
- package/dist/agent/model-errors.js +188 -0
- package/dist/agent/model-errors.js.map +1 -0
- package/dist/agent/model-guard.js +311 -0
- package/dist/agent/model-guard.js.map +1 -0
- package/dist/agent/prompts.js +374 -0
- package/dist/agent/prompts.js.map +1 -0
- package/dist/agent/qa-lenses/qa-auth.md +32 -0
- package/dist/agent/qa-lenses/qa-check-then-act.md +33 -0
- package/dist/agent/qa-lenses/qa-control-has-no-effect.md +32 -0
- package/dist/agent/qa-lenses/qa-dates.md +31 -0
- package/dist/agent/qa-lenses/qa-double-submit.md +34 -0
- package/dist/agent/qa-lenses/qa-feature-flag.md +29 -0
- package/dist/agent/qa-lenses/qa-migration.md +35 -0
- package/dist/agent/qa-lenses/qa-money.md +38 -0
- package/dist/agent/qa-lenses/qa-response-shape-break.md +33 -0
- package/dist/agent/qa-lenses/qa-validation.md +29 -0
- package/dist/agent/qa-prompts.js +660 -0
- package/dist/agent/qa-prompts.js.map +1 -0
- package/dist/agent/reasoning.js +55 -0
- package/dist/agent/reasoning.js.map +1 -0
- package/dist/agent/redact.js +48 -0
- package/dist/agent/redact.js.map +1 -0
- package/dist/agent/rules.js +927 -0
- package/dist/agent/rules.js.map +1 -0
- package/dist/agent/runs.js +440 -0
- package/dist/agent/runs.js.map +1 -0
- package/dist/agent/send-retry.js +62 -0
- package/dist/agent/send-retry.js.map +1 -0
- package/dist/agent/team-notes.js +288 -0
- package/dist/agent/team-notes.js.map +1 -0
- package/dist/agent/token-meter.js +31 -0
- package/dist/agent/token-meter.js.map +1 -0
- package/dist/agent/triage.js +506 -0
- package/dist/agent/triage.js.map +1 -0
- package/dist/api.js +1047 -0
- package/dist/api.js.map +1 -0
- package/dist/approval-targets.js +107 -0
- package/dist/approval-targets.js.map +1 -0
- package/dist/bee/cloudflared.js +108 -0
- package/dist/bee/cloudflared.js.map +1 -0
- package/dist/bee/health.js +99 -0
- package/dist/bee/health.js.map +1 -0
- package/dist/bee/hive-client.js +75 -0
- package/dist/bee/hive-client.js.map +1 -0
- package/dist/bee/identity.js +39 -0
- package/dist/bee/identity.js.map +1 -0
- package/dist/bee/models.js +510 -0
- package/dist/bee/models.js.map +1 -0
- package/dist/bee/pairing.js +351 -0
- package/dist/bee/pairing.js.map +1 -0
- package/dist/bee/relay-client.js +50 -0
- package/dist/bee/relay-client.js.map +1 -0
- package/dist/bee/routes.js +214 -0
- package/dist/bee/routes.js.map +1 -0
- package/dist/bee/setup-page.js +223 -0
- package/dist/bee/setup-page.js.map +1 -0
- package/dist/bee/state.js +41 -0
- package/dist/bee/state.js.map +1 -0
- package/dist/bee/status.js +117 -0
- package/dist/bee/status.js.map +1 -0
- package/dist/bee/tunnel-manager.js +245 -0
- package/dist/bee/tunnel-manager.js.map +1 -0
- package/dist/bee/updater.js +268 -0
- package/dist/bee/updater.js.map +1 -0
- package/dist/boot.js +41 -0
- package/dist/boot.js.map +1 -0
- package/dist/call/index.js +145 -0
- package/dist/call/index.js.map +1 -0
- package/dist/case-folders.js +294 -0
- package/dist/case-folders.js.map +1 -0
- package/dist/cases.js +554 -0
- package/dist/cases.js.map +1 -0
- package/dist/chat-blocks.js +709 -0
- package/dist/chat-blocks.js.map +1 -0
- package/dist/chat-parse-util.js +38 -0
- package/dist/chat-parse-util.js.map +1 -0
- package/dist/chat-results.js +96 -0
- package/dist/chat-results.js.map +1 -0
- package/dist/chat-stats.js +193 -0
- package/dist/chat-stats.js.map +1 -0
- package/dist/chat-view-specs.js +186 -0
- package/dist/chat-view-specs.js.map +1 -0
- package/dist/cli/doctor-ops.js +361 -0
- package/dist/cli/doctor-ops.js.map +1 -0
- package/dist/cli/doctor.js +289 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/dogfood.js +70 -0
- package/dist/cli/dogfood.js.map +1 -0
- package/dist/cli/env-file.js +34 -0
- package/dist/cli/env-file.js.map +1 -0
- package/dist/cli/eval-chat-cases.js +359 -0
- package/dist/cli/eval-chat-cases.js.map +1 -0
- package/dist/cli/eval-chat-results.js +85 -0
- package/dist/cli/eval-chat-results.js.map +1 -0
- package/dist/cli/eval-chat.js +508 -0
- package/dist/cli/eval-chat.js.map +1 -0
- package/dist/cli/eval-designer.js +67 -0
- package/dist/cli/eval-designer.js.map +1 -0
- package/dist/cli/eval-qa-review.js +284 -0
- package/dist/cli/eval-qa-review.js.map +1 -0
- package/dist/cli/golden-export.js +35 -0
- package/dist/cli/golden-export.js.map +1 -0
- package/dist/cli/index.js +90 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.js +746 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/scan-repo.js +199 -0
- package/dist/cli/scan-repo.js.map +1 -0
- package/dist/cli/seed.js +108 -0
- package/dist/cli/seed.js.map +1 -0
- package/dist/cli/tinybee.js +247 -0
- package/dist/cli/tinybee.js.map +1 -0
- package/dist/config.js +138 -0
- package/dist/config.js.map +1 -0
- package/dist/connect/aivision-local.js +833 -0
- package/dist/connect/aivision-local.js.map +1 -0
- package/dist/connect/aivision-token.js +84 -0
- package/dist/connect/aivision-token.js.map +1 -0
- package/dist/connect/crypto.js +65 -0
- package/dist/connect/crypto.js.map +1 -0
- package/dist/connect/deps.js +110 -0
- package/dist/connect/deps.js.map +1 -0
- package/dist/connect/engines.js +144 -0
- package/dist/connect/engines.js.map +1 -0
- package/dist/connect/github-access.js +74 -0
- package/dist/connect/github-access.js.map +1 -0
- package/dist/connect/github-app.js +129 -0
- package/dist/connect/github-app.js.map +1 -0
- package/dist/connect/index.js +9 -0
- package/dist/connect/index.js.map +1 -0
- package/dist/connect/model-choices.js +201 -0
- package/dist/connect/model-choices.js.map +1 -0
- package/dist/connect/providers.js +900 -0
- package/dist/connect/providers.js.map +1 -0
- package/dist/connect/readiness.js +232 -0
- package/dist/connect/readiness.js.map +1 -0
- package/dist/connect/redact.js +16 -0
- package/dist/connect/redact.js.map +1 -0
- package/dist/connect/registry.js +406 -0
- package/dist/connect/registry.js.map +1 -0
- package/dist/connect/store.js +283 -0
- package/dist/connect/store.js.map +1 -0
- package/dist/connect/tunnel.js +134 -0
- package/dist/connect/tunnel.js.map +1 -0
- package/dist/connect/types.js +8 -0
- package/dist/connect/types.js.map +1 -0
- package/dist/connections/index.js +5 -0
- package/dist/connections/index.js.map +1 -0
- package/dist/connections/manager.js +223 -0
- package/dist/connections/manager.js.map +1 -0
- package/dist/connections/presets.js +264 -0
- package/dist/connections/presets.js.map +1 -0
- package/dist/connections/store.js +132 -0
- package/dist/connections/store.js.map +1 -0
- package/dist/connections/tools.js +206 -0
- package/dist/connections/tools.js.map +1 -0
- package/dist/db-schema.js +355 -0
- package/dist/db-schema.js.map +1 -0
- package/dist/db.js +379 -0
- package/dist/db.js.map +1 -0
- package/dist/delivery.js +49 -0
- package/dist/delivery.js.map +1 -0
- package/dist/deploy-wait.js +701 -0
- package/dist/deploy-wait.js.map +1 -0
- package/dist/disk.js +243 -0
- package/dist/disk.js.map +1 -0
- package/dist/dot-health.js +966 -0
- package/dist/dot-health.js.map +1 -0
- package/dist/dot-remove.js +246 -0
- package/dist/dot-remove.js.map +1 -0
- package/dist/events.js +38 -0
- package/dist/events.js.map +1 -0
- package/dist/evidence-pack.js +423 -0
- package/dist/evidence-pack.js.map +1 -0
- package/dist/flows/answers.js +289 -0
- package/dist/flows/answers.js.map +1 -0
- package/dist/flows/change.js +811 -0
- package/dist/flows/change.js.map +1 -0
- package/dist/flows/checkStatus.js +524 -0
- package/dist/flows/checkStatus.js.map +1 -0
- package/dist/flows/ci.js +236 -0
- package/dist/flows/ci.js.map +1 -0
- package/dist/flows/ciFailure.js +76 -0
- package/dist/flows/ciFailure.js.map +1 -0
- package/dist/flows/common.js +522 -0
- package/dist/flows/common.js.map +1 -0
- package/dist/flows/default-branch.js +29 -0
- package/dist/flows/default-branch.js.map +1 -0
- package/dist/flows/defectDraft.js +447 -0
- package/dist/flows/defectDraft.js.map +1 -0
- package/dist/flows/defects.js +623 -0
- package/dist/flows/defects.js.map +1 -0
- package/dist/flows/digest.js +295 -0
- package/dist/flows/digest.js.map +1 -0
- package/dist/flows/impact.js +82 -0
- package/dist/flows/impact.js.map +1 -0
- package/dist/flows/index.js +23 -0
- package/dist/flows/index.js.map +1 -0
- package/dist/flows/intent-sources.js +72 -0
- package/dist/flows/intent-sources.js.map +1 -0
- package/dist/flows/mergeCases.js +169 -0
- package/dist/flows/mergeCases.js.map +1 -0
- package/dist/flows/nightlyRun.js +82 -0
- package/dist/flows/nightlyRun.js.map +1 -0
- package/dist/flows/outage.js +64 -0
- package/dist/flows/outage.js.map +1 -0
- package/dist/flows/postMerge.js +5 -0
- package/dist/flows/postMerge.js.map +1 -0
- package/dist/flows/prCheck.js +5 -0
- package/dist/flows/prCheck.js.map +1 -0
- package/dist/flows/prTarget.js +66 -0
- package/dist/flows/prTarget.js.map +1 -0
- package/dist/flows/qa.js +363 -0
- package/dist/flows/qa.js.map +1 -0
- package/dist/flows/questions.js +160 -0
- package/dist/flows/questions.js.map +1 -0
- package/dist/flows/recovery.js +64 -0
- package/dist/flows/recovery.js.map +1 -0
- package/dist/flows/rerun.js +50 -0
- package/dist/flows/rerun.js.map +1 -0
- package/dist/flows/testDesign.js +294 -0
- package/dist/flows/testDesign.js.map +1 -0
- package/dist/flows/testDesignJob.js +46 -0
- package/dist/flows/testDesignJob.js.map +1 -0
- package/dist/flows/testRepo.js +228 -0
- package/dist/flows/testRepo.js.map +1 -0
- package/dist/flows/testSavings.js +381 -0
- package/dist/flows/testSavings.js.map +1 -0
- package/dist/flows/testStatus.js +343 -0
- package/dist/flows/testStatus.js.map +1 -0
- package/dist/flows/ticketContext.js +425 -0
- package/dist/flows/ticketContext.js.map +1 -0
- package/dist/flows/ticketReview.js +249 -0
- package/dist/flows/ticketReview.js.map +1 -0
- package/dist/flows/ticks.js +73 -0
- package/dist/flows/ticks.js.map +1 -0
- package/dist/hive/admin.js +122 -0
- package/dist/hive/admin.js.map +1 -0
- package/dist/hive/auth.js +130 -0
- package/dist/hive/auth.js.map +1 -0
- package/dist/hive/billing.js +278 -0
- package/dist/hive/billing.js.map +1 -0
- package/dist/hive/config.js +63 -0
- package/dist/hive/config.js.map +1 -0
- package/dist/hive/db.js +186 -0
- package/dist/hive/db.js.map +1 -0
- package/dist/hive/embed.js +76 -0
- package/dist/hive/embed.js.map +1 -0
- package/dist/hive/errors.js +17 -0
- package/dist/hive/errors.js.map +1 -0
- package/dist/hive/google.js +75 -0
- package/dist/hive/google.js.map +1 -0
- package/dist/hive/index.js +25 -0
- package/dist/hive/index.js.map +1 -0
- package/dist/hive/jwt.js +57 -0
- package/dist/hive/jwt.js.map +1 -0
- package/dist/hive/keys.js +61 -0
- package/dist/hive/keys.js.map +1 -0
- package/dist/hive/mail.js +27 -0
- package/dist/hive/mail.js.map +1 -0
- package/dist/hive/native-push.js +255 -0
- package/dist/hive/native-push.js.map +1 -0
- package/dist/hive/pairing.js +342 -0
- package/dist/hive/pairing.js.map +1 -0
- package/dist/hive/presence.js +121 -0
- package/dist/hive/presence.js.map +1 -0
- package/dist/hive/push.js +185 -0
- package/dist/hive/push.js.map +1 -0
- package/dist/hive/relay.js +205 -0
- package/dist/hive/relay.js.map +1 -0
- package/dist/hive/routes.js +228 -0
- package/dist/hive/routes.js.map +1 -0
- package/dist/hive/server.js +12 -0
- package/dist/hive/server.js.map +1 -0
- package/dist/hive/session.js +28 -0
- package/dist/hive/session.js.map +1 -0
- package/dist/hive/tenants.js +287 -0
- package/dist/hive/tenants.js.map +1 -0
- package/dist/hive/tunnel/cloudflare.js +160 -0
- package/dist/hive/tunnel/cloudflare.js.map +1 -0
- package/dist/hive/tunnel/mock.js +52 -0
- package/dist/hive/tunnel/mock.js.map +1 -0
- package/dist/hive/tunnel/provider.js +19 -0
- package/dist/hive/tunnel/provider.js.map +1 -0
- package/dist/hive/updates.js +61 -0
- package/dist/hive/updates.js.map +1 -0
- package/dist/http-error.js +11 -0
- package/dist/http-error.js.map +1 -0
- package/dist/impact/classify.js +226 -0
- package/dist/impact/classify.js.map +1 -0
- package/dist/impact/find.js +259 -0
- package/dist/impact/find.js.map +1 -0
- package/dist/impact/pr.js +54 -0
- package/dist/impact/pr.js.map +1 -0
- package/dist/impact/store.js +365 -0
- package/dist/impact/store.js.map +1 -0
- package/dist/impact/text.js +158 -0
- package/dist/impact/text.js.map +1 -0
- package/dist/index.js +288 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/adf.js +397 -0
- package/dist/integrations/adf.js.map +1 -0
- package/dist/integrations/bot-marker.js +103 -0
- package/dist/integrations/bot-marker.js.map +1 -0
- package/dist/integrations/github-issues.js +705 -0
- package/dist/integrations/github-issues.js.map +1 -0
- package/dist/integrations/github.js +1284 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations/issue-create.js +60 -0
- package/dist/integrations/issue-create.js.map +1 -0
- package/dist/integrations/jira.js +279 -0
- package/dist/integrations/jira.js.map +1 -0
- package/dist/integrations/linear.js +273 -0
- package/dist/integrations/linear.js.map +1 -0
- package/dist/integrations/own-identity-warm.js +47 -0
- package/dist/integrations/own-identity-warm.js.map +1 -0
- package/dist/integrations/own-identity.js +39 -0
- package/dist/integrations/own-identity.js.map +1 -0
- package/dist/integrations/status-change.js +42 -0
- package/dist/integrations/status-change.js.map +1 -0
- package/dist/lead-defects.js +202 -0
- package/dist/lead-defects.js.map +1 -0
- package/dist/lead-digest.js +333 -0
- package/dist/lead-digest.js.map +1 -0
- package/dist/lead-release.js +286 -0
- package/dist/lead-release.js.map +1 -0
- package/dist/lead.js +328 -0
- package/dist/lead.js.map +1 -0
- package/dist/legacy-names.js +51 -0
- package/dist/legacy-names.js.map +1 -0
- package/dist/limits.js +44 -0
- package/dist/limits.js.map +1 -0
- package/dist/link-trust.js +224 -0
- package/dist/link-trust.js.map +1 -0
- package/dist/links.js +219 -0
- package/dist/links.js.map +1 -0
- package/dist/loopback.js +35 -0
- package/dist/loopback.js.map +1 -0
- package/dist/model-cap.js +60 -0
- package/dist/model-cap.js.map +1 -0
- package/dist/model-usage.js +173 -0
- package/dist/model-usage.js.map +1 -0
- package/dist/monitor/backfill.js +39 -0
- package/dist/monitor/backfill.js.map +1 -0
- package/dist/monitor/browser-probe.js +198 -0
- package/dist/monitor/browser-probe.js.map +1 -0
- package/dist/monitor/catch-up.js +31 -0
- package/dist/monitor/catch-up.js.map +1 -0
- package/dist/monitor/pollers.js +1606 -0
- package/dist/monitor/pollers.js.map +1 -0
- package/dist/monitor/probes.js +178 -0
- package/dist/monitor/probes.js.map +1 -0
- package/dist/monitor/self-health.js +228 -0
- package/dist/monitor/self-health.js.map +1 -0
- package/dist/monitor/statuspage.js +105 -0
- package/dist/monitor/statuspage.js.map +1 -0
- package/dist/nightly-scheduler.js +127 -0
- package/dist/nightly-scheduler.js.map +1 -0
- package/dist/nightly.js +141 -0
- package/dist/nightly.js.map +1 -0
- package/dist/notify/headline.js +114 -0
- package/dist/notify/headline.js.map +1 -0
- package/dist/notify/push.js +174 -0
- package/dist/notify/push.js.map +1 -0
- package/dist/object-cards.js +585 -0
- package/dist/object-cards.js.map +1 -0
- package/dist/object-links.js +47 -0
- package/dist/object-links.js.map +1 -0
- package/dist/onboarding-checks.js +141 -0
- package/dist/onboarding-checks.js.map +1 -0
- package/dist/onboarding.js +401 -0
- package/dist/onboarding.js.map +1 -0
- package/dist/path-filters.js +62 -0
- package/dist/path-filters.js.map +1 -0
- package/dist/phone/alerts.js +67 -0
- package/dist/phone/alerts.js.map +1 -0
- package/dist/phone/auth.js +240 -0
- package/dist/phone/auth.js.map +1 -0
- package/dist/phone/pairing.js +442 -0
- package/dist/phone/pairing.js.map +1 -0
- package/dist/phone/prefs.js +27 -0
- package/dist/phone/prefs.js.map +1 -0
- package/dist/phone/tailscale.js +161 -0
- package/dist/phone/tailscale.js.map +1 -0
- package/dist/pr-scope.js +441 -0
- package/dist/pr-scope.js.map +1 -0
- package/dist/proc.js +156 -0
- package/dist/proc.js.map +1 -0
- package/dist/profile/cache.js +108 -0
- package/dist/profile/cache.js.map +1 -0
- package/dist/profile/ci.js +216 -0
- package/dist/profile/ci.js.map +1 -0
- package/dist/profile/index.js +8 -0
- package/dist/profile/index.js.map +1 -0
- package/dist/profile/job.js +135 -0
- package/dist/profile/job.js.map +1 -0
- package/dist/profile/queries/go-tags.scm +47 -0
- package/dist/profile/queries/java-tags.scm +25 -0
- package/dist/profile/queries/javascript-tags.scm +104 -0
- package/dist/profile/queries/python-tags.scm +19 -0
- package/dist/profile/queries/typescript-tags.scm +28 -0
- package/dist/profile/scan-thread.js +42 -0
- package/dist/profile/scan-thread.js.map +1 -0
- package/dist/profile/scan-worker.js +16 -0
- package/dist/profile/scan-worker.js.map +1 -0
- package/dist/profile/scan.js +351 -0
- package/dist/profile/scan.js.map +1 -0
- package/dist/profile/stack.js +528 -0
- package/dist/profile/stack.js.map +1 -0
- package/dist/profile/store.js +35 -0
- package/dist/profile/store.js.map +1 -0
- package/dist/profile/tests.js +399 -0
- package/dist/profile/tests.js.map +1 -0
- package/dist/queue.js +350 -0
- package/dist/queue.js.map +1 -0
- package/dist/refs.js +756 -0
- package/dist/refs.js.map +1 -0
- package/dist/release-notes.js +272 -0
- package/dist/release-notes.js.map +1 -0
- package/dist/reports/build.js +243 -0
- package/dist/reports/build.js.map +1 -0
- package/dist/reports/index.js +211 -0
- package/dist/reports/index.js.map +1 -0
- package/dist/reports/store.js +150 -0
- package/dist/reports/store.js.map +1 -0
- package/dist/reports/ticket-titles.js +124 -0
- package/dist/reports/ticket-titles.js.map +1 -0
- package/dist/reports/trace-map.js +1278 -0
- package/dist/reports/trace-map.js.map +1 -0
- package/dist/reports/traceability.js +422 -0
- package/dist/reports/traceability.js.map +1 -0
- package/dist/repos/mirror.js +356 -0
- package/dist/repos/mirror.js.map +1 -0
- package/dist/repos-github.js +108 -0
- package/dist/repos-github.js.map +1 -0
- package/dist/repos.js +891 -0
- package/dist/repos.js.map +1 -0
- package/dist/review/backend-anthropic.js +90 -0
- package/dist/review/backend-anthropic.js.map +1 -0
- package/dist/review/backend-cli.js +248 -0
- package/dist/review/backend-cli.js.map +1 -0
- package/dist/review/backend-codex.js +81 -0
- package/dist/review/backend-codex.js.map +1 -0
- package/dist/review/backend-mock.js +686 -0
- package/dist/review/backend-mock.js.map +1 -0
- package/dist/review/backend-only.js +168 -0
- package/dist/review/backend-only.js.map +1 -0
- package/dist/review/backend.js +105 -0
- package/dist/review/backend.js.map +1 -0
- package/dist/review/baseline/render-v1.js +483 -0
- package/dist/review/baseline/render-v1.js.map +1 -0
- package/dist/review/change-summary.js +197 -0
- package/dist/review/change-summary.js.map +1 -0
- package/dist/review/changelog.js +40 -0
- package/dist/review/changelog.js.map +1 -0
- package/dist/review/check.js +494 -0
- package/dist/review/check.js.map +1 -0
- package/dist/review/checkout.js +185 -0
- package/dist/review/checkout.js.map +1 -0
- package/dist/review/classify.js +178 -0
- package/dist/review/classify.js.map +1 -0
- package/dist/review/coverage.js +347 -0
- package/dist/review/coverage.js.map +1 -0
- package/dist/review/decision-eval.js +121 -0
- package/dist/review/decision-eval.js.map +1 -0
- package/dist/review/deliver.js +104 -0
- package/dist/review/deliver.js.map +1 -0
- package/dist/review/designer-eval.js +280 -0
- package/dist/review/designer-eval.js.map +1 -0
- package/dist/review/dogfood-digest.js +136 -0
- package/dist/review/dogfood-digest.js.map +1 -0
- package/dist/review/dogfood-eval.js +486 -0
- package/dist/review/dogfood-eval.js.map +1 -0
- package/dist/review/environment.js +109 -0
- package/dist/review/environment.js.map +1 -0
- package/dist/review/eval.js +405 -0
- package/dist/review/eval.js.map +1 -0
- package/dist/review/glob.js +110 -0
- package/dist/review/glob.js.map +1 -0
- package/dist/review/golden-export.js +219 -0
- package/dist/review/golden-export.js.map +1 -0
- package/dist/review/impact.js +406 -0
- package/dist/review/impact.js.map +1 -0
- package/dist/review/imports.js +542 -0
- package/dist/review/imports.js.map +1 -0
- package/dist/review/index.js +512 -0
- package/dist/review/index.js.map +1 -0
- package/dist/review/intent.js +128 -0
- package/dist/review/intent.js.map +1 -0
- package/dist/review/lenses.js +167 -0
- package/dist/review/lenses.js.map +1 -0
- package/dist/review/linked-tickets.js +293 -0
- package/dist/review/linked-tickets.js.map +1 -0
- package/dist/review/merge.js +370 -0
- package/dist/review/merge.js.map +1 -0
- package/dist/review/on-demand.js +218 -0
- package/dist/review/on-demand.js.map +1 -0
- package/dist/review/prompt-eval.js +299 -0
- package/dist/review/prompt-eval.js.map +1 -0
- package/dist/review/prompt-variants.js +37 -0
- package/dist/review/prompt-variants.js.map +1 -0
- package/dist/review/readability-eval.js +217 -0
- package/dist/review/readability-eval.js.map +1 -0
- package/dist/review/readability.js +295 -0
- package/dist/review/readability.js.map +1 -0
- package/dist/review/render.js +2409 -0
- package/dist/review/render.js.map +1 -0
- package/dist/review/repo.js +194 -0
- package/dist/review/repo.js.map +1 -0
- package/dist/review/report-plan.js +90 -0
- package/dist/review/report-plan.js.map +1 -0
- package/dist/review/routes.js +195 -0
- package/dist/review/routes.js.map +1 -0
- package/dist/review/run-summary.js +34 -0
- package/dist/review/run-summary.js.map +1 -0
- package/dist/review/run.js +245 -0
- package/dist/review/run.js.map +1 -0
- package/dist/review/scope.js +454 -0
- package/dist/review/scope.js.map +1 -0
- package/dist/review/security.js +432 -0
- package/dist/review/security.js.map +1 -0
- package/dist/review/signals.js +550 -0
- package/dist/review/signals.js.map +1 -0
- package/dist/review/specs.js +150 -0
- package/dist/review/specs.js.map +1 -0
- package/dist/review/ste.js +133 -0
- package/dist/review/ste.js.map +1 -0
- package/dist/review/test-repo-file.js +18 -0
- package/dist/review/test-repo-file.js.map +1 -0
- package/dist/review/test-repo-lines.js +53 -0
- package/dist/review/test-repo-lines.js.map +1 -0
- package/dist/review/ticket-on-demand.js +232 -0
- package/dist/review/ticket-on-demand.js.map +1 -0
- package/dist/review/yaml.js +298 -0
- package/dist/review/yaml.js.map +1 -0
- package/dist/room.js +361 -0
- package/dist/room.js.map +1 -0
- package/dist/routes/activity.js +219 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/call.js +29 -0
- package/dist/routes/call.js.map +1 -0
- package/dist/routes/cards.js +40 -0
- package/dist/routes/cards.js.map +1 -0
- package/dist/routes/cases.js +133 -0
- package/dist/routes/cases.js.map +1 -0
- package/dist/routes/connect.js +268 -0
- package/dist/routes/connect.js.map +1 -0
- package/dist/routes/connection-presets.js +32 -0
- package/dist/routes/connection-presets.js.map +1 -0
- package/dist/routes/connections.js +177 -0
- package/dist/routes/connections.js.map +1 -0
- package/dist/routes/disk.js +19 -0
- package/dist/routes/disk.js.map +1 -0
- package/dist/routes/docs.js +71 -0
- package/dist/routes/docs.js.map +1 -0
- package/dist/routes/dot-suggest.js +1002 -0
- package/dist/routes/dot-suggest.js.map +1 -0
- package/dist/routes/evidence-pack.js +16 -0
- package/dist/routes/evidence-pack.js.map +1 -0
- package/dist/routes/health.js +121 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/impact.js +48 -0
- package/dist/routes/impact.js.map +1 -0
- package/dist/routes/lead.js +50 -0
- package/dist/routes/lead.js.map +1 -0
- package/dist/routes/links.js +13 -0
- package/dist/routes/links.js.map +1 -0
- package/dist/routes/markdown.js +62 -0
- package/dist/routes/markdown.js.map +1 -0
- package/dist/routes/onboarding.js +47 -0
- package/dist/routes/onboarding.js.map +1 -0
- package/dist/routes/phone.js +231 -0
- package/dist/routes/phone.js.map +1 -0
- package/dist/routes/release-notes.js +30 -0
- package/dist/routes/release-notes.js.map +1 -0
- package/dist/routes/reports.js +102 -0
- package/dist/routes/reports.js.map +1 -0
- package/dist/routes/repos.js +91 -0
- package/dist/routes/repos.js.map +1 -0
- package/dist/routes/review.js +283 -0
- package/dist/routes/review.js.map +1 -0
- package/dist/routes/roles.js +114 -0
- package/dist/routes/roles.js.map +1 -0
- package/dist/routes/room.js +21 -0
- package/dist/routes/room.js.map +1 -0
- package/dist/routes/runner.js +156 -0
- package/dist/routes/runner.js.map +1 -0
- package/dist/routes/runs.js +84 -0
- package/dist/routes/runs.js.map +1 -0
- package/dist/routes/setup.js +18 -0
- package/dist/routes/setup.js.map +1 -0
- package/dist/routes/test-runs.js +220 -0
- package/dist/routes/test-runs.js.map +1 -0
- package/dist/routes/ticket-status.js +17 -0
- package/dist/routes/ticket-status.js.map +1 -0
- package/dist/routes/traceability.js +143 -0
- package/dist/routes/traceability.js.map +1 -0
- package/dist/routes/true.js +142 -0
- package/dist/routes/true.js.map +1 -0
- package/dist/routes/updates.js +70 -0
- package/dist/routes/updates.js.map +1 -0
- package/dist/runner/adapters/index.js +12 -0
- package/dist/runner/adapters/index.js.map +1 -0
- package/dist/runner/adapters/playwright.js +68 -0
- package/dist/runner/adapters/playwright.js.map +1 -0
- package/dist/runner/aivision/cases.js +172 -0
- package/dist/runner/aivision/cases.js.map +1 -0
- package/dist/runner/aivision/cli.js +201 -0
- package/dist/runner/aivision/cli.js.map +1 -0
- package/dist/runner/aivision/index.js +281 -0
- package/dist/runner/aivision/index.js.map +1 -0
- package/dist/runner/aivision/server.js +394 -0
- package/dist/runner/aivision/server.js.map +1 -0
- package/dist/runner/aivision/shared.js +140 -0
- package/dist/runner/aivision/shared.js.map +1 -0
- package/dist/runner/artifact-cap.js +189 -0
- package/dist/runner/artifact-cap.js.map +1 -0
- package/dist/runner/artifacts.js +433 -0
- package/dist/runner/artifacts.js.map +1 -0
- package/dist/runner/command.js +564 -0
- package/dist/runner/command.js.map +1 -0
- package/dist/runner/copyfiles.js +221 -0
- package/dist/runner/copyfiles.js.map +1 -0
- package/dist/runner/engines.js +32 -0
- package/dist/runner/engines.js.map +1 -0
- package/dist/runner/evidence.js +114 -0
- package/dist/runner/evidence.js.map +1 -0
- package/dist/runner/host-env.js +160 -0
- package/dist/runner/host-env.js.map +1 -0
- package/dist/runner/index.js +1221 -0
- package/dist/runner/index.js.map +1 -0
- package/dist/runner/observe.js +603 -0
- package/dist/runner/observe.js.map +1 -0
- package/dist/runner/playwright-json.js +108 -0
- package/dist/runner/playwright-json.js.map +1 -0
- package/dist/runner/pwgen/exec.js +192 -0
- package/dist/runner/pwgen/exec.js.map +1 -0
- package/dist/runner/pwgen/explore.js +111 -0
- package/dist/runner/pwgen/explore.js.map +1 -0
- package/dist/runner/pwgen/index.js +423 -0
- package/dist/runner/pwgen/index.js.map +1 -0
- package/dist/runner/pwgen/propose.js +69 -0
- package/dist/runner/pwgen/propose.js.map +1 -0
- package/dist/runner/pwgen/source.js +632 -0
- package/dist/runner/pwgen/source.js.map +1 -0
- package/dist/runner/setup.js +222 -0
- package/dist/runner/setup.js.map +1 -0
- package/dist/runner/step-reporter.js +107 -0
- package/dist/runner/step-reporter.js.map +1 -0
- package/dist/runner/test-repo.js +326 -0
- package/dist/runner/test-repo.js.map +1 -0
- package/dist/runner/typed-value.js +46 -0
- package/dist/runner/typed-value.js.map +1 -0
- package/dist/runner/verify.js +133 -0
- package/dist/runner/verify.js.map +1 -0
- package/dist/runner/worktree.js +349 -0
- package/dist/runner/worktree.js.map +1 -0
- package/dist/sandbox.js +61 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/server.js +262 -0
- package/dist/server.js.map +1 -0
- package/dist/shadow-week.js +126 -0
- package/dist/shadow-week.js.map +1 -0
- package/dist/shutdown.js +92 -0
- package/dist/shutdown.js.map +1 -0
- package/dist/slack/index.js +645 -0
- package/dist/slack/index.js.map +1 -0
- package/dist/stages/config.js +147 -0
- package/dist/stages/config.js.map +1 -0
- package/dist/stages/index.js +41 -0
- package/dist/stages/index.js.map +1 -0
- package/dist/suites.js +293 -0
- package/dist/suites.js.map +1 -0
- package/dist/target-url.js +116 -0
- package/dist/target-url.js.map +1 -0
- package/dist/test-runs.js +662 -0
- package/dist/test-runs.js.map +1 -0
- package/dist/ticket-keys.js +97 -0
- package/dist/ticket-keys.js.map +1 -0
- package/dist/ticket-supersede.js +105 -0
- package/dist/ticket-supersede.js.map +1 -0
- package/dist/tinybee/ed25519.js +99 -0
- package/dist/tinybee/ed25519.js.map +1 -0
- package/dist/tinybee/models-routes.js +19 -0
- package/dist/tinybee/models-routes.js.map +1 -0
- package/dist/tinybee/pair-payload.js +41 -0
- package/dist/tinybee/pair-payload.js.map +1 -0
- package/dist/tinybee/update-manifest.js +77 -0
- package/dist/tinybee/update-manifest.js.map +1 -0
- package/dist/tinybee/well-known.js +64 -0
- package/dist/tinybee/well-known.js.map +1 -0
- package/dist/tinybee/workflows/activity.js +23 -0
- package/dist/tinybee/workflows/activity.js.map +1 -0
- package/dist/tinybee/workflows/cron.js +210 -0
- package/dist/tinybee/workflows/cron.js.map +1 -0
- package/dist/tinybee/workflows/engine.js +371 -0
- package/dist/tinybee/workflows/engine.js.map +1 -0
- package/dist/tinybee/workflows/from-text.js +203 -0
- package/dist/tinybee/workflows/from-text.js.map +1 -0
- package/dist/tinybee/workflows/routes.js +189 -0
- package/dist/tinybee/workflows/routes.js.map +1 -0
- package/dist/tinybee/workflows/scheduler.js +100 -0
- package/dist/tinybee/workflows/scheduler.js.map +1 -0
- package/dist/tinybee/workflows/skills.js +227 -0
- package/dist/tinybee/workflows/skills.js.map +1 -0
- package/dist/tinybee/workflows/store.js +205 -0
- package/dist/tinybee/workflows/store.js.map +1 -0
- package/dist/tinybee/workflows/types.js +180 -0
- package/dist/tinybee/workflows/types.js.map +1 -0
- package/dist/tinybee/workflows/yaml.js +323 -0
- package/dist/tinybee/workflows/yaml.js.map +1 -0
- package/dist/true/api.js +495 -0
- package/dist/true/api.js.map +1 -0
- package/dist/true/client.js +292 -0
- package/dist/true/client.js.map +1 -0
- package/dist/true/clock.js +36 -0
- package/dist/true/clock.js.map +1 -0
- package/dist/true/events.js +125 -0
- package/dist/true/events.js.map +1 -0
- package/dist/true/hosts.js +98 -0
- package/dist/true/hosts.js.map +1 -0
- package/dist/true/index.js +29 -0
- package/dist/true/index.js.map +1 -0
- package/dist/true/key.js +54 -0
- package/dist/true/key.js.map +1 -0
- package/dist/true/line.js +69 -0
- package/dist/true/line.js.map +1 -0
- package/dist/true/mask.js +31 -0
- package/dist/true/mask.js.map +1 -0
- package/dist/true/names.js +114 -0
- package/dist/true/names.js.map +1 -0
- package/dist/true/outbox.js +233 -0
- package/dist/true/outbox.js.map +1 -0
- package/dist/true/replay.js +164 -0
- package/dist/true/replay.js.map +1 -0
- package/dist/true/settings.js +123 -0
- package/dist/true/settings.js.map +1 -0
- package/dist/true/status.js +189 -0
- package/dist/true/status.js.map +1 -0
- package/dist/true/sync-cases.js +222 -0
- package/dist/true/sync-cases.js.map +1 -0
- package/dist/true/sync-manual.js +295 -0
- package/dist/true/sync-manual.js.map +1 -0
- package/dist/true/sync-runs.js +109 -0
- package/dist/true/sync-runs.js.map +1 -0
- package/dist/true/worker.js +414 -0
- package/dist/true/worker.js.map +1 -0
- package/dist/types.js +32 -0
- package/dist/types.js.map +1 -0
- package/dist/updates/asks.js +38 -0
- package/dist/updates/asks.js.map +1 -0
- package/dist/updates/index.js +835 -0
- package/dist/updates/index.js.map +1 -0
- package/dist/updates/owner.js +24 -0
- package/dist/updates/owner.js.map +1 -0
- package/dist/updates/prefs.js +118 -0
- package/dist/updates/prefs.js.map +1 -0
- package/dist/updates/route.js +78 -0
- package/dist/updates/route.js.map +1 -0
- package/dist/updates/sources.js +345 -0
- package/dist/updates/sources.js.map +1 -0
- package/dist/updates/words.js +452 -0
- package/dist/updates/words.js.map +1 -0
- package/dist/version.js +53 -0
- package/dist/version.js.map +1 -0
- package/dist/view-compute.js +382 -0
- package/dist/view-compute.js.map +1 -0
- package/dist/view-spec.js +894 -0
- package/dist/view-spec.js.map +1 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/ApprovalCard-Br_z5z7G.css +1 -0
- package/dist/web/assets/ApprovalCard-CfGhGFo_.js +3 -0
- package/dist/web/assets/CardFromRef-BaTGmZ9c.js +2 -0
- package/dist/web/assets/CardFromRef-DO7i2uDd.css +1 -0
- package/dist/web/assets/Cases-8l_6UcLh.js +2 -0
- package/dist/web/assets/Cases-CpfED0uB.css +1 -0
- package/dist/web/assets/Chat-BKDiR0qt.js +3 -0
- package/dist/web/assets/Chat-DxjdyYUu.css +1 -0
- package/dist/web/assets/ConfirmDialog-CIMveD-b.js +2 -0
- package/dist/web/assets/ConnectSheet-Cm5vbZdR.js +4 -0
- package/dist/web/assets/Docs-HKGuY0WE.js +2 -0
- package/dist/web/assets/Docs-fodKx8S7.css +1 -0
- package/dist/web/assets/DotDetail-DskS3NWe.js +2 -0
- package/dist/web/assets/DotDetail-OWRSD_1x.css +1 -0
- package/dist/web/assets/DotStage-BZDclfzV.css +1 -0
- package/dist/web/assets/DotStage-CtMNzCN3.js +2 -0
- package/dist/web/assets/Dots-BqCsS34m.js +2 -0
- package/dist/web/assets/EvidencePack-DQ3kCJX4.js +2 -0
- package/dist/web/assets/EvidencePack-DV_9ETsA.css +1 -0
- package/dist/web/assets/FailedDeliveries-1F2kFcs9.js +2 -0
- package/dist/web/assets/FailedDeliveries-DXa2_VvI.css +1 -0
- package/dist/web/assets/Feed-5wiHRTFK.js +2 -0
- package/dist/web/assets/GfmView-C-NYaduC.js +4 -0
- package/dist/web/assets/ImpactList-C-RNuDB0.js +2 -0
- package/dist/web/assets/ImpactList-C612dFdw.css +1 -0
- package/dist/web/assets/Inbox-kc1bKVL_.js +2 -0
- package/dist/web/assets/JoinScreen-C3L4wmP3.js +2 -0
- package/dist/web/assets/JoinScreen-COeAUv5v.css +1 -0
- package/dist/web/assets/LeadDialog-N3Zul9Rw.js +8 -0
- package/dist/web/assets/LeadDialog-xyWjAuAS.css +1 -0
- package/dist/web/assets/ListeningLine-BRFNaczY.js +2 -0
- package/dist/web/assets/ListeningLine-pN8CgyiK.css +1 -0
- package/dist/web/assets/Mascots-pIyGpxtD.js +2 -0
- package/dist/web/assets/ModelChoices-BxO1zyyr.js +2 -0
- package/dist/web/assets/ModelChoices-DQJnVxfs.css +1 -0
- package/dist/web/assets/NewDotDialog-CDPUWmgN.js +2 -0
- package/dist/web/assets/NewDotDialog-DrPPqkSZ.css +1 -0
- package/dist/web/assets/NewTestRunDialog-BQBthVHE.js +2 -0
- package/dist/web/assets/NoBreakNames-Csxdqdt6.js +2 -0
- package/dist/web/assets/Pair-omex_K7H.js +2 -0
- package/dist/web/assets/PushSection-CmqHlE_2.js +2 -0
- package/dist/web/assets/ReleaseNotes-B6VqFewP.js +2 -0
- package/dist/web/assets/ReleaseNotes-DBJUL7Vm.css +1 -0
- package/dist/web/assets/ReportPage-BHS1xWEt.js +162 -0
- package/dist/web/assets/Reports-BEHnDIb1.js +2 -0
- package/dist/web/assets/ReviewChangeDialog-BVSt-cw-.js +2 -0
- package/dist/web/assets/ReviewChangeDialog-BbIlHRx0.css +1 -0
- package/dist/web/assets/Room-BoG_uge4.js +2 -0
- package/dist/web/assets/Room-evAfiW_e.css +1 -0
- package/dist/web/assets/RunActions-ZY7yf1AF.js +2 -0
- package/dist/web/assets/RunDetail-C8St20GQ.css +1 -0
- package/dist/web/assets/RunDetail-qADXjBvd.js +9 -0
- package/dist/web/assets/RunRow-BKEJv8ib.js +2 -0
- package/dist/web/assets/RunnerTry-Dgpp67-s.js +6 -0
- package/dist/web/assets/RunnerTry-DsweWOfO.css +1 -0
- package/dist/web/assets/Settings-fs-QJwNM.js +2 -0
- package/dist/web/assets/Setup-DI6hBEP-.js +2 -0
- package/dist/web/assets/TestRows-wh03T5Li.js +4 -0
- package/dist/web/assets/TestRun-DTOyF-Pv.js +2 -0
- package/dist/web/assets/TestRuns-ggDjm1Q8.js +2 -0
- package/dist/web/assets/TinyBeeAddLaptop-bc3pmh3T.js +2 -0
- package/dist/web/assets/TinyBeeAi-CkyOzhm5.js +2 -0
- package/dist/web/assets/TinyBeeConnections-B2K_P38u.js +2 -0
- package/dist/web/assets/TinyBeeHome-DSpxX5KS.js +3 -0
- package/dist/web/assets/TinyBeePlan-BamiLziu.js +2 -0
- package/dist/web/assets/TinyBeeSignup-vHcz0Wen.js +2 -0
- package/dist/web/assets/TinyBeeSkills-By9RLUse.js +2 -0
- package/dist/web/assets/TinyBeeWorkflow-DeNO7Jsh.js +2 -0
- package/dist/web/assets/TinyBeeWorkflowEditor-B6OGC2OT.js +2 -0
- package/dist/web/assets/TinyBeeWorkflows-BN4-h6lt.js +2 -0
- package/dist/web/assets/Trace-BBCI1aIB.css +1 -0
- package/dist/web/assets/Trace-aF92UWaH.js +2 -0
- package/dist/web/assets/UpdatesSection-CCWEbkue.js +2 -0
- package/dist/web/assets/Welcome-CCcy558Q.js +2 -0
- package/dist/web/assets/activity-C3qilrDm.css +1 -0
- package/dist/web/assets/activity-CBfTSxqw.js +2 -0
- package/dist/web/assets/authedMedia-BTYt_tyu.js +2 -0
- package/dist/web/assets/beeApi-BHW8Tx1s.js +2 -0
- package/dist/web/assets/data-CmXvx3Mj.js +2 -0
- package/dist/web/assets/engines-CfdJODFj.css +1 -0
- package/dist/web/assets/format-BBZ8vFyT.js +2 -0
- package/dist/web/assets/forms-Coc7p2UF.css +1 -0
- package/dist/web/assets/hivePush-DZJDokj7.js +2 -0
- package/dist/web/assets/impactModel-MY1OFuiP.js +2 -0
- package/dist/web/assets/index-B6qKm1NW.js +13 -0
- package/dist/web/assets/index-BvKyT1X7.css +1 -0
- package/dist/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/web/assets/lead-BL_P_kri.css +1 -0
- package/dist/web/assets/model-BIQy5IVG.js +2 -0
- package/dist/web/assets/modelsApi-PUSySRQw.js +2 -0
- package/dist/web/assets/newDotModel-BaLLyw57.js +2 -0
- package/dist/web/assets/onboarding-bfKqnM4Z.css +1 -0
- package/dist/web/assets/phone-Rs7FoyG0.css +1 -0
- package/dist/web/assets/phoneApi-BG_1-M-D.js +3 -0
- package/dist/web/assets/releaseModel-EynYMVOC.js +2 -0
- package/dist/web/assets/releaseNotesModel-A1Rtv3cV.js +2 -0
- package/dist/web/assets/reports-BWaA1HhM.js +37 -0
- package/dist/web/assets/reports-DXwdWQsB.css +1 -0
- package/dist/web/assets/router-W6nZsQ33.js +2 -0
- package/dist/web/assets/runs-BY0Owv93.css +1 -0
- package/dist/web/assets/shadowWeekModel-BUUiww09.js +2 -0
- package/dist/web/assets/storage-yvL36AWt.js +2 -0
- package/dist/web/assets/store-IqrqHuFR.js +2 -0
- package/dist/web/assets/testruns-BzIZ0cR0.css +1 -0
- package/dist/web/assets/testruns-DyaqX4ji.js +2 -0
- package/dist/web/assets/true-9Q68cs5b.js +2 -0
- package/dist/web/assets/true-DPGBpoir.css +1 -0
- package/dist/web/assets/ui-DcmhYyYp.js +13 -0
- package/dist/web/assets/ui-DxEDgbIx.css +1 -0
- package/dist/web/assets/usePauseDot-BV60ZVy1.js +2 -0
- package/dist/web/assets/usePauseDot-NrSzMI1C.css +1 -0
- package/dist/web/assets/usePush-Dsm-TjlG.js +2 -0
- package/dist/web/assets/useRefChips-CbafNx59.js +68 -0
- package/dist/web/assets/useRestoreDot-D0B6W82H.js +2 -0
- package/dist/web/assets/view-worker-pyeVAhUT.js +2 -0
- package/dist/web/assets/workflowUi-QYsd3n9J.js +2 -0
- package/dist/web/favicon.png +0 -0
- package/dist/web/icon-192.png +0 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/icon-maskable-512.png +0 -0
- package/dist/web/index.html +45 -0
- package/dist/web/manifest.webmanifest +23 -0
- package/dist/web/mascots/argus-attention.svg +7 -0
- package/dist/web/mascots/argus-ok.svg +7 -0
- package/dist/web/mascots/argus-outage.svg +7 -0
- package/dist/web/mascots/argus-running.svg +7 -0
- package/dist/web/mascots/bolt-attention.svg +7 -0
- package/dist/web/mascots/bolt-ok.svg +7 -0
- package/dist/web/mascots/bolt-outage.svg +7 -0
- package/dist/web/mascots/bolt-running.svg +7 -0
- package/dist/web/mascots/echo-attention.svg +7 -0
- package/dist/web/mascots/echo-ok.svg +7 -0
- package/dist/web/mascots/echo-outage.svg +7 -0
- package/dist/web/mascots/echo-running.svg +7 -0
- package/dist/web/mascots/lens-attention.svg +7 -0
- package/dist/web/mascots/lens-ok.svg +7 -0
- package/dist/web/mascots/lens-outage.svg +7 -0
- package/dist/web/mascots/lens-running.svg +7 -0
- package/dist/web/mascots/moss-attention.svg +7 -0
- package/dist/web/mascots/moss-ok.svg +7 -0
- package/dist/web/mascots/moss-outage.svg +7 -0
- package/dist/web/mascots/moss-running.svg +7 -0
- package/dist/web/mascots/nit-attention.svg +7 -0
- package/dist/web/mascots/nit-ok.svg +7 -0
- package/dist/web/mascots/nit-outage.svg +7 -0
- package/dist/web/mascots/nit-running.svg +7 -0
- package/dist/web/mascots/nova-attention.svg +7 -0
- package/dist/web/mascots/nova-ok.svg +7 -0
- package/dist/web/mascots/nova-outage.svg +7 -0
- package/dist/web/mascots/nova-running.svg +7 -0
- package/dist/web/mascots/penn-attention.svg +7 -0
- package/dist/web/mascots/penn-ok.svg +7 -0
- package/dist/web/mascots/penn-outage.svg +7 -0
- package/dist/web/mascots/penn-running.svg +7 -0
- package/dist/web/mascots/pip-attention.svg +7 -0
- package/dist/web/mascots/pip-ok.svg +7 -0
- package/dist/web/mascots/pip-outage.svg +7 -0
- package/dist/web/mascots/pip-running.svg +7 -0
- package/dist/web/mascots/ward-attention.svg +7 -0
- package/dist/web/mascots/ward-ok.svg +7 -0
- package/dist/web/mascots/ward-outage.svg +7 -0
- package/dist/web/mascots/ward-running.svg +7 -0
- package/dist/web/sw.js +98 -0
- package/dist/webhook-seen.js +53 -0
- package/dist/webhook-seen.js.map +1 -0
- package/dist/webhooks.js +625 -0
- package/dist/webhooks.js.map +1 -0
- package/dist/zip.js +63 -0
- package/dist/zip.js.map +1 -0
- package/docs/API-V2.md +532 -0
- package/docs/CHAT-EVAL.md +130 -0
- package/docs/CHAT-UI.md +416 -0
- package/docs/CONNECTIONS.md +89 -0
- package/docs/DATA-MODEL.md +518 -0
- package/docs/DEPLOY.md +577 -0
- package/docs/DOGFOOD-LEDGER.md +81 -0
- package/docs/GITHUB.md +148 -0
- package/docs/INTEGRATION-MODEL.md +200 -0
- package/docs/LEAD-DOT.md +553 -0
- package/docs/LIST-PATTERNS.md +52 -0
- package/docs/MULTI-REPO.md +766 -0
- package/docs/PARITY.md +276 -0
- package/docs/PR-REVIEW-QA.md +1266 -0
- package/docs/PR-REVIEW.md +357 -0
- package/docs/SANDBOX.md +320 -0
- package/docs/SETUP.md +776 -0
- package/docs/SLACK.md +80 -0
- package/docs/STAGES.md +619 -0
- package/docs/TRACE-MAP.md +89 -0
- package/docs/TRACKERS.md +225 -0
- package/docs/VOICE.md +36 -0
- package/docs/tinybee/00-README.md +45 -0
- package/docs/tinybee/01-PRD.md +216 -0
- package/docs/tinybee/02-architecture-pairing.md +199 -0
- package/docs/tinybee/03-local-agent.md +222 -0
- package/docs/tinybee/04-local-setup-ui.md +128 -0
- package/docs/tinybee/05-mobile-app-ux.md +199 -0
- package/docs/tinybee/06-environments-and-delivery.md +148 -0
- package/docs/tinybee/07-coding-agent-handoff.md +116 -0
- package/docs/tinybee/OPEN-QUESTIONS.md +26 -0
- package/docs/tinybee/PROGRESS.md +134 -0
- package/package.json +111 -4
|
@@ -0,0 +1,1278 @@
|
|
|
1
|
+
// Traceability map (docs/TRACE-MAP.md, KAT-189). The app under test as the dot's test runs saw it: app (host) >
|
|
2
|
+
// section (first path segment) > page (route pattern) > state (a distinct heading set or dialog on one route),
|
|
3
|
+
// built from the observations.jsonl of recent runs (src/runner/observe.ts). A section with one page is that page.
|
|
4
|
+
// A requirement attaches to a page only on evidence: a spec linked to it visited the page, its text names the
|
|
5
|
+
// route, or a file its PRs changed is the page's source or sits in the page's product area (the repo profile). A
|
|
6
|
+
// run on the requirement's PR that merely visited a page is not evidence: a full suite visits every page. A
|
|
7
|
+
// requirement with no evidence sits in Requirements without a page only. Routes the code (the
|
|
8
|
+
// repo scan's page routes and the routes its specs open), a requirement or a probe names that no run visited are
|
|
9
|
+
// coverage gaps: Needs tests, or Not run yet when a spec covers them. A visited page whose source a merge changed
|
|
10
|
+
// after its latest visit is Not run yet too.
|
|
11
|
+
//
|
|
12
|
+
// Two halves: gatherFacts reads what is stored (observations, runs, the traceability rows, links, profiles), and
|
|
13
|
+
// mapTree is a pure function from those facts to the tree, so the rules are unit tested without a database.
|
|
14
|
+
// Nothing here calls a tracker or a model; the routes warm the ticket titles first (ticket-titles.ts).
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { log } from '../config.js';
|
|
17
|
+
import { list } from '../db.js';
|
|
18
|
+
import { enqueue } from '../queue.js';
|
|
19
|
+
import { parseIssueRef } from '../integrations/github-issues.js';
|
|
20
|
+
import { linksOfWatch } from '../links.js';
|
|
21
|
+
import { latestProfile } from '../profile/store.js';
|
|
22
|
+
import { watchRepos } from '../repos.js';
|
|
23
|
+
import { anyGlob } from '../review/glob.js';
|
|
24
|
+
import { artifactUrl, resolveArtifactPath } from '../runner/artifacts.js';
|
|
25
|
+
import { readObservations, routeOf } from '../runner/observe.js';
|
|
26
|
+
import { buildAll, classify, issueUrl, mdLink, mdText } from './traceability.js';
|
|
27
|
+
/**
|
|
28
|
+
* A screenshot check against the artifacts folder, cached for one build. The artifact cap deletes old runs' files
|
|
29
|
+
* while their observations stay, so a URL in the payload could 404 and the app would show a broken box. A missing
|
|
30
|
+
* file is left out (the node takes an older screenshot, else none) and logged once per process.
|
|
31
|
+
*/
|
|
32
|
+
export function artifactShotCheck() {
|
|
33
|
+
const seen = new Map();
|
|
34
|
+
return (runId, name) => {
|
|
35
|
+
const k = `${runId}/${name}`;
|
|
36
|
+
let ok = seen.get(k);
|
|
37
|
+
if (ok === undefined) {
|
|
38
|
+
ok = resolveArtifactPath(runId, name) !== null;
|
|
39
|
+
seen.set(k, ok);
|
|
40
|
+
if (!ok)
|
|
41
|
+
log.warnOnce('trace-map-missing-shot', `trace map: screenshot ${artifactUrl(runId, name)} is gone from the artifacts folder (artifact cap?); the map leaves it and any other missing file out`);
|
|
42
|
+
}
|
|
43
|
+
return ok;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export const STATUS_WORD = {
|
|
47
|
+
covered: 'Covered',
|
|
48
|
+
partly: 'Partly covered',
|
|
49
|
+
failing: 'Failing',
|
|
50
|
+
needs_tests: 'Needs tests',
|
|
51
|
+
not_run: 'Not run yet',
|
|
52
|
+
};
|
|
53
|
+
const STATS_KEY = {
|
|
54
|
+
covered: 'covered',
|
|
55
|
+
partly: 'partly',
|
|
56
|
+
failing: 'failing',
|
|
57
|
+
needs_tests: 'needsTests',
|
|
58
|
+
not_run: 'notRun',
|
|
59
|
+
};
|
|
60
|
+
export const isGap = (s) => s === 'needs_tests' || s === 'not_run';
|
|
61
|
+
const NO_PAGE_SECTION = 'Requirements without a page';
|
|
62
|
+
/** A route as a pattern: `/products/[id]` and `/products/{id}` read `/products/:id`. Catch-all routes are not pages. */
|
|
63
|
+
export function patternOf(raw) {
|
|
64
|
+
const r = trimRoute(raw.trim().split(/[?#]/)[0] ?? '');
|
|
65
|
+
if (!r.startsWith('/') || r.includes('*') || /\[\.\.\./.test(r))
|
|
66
|
+
return null;
|
|
67
|
+
const out = r
|
|
68
|
+
.split('/')
|
|
69
|
+
.filter((seg) => !/^\(.*\)$/.test(seg))
|
|
70
|
+
.map((seg) => seg.replace(/^\[(\w+)\]$/, ':$1').replace(/^\{(\w+)\}$/, ':$1'))
|
|
71
|
+
.join('/');
|
|
72
|
+
return /[:]/.test(out) ? trimRoute(out || '/') : routeOf(out || '/');
|
|
73
|
+
}
|
|
74
|
+
const nodeId = (kind, key) => `${kind}-${createHash('sha256').update(key).digest('hex').slice(0, 10)}`;
|
|
75
|
+
const bag = () => ({ obs: [], tests: new Set(), reqs: new Set(), defects: new Set(), dots: new Set(), needsTests: false, notRun: false });
|
|
76
|
+
const natural = (a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
|
|
77
|
+
export const testKey = (file, title, host) => (host ? `${file}\u0000${title}\u0000${host}` : `${file}\u0000${title}`);
|
|
78
|
+
/** The key of the test an observation ran. It carries the app host, so a test of another app with the same file and title is not this one. Two dots on one app run one test. */
|
|
79
|
+
const obsKey = (o) => testKey(o.test, o.title, o.host);
|
|
80
|
+
/** The stored result of a test key: the app's own, else one saved without an app. */
|
|
81
|
+
const factOf = (tests, key) => tests.get(key) ?? tests.get(key.split('\u0000').slice(0, 2).join('\u0000'));
|
|
82
|
+
/** Same file whichever side carries a folder prefix: `e2e/a.spec.ts` and `tests/e2e/a.spec.ts`. */
|
|
83
|
+
export const samePath = (a, b) => {
|
|
84
|
+
const x = a.replace(/^\.\//, '');
|
|
85
|
+
const y = b.replace(/^\.\//, '');
|
|
86
|
+
return x === y || x.endsWith(`/${y}`) || y.endsWith(`/${x}`);
|
|
87
|
+
};
|
|
88
|
+
const trimRoute = (r) => (r.length > 1 ? r.replace(/\/+$/, '') : r);
|
|
89
|
+
/** `/products/:id`, `/products/[id]` and `/products/{id}` match `/products/12`. Exact text always matches. */
|
|
90
|
+
export function routeMatches(pattern, route) {
|
|
91
|
+
const p = trimRoute(pattern.trim());
|
|
92
|
+
const r = trimRoute(route.trim().split(/[?#]/)[0] ?? '');
|
|
93
|
+
if (!p || !r)
|
|
94
|
+
return false;
|
|
95
|
+
if (p.toLowerCase() === r.toLowerCase())
|
|
96
|
+
return true;
|
|
97
|
+
const re = p
|
|
98
|
+
.split('/')
|
|
99
|
+
.map((seg) => (/^(:\w+\??|\[\.{0,3}[^\]]+\]|\{[^}]+\}|\*)$/.test(seg) ? '[^/]+' : seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
|
|
100
|
+
.join('/');
|
|
101
|
+
return new RegExp(`^${re}$`, 'i').test(r);
|
|
102
|
+
}
|
|
103
|
+
/** Either side may be the pattern: a scanned `/products/[id]` and an observed `/products/:id` are the same page. */
|
|
104
|
+
const sameRoute = (a, b) => routeMatches(a, b) || routeMatches(b, a);
|
|
105
|
+
const hostOf = (u) => {
|
|
106
|
+
try {
|
|
107
|
+
return new URL(u).host.toLowerCase();
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Page routes a text names: absolute URLs, and paths that stand on their own (`/checkout` in "fails on /checkout.").
|
|
115
|
+
* API paths and file names are not pages. Each comes with the host of its URL, when it had one.
|
|
116
|
+
*/
|
|
117
|
+
export function routesIn(text) {
|
|
118
|
+
const out = new Map();
|
|
119
|
+
for (const m of text.matchAll(/https?:\/\/[^\s)<>"'`]+/gi)) {
|
|
120
|
+
const host = hostOf(m[0]);
|
|
121
|
+
const route = routeOf(m[0].replace(/[.,;:!?]+$/, ''));
|
|
122
|
+
if (host)
|
|
123
|
+
out.set(`${host} ${route}`, { host, route });
|
|
124
|
+
}
|
|
125
|
+
const bare = text.replace(/https?:\/\/[^\s)<>"'`]+/gi, ' ');
|
|
126
|
+
for (const m of bare.matchAll(/(?:^|[\s(`'"])(\/[a-z0-9][\w\-./:[\]{}]*)/gi)) {
|
|
127
|
+
const raw = m[1].replace(/[.,;:!?]+$/, '');
|
|
128
|
+
if (/\.[a-z0-9]{1,5}$/i.test(raw))
|
|
129
|
+
continue;
|
|
130
|
+
const route = raw.includes(':') || raw.includes('[') || raw.includes('{') ? trimRoute(raw.toLowerCase()) : routeOf(raw);
|
|
131
|
+
out.set(` ${route}`, { route });
|
|
132
|
+
}
|
|
133
|
+
return [...out.values()].filter((r) => !/^\/api(\/|$)/.test(r.route));
|
|
134
|
+
}
|
|
135
|
+
/** The first path segment, the key of a page's section: `/runs/:id` reads `runs`, `/` reads ``. */
|
|
136
|
+
export const sectionOf = (route) => route.split('/').filter(Boolean)[0] ?? '';
|
|
137
|
+
/** The app that holds the local servers test runs start on a random port (an e2e suite that boots its own server). */
|
|
138
|
+
export const LOCAL_TEST_APP = 'Local test servers';
|
|
139
|
+
const LOOPBACK_PORT = /^(?:127\.0\.0\.1|localhost|\[::1\]|0\.0\.0\.0):\d+$/i;
|
|
140
|
+
/**
|
|
141
|
+
* The app key of a host. A loopback host with a port that no dot names (base URL or probe) is a server a test run
|
|
142
|
+
* started for itself, on a new port each run: all of them are one app, not one app per port.
|
|
143
|
+
*/
|
|
144
|
+
export function appHostFor(owned) {
|
|
145
|
+
return (host) => (host && LOOPBACK_PORT.test(host) && !owned.has(host.toLowerCase()) ? LOCAL_TEST_APP : host);
|
|
146
|
+
}
|
|
147
|
+
/** The input with the hosts of local test servers folded into one app. */
|
|
148
|
+
function foldTestServers(input) {
|
|
149
|
+
const owned = new Set([...input.baseUrls, ...input.knownUrls.map((k) => k.url)].map(hostOf).filter(Boolean));
|
|
150
|
+
const appHost = appHostFor(owned);
|
|
151
|
+
if (!input.observations.some((o) => o.host && appHost(o.host) !== o.host))
|
|
152
|
+
return { input, appHost };
|
|
153
|
+
return { input: { ...input, observations: input.observations.map((o) => (o.host ? { ...o, host: appHost(o.host) } : o)) }, appHost };
|
|
154
|
+
}
|
|
155
|
+
const titleCase = (s) => (s ? s.replace(/[-_]+/g, ' ').replace(/^\w/, (c) => c.toUpperCase()) : 'Home');
|
|
156
|
+
/** The test result a lookup gives, else the observation's own. */
|
|
157
|
+
function resultOf(key, tests, obs) {
|
|
158
|
+
const t = factOf(tests, key);
|
|
159
|
+
if (t)
|
|
160
|
+
return t.result;
|
|
161
|
+
const o = obs.find((x) => obsKey(x) === key && x.result);
|
|
162
|
+
if (!o)
|
|
163
|
+
return 'not_run';
|
|
164
|
+
return o.result === 'passed' ? 'passed' : o.result === 'failed' || o.result === 'timedOut' ? 'failed' : 'not_run';
|
|
165
|
+
}
|
|
166
|
+
function countsOf(b, l) {
|
|
167
|
+
// Gap counts are filled once the tree stands (mapTree, countGaps).
|
|
168
|
+
const results = [...b.tests].map((k) => resultOf(k, l.tests, b.obs));
|
|
169
|
+
const defects = [...b.defects].map((r) => l.defects.get(r)).filter((d) => !!d);
|
|
170
|
+
return {
|
|
171
|
+
requirements: b.reqs.size,
|
|
172
|
+
tests: { passed: results.filter((r) => r === 'passed').length, failed: results.filter((r) => r === 'failed').length, total: results.length },
|
|
173
|
+
defects: { open: defects.filter((d) => d.state === 'open').length, total: defects.length },
|
|
174
|
+
gaps: { needsTests: 0, notRun: 0 },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The status, bottom-up. No run saw anything here: Needs tests when a page below has no test, else Not run yet.
|
|
179
|
+
* Failing: a test that visited it failed its latest run, an open defect, or a failing requirement. A page whose
|
|
180
|
+
* source changed after its latest visit (`changed`) is Not run yet. Covered: every test passed, every requirement
|
|
181
|
+
* passes, and no page below is a gap. Anything in between is Partly covered.
|
|
182
|
+
*/
|
|
183
|
+
export function statusOf(b, l, changed = false) {
|
|
184
|
+
if (!b.obs.length)
|
|
185
|
+
return b.needsTests || !b.notRun ? 'needs_tests' : 'not_run';
|
|
186
|
+
const results = [...b.tests].map((k) => resultOf(k, l.tests, b.obs));
|
|
187
|
+
const reqs = [...b.reqs].map((r) => l.reqs.get(r)).filter((r) => !!r);
|
|
188
|
+
const openDefect = [...b.defects].some((r) => l.defects.get(r)?.state === 'open');
|
|
189
|
+
if (openDefect || results.includes('failed') || reqs.some((r) => r.row.statusCode === 'failing'))
|
|
190
|
+
return 'failing';
|
|
191
|
+
if (changed)
|
|
192
|
+
return 'not_run';
|
|
193
|
+
const ok = results.every((r) => r === 'passed') && reqs.every((r) => r.row.statusCode === 'passing');
|
|
194
|
+
return ok && !b.needsTests && !b.notRun ? 'covered' : 'partly';
|
|
195
|
+
}
|
|
196
|
+
function union(into, from) {
|
|
197
|
+
for (const o of from.obs)
|
|
198
|
+
into.obs.push(o);
|
|
199
|
+
for (const k of ['tests', 'reqs', 'defects', 'dots'])
|
|
200
|
+
for (const v of from[k])
|
|
201
|
+
into[k].add(v);
|
|
202
|
+
if (from.needsTests)
|
|
203
|
+
into.needsTests = true;
|
|
204
|
+
if (from.notRun)
|
|
205
|
+
into.notRun = true;
|
|
206
|
+
}
|
|
207
|
+
/** The state of a page an observation shows: its dialog, else its heading set. */
|
|
208
|
+
const stateKeyOf = (o) => (o.dialog ? `d:${o.dialog}` : `h:${o.headings.map((h) => h.text).join('|')}`);
|
|
209
|
+
/** The most common heading over observations, from the top level present: a section's or a state's name. */
|
|
210
|
+
function commonHeading(obs) {
|
|
211
|
+
const all = obs.flatMap((o) => o.headings);
|
|
212
|
+
if (!all.length)
|
|
213
|
+
return undefined;
|
|
214
|
+
const top = Math.min(...all.map((h) => h.level));
|
|
215
|
+
const votes = new Map();
|
|
216
|
+
for (const h of all)
|
|
217
|
+
if (h.level === top)
|
|
218
|
+
votes.set(h.text, (votes.get(h.text) ?? 0) + 1);
|
|
219
|
+
return [...votes].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
220
|
+
}
|
|
221
|
+
const MAX_STATES = 8;
|
|
222
|
+
/** The pure half: facts in, tree and index out. */
|
|
223
|
+
export function mapTree(raw, opts = {}) {
|
|
224
|
+
const { input, appHost } = foldTestServers(raw);
|
|
225
|
+
const hasShot = opts.hasShot ?? (() => true);
|
|
226
|
+
const shown = (o) => !!o.screenshot && hasShot(o.runId, o.screenshot);
|
|
227
|
+
const tests = new Map(input.tests.map((t) => [testKey(t.file, t.title, t.host), t]));
|
|
228
|
+
const l = {
|
|
229
|
+
tests,
|
|
230
|
+
reqs: new Map(input.requirements.map((r) => [r.row.ref, r])),
|
|
231
|
+
defects: new Map(input.defects.map((d) => [d.ref, d])),
|
|
232
|
+
};
|
|
233
|
+
const index = new Map();
|
|
234
|
+
const reqRoutes = new Map(input.requirements.map((r) => [r.row.ref, routesIn(r.text)]));
|
|
235
|
+
const defectRoutes = new Map(input.defects.map((d) => [d.ref, routesIn(d.text)]));
|
|
236
|
+
// The page routes the files of a PR touch: routes whose scanned source it changed, and the routes of an area whose
|
|
237
|
+
// paths hold a changed file.
|
|
238
|
+
const prFiles = new Map();
|
|
239
|
+
for (const c of input.prChanges ?? [])
|
|
240
|
+
prFiles.set(c.pr.toLowerCase(), [...(prFiles.get(c.pr.toLowerCase()) ?? []), c]);
|
|
241
|
+
const pages = input.code.filter((c) => !c.method && !c.api);
|
|
242
|
+
const touched = (prs) => {
|
|
243
|
+
const out = new Map();
|
|
244
|
+
for (const f of prs.flatMap((p) => prFiles.get(p.toLowerCase()) ?? [])) {
|
|
245
|
+
for (const c of pages)
|
|
246
|
+
if (c.repo === f.repo && samePath(c.file, f.path))
|
|
247
|
+
out.set(`${c.host ?? ''} ${c.route}`, { ...(c.host ? { host: c.host } : {}), route: c.route });
|
|
248
|
+
for (const a of input.areas ?? [])
|
|
249
|
+
if (a.paths?.length && (!a.repo || a.repo === f.repo) && anyGlob(f.path, a.paths))
|
|
250
|
+
for (const r of a.routes)
|
|
251
|
+
out.set(`${a.host ?? ''} ${r}`, { ...(a.host ? { host: a.host } : {}), route: r });
|
|
252
|
+
}
|
|
253
|
+
return [...out.values()];
|
|
254
|
+
};
|
|
255
|
+
const reqChanged = new Map(input.requirements.map((r) => [r.row.ref, touched(r.prs)]));
|
|
256
|
+
const defectChanged = new Map(input.defects.map((d) => [d.ref, touched(d.prs)]));
|
|
257
|
+
/**
|
|
258
|
+
* A route a PR touched is this page: on the host the scan is for, any host when the scan names none, and the local
|
|
259
|
+
* test servers, which serve the dot's own app on a random port.
|
|
260
|
+
*/
|
|
261
|
+
const touches = (rs, host, route) => (rs ?? []).some((r) => (!r.host || r.host === host || host === LOCAL_TEST_APP) && sameRoute(route, r.route));
|
|
262
|
+
const placed = new Set();
|
|
263
|
+
const make = (kind, key, name, extra = {}) => ({
|
|
264
|
+
id: nodeId(kind, key),
|
|
265
|
+
kind,
|
|
266
|
+
name,
|
|
267
|
+
status: 'needs_tests',
|
|
268
|
+
counts: { requirements: 0, tests: { passed: 0, failed: 0, total: 0 }, defects: { open: 0, total: 0 }, gaps: { needsTests: 0, notRun: 0 } },
|
|
269
|
+
children: [],
|
|
270
|
+
...extra,
|
|
271
|
+
});
|
|
272
|
+
const fill = (n, b, changed = false) => {
|
|
273
|
+
n.counts = countsOf(b, l);
|
|
274
|
+
n.status = statusOf(b, l, changed);
|
|
275
|
+
const ms = {};
|
|
276
|
+
for (const r of b.defects) {
|
|
277
|
+
const d = l.defects.get(r);
|
|
278
|
+
if (d?.state === 'open' && d.milestone)
|
|
279
|
+
ms[d.milestone] = (ms[d.milestone] ?? 0) + 1;
|
|
280
|
+
}
|
|
281
|
+
if (Object.keys(ms).length)
|
|
282
|
+
n.milestones = ms;
|
|
283
|
+
// A coverage gap shows no screenshot: what a run saw before the change is not the page now.
|
|
284
|
+
// A screenshot whose file is gone is left out: the node takes the newest one still on disk.
|
|
285
|
+
const shot = isGap(n.status) ? undefined : [...b.obs].filter(shown).sort((a, c) => c.at.localeCompare(a.at))[0];
|
|
286
|
+
if (shot)
|
|
287
|
+
n.screenshot = artifactUrl(shot.runId, shot.screenshot);
|
|
288
|
+
else
|
|
289
|
+
delete n.screenshot;
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* Requirements and defects of a set of observations on one route, on evidence only: a linked spec visited it, their
|
|
293
|
+
* text names the route, or their PRs changed the route's source or a file of its area. A run on their PR that
|
|
294
|
+
* visited the page is not enough: the run visits every page its suite opens.
|
|
295
|
+
*/
|
|
296
|
+
const attach = (b, host, route, primary) => {
|
|
297
|
+
const files = [...new Set(b.obs.map((o) => o.test))];
|
|
298
|
+
const names = (rs) => (rs ?? []).some((r) => (r.host ? r.host === host : primary) && sameRoute(route, r.route));
|
|
299
|
+
for (const r of input.requirements) {
|
|
300
|
+
if (r.specs.some((s) => files.some((f) => samePath(s, f))) || names(reqRoutes.get(r.row.ref)) || touches(reqChanged.get(r.row.ref), host, route))
|
|
301
|
+
b.reqs.add(r.row.ref);
|
|
302
|
+
}
|
|
303
|
+
for (const d of input.defects) {
|
|
304
|
+
if (d.requirements.some((r) => b.reqs.has(r)) || names(defectRoutes.get(d.ref)) || touches(defectChanged.get(d.ref), host, route))
|
|
305
|
+
b.defects.add(d.ref);
|
|
306
|
+
}
|
|
307
|
+
for (const r of b.reqs)
|
|
308
|
+
placed.add(r);
|
|
309
|
+
};
|
|
310
|
+
// Apps: the dots' base URL hosts first, then every host a run visited, the most visited first.
|
|
311
|
+
const visits = new Map();
|
|
312
|
+
for (const o of input.observations)
|
|
313
|
+
if (o.host)
|
|
314
|
+
visits.set(o.host, (visits.get(o.host) ?? 0) + 1);
|
|
315
|
+
const hosts = [...new Set([...input.baseUrls.map(hostOf).filter(Boolean), ...[...visits].sort((a, b) => b[1] - a[1]).map(([h]) => h)])];
|
|
316
|
+
const primaryHost = hosts[0];
|
|
317
|
+
const pagesBy = new Map();
|
|
318
|
+
for (const o of input.observations) {
|
|
319
|
+
if (!o.host)
|
|
320
|
+
continue;
|
|
321
|
+
const route = routeOf(o.url || o.route);
|
|
322
|
+
const key = `${o.host} ${route}`;
|
|
323
|
+
const p = pagesBy.get(key) ?? { host: o.host, route, obs: [] };
|
|
324
|
+
p.obs.push(o);
|
|
325
|
+
pagesBy.set(key, p);
|
|
326
|
+
}
|
|
327
|
+
const gaps = new Map();
|
|
328
|
+
const visitedRoute = (host, route) => [...pagesBy.values()].some((p) => p.host === host && sameRoute(p.route, route));
|
|
329
|
+
const addGap = (host, raw, more = {}) => {
|
|
330
|
+
const h = host ?? primaryHost;
|
|
331
|
+
const route = patternOf(raw);
|
|
332
|
+
if (!h || !hosts.includes(h) || !route || visitedRoute(h, route))
|
|
333
|
+
return;
|
|
334
|
+
const had = [...gaps.values()].find((g) => g.host === h && sameRoute(g.route, route));
|
|
335
|
+
const g = had ?? { host: h, route, reqs: new Set(), dots: new Set(), specs: new Set() };
|
|
336
|
+
if (more.ref)
|
|
337
|
+
g.reqs.add(more.ref);
|
|
338
|
+
if (more.dot)
|
|
339
|
+
g.dots.add(more.dot);
|
|
340
|
+
if (more.spec)
|
|
341
|
+
g.specs.add(more.spec);
|
|
342
|
+
if (!had)
|
|
343
|
+
gaps.set(`${h} ${route}`, g);
|
|
344
|
+
};
|
|
345
|
+
for (const r of input.requirements)
|
|
346
|
+
for (const x of reqRoutes.get(r.row.ref) ?? [])
|
|
347
|
+
addGap(x.host, x.route, { ref: r.row.ref, ...(r.dots?.[0] ? { dot: r.dots[0] } : {}) });
|
|
348
|
+
for (const k of input.knownUrls) {
|
|
349
|
+
const h = hostOf(k.url);
|
|
350
|
+
if (h)
|
|
351
|
+
addGap(h, routeOf(k.url), k.dotId ? { dot: k.dotId } : {});
|
|
352
|
+
}
|
|
353
|
+
for (const c of input.code)
|
|
354
|
+
if (!c.method && !c.api)
|
|
355
|
+
addGap(c.host, c.route, c.dotId ? { dot: c.dotId } : {});
|
|
356
|
+
for (const s of input.specs ?? [])
|
|
357
|
+
for (const r of s.routes)
|
|
358
|
+
addGap(s.host, r, { spec: s.file, ...(s.dotId ? { dot: s.dotId } : {}) });
|
|
359
|
+
// A requirement whose PRs changed the source of a page no run visited belongs to that page.
|
|
360
|
+
for (const g of gaps.values())
|
|
361
|
+
for (const r of input.requirements)
|
|
362
|
+
if (touches(reqChanged.get(r.row.ref), g.host, g.route))
|
|
363
|
+
g.reqs.add(r.row.ref);
|
|
364
|
+
// A requirement's specs cover the routes the requirement names.
|
|
365
|
+
for (const g of gaps.values())
|
|
366
|
+
for (const ref of g.reqs)
|
|
367
|
+
for (const s of l.reqs.get(ref)?.specs ?? [])
|
|
368
|
+
g.specs.add(s);
|
|
369
|
+
for (const g of gaps.values())
|
|
370
|
+
for (const s of input.specs ?? [])
|
|
371
|
+
if ((!s.host || s.host === g.host) && s.routes.some((r) => sameRoute(r, g.route)))
|
|
372
|
+
g.specs.add(s.file);
|
|
373
|
+
/** Visited pages whose source changed after the latest run that saw them: Not run yet. */
|
|
374
|
+
const changedSince = (host, route, obs) => {
|
|
375
|
+
const changes = input.changes ?? [];
|
|
376
|
+
if (!changes.length)
|
|
377
|
+
return false;
|
|
378
|
+
const seen = obs.reduce((m, o) => (o.at > m ? o.at : m), '');
|
|
379
|
+
const files = input.code.filter((c) => !c.method && !c.api && (!c.host || c.host === host) && sameRoute(c.route, route));
|
|
380
|
+
return changes.some((ch) => ch.at > seen && files.some((f) => f.repo === ch.repo && samePath(f.file, ch.path)));
|
|
381
|
+
};
|
|
382
|
+
const appNodes = [];
|
|
383
|
+
const multi = hosts.length !== 1;
|
|
384
|
+
const root = multi ? make('root', `root:${input.title}`, input.title) : null;
|
|
385
|
+
const rootBag = bag();
|
|
386
|
+
if (root)
|
|
387
|
+
index.set(root.id, { node: root, bag: rootBag, path: [], pages: [] });
|
|
388
|
+
/** Moves a node and everything below it one level up: the path loses the collapsed section. */
|
|
389
|
+
const reparent = (n, drop) => {
|
|
390
|
+
const e = index.get(n.id);
|
|
391
|
+
if (e)
|
|
392
|
+
e.path = e.path.filter((p) => p.id !== drop);
|
|
393
|
+
for (const c of n.children)
|
|
394
|
+
reparent(c, drop);
|
|
395
|
+
};
|
|
396
|
+
/** Puts a section under its parent, or its only page in its place: a one-page section says the same thing twice. */
|
|
397
|
+
const addSection = (parent, parentBag, sNode, sBag) => {
|
|
398
|
+
const only = sNode.children.length === 1 ? sNode.children[0] : null;
|
|
399
|
+
if (only) {
|
|
400
|
+
index.delete(sNode.id);
|
|
401
|
+
reparent(only, sNode.id);
|
|
402
|
+
parent.children.push(only);
|
|
403
|
+
}
|
|
404
|
+
else
|
|
405
|
+
parent.children.push(sNode);
|
|
406
|
+
union(parentBag, sBag);
|
|
407
|
+
};
|
|
408
|
+
for (const host of hosts) {
|
|
409
|
+
const aNode = make('app', `app:${host}`, host);
|
|
410
|
+
const aBag = bag();
|
|
411
|
+
const aPath = root ? [{ id: root.id, name: root.name }] : [];
|
|
412
|
+
const aPages = [];
|
|
413
|
+
index.set(aNode.id, { node: aNode, bag: aBag, path: aPath, host, pages: aPages });
|
|
414
|
+
const primary = host === primaryHost;
|
|
415
|
+
// A section is a product area of the repo scan when one lists the route, else the first path segment.
|
|
416
|
+
const areas = (input.areas ?? []).filter((a) => !a.host || a.host === host);
|
|
417
|
+
const sections = new Map();
|
|
418
|
+
const sectionFor = (route) => {
|
|
419
|
+
const area = areas.find((a) => a.routes.some((r) => sameRoute(r, route)));
|
|
420
|
+
const key = area ? `area:${area.name}` : sectionOf(route);
|
|
421
|
+
const s = sections.get(key) ?? { ...(area ? { name: area.name } : {}), seg: area ? '' : sectionOf(route), pages: [], gaps: [] };
|
|
422
|
+
sections.set(key, s);
|
|
423
|
+
return s;
|
|
424
|
+
};
|
|
425
|
+
for (const p of pagesBy.values())
|
|
426
|
+
if (p.host === host)
|
|
427
|
+
sectionFor(p.route).pages.push(p);
|
|
428
|
+
for (const g of gaps.values())
|
|
429
|
+
if (g.host === host)
|
|
430
|
+
sectionFor(g.route).gaps.push(g);
|
|
431
|
+
const sectionPath = [...aPath, { id: aNode.id, name: aNode.name }];
|
|
432
|
+
const sortKey = (s) => s.name ?? s.seg;
|
|
433
|
+
for (const [key, s] of [...sections].sort((a, b) => (a[0] ? 1 : 0) - (b[0] ? 1 : 0) || natural(sortKey(a[1]), sortKey(b[1])))) {
|
|
434
|
+
const seg = s.seg;
|
|
435
|
+
const sName = s.name ?? commonHeading(s.pages.flatMap((p) => p.obs)) ?? titleCase(seg);
|
|
436
|
+
const sNode = make('section', `section:${host}:${key}`, sName, s.name ? {} : { route: seg ? `/${seg}` : '/' });
|
|
437
|
+
const sBag = bag();
|
|
438
|
+
const sPages = [];
|
|
439
|
+
index.set(sNode.id, { node: sNode, bag: sBag, path: sectionPath, host, pages: sPages });
|
|
440
|
+
const pagePath = [...sectionPath, { id: sNode.id, name: sNode.name }];
|
|
441
|
+
const rows = [
|
|
442
|
+
...s.pages.map((p) => ({ route: p.route, page: p, g: undefined })),
|
|
443
|
+
...s.gaps.map((g) => ({ route: g.route, page: undefined, g })),
|
|
444
|
+
].sort((a, b) => natural(a.route, b.route));
|
|
445
|
+
for (const row of rows) {
|
|
446
|
+
const pBag = bag();
|
|
447
|
+
const latest = row.page ? [...row.page.obs].filter((o) => o.pageTitle).sort((a, b) => b.at.localeCompare(a.at))[0] : undefined;
|
|
448
|
+
const pNode = make('page', `page:${host}:${row.route}`, row.route, { route: row.route, ...(latest?.pageTitle ? { pageTitle: latest.pageTitle } : {}) });
|
|
449
|
+
const pPath = [...pagePath, { id: pNode.id, name: pNode.name }];
|
|
450
|
+
const entry = { node: pNode, bag: pBag, path: pagePath, host, pages: [{ host, route: row.route }] };
|
|
451
|
+
index.set(pNode.id, entry);
|
|
452
|
+
let changed = false;
|
|
453
|
+
if (row.page) {
|
|
454
|
+
for (const o of row.page.obs) {
|
|
455
|
+
pBag.obs.push(o);
|
|
456
|
+
pBag.tests.add(obsKey(o));
|
|
457
|
+
if (o.dotId)
|
|
458
|
+
pBag.dots.add(o.dotId);
|
|
459
|
+
}
|
|
460
|
+
attach(pBag, host, row.route, primary);
|
|
461
|
+
changed = changedSince(host, row.route, row.page.obs);
|
|
462
|
+
if (changed) {
|
|
463
|
+
pBag.notRun = true;
|
|
464
|
+
entry.changed = true;
|
|
465
|
+
}
|
|
466
|
+
// States: distinct dialogs and heading sets, when there is more than one. A visit with no heading yet
|
|
467
|
+
// (the page still loading) is not a state of its own.
|
|
468
|
+
const byState = new Map();
|
|
469
|
+
for (const o of row.page.obs) {
|
|
470
|
+
const k = stateKeyOf(o);
|
|
471
|
+
if (k === 'h:')
|
|
472
|
+
continue;
|
|
473
|
+
byState.set(k, [...(byState.get(k) ?? []), o]);
|
|
474
|
+
}
|
|
475
|
+
const states = [...byState].sort((a, b) => b[1].length - a[1].length).slice(0, MAX_STATES);
|
|
476
|
+
if (states.length > 1) {
|
|
477
|
+
const names = new Map();
|
|
478
|
+
for (const [k, obs] of states) {
|
|
479
|
+
const base = k.startsWith('d:') ? `Dialog: ${k.slice(2)}` : (commonHeading(obs) ?? 'No heading');
|
|
480
|
+
const n = (names.get(base) ?? 0) + 1;
|
|
481
|
+
names.set(base, n);
|
|
482
|
+
const stNode = make('state', `state:${host}:${row.route}:${k}`, n > 1 ? `${base} (${n})` : base, { route: row.route });
|
|
483
|
+
const stBag = bag();
|
|
484
|
+
for (const o of obs) {
|
|
485
|
+
stBag.obs.push(o);
|
|
486
|
+
stBag.tests.add(obsKey(o));
|
|
487
|
+
if (o.dotId)
|
|
488
|
+
stBag.dots.add(o.dotId);
|
|
489
|
+
}
|
|
490
|
+
attach(stBag, host, row.route, primary);
|
|
491
|
+
fill(stNode, stBag);
|
|
492
|
+
pNode.children.push(stNode);
|
|
493
|
+
index.set(stNode.id, { node: stNode, bag: stBag, path: pPath, host, pages: [{ host, route: row.route }] });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else if (row.g) {
|
|
498
|
+
for (const r of row.g.reqs) {
|
|
499
|
+
pBag.reqs.add(r);
|
|
500
|
+
placed.add(r);
|
|
501
|
+
for (const d of l.reqs.get(r)?.dots ?? [])
|
|
502
|
+
pBag.dots.add(d);
|
|
503
|
+
}
|
|
504
|
+
for (const d of row.g.dots)
|
|
505
|
+
pBag.dots.add(d);
|
|
506
|
+
for (const f of row.g.specs)
|
|
507
|
+
pBag.tests.add(testKey(f, ''));
|
|
508
|
+
for (const d of input.defects)
|
|
509
|
+
if ((defectRoutes.get(d.ref) ?? []).some((x) => sameRoute(row.route, x.route)) || touches(defectChanged.get(d.ref), host, row.route))
|
|
510
|
+
pBag.defects.add(d.ref);
|
|
511
|
+
if (row.g.specs.size)
|
|
512
|
+
pBag.notRun = true;
|
|
513
|
+
else
|
|
514
|
+
pBag.needsTests = true;
|
|
515
|
+
}
|
|
516
|
+
fill(pNode, pBag, changed);
|
|
517
|
+
sNode.children.push(pNode);
|
|
518
|
+
sPages.push({ host, route: row.route });
|
|
519
|
+
union(sBag, pBag);
|
|
520
|
+
}
|
|
521
|
+
fill(sNode, sBag);
|
|
522
|
+
aPages.push(...sPages);
|
|
523
|
+
addSection(aNode, aBag, sNode, sBag);
|
|
524
|
+
}
|
|
525
|
+
appNodes.push({ node: aNode, bag: aBag, host });
|
|
526
|
+
}
|
|
527
|
+
// Requirements no page took: one node each, under the app of each dot the requirement belongs to (the first app when
|
|
528
|
+
// no dot names one, or the root when there is no app). Linked specs mean Not run yet.
|
|
529
|
+
const left = input.requirements.filter((r) => !placed.has(r.row.ref));
|
|
530
|
+
if (left.length) {
|
|
531
|
+
const dotHosts = new Map();
|
|
532
|
+
const own = (dotId, host) => {
|
|
533
|
+
if (!dotId || !host || !hosts.includes(host))
|
|
534
|
+
return;
|
|
535
|
+
dotHosts.set(dotId, (dotHosts.get(dotId) ?? new Set()).add(host));
|
|
536
|
+
};
|
|
537
|
+
for (const a of input.dotApps ?? [])
|
|
538
|
+
own(a.dotId, a.host);
|
|
539
|
+
// A dot with no base URL owns the hosts its probes, scanned routes and specs name.
|
|
540
|
+
const based = new Set((input.dotApps ?? []).map((a) => a.dotId));
|
|
541
|
+
for (const k of input.knownUrls)
|
|
542
|
+
if (!based.has(k.dotId ?? ''))
|
|
543
|
+
own(k.dotId, hostOf(k.url));
|
|
544
|
+
for (const c of [...input.code, ...(input.specs ?? [])])
|
|
545
|
+
if (!based.has(c.dotId ?? ''))
|
|
546
|
+
own(c.dotId, c.host);
|
|
547
|
+
const byApp = new Map();
|
|
548
|
+
for (const r of left) {
|
|
549
|
+
const targets = new Set((r.dots ?? []).flatMap((d) => [...(dotHosts.get(d) ?? [])]));
|
|
550
|
+
const into = appNodes.filter((a) => targets.has(a.host)).map((a) => a.host);
|
|
551
|
+
for (const h of into.length ? into : [appNodes[0]?.host ?? ''])
|
|
552
|
+
byApp.set(h, [...(byApp.get(h) ?? []), r]);
|
|
553
|
+
}
|
|
554
|
+
for (const [host, reqs] of byApp) {
|
|
555
|
+
const parent = appNodes.find((a) => a.host === host);
|
|
556
|
+
const sNode = make('section', `section:${host}:${NO_PAGE_SECTION}`, NO_PAGE_SECTION);
|
|
557
|
+
const sBag = bag();
|
|
558
|
+
const parentPath = parent
|
|
559
|
+
? [...(root ? [{ id: root.id, name: root.name }] : []), { id: parent.node.id, name: parent.node.name }]
|
|
560
|
+
: [{ id: root.id, name: root.name }];
|
|
561
|
+
index.set(sNode.id, { node: sNode, bag: sBag, path: parentPath, ...(host ? { host } : {}), pages: [] });
|
|
562
|
+
for (const r of reqs) {
|
|
563
|
+
const rBag = bag();
|
|
564
|
+
rBag.reqs.add(r.row.ref);
|
|
565
|
+
for (const d of r.dots ?? [])
|
|
566
|
+
rBag.dots.add(d);
|
|
567
|
+
for (const d of input.defects)
|
|
568
|
+
if (d.requirements.includes(r.row.ref))
|
|
569
|
+
rBag.defects.add(d.ref);
|
|
570
|
+
for (const f of r.specs)
|
|
571
|
+
rBag.tests.add(testKey(f, ''));
|
|
572
|
+
if (r.specs.length)
|
|
573
|
+
rBag.notRun = true;
|
|
574
|
+
else
|
|
575
|
+
rBag.needsTests = true;
|
|
576
|
+
const rNode = make('page', `req:${host}:${r.row.ref}`, `${r.row.key}${r.row.title ? ` ${r.row.title}` : ''}`);
|
|
577
|
+
fill(rNode, rBag);
|
|
578
|
+
sNode.children.push(rNode);
|
|
579
|
+
index.set(rNode.id, { node: rNode, bag: rBag, path: [...parentPath, { id: sNode.id, name: sNode.name }], pages: [] });
|
|
580
|
+
union(sBag, rBag);
|
|
581
|
+
}
|
|
582
|
+
fill(sNode, sBag);
|
|
583
|
+
if (parent)
|
|
584
|
+
addSection(parent.node, parent.bag, sNode, sBag);
|
|
585
|
+
else
|
|
586
|
+
addSection(root, rootBag, sNode, sBag);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
for (const a of appNodes) {
|
|
590
|
+
fill(a.node, a.bag);
|
|
591
|
+
if (root) {
|
|
592
|
+
root.children.push(a.node);
|
|
593
|
+
union(rootBag, a.bag);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (root)
|
|
597
|
+
fill(root, rootBag);
|
|
598
|
+
const tree = root ?? appNodes[0].node;
|
|
599
|
+
// Gap counts: the pages at or below each node that need tests or a run.
|
|
600
|
+
const countGaps = (n) => {
|
|
601
|
+
const g = { needsTests: 0, notRun: 0 };
|
|
602
|
+
if (n.kind === 'page' && n.status === 'needs_tests')
|
|
603
|
+
g.needsTests++;
|
|
604
|
+
if (n.kind === 'page' && n.status === 'not_run')
|
|
605
|
+
g.notRun++;
|
|
606
|
+
for (const c of n.children) {
|
|
607
|
+
const x = countGaps(c);
|
|
608
|
+
g.needsTests += x.needsTests;
|
|
609
|
+
g.notRun += x.notRun;
|
|
610
|
+
}
|
|
611
|
+
n.counts.gaps = g;
|
|
612
|
+
return g;
|
|
613
|
+
};
|
|
614
|
+
countGaps(tree);
|
|
615
|
+
const stats = { total: 0, covered: 0, partly: 0, failing: 0, needsTests: 0, notRun: 0 };
|
|
616
|
+
for (const { node } of index.values()) {
|
|
617
|
+
stats.total++;
|
|
618
|
+
stats[STATS_KEY[node.status]]++;
|
|
619
|
+
}
|
|
620
|
+
const milestones = [...new Set([...input.requirements.map((r) => r.row.milestone), ...input.defects.map((d) => d.milestone ?? '')].filter(Boolean))].sort(natural);
|
|
621
|
+
return { map: { tree, stats, milestones, observed: input.observations.length > 0 }, index, input, tests, appHost, hasShot };
|
|
622
|
+
}
|
|
623
|
+
// ---------- the code drawer ----------
|
|
624
|
+
/** Source files of the node's pages and the API routes they called, from the repo scan. Detail only. */
|
|
625
|
+
export function codeOf(m, n) {
|
|
626
|
+
const files = new Map();
|
|
627
|
+
for (const pg of n.pages)
|
|
628
|
+
for (const c of m.input.code) {
|
|
629
|
+
if (c.method || c.api || !sameRoute(c.route, pg.route))
|
|
630
|
+
continue;
|
|
631
|
+
files.set(`${c.repo} ${c.file}`, { repo: c.repo, file: c.file, ...(c.line ? { line: c.line } : {}) });
|
|
632
|
+
}
|
|
633
|
+
const api = new Map();
|
|
634
|
+
for (const req of new Set(n.bag.obs.flatMap((o) => o.requests ?? []))) {
|
|
635
|
+
const [method, route = ''] = req.split(' ');
|
|
636
|
+
const c = m.input.code.find((x) => (x.method || x.api) && (!x.method || x.method.toUpperCase() === method) && sameRoute(x.route, route));
|
|
637
|
+
api.set(req, { ...(method ? { method } : {}), route: c?.route ?? route, ...(c ? { repo: c.repo, file: c.line ? `${c.file}:${c.line}` : c.file } : {}) });
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
files: [...files.values()].sort((a, b) => natural(a.file, b.file)),
|
|
641
|
+
api: [...api.values()].sort((a, b) => natural(a.route, b.route) || natural(a.method ?? '', b.method ?? '')),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
const MASCOTS = ['pip', 'nova', 'bolt', 'moss'];
|
|
645
|
+
/** The dot's mascot, else the one its id hashes to: the same hash as the client's mascotFor. */
|
|
646
|
+
export function avatarOf(w) {
|
|
647
|
+
if (w.mascot && MASCOTS.includes(w.mascot))
|
|
648
|
+
return w.mascot;
|
|
649
|
+
let h = 0;
|
|
650
|
+
for (let i = 0; i < w.id.length; i++)
|
|
651
|
+
h = (Math.imul(h, 31) + w.id.charCodeAt(i)) | 0;
|
|
652
|
+
return MASCOTS[Math.abs(h) % MASCOTS.length];
|
|
653
|
+
}
|
|
654
|
+
/** The stage words of a run: "Reviewing PR #34", or "Running tests" without a PR. */
|
|
655
|
+
export function runLabel(kind, pr) {
|
|
656
|
+
if (pr == null)
|
|
657
|
+
return kind === 'rerun' ? 'Rerunning tests' : 'Running tests';
|
|
658
|
+
const word = {
|
|
659
|
+
pr_check: 'Reviewing',
|
|
660
|
+
post_merge: 'Testing the merge of',
|
|
661
|
+
ci_failure: 'Reading the CI failure on',
|
|
662
|
+
rerun: 'Rerunning',
|
|
663
|
+
};
|
|
664
|
+
return `${word[kind] ?? 'Working on'} PR #${pr}`;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Puts `dots` on every section, page and state node: the dots whose runs visited it, or whose requirement it is.
|
|
668
|
+
* A dot with a run in progress that has reached the page, or that is on a PR whose changed files hold the page's
|
|
669
|
+
* source (from the repo scan), carries the run as `running`.
|
|
670
|
+
*/
|
|
671
|
+
export function attachDots(model, dots) {
|
|
672
|
+
for (const n of model.index.values()) {
|
|
673
|
+
if (n.node.kind === 'root' || n.node.kind === 'app')
|
|
674
|
+
continue;
|
|
675
|
+
const sources = codeOf(model, n).files;
|
|
676
|
+
const out = [];
|
|
677
|
+
for (const d of dots) {
|
|
678
|
+
const run = d.running.find((r) => r.pages.some((p) => n.pages.some((x) => x.host === model.appHost(p.host) && sameRoute(x.route, p.route))) ||
|
|
679
|
+
r.files.some((f) => sources.some((s) => s.repo === f.repo && samePath(s.file, f.path))));
|
|
680
|
+
if (!n.bag.dots.has(d.id) && !run)
|
|
681
|
+
continue;
|
|
682
|
+
out.push({ id: d.id, name: d.name, avatar: d.avatar, ...(run ? { running: { runId: run.runId, label: run.label } } : {}) });
|
|
683
|
+
}
|
|
684
|
+
n.node.dots = out;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// ---------- the all-dots map ----------
|
|
688
|
+
/** One input from several dots' facts: observations side by side, requirements and defects merged, the newest test result wins. */
|
|
689
|
+
export function mergeInputs(title, inputs) {
|
|
690
|
+
const reqs = new Map();
|
|
691
|
+
const tests = new Map();
|
|
692
|
+
const defects = new Map();
|
|
693
|
+
const code = new Map();
|
|
694
|
+
const uniq = (xs) => [...new Set(xs)];
|
|
695
|
+
for (const i of inputs) {
|
|
696
|
+
for (const r of i.requirements) {
|
|
697
|
+
const had = reqs.get(r.row.ref);
|
|
698
|
+
if (!had)
|
|
699
|
+
reqs.set(r.row.ref, { ...r, prs: [...r.prs], specs: [...r.specs], dots: [...(r.dots ?? [])] });
|
|
700
|
+
else {
|
|
701
|
+
had.prs = uniq([...had.prs, ...r.prs]);
|
|
702
|
+
had.specs = uniq([...had.specs, ...r.specs]);
|
|
703
|
+
had.dots = uniq([...(had.dots ?? []), ...(r.dots ?? [])]);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
for (const t of i.tests) {
|
|
707
|
+
const k = testKey(t.file, t.title, t.host);
|
|
708
|
+
const had = tests.get(k);
|
|
709
|
+
if (!had || (t.at ?? '') > (had.at ?? ''))
|
|
710
|
+
tests.set(k, t);
|
|
711
|
+
}
|
|
712
|
+
for (const d of i.defects) {
|
|
713
|
+
const had = defects.get(d.ref);
|
|
714
|
+
if (!had)
|
|
715
|
+
defects.set(d.ref, { ...d, requirements: [...d.requirements], prs: [...d.prs] });
|
|
716
|
+
else {
|
|
717
|
+
had.requirements = uniq([...had.requirements, ...d.requirements]);
|
|
718
|
+
had.prs = uniq([...had.prs, ...d.prs]);
|
|
719
|
+
if (had.state === 'unknown')
|
|
720
|
+
had.state = d.state;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
for (const c of i.code)
|
|
724
|
+
code.set(`${c.repo} ${c.host ?? ''} ${c.method ?? ''} ${c.route} ${c.file}`, c);
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
title,
|
|
728
|
+
baseUrls: uniq(inputs.flatMap((i) => i.baseUrls)),
|
|
729
|
+
observations: inputs.flatMap((i) => i.observations),
|
|
730
|
+
tests: [...tests.values()],
|
|
731
|
+
requirements: [...reqs.values()],
|
|
732
|
+
defects: [...defects.values()],
|
|
733
|
+
code: [...code.values()],
|
|
734
|
+
knownUrls: inputs.flatMap((i) => i.knownUrls),
|
|
735
|
+
specs: inputs.flatMap((i) => i.specs ?? []),
|
|
736
|
+
changes: inputs.flatMap((i) => i.changes ?? []),
|
|
737
|
+
areas: inputs.flatMap((i) => i.areas ?? []),
|
|
738
|
+
prChanges: inputs.flatMap((i) => i.prChanges ?? []),
|
|
739
|
+
dotApps: inputs.flatMap((i) => i.dotApps ?? []),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
// ---------- facts from the database ----------
|
|
743
|
+
const MAX_RUNS = 1000;
|
|
744
|
+
/** The recent runs whose observations make the map. */
|
|
745
|
+
const MAX_OBS_RUNS = 60;
|
|
746
|
+
const MAX_OBS_LINES = 20_000;
|
|
747
|
+
const MAX_SHOTS = 12;
|
|
748
|
+
const FAILED = (c) => c.status === 'failed' || c.status === 'timedOut';
|
|
749
|
+
const prRefOf = (r) => r.trigger?.pr && r.trigger.repo ? `${r.trigger.repo.owner}/${r.trigger.repo.name}#${r.trigger.pr.number}`.toLowerCase() : null;
|
|
750
|
+
const ranTests = (r) => (r.status === 'done' || r.status === 'running') && !!r.report && !r.report.notRun;
|
|
751
|
+
/** Up to three lines a person can repeat: numbered or bulleted steps first, else the first line of the text. */
|
|
752
|
+
export function reproLines(text) {
|
|
753
|
+
const lines = (text ?? '')
|
|
754
|
+
.split(/\r?\n/)
|
|
755
|
+
.map((x) => x.trim())
|
|
756
|
+
.filter(Boolean);
|
|
757
|
+
const steps = lines.filter((x) => /^(\d+[.)]|[-*])\s+/.test(x)).map((x) => x.replace(/^(\d+[.)]|[-*])\s+/, ''));
|
|
758
|
+
return (steps.length ? steps : lines.slice(0, 1)).slice(0, 3).map((x) => x.slice(0, 200));
|
|
759
|
+
}
|
|
760
|
+
/** The observation lines of a run with the run's facts on each. */
|
|
761
|
+
export function observationsOfRun(r) {
|
|
762
|
+
const pr = prRefOf(r);
|
|
763
|
+
return readObservations(r.id).map((o) => ({ ...o, runId: r.id, runAt: r.createdAt, ...(pr ? { pr } : {}), dotId: r.watchId }));
|
|
764
|
+
}
|
|
765
|
+
function gatherFacts(w) {
|
|
766
|
+
const built = buildAll(w);
|
|
767
|
+
const runs = list('runs', { watchId: w.id }, { orderBy: 'createdAt DESC', limit: MAX_RUNS });
|
|
768
|
+
// Observations of the recent runs that ran tests.
|
|
769
|
+
const observations = [];
|
|
770
|
+
for (const r of runs.filter((x) => ranTests(x) || x.status === 'running').slice(0, MAX_OBS_RUNS)) {
|
|
771
|
+
if (observations.length >= MAX_OBS_LINES)
|
|
772
|
+
break;
|
|
773
|
+
observations.push(...observationsOfRun(r).slice(0, MAX_OBS_LINES - observations.length));
|
|
774
|
+
}
|
|
775
|
+
// The latest result of each test: the newest run that holds it, the same reading as flows/testStatus.ts.
|
|
776
|
+
// A result belongs to the app the dot tests, so two dots on one app share a test (KAT-250).
|
|
777
|
+
const appHost = w.baseUrl ? hostOf(w.baseUrl) : '';
|
|
778
|
+
const tests = new Map();
|
|
779
|
+
for (const r of runs.filter(ranTests)) {
|
|
780
|
+
for (const c of r.report.cases) {
|
|
781
|
+
const k = testKey(c.file, c.title, appHost);
|
|
782
|
+
if (tests.has(k))
|
|
783
|
+
continue;
|
|
784
|
+
const result = FAILED(c) ? 'failed' : c.status === 'passed' || c.status === 'flaky' ? 'passed' : 'not_run';
|
|
785
|
+
tests.set(k, {
|
|
786
|
+
file: c.file,
|
|
787
|
+
title: c.title,
|
|
788
|
+
result,
|
|
789
|
+
runId: r.id,
|
|
790
|
+
at: r.createdAt,
|
|
791
|
+
...(appHost ? { host: appHost } : {}),
|
|
792
|
+
...(FAILED(c) && c.error ? { error: c.error.split('\n')[0].slice(0, 240) } : {}),
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const requirements = built.all.map((row) => {
|
|
797
|
+
const s = built.statuses.get(row.ref);
|
|
798
|
+
const info = built.catalog.get(row.ref);
|
|
799
|
+
return {
|
|
800
|
+
row,
|
|
801
|
+
prs: (s?.prs ?? []).map((p) => p.ref),
|
|
802
|
+
specs: (s?.specs ?? []).map((x) => x.path),
|
|
803
|
+
text: `${info?.title ?? row.title}\n${info?.description ?? ''}`,
|
|
804
|
+
dots: [w.id],
|
|
805
|
+
};
|
|
806
|
+
});
|
|
807
|
+
// Defects: the ones a requirement's status names, the defect issues the dot has read, and localdost' own defect links.
|
|
808
|
+
const prRefs = new Set(runs.map(prRefOf).filter((p) => !!p));
|
|
809
|
+
const defects = new Map();
|
|
810
|
+
const defect = (ref) => {
|
|
811
|
+
let d = defects.get(ref);
|
|
812
|
+
if (d)
|
|
813
|
+
return d;
|
|
814
|
+
const info = built.catalog.get(ref);
|
|
815
|
+
const gh = parseIssueRef(ref);
|
|
816
|
+
const url = info?.url ?? (gh ? issueUrl(gh.full, gh.number) : undefined);
|
|
817
|
+
d = {
|
|
818
|
+
ref,
|
|
819
|
+
key: info?.key ?? (gh ? ref : ref.toUpperCase()),
|
|
820
|
+
state: info?.state ?? 'unknown',
|
|
821
|
+
...(info?.title ? { title: info.title } : {}),
|
|
822
|
+
...(url ? { url } : {}),
|
|
823
|
+
...(info?.milestone ? { milestone: info.milestone } : {}),
|
|
824
|
+
repro: reproLines(info?.description),
|
|
825
|
+
text: `${info?.title ?? ''}\n${info?.description ?? ''}`,
|
|
826
|
+
requirements: [],
|
|
827
|
+
prs: [],
|
|
828
|
+
};
|
|
829
|
+
defects.set(ref, d);
|
|
830
|
+
return d;
|
|
831
|
+
};
|
|
832
|
+
for (const s of built.statuses.values()) {
|
|
833
|
+
for (const x of s.defects) {
|
|
834
|
+
const d = defect(x.ref);
|
|
835
|
+
if (x.state !== 'unknown')
|
|
836
|
+
d.state = x.state;
|
|
837
|
+
if (!d.requirements.includes(s.ref))
|
|
838
|
+
d.requirements.push(s.ref);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
for (const info of built.catalog.values())
|
|
842
|
+
if (classify(w, info) === 'defect')
|
|
843
|
+
defect(info.ref);
|
|
844
|
+
for (const lk of linksOfWatch(w.id, 5000)) {
|
|
845
|
+
if (lk.source !== 'defect')
|
|
846
|
+
continue;
|
|
847
|
+
const d = defect(lk.fromRef);
|
|
848
|
+
if (prRefs.has(lk.toRef))
|
|
849
|
+
d.prs.push(lk.toRef);
|
|
850
|
+
else if (!d.requirements.includes(lk.toRef))
|
|
851
|
+
d.requirements.push(lk.toRef);
|
|
852
|
+
}
|
|
853
|
+
const rowOf = new Map(built.all.map((r) => [r.ref, r]));
|
|
854
|
+
for (const d of defects.values()) {
|
|
855
|
+
const ms = d.milestone ?? d.requirements.map((r) => rowOf.get(r)?.milestone).find(Boolean);
|
|
856
|
+
if (ms)
|
|
857
|
+
d.milestone = ms;
|
|
858
|
+
}
|
|
859
|
+
// The repo scan: routes with their source files, for the drawer only.
|
|
860
|
+
const code = [];
|
|
861
|
+
const specs = [];
|
|
862
|
+
const areas = [];
|
|
863
|
+
const host = w.baseUrl ? hostOf(w.baseUrl) : '';
|
|
864
|
+
const at = host ? { host, dotId: w.id } : { dotId: w.id };
|
|
865
|
+
for (const repo of watchRepos(w)) {
|
|
866
|
+
const p = latestProfile(repo.id)?.profile;
|
|
867
|
+
if (!p)
|
|
868
|
+
continue;
|
|
869
|
+
areas.push(...areasOfProfile(p, host, repo.id));
|
|
870
|
+
for (const sp of p.tests.specs)
|
|
871
|
+
if (sp.routes.length)
|
|
872
|
+
specs.push({ file: sp.path, routes: sp.routes, ...at });
|
|
873
|
+
const apiApps = new Set(p.apps.filter((a) => a.kind === 'api').map((a) => a.id));
|
|
874
|
+
for (const r of p.routes)
|
|
875
|
+
code.push({
|
|
876
|
+
repo: repo.id,
|
|
877
|
+
route: r.route,
|
|
878
|
+
file: r.file,
|
|
879
|
+
...(r.line ? { line: r.line } : {}),
|
|
880
|
+
...(r.method ? { method: r.method.toUpperCase() } : {}),
|
|
881
|
+
...(apiApps.has(r.app) || /^\/api(\/|$)/.test(r.route) ? { api: true } : {}),
|
|
882
|
+
...at,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
// What merges changed, and when: the source of a page that changed after its latest visit makes it Not run yet.
|
|
886
|
+
const changes = [];
|
|
887
|
+
for (const r of runs) {
|
|
888
|
+
if (r.kind !== 'post_merge')
|
|
889
|
+
continue;
|
|
890
|
+
const repo = r.trigger?.repo ? `${r.trigger.repo.owner}/${r.trigger.repo.name}`.toLowerCase() : watchRepos(w)[0]?.id;
|
|
891
|
+
if (!repo)
|
|
892
|
+
continue;
|
|
893
|
+
for (const f of r.diff ?? r.trigger?.inlineDiff ?? [])
|
|
894
|
+
changes.push({ repo, path: f.path, at: r.createdAt });
|
|
895
|
+
}
|
|
896
|
+
// The files each PR changed, from the runs on it: what ties the PR's requirements to pages.
|
|
897
|
+
const prChanges = new Map();
|
|
898
|
+
for (const r of runs) {
|
|
899
|
+
const pr = prRefOf(r);
|
|
900
|
+
if (!pr)
|
|
901
|
+
continue;
|
|
902
|
+
const repo = `${r.trigger.repo.owner}/${r.trigger.repo.name}`.toLowerCase();
|
|
903
|
+
for (const f of r.diff ?? r.trigger?.inlineDiff ?? [])
|
|
904
|
+
prChanges.set(`${pr} ${f.path}`, { pr, repo, path: f.path });
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
title: w.name,
|
|
908
|
+
baseUrls: w.baseUrl ? [w.baseUrl] : [],
|
|
909
|
+
observations,
|
|
910
|
+
tests: [...tests.values()],
|
|
911
|
+
requirements,
|
|
912
|
+
defects: [...defects.values()],
|
|
913
|
+
code,
|
|
914
|
+
knownUrls: (w.probes ?? []).map((p) => ({ url: p.url, dotId: w.id })),
|
|
915
|
+
specs,
|
|
916
|
+
changes,
|
|
917
|
+
areas,
|
|
918
|
+
prChanges: [...prChanges.values()],
|
|
919
|
+
...(host ? { dotApps: [{ dotId: w.id, host }] } : {}),
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* The product areas of a repo profile with their page routes (the area's own list, and the scanned routes tagged
|
|
924
|
+
* with it) and the path globs of their files.
|
|
925
|
+
*/
|
|
926
|
+
export function areasOfProfile(p, host, repo) {
|
|
927
|
+
const by = new Map();
|
|
928
|
+
const get = (id, name) => {
|
|
929
|
+
const a = by.get(id) ?? { name: name ?? titleCase(id), routes: new Set(), paths: [] };
|
|
930
|
+
by.set(id, a);
|
|
931
|
+
return a;
|
|
932
|
+
};
|
|
933
|
+
for (const a of p.areas ?? []) {
|
|
934
|
+
const x = get(a.id, a.name);
|
|
935
|
+
x.paths.push(...(a.paths ?? []));
|
|
936
|
+
for (const r of a.routes)
|
|
937
|
+
x.routes.add(r);
|
|
938
|
+
}
|
|
939
|
+
for (const r of p.routes ?? []) {
|
|
940
|
+
if (!r.area || r.method || /^\/api(\/|$)/.test(r.route))
|
|
941
|
+
continue;
|
|
942
|
+
get(r.area, p.areas?.find((a) => a.id === r.area)?.name).routes.add(r.route);
|
|
943
|
+
}
|
|
944
|
+
return [...by.values()]
|
|
945
|
+
.filter((a) => a.routes.size)
|
|
946
|
+
.map((a) => ({
|
|
947
|
+
name: a.name,
|
|
948
|
+
routes: [...a.routes],
|
|
949
|
+
...(host ? { host } : {}),
|
|
950
|
+
...(a.paths.length ? { paths: a.paths } : {}),
|
|
951
|
+
...(a.paths.length && repo ? { repo } : {}),
|
|
952
|
+
}));
|
|
953
|
+
}
|
|
954
|
+
// ---------- reads ----------
|
|
955
|
+
/** GET /api/dots/:id/trace/map. */
|
|
956
|
+
export function buildTraceMap(w) {
|
|
957
|
+
const model = mapTree(gatherFacts(w), { hasShot: artifactShotCheck() });
|
|
958
|
+
attachDots(model, dotFacts(liveDots()));
|
|
959
|
+
return model;
|
|
960
|
+
}
|
|
961
|
+
const liveDots = () => list('watches').filter((w) => !w.archived);
|
|
962
|
+
/** The presence facts of the dots: their runs in progress, with the pages seen so far and the PR's files. */
|
|
963
|
+
function dotFacts(dots) {
|
|
964
|
+
const running = list('runs', { status: 'running' }, { orderBy: 'createdAt DESC', limit: 200 });
|
|
965
|
+
return dots.map((w) => {
|
|
966
|
+
const repo = watchRepos(w)[0]?.id;
|
|
967
|
+
return {
|
|
968
|
+
id: w.id,
|
|
969
|
+
name: w.name,
|
|
970
|
+
avatar: avatarOf(w),
|
|
971
|
+
running: running
|
|
972
|
+
.filter((r) => r.watchId === w.id)
|
|
973
|
+
.map((r) => {
|
|
974
|
+
const runRepo = r.trigger?.repo ? `${r.trigger.repo.owner}/${r.trigger.repo.name}`.toLowerCase() : repo;
|
|
975
|
+
return {
|
|
976
|
+
runId: r.id,
|
|
977
|
+
label: runLabel(r.kind, r.trigger?.pr?.number),
|
|
978
|
+
pages: readObservations(r.id).map((o) => ({ host: o.host, route: routeOf(o.url || o.route) })),
|
|
979
|
+
files: runRepo ? (r.diff ?? r.trigger?.inlineDiff ?? []).map((f) => ({ repo: runRepo, path: f.path })) : [],
|
|
980
|
+
};
|
|
981
|
+
}),
|
|
982
|
+
};
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
/** GET /api/trace/map: every live dot's observed apps on one canvas. */
|
|
986
|
+
export function buildAllDotsMap() {
|
|
987
|
+
const dots = liveDots();
|
|
988
|
+
const model = mapTree(mergeInputs(ALL_DOTS, dots.map(gatherFacts)), { hasShot: artifactShotCheck() });
|
|
989
|
+
attachDots(model, dotFacts(dots));
|
|
990
|
+
return model;
|
|
991
|
+
}
|
|
992
|
+
export const ALL_DOTS = 'All dots';
|
|
993
|
+
/** The skill scope of the all-dots map: no single dot, so the base URL is the page's app. */
|
|
994
|
+
export const ALL_DOTS_SCOPE = { name: ALL_DOTS };
|
|
995
|
+
const resultWord = { passed: 'Passed', failed: 'Failed', not_run: 'Not run' };
|
|
996
|
+
function testsOf(m, n) {
|
|
997
|
+
return [...n.bag.tests]
|
|
998
|
+
.map((k) => {
|
|
999
|
+
const t = factOf(m.tests, k);
|
|
1000
|
+
const o = n.bag.obs.find((x) => obsKey(x) === k);
|
|
1001
|
+
// A spec that covers a page no run visited has no title: the key is the file alone.
|
|
1002
|
+
const [file = '', title = ''] = k.split('\u0000');
|
|
1003
|
+
const name = t?.title ?? o?.title ?? title;
|
|
1004
|
+
return {
|
|
1005
|
+
path: t?.file ?? o?.test ?? file,
|
|
1006
|
+
...(name ? { title: name } : {}),
|
|
1007
|
+
result: resultOf(k, m.tests, n.bag.obs),
|
|
1008
|
+
...(t?.runId ? { runId: t.runId } : o ? { runId: o.runId } : {}),
|
|
1009
|
+
...(t?.at ? { at: t.at } : {}),
|
|
1010
|
+
...(t?.error ? { error: t.error } : {}),
|
|
1011
|
+
};
|
|
1012
|
+
})
|
|
1013
|
+
.sort((a, b) => (a.result === 'failed' ? 0 : 1) - (b.result === 'failed' ? 0 : 1) || natural(a.path, b.path) || natural(a.title ?? '', b.title ?? ''));
|
|
1014
|
+
}
|
|
1015
|
+
const SHOT_RESULT = (r) => r === 'passed' ? 'passed' : r === 'failed' || r === 'timedOut' ? 'failed' : r ? 'not_run' : undefined;
|
|
1016
|
+
/** One screenshot per test per route, the latest, with the test's result in that run. Newest first. */
|
|
1017
|
+
function shotsOf(m, n) {
|
|
1018
|
+
const seen = new Set();
|
|
1019
|
+
return n.bag.obs
|
|
1020
|
+
.filter((o) => !!o.screenshot && m.hasShot(o.runId, o.screenshot))
|
|
1021
|
+
.sort((a, b) => b.at.localeCompare(a.at))
|
|
1022
|
+
.filter((o) => {
|
|
1023
|
+
const k = `${obsKey(o)}\u0000${routeOf(o.url || o.route)}`;
|
|
1024
|
+
return seen.has(k) ? false : (seen.add(k), true);
|
|
1025
|
+
})
|
|
1026
|
+
.slice(0, MAX_SHOTS)
|
|
1027
|
+
.map((o) => {
|
|
1028
|
+
const result = SHOT_RESULT(o.result) ?? resultOf(obsKey(o), m.tests, n.bag.obs);
|
|
1029
|
+
return {
|
|
1030
|
+
url: artifactUrl(o.runId, o.screenshot),
|
|
1031
|
+
name: o.screenshot,
|
|
1032
|
+
runId: o.runId,
|
|
1033
|
+
spec: o.test,
|
|
1034
|
+
test: o.title,
|
|
1035
|
+
route: routeOf(o.url || o.route),
|
|
1036
|
+
at: o.at,
|
|
1037
|
+
result,
|
|
1038
|
+
};
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
/** Spec files of the tests at or below a node: what Run tests runs. */
|
|
1042
|
+
export function specsOf(n) {
|
|
1043
|
+
return [...new Set([...n.bag.tests].map((k) => k.split('\u0000')[0]).filter(Boolean))].sort(natural);
|
|
1044
|
+
}
|
|
1045
|
+
/** The pages at or below a node, itself included, that need tests or a run. */
|
|
1046
|
+
export function gapsOf(m, n) {
|
|
1047
|
+
const out = [];
|
|
1048
|
+
const walk = (x) => {
|
|
1049
|
+
const e = m.index.get(x.id);
|
|
1050
|
+
if (e && x.kind === 'page' && isGap(x.status))
|
|
1051
|
+
out.push({
|
|
1052
|
+
id: x.id,
|
|
1053
|
+
name: x.name,
|
|
1054
|
+
...(x.route ? { route: x.route } : {}),
|
|
1055
|
+
status: x.status,
|
|
1056
|
+
...(e.changed ? { changed: true } : {}),
|
|
1057
|
+
specs: specsOf(e),
|
|
1058
|
+
});
|
|
1059
|
+
for (const c of x.children)
|
|
1060
|
+
walk(c);
|
|
1061
|
+
};
|
|
1062
|
+
walk(n.node);
|
|
1063
|
+
return out;
|
|
1064
|
+
}
|
|
1065
|
+
/** A code span that cannot be broken out of: backticks become quotes. */
|
|
1066
|
+
const code = (s) => `\`${s.replace(/`/g, "'").replace(/\s+/g, ' ')}\``;
|
|
1067
|
+
const joinUrl = (base, route) => `${base.replace(/\/+$/, '')}/${route.replace(/^\/+/, '')}`;
|
|
1068
|
+
/** The base URL of a node's app: the dot's own when it is on that host, else the origin a run saw. */
|
|
1069
|
+
function baseOf(w, n) {
|
|
1070
|
+
if (w.baseUrl && (!n.host || hostOf(w.baseUrl) === n.host))
|
|
1071
|
+
return w.baseUrl;
|
|
1072
|
+
const seen = n.bag.obs.find((o) => o.url)?.url;
|
|
1073
|
+
if (seen) {
|
|
1074
|
+
try {
|
|
1075
|
+
return new URL(seen).origin;
|
|
1076
|
+
}
|
|
1077
|
+
catch {
|
|
1078
|
+
/* fall through */
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return w.baseUrl;
|
|
1082
|
+
}
|
|
1083
|
+
const uniqBy = (xs, key) => {
|
|
1084
|
+
const seen = new Set();
|
|
1085
|
+
return xs.filter((x) => (seen.has(key(x)) ? false : (seen.add(key(x)), true)));
|
|
1086
|
+
};
|
|
1087
|
+
/** Headings and landmarks of the newest observations of a node. */
|
|
1088
|
+
function onPage(n) {
|
|
1089
|
+
const obs = [...n.bag.obs].sort((a, b) => b.at.localeCompare(a.at));
|
|
1090
|
+
return {
|
|
1091
|
+
headings: uniqBy(obs.flatMap((o) => o.headings.map((h) => `h${h.level} ${h.text}`)), (x) => x).slice(0, 12),
|
|
1092
|
+
landmarks: [...new Set(obs.flatMap((o) => o.landmarks))].slice(0, 12),
|
|
1093
|
+
urls: [...new Set(obs.map((o) => o.url))].slice(0, 5),
|
|
1094
|
+
titles: [...new Set(obs.map((o) => o.pageTitle).filter((t) => !!t))].slice(0, 3),
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
/** Markdown an agent can follow for one node, from observations only. `depth` 1 makes the title an h1. */
|
|
1098
|
+
export function nodeSkill(w, m, n, depth = 1) {
|
|
1099
|
+
const h = (k) => '#'.repeat(Math.min(6, depth + k - 1));
|
|
1100
|
+
const c = n.node.counts;
|
|
1101
|
+
const tests = testsOf(m, n);
|
|
1102
|
+
const defects = [...n.bag.defects].map((r) => m.input.defects.find((d) => d.ref === r)).filter(Boolean);
|
|
1103
|
+
const open = defects.filter((d) => d.state === 'open');
|
|
1104
|
+
const kind = { root: 'Map', app: 'App', section: 'Section', page: 'Page', state: 'State' }[n.node.kind];
|
|
1105
|
+
const seen = onPage(n);
|
|
1106
|
+
const out = [
|
|
1107
|
+
`${h(1)} ${kind}: ${mdText(n.node.name)}`,
|
|
1108
|
+
'',
|
|
1109
|
+
`Status: ${STATUS_WORD[n.node.status]}. ${c.tests.passed} of ${c.tests.total} tests passed, ${c.tests.failed} failed, ${c.defects.open} open ${c.defects.open === 1 ? 'bug' : 'bugs'}, ${c.requirements} ${c.requirements === 1 ? 'requirement' : 'requirements'}.`,
|
|
1110
|
+
`Node id: \`${n.node.id}\`${n.path.length ? `. Path: ${n.path.map((p) => mdText(p.name)).join(' > ')} > ${mdText(n.node.name)}` : ''}`,
|
|
1111
|
+
'',
|
|
1112
|
+
`${h(2)} How to reach it`,
|
|
1113
|
+
'',
|
|
1114
|
+
];
|
|
1115
|
+
const base = baseOf(w, n);
|
|
1116
|
+
out.push(`- ${base ? `App base URL: ${mdText(base)}` : 'App base URL: not set on the dot. Set it in the dot settings.'}`);
|
|
1117
|
+
if (n.node.route && n.node.kind !== 'section') {
|
|
1118
|
+
const open = base && !/[:[{*]/.test(n.node.route) ? `, open ${mdLink(joinUrl(base, n.node.route), joinUrl(base, n.node.route))}` : '';
|
|
1119
|
+
out.push(`- Route: \`${n.node.route}\`${open}`);
|
|
1120
|
+
}
|
|
1121
|
+
else if (n.node.kind === 'section' || n.node.kind === 'app') {
|
|
1122
|
+
const pages = n.pages.map((p) => `\`${p.route}\``);
|
|
1123
|
+
if (pages.length)
|
|
1124
|
+
out.push(`- Pages: ${pages.join(', ')}`);
|
|
1125
|
+
}
|
|
1126
|
+
if (n.node.status === 'needs_tests')
|
|
1127
|
+
out.push('- No test covers it yet, and no run has visited it. Write a test that opens it: the next run adds what is on it, a screenshot, and the steps.');
|
|
1128
|
+
if (n.node.status === 'not_run')
|
|
1129
|
+
out.push(n.changed
|
|
1130
|
+
? '- Its source changed after the latest run that visited it. Run its tests again.'
|
|
1131
|
+
: `- Tests cover it, but no run has executed them yet. Run them: ${specsOf(n)
|
|
1132
|
+
.map((f) => `\`${f}\``)
|
|
1133
|
+
.join(', ')}.`);
|
|
1134
|
+
for (const u of seen.urls.slice(0, 3))
|
|
1135
|
+
out.push(`- Seen at ${mdText(u)}`);
|
|
1136
|
+
// The clicks tests used to get here, with the page they came from.
|
|
1137
|
+
const ways = uniqBy(n.bag.obs.filter((o) => o.action), (o) => `${o.from ?? ''} ${o.action}`).slice(0, 4);
|
|
1138
|
+
for (const o of ways)
|
|
1139
|
+
out.push(`- ${o.from ? `From \`${o.from}\`: ` : ''}${code(o.action)} (${mdText(o.title)})`);
|
|
1140
|
+
if (n.node.kind === 'page' || n.node.kind === 'state') {
|
|
1141
|
+
out.push('', `${h(2)} What is on it`, '');
|
|
1142
|
+
if (!n.bag.obs.length)
|
|
1143
|
+
out.push('Not seen yet.');
|
|
1144
|
+
else {
|
|
1145
|
+
if (seen.titles.length)
|
|
1146
|
+
out.push(`- Page title: ${seen.titles.map(mdText).join(' / ')}`);
|
|
1147
|
+
out.push(`- Headings: ${seen.headings.length ? seen.headings.map(mdText).join('; ') : 'none seen'}`);
|
|
1148
|
+
out.push(`- Landmarks: ${seen.landmarks.length ? seen.landmarks.map(mdText).join(', ') : 'none seen'}`);
|
|
1149
|
+
const states = n.node.children.filter((x) => x.kind === 'state').map((x) => mdText(x.name));
|
|
1150
|
+
if (states.length)
|
|
1151
|
+
out.push(`- States: ${states.join('; ')}`);
|
|
1152
|
+
const reqs = [...new Set(n.bag.obs.flatMap((o) => o.requests ?? []))].slice(0, 10);
|
|
1153
|
+
if (reqs.length)
|
|
1154
|
+
out.push(`- Requests: ${reqs.map((r) => `\`${r}\``).join(', ')}`);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
out.push('', `${h(2)} Tests that cover it`, '');
|
|
1158
|
+
if (!tests.length)
|
|
1159
|
+
out.push('No test has visited it yet.');
|
|
1160
|
+
else {
|
|
1161
|
+
out.push('| Test | Spec | Last result | Run |', '|---|---|---|---|');
|
|
1162
|
+
for (const t of tests)
|
|
1163
|
+
out.push(`| ${mdText(t.title ?? '')} | \`${t.path}\` | ${resultWord[t.result]} | ${t.runId ? `\`${t.runId}\`` : '-'} |`);
|
|
1164
|
+
}
|
|
1165
|
+
out.push('', `${h(2)} Open bugs`, '');
|
|
1166
|
+
if (!open.length)
|
|
1167
|
+
out.push('None open.');
|
|
1168
|
+
for (const d of open) {
|
|
1169
|
+
out.push(`- ${mdLink(`${d.key}${d.title ? ` ${d.title}` : ''}`, d.url)}`);
|
|
1170
|
+
for (const r of d.repro)
|
|
1171
|
+
out.push(` - Repro: ${mdText(r)}`);
|
|
1172
|
+
}
|
|
1173
|
+
// Gotchas: steps that failed on this page, then failed tests that visited it.
|
|
1174
|
+
const failedSteps = uniqBy(n.bag.obs.flatMap((o) => (o.steps ?? []).filter((s) => s.error).map((s) => ({ step: s.title, error: s.error }))), (x) => `${x.step} ${x.error}`).slice(0, 6);
|
|
1175
|
+
const failedTests = tests.filter((t) => t.result === 'failed' && t.error && !failedSteps.some((s) => s.error === t.error));
|
|
1176
|
+
out.push('', `${h(2)} Known gotchas`, '');
|
|
1177
|
+
if (!failedSteps.length && !failedTests.length)
|
|
1178
|
+
out.push('None seen in the latest results.');
|
|
1179
|
+
for (const s of failedSteps)
|
|
1180
|
+
out.push(`- ${code(s.step)} failed: ${mdText(s.error)}`);
|
|
1181
|
+
for (const t of failedTests)
|
|
1182
|
+
out.push(`- \`${t.path}\` failed: ${mdText(t.error)}`);
|
|
1183
|
+
const shot = shotsOf(m, n)[0];
|
|
1184
|
+
out.push('', `${h(2)} Latest screenshot`, '', shot ? `${shot.url} (run \`${shot.runId}\`, ${shot.at.slice(0, 16).replace('T', ' ')} UTC)` : 'None yet.');
|
|
1185
|
+
const reqs = [...n.bag.reqs].map((r) => m.input.requirements.find((x) => x.row.ref === r)?.row).filter((r) => !!r);
|
|
1186
|
+
if (reqs.length) {
|
|
1187
|
+
out.push('', `${h(2)} Requirements`, '');
|
|
1188
|
+
for (const r of reqs)
|
|
1189
|
+
out.push(`- ${mdLink(`${r.key}${r.title ? ` ${r.title}` : ''}`, r.url)}: ${mdText(r.status)}`);
|
|
1190
|
+
}
|
|
1191
|
+
const scan = codeOf(m, n);
|
|
1192
|
+
if (scan.files.length || scan.api.length) {
|
|
1193
|
+
out.push('', `${h(2)} Code`, '');
|
|
1194
|
+
for (const f of scan.files.slice(0, 10))
|
|
1195
|
+
out.push(`- Source: \`${f.file}${f.line ? `:${f.line}` : ''}\` (${mdText(f.repo)})`);
|
|
1196
|
+
for (const a of scan.api.slice(0, 10))
|
|
1197
|
+
out.push(`- API: \`${a.method ? `${a.method} ` : ''}${a.route}\`${a.file ? ` in \`${a.file}\`` : ''}`);
|
|
1198
|
+
}
|
|
1199
|
+
out.push('');
|
|
1200
|
+
return out.join('\n');
|
|
1201
|
+
}
|
|
1202
|
+
/** GET /api/dots/:id/trace/map/:nodeId and GET /api/trace/map/:nodeId. Null when the node is not in the tree. */
|
|
1203
|
+
export function traceMapDetail(w, id, model) {
|
|
1204
|
+
const n = model.index.get(id);
|
|
1205
|
+
if (!n)
|
|
1206
|
+
return null;
|
|
1207
|
+
const { children, ...node } = n.node;
|
|
1208
|
+
const seen = onPage(n);
|
|
1209
|
+
return {
|
|
1210
|
+
node: {
|
|
1211
|
+
...node,
|
|
1212
|
+
path: n.path,
|
|
1213
|
+
children: children.map((x) => ({ id: x.id, name: x.name, kind: x.kind, status: x.status })),
|
|
1214
|
+
urls: seen.urls,
|
|
1215
|
+
headings: seen.headings,
|
|
1216
|
+
landmarks: seen.landmarks,
|
|
1217
|
+
},
|
|
1218
|
+
requirements: [...n.bag.reqs]
|
|
1219
|
+
.map((r) => model.input.requirements.find((x) => x.row.ref === r))
|
|
1220
|
+
.filter((r) => !!r)
|
|
1221
|
+
.map((r) => ({ ...r.row, ...(r.dots?.length ? { dotIds: [...r.dots] } : {}) })),
|
|
1222
|
+
tests: testsOf(model, n),
|
|
1223
|
+
defects: [...n.bag.defects]
|
|
1224
|
+
.map((r) => model.input.defects.find((d) => d.ref === r))
|
|
1225
|
+
.filter(Boolean)
|
|
1226
|
+
.sort((a, b) => (a.state === 'open' ? 0 : 1) - (b.state === 'open' ? 0 : 1) || natural(a.key, b.key))
|
|
1227
|
+
.map(({ text: _t, requirements: _r, prs: _p, ...d }) => d),
|
|
1228
|
+
screenshots: shotsOf(model, n),
|
|
1229
|
+
gaps: gapsOf(model, n),
|
|
1230
|
+
code: codeOf(model, n),
|
|
1231
|
+
skill: nodeSkill(w, model, n),
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
/** GET /api/dots/:id/trace/map/skill.md (and the all-dots one): a router (the tree with status and ids), then one section per page and state. */
|
|
1235
|
+
export function traceMapSkill(w, model) {
|
|
1236
|
+
const out = [
|
|
1237
|
+
`# App map: ${mdText(w.name)}`,
|
|
1238
|
+
'',
|
|
1239
|
+
'The app as the test runs saw it. Find the page for your task in the router, then read its section: how to reach it, what is on it, the tests that cover it, open bugs, and known gotchas.',
|
|
1240
|
+
'',
|
|
1241
|
+
'## Router',
|
|
1242
|
+
'',
|
|
1243
|
+
];
|
|
1244
|
+
const walk = (n, depth) => {
|
|
1245
|
+
out.push(`${' '.repeat(depth)}- ${mdText(n.name)} (${n.kind}): ${STATUS_WORD[n.status]}, ${n.counts.tests.passed}/${n.counts.tests.total} tests, ${n.counts.defects.open} open bugs. \`${n.id}\``);
|
|
1246
|
+
for (const c of n.children)
|
|
1247
|
+
walk(c, depth + 1);
|
|
1248
|
+
};
|
|
1249
|
+
walk(model.map.tree, 0);
|
|
1250
|
+
out.push('');
|
|
1251
|
+
for (const n of model.index.values())
|
|
1252
|
+
if (n.node.kind === 'page' || n.node.kind === 'state')
|
|
1253
|
+
out.push(nodeSkill(w, model, n, 2));
|
|
1254
|
+
return out.join('\n');
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* POST .../trace/map/:nodeId/run: queues a rerun job of the dot with the spec files that cover the node. A node no
|
|
1258
|
+
* test covers has nothing to run: 409 with the next step in words.
|
|
1259
|
+
*/
|
|
1260
|
+
export function queueNodeRun(w, model, id) {
|
|
1261
|
+
const n = model.index.get(id);
|
|
1262
|
+
if (!n)
|
|
1263
|
+
return { code: 404, body: { error: "no such node in this dot's map" } };
|
|
1264
|
+
const specs = specsOf(n);
|
|
1265
|
+
if (!specs.length)
|
|
1266
|
+
return { code: 409, body: { error: 'No test covers this part yet. Write a test for it first.' } };
|
|
1267
|
+
const minute = new Date().toISOString().slice(0, 16);
|
|
1268
|
+
const jobId = enqueue({
|
|
1269
|
+
kind: 'rerun',
|
|
1270
|
+
watchId: w.id,
|
|
1271
|
+
dedupeKey: `rerun:${w.id}:map:${id}:${minute}`,
|
|
1272
|
+
payload: { specs, requestedBy: 'user' },
|
|
1273
|
+
});
|
|
1274
|
+
if (!jobId)
|
|
1275
|
+
return { code: 200, body: { queued: false, specs, reason: 'The same tests were queued in the last minute.' } };
|
|
1276
|
+
return { code: 202, body: { queued: true, jobId, specs } };
|
|
1277
|
+
}
|
|
1278
|
+
//# sourceMappingURL=trace-map.js.map
|