dsh-pstack 0.2.0-beta.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -3
- package/README.md +148 -84
- package/README.zh.md +198 -0
- package/assets/pstack-config-entry.png +0 -0
- package/cordis.patch.yml +105 -27
- package/dist/client.mjs +2341 -0
- package/dist/client.mjs.map +1 -0
- package/dist/host-contract-CUPSpeNk.d.mts +22 -0
- package/dist/index.d.mts +49 -238
- package/dist/index.mjs +250 -123
- package/dist/models.d.mts +44 -0
- package/dist/models.mjs +201 -0
- package/dist/paths-BT77khwm.mjs +48 -0
- package/dist/pstack/LICENSE +21 -0
- package/dist/pstack/README.md +291 -0
- package/dist/pstack/agents/poteto-agent.md +9 -0
- package/{assets → dist/pstack}/docs/guide/01-setup.md +13 -11
- package/{assets → dist/pstack}/docs/guide/02-poteto-mode.md +4 -4
- package/{assets → dist/pstack}/docs/guide/04-design.md +4 -4
- package/{assets → dist/pstack}/docs/guide/05-build-and-clean.md +2 -2
- package/{assets → dist/pstack}/docs/guide/06-verify-and-ship.md +13 -3
- package/{assets → dist/pstack}/docs/guide/07-overnight.md +15 -13
- package/{assets → dist/pstack}/docs/guide/09-make-it-yours.md +6 -2
- package/{assets → dist/pstack}/docs/guide/10-recipes-and-pitfalls.md +7 -5
- package/{assets → dist/pstack}/docs/guide/README.md +1 -1
- package/dist/pstack/docs/harness/dsh.md +98 -0
- package/{assets → dist/pstack}/skills/architect/SKILL.md +12 -12
- package/dist/pstack/skills/arena/SKILL.md +50 -0
- package/{assets → dist/pstack}/skills/automate-me/SKILL.md +14 -14
- package/{assets → dist/pstack}/skills/benchmark-checklist/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/blast-radius/SKILL.md +4 -6
- package/{assets → dist/pstack}/skills/correct/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/create-formal-verification-skill/SKILL.md +4 -2
- package/{assets → dist/pstack}/skills/create-verification-skill/SKILL.md +5 -5
- package/dist/pstack/skills/figure-it-out/SKILL.md +54 -0
- package/dist/pstack/skills/how/SKILL.md +38 -0
- package/{assets → dist/pstack}/skills/in-your-own-words/SKILL.md +1 -1
- package/dist/pstack/skills/interrogate/SKILL.md +33 -0
- package/{assets → dist/pstack}/skills/ketchup/SKILL.md +3 -5
- package/dist/pstack/skills/ketchup-video/SKILL.md +29 -0
- package/{assets → dist/pstack}/skills/maintain-formal-verification-skill/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/maintain-verification-skill/SKILL.md +3 -3
- package/dist/pstack/skills/make-bot-ui/SKILL.md +117 -0
- package/dist/pstack/skills/no-comments/SKILL.md +26 -0
- package/{assets → dist/pstack}/skills/poteto-help/SKILL.md +34 -31
- package/{assets → dist/pstack}/skills/poteto-help/references/prompting.md +1 -1
- package/{assets → dist/pstack}/skills/poteto-help/references/recipes.md +3 -3
- package/{assets → dist/pstack}/skills/poteto-mode/SKILL.md +20 -19
- package/dist/pstack/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
- package/dist/pstack/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
- package/dist/pstack/skills/poteto-mode/playbooks/autopilot-full.md +25 -0
- package/dist/pstack/skills/poteto-mode/playbooks/autopilot-stack.md +8 -0
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/babysit.md +4 -4
- package/dist/pstack/skills/poteto-mode/playbooks/bug-fix.md +15 -0
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/eval.md +3 -3
- package/dist/pstack/skills/poteto-mode/playbooks/feature.md +21 -0
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/hillclimb.md +3 -3
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/investigation.md +2 -2
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/multi-phase-plan.md +23 -23
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/opening-a-pr.md +6 -6
- package/dist/pstack/skills/poteto-mode/playbooks/orchestrate.md +25 -0
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/pause-safely.md +2 -2
- package/dist/pstack/skills/poteto-mode/playbooks/perf-issue.md +25 -0
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/prototype.md +4 -4
- package/dist/pstack/skills/poteto-mode/playbooks/refactoring.md +16 -0
- package/dist/pstack/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
- package/dist/pstack/skills/poteto-mode/playbooks/session-pickup.md +11 -0
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/shipping.md +2 -2
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/trace-forensics.md +2 -2
- package/dist/pstack/skills/poteto-mode/playbooks/visual-parity.md +11 -0
- package/dist/pstack/skills/poteto-mode/playbooks/worktree-cleanup.md +12 -0
- package/{assets → dist/pstack}/skills/poteto-mode/scripts/check-plan.mjs +6 -8
- package/dist/pstack/skills/poteto-mode/scripts/check-plan.test.mjs +128 -0
- package/{assets/skills/poteto-mode/scripts/check-plan.test.mjs → dist/pstack/skills/poteto-mode/scripts/expect.mjs} +12 -101
- package/dist/pstack/skills/poteto-mode/scripts/support.mjs +93 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/cli.mjs +123 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/cli.test.mjs +217 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.mjs +119 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/github.mjs +626 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/github.test.mjs +521 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/policy.mjs +657 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/policy.test.mjs +523 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/render.mjs +106 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/types.mjs +9 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/watch-pr.mjs +5 -0
- package/{assets → dist/pstack}/skills/poteto-mode/scripts/worktree-audit.mjs +15 -22
- package/dist/pstack/skills/poteto-mode/scripts/worktree-audit.test.mjs +134 -0
- package/{assets → dist/pstack}/skills/principle-attack-the-premise/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-boundary-discipline/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-encode-lessons-in-structure/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-exhaust-the-design-space/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-experience-first/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-explain-the-number/SKILL.md +3 -2
- package/{assets → dist/pstack}/skills/principle-fix-root-causes/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-laziness-protocol/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-make-operations-idempotent/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-minimize-reader-load/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-model-the-domain/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-never-block-on-the-human/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-outcome-oriented-execution/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-prove-it-works/SKILL.md +2 -2
- package/{assets → dist/pstack}/skills/principle-redesign-from-first-principles/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-separate-before-serializing-shared-state/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-sequence-verifiable-units/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-subtract-before-you-add/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-test-behavior-not-implementation/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-type-system-discipline/SKILL.md +1 -1
- package/dist/pstack/skills/recall/SKILL.md +37 -0
- package/dist/pstack/skills/reflect/SKILL.md +71 -0
- package/{assets → dist/pstack}/skills/reflect/references/divergent-reviewer.md +4 -4
- package/{assets → dist/pstack}/skills/reflect/references/judgment-reviewer.md +2 -2
- package/{assets → dist/pstack}/skills/reflect/references/synthesizer.md +5 -5
- package/{assets → dist/pstack}/skills/reflect/references/tooling-reviewer.md +2 -2
- package/dist/pstack/skills/setup-pstack/SKILL.md +80 -0
- package/{assets → dist/pstack}/skills/sharpen/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/show-me-your-work/SKILL.md +8 -8
- package/{assets → dist/pstack}/skills/show-me-your-work/scripts/log.mjs +1 -3
- package/dist/pstack/skills/show-me-your-work/scripts/log.test.mjs +56 -0
- package/dist/pstack/skills/statechart/SKILL.md +82 -0
- package/dist/pstack/skills/statechart/references/render.md +93 -0
- package/dist/pstack/skills/statechart/scripts/backends/cfg.mjs +31 -0
- package/dist/pstack/skills/statechart/scripts/backends/mermaid.mjs +186 -0
- package/dist/pstack/skills/statechart/scripts/backends/plain-text.mjs +62 -0
- package/dist/pstack/skills/statechart/scripts/backends/report.mjs +84 -0
- package/dist/pstack/skills/statechart/scripts/frontends/config.mjs +268 -0
- package/dist/pstack/skills/statechart/scripts/frontends/dot.mjs +67 -0
- package/dist/pstack/skills/statechart/scripts/frontends/marks.mjs +94 -0
- package/dist/pstack/skills/statechart/scripts/frontends/messages.mjs +48 -0
- package/dist/pstack/skills/statechart/scripts/frontends/spec.mjs +101 -0
- package/dist/pstack/skills/statechart/scripts/frontends/trace.mjs +39 -0
- package/dist/pstack/skills/statechart/scripts/frontends/value.mjs +180 -0
- package/dist/pstack/skills/statechart/scripts/frontends/verdict.mjs +40 -0
- package/dist/pstack/skills/statechart/scripts/frontends/view.mjs +99 -0
- package/dist/pstack/skills/statechart/scripts/ir.mjs +277 -0
- package/dist/pstack/skills/statechart/scripts/passes/annotate.mjs +92 -0
- package/dist/pstack/skills/statechart/scripts/passes/label.mjs +210 -0
- package/dist/pstack/skills/statechart/scripts/passes/lift.mjs +84 -0
- package/dist/pstack/skills/statechart/scripts/passes/loop-check.mjs +47 -0
- package/dist/pstack/skills/statechart/scripts/passes/paths.mjs +72 -0
- package/dist/pstack/skills/statechart/scripts/passes/reach.mjs +135 -0
- package/dist/pstack/skills/statechart/scripts/passes/region-check.mjs +82 -0
- package/dist/pstack/skills/statechart/scripts/passes/regions.mjs +268 -0
- package/dist/pstack/skills/statechart/scripts/pipeline.mjs +72 -0
- package/dist/pstack/skills/statechart/scripts/run.mjs +440 -0
- package/dist/pstack/skills/statechart/scripts/statechart.mjs +155 -0
- package/dist/pstack/skills/statechart/scripts/statechart.test.mjs +198 -0
- package/dist/pstack/skills/statechart/scripts/worker.mjs +31 -0
- package/dist/pstack/skills/swarm/SKILL.md +43 -0
- package/{assets → dist/pstack}/skills/tdd/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/teach/SKILL.md +2 -2
- package/{assets → dist/pstack}/skills/technical-writing/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/typescript-best-practices/SKILL.md +3 -6
- package/{assets → dist/pstack}/skills/why/SKILL.md +8 -8
- package/dist/remote.d.mts +27 -0
- package/dist/remote.mjs +234 -0
- package/dist/role-config-NNn5r1JX.mjs +415 -0
- package/dist/role-manifest-C3fUzjLI.d.mts +19 -0
- package/dist/role-wire-DZwTn2wF.d.mts +55 -0
- package/dist/roles.d.mts +18 -0
- package/dist/roles.mjs +323 -0
- package/dist/script.d.mts +9 -111
- package/dist/script.mjs +32 -329
- package/dist/skill.d.mts +38 -0
- package/dist/skill.mjs +183 -0
- package/dist/skills/authored/decision-audit/SKILL.md +103 -0
- package/dist/skills/authored/decision-audit/SKILL.zh.md +103 -0
- package/dist/skills/authored/decision-audit/references/recover-commit.md +17 -0
- package/dist/skills/authored/decision-audit/references/recover-commit.zh.md +17 -0
- package/dist/skills/authored/decision-write/SKILL.md +133 -0
- package/dist/skills/authored/decision-write/SKILL.zh.md +133 -0
- package/dist/skills/authored/decision-write/references/land.md +48 -0
- package/dist/skills/authored/decision-write/references/land.zh.md +48 -0
- package/dist/skills/authored/decision-write/references/scope.md +16 -0
- package/dist/skills/authored/decision-write/references/scope.zh.md +16 -0
- package/dist/skills/authored/docs-drift/SKILL.md +120 -0
- package/dist/skills/authored/docs-drift/SKILL.zh.md +120 -0
- package/dist/skills/authored/setup-project-docs/SKILL.md +63 -0
- package/dist/skills/authored/setup-project-docs/SKILL.zh.md +63 -0
- package/dist/skills/authored/setup-project-docs/references/decision-conventions.md +13 -0
- package/dist/skills/authored/setup-project-docs/references/decision-conventions.zh.md +13 -0
- package/dist/skills/vendored/retro/SKILL.md +44 -0
- package/dist/skills/vendored/retro/agents/openai.yaml +5 -0
- package/dist/skills/vendored/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/dist/skills/vendored/writing-for-agents/SKILL.md +81 -0
- package/dist/skills/vendored/writing-for-agents/agents/openai.yaml +3 -0
- package/dist/subagent.d.mts +54 -206
- package/dist/subagent.mjs +258 -963
- package/locale/en.json +3 -2
- package/locale/zh.json +3 -2
- package/package.json +93 -66
- package/assets/agents/poteto-agent.md +0 -8
- package/assets/docs/harness/dsh.md +0 -60
- package/assets/docs/pstack-models.example.md +0 -54
- package/assets/skills/arena/SKILL.md +0 -50
- package/assets/skills/figure-it-out/SKILL.md +0 -54
- package/assets/skills/how/SKILL.md +0 -38
- package/assets/skills/interrogate/SKILL.md +0 -71
- package/assets/skills/interrogate/references/code-quality-review.md +0 -47
- package/assets/skills/interrogate/references/lead-judgment.md +0 -39
- package/assets/skills/interrogate/references/reviewer-prompt.md +0 -62
- package/assets/skills/interrogate/references/rubric.md +0 -77
- package/assets/skills/no-comments/SKILL.md +0 -26
- package/assets/skills/poteto-mode/playbooks/authoring-a-skill.md +0 -12
- package/assets/skills/poteto-mode/playbooks/autonomous-run.md +0 -15
- package/assets/skills/poteto-mode/playbooks/autopilot-full.md +0 -25
- package/assets/skills/poteto-mode/playbooks/autopilot-stack.md +0 -12
- package/assets/skills/poteto-mode/playbooks/bug-fix.md +0 -15
- package/assets/skills/poteto-mode/playbooks/feature.md +0 -21
- package/assets/skills/poteto-mode/playbooks/orchestrate.md +0 -112
- package/assets/skills/poteto-mode/playbooks/perf-issue.md +0 -25
- package/assets/skills/poteto-mode/playbooks/refactoring.md +0 -16
- package/assets/skills/poteto-mode/playbooks/runtime-forensics.md +0 -11
- package/assets/skills/poteto-mode/playbooks/session-pickup.md +0 -11
- package/assets/skills/poteto-mode/playbooks/visual-parity.md +0 -11
- package/assets/skills/poteto-mode/playbooks/worktree-cleanup.md +0 -20
- package/assets/skills/poteto-mode/scripts/orch/orch.mjs +0 -4282
- package/assets/skills/poteto-mode/scripts/orch/orch.test.mjs +0 -2042
- package/assets/skills/poteto-mode/scripts/watch-pr/cli.test.mjs +0 -5101
- package/assets/skills/poteto-mode/scripts/watch-pr/github.test.mjs +0 -1177
- package/assets/skills/poteto-mode/scripts/watch-pr/policy.test.mjs +0 -1441
- package/assets/skills/poteto-mode/scripts/watch-pr/watch-pr.mjs +0 -4601
- package/assets/skills/recall/SKILL.md +0 -37
- package/assets/skills/reflect/SKILL.md +0 -69
- package/assets/skills/setup-pstack/SKILL.md +0 -67
- package/assets/skills/swarm/SKILL.md +0 -43
- package/dist/frontmatter-BQX3ekwP.mjs +0 -229
- package/dist/frontmatter-BQX3ekwP.mjs.map +0 -1
- package/dist/history.d.mts +0 -429
- package/dist/history.mjs +0 -1129
- package/dist/history.mjs.map +0 -1
- package/dist/host-contract-o47JH_r8.d.mts +0 -527
- package/dist/index.mjs.map +0 -1
- package/dist/paths-D_JlVmBk.mjs +0 -42
- package/dist/paths-D_JlVmBk.mjs.map +0 -1
- package/dist/script.mjs.map +0 -1
- package/dist/subagent.mjs.map +0 -1
- /package/{static/icon.png → assets/logo.png} +0 -0
- /package/{assets → dist/pstack}/agents/comment-sicko.md +0 -0
- /package/{assets → dist/pstack}/docs/guide/03-understand.md +0 -0
- /package/{assets → dist/pstack}/docs/guide/08-principles.md +0 -0
- /package/{assets → dist/pstack}/docs/guide/images/design.jpg +0 -0
- /package/{assets → dist/pstack}/docs/guide/images/overnight.jpg +0 -0
- /package/{assets → dist/pstack}/docs/guide/images/recipes.jpg +0 -0
- /package/{assets → dist/pstack}/docs/guide/images/router.jpg +0 -0
- /package/{assets → dist/pstack}/docs/guide/images/understanding.jpg +0 -0
- /package/{assets → dist/pstack}/docs/guide/images/verification.jpg +0 -0
- /package/{assets → dist/pstack}/skills/architect/references/design-red-flags.md +0 -0
- /package/{assets → dist/pstack}/skills/architect/references/rationale-template.md +0 -0
- /package/{assets → dist/pstack}/skills/architect/references/runner-prompt.md +0 -0
- /package/{assets → dist/pstack}/skills/bro/SKILL.md +0 -0
- /package/{assets → dist/pstack}/skills/create-verification-skill/references/feature-map-example/README.md +0 -0
- /package/{assets → dist/pstack}/skills/create-verification-skill/references/feature-map-example/create-note.md +0 -0
- /package/{assets → dist/pstack}/skills/create-verification-skill/references/feature-map-example/search.md +0 -0
- /package/{assets → dist/pstack}/skills/how/references/explainer-prompt.md +0 -0
- /package/{assets → dist/pstack}/skills/how/references/explorer-prompt.md +0 -0
- /package/{assets → dist/pstack}/skills/poteto-mode/references/bugbot-triage.md +0 -0
- /package/{assets → dist/pstack}/skills/principle-build-the-lever/SKILL.md +0 -0
- /package/{assets → dist/pstack}/skills/principle-foundational-thinking/SKILL.md +0 -0
- /package/{assets → dist/pstack}/skills/principle-guard-the-context-window/SKILL.md +0 -0
- /package/{assets → dist/pstack}/skills/show-me-your-work/references/decision-log-template.tsv +0 -0
- /package/{assets → dist/pstack}/skills/typescript-best-practices/references/patterns.md +0 -0
- /package/{assets → dist/pstack}/skills/unslop/SKILL.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/epistemics.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/investigator-prompt.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/source-playbook.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/code-archaeology.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/databricks.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/datadog.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/incident-postmortem.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/linear.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/notion.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/sentry.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/slack.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/synthesizer-prompt.md +0 -0
|
@@ -1,1441 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { inspect } from "node:util";
|
|
4
|
-
//#region .runtime-sources/skills/poteto-mode/scripts/expect.ts
|
|
5
|
-
/**
|
|
6
|
-
* A small `expect` for `node:test`.
|
|
7
|
-
*
|
|
8
|
-
* The five upstream test files were written against `bun:test`. Their assertions
|
|
9
|
-
* are ordinary value comparisons, so this module supplies exactly the matchers
|
|
10
|
-
* they use rather than a general test framework. It is deliberately not a drop-in
|
|
11
|
-
* replacement: a matcher that is not listed here fails to compile, which keeps the
|
|
12
|
-
* port auditable against the calls that actually exist.
|
|
13
|
-
*
|
|
14
|
-
* Covered: `toBe`, `toEqual`, `toContain`, `toMatch`, `toThrow`, `toBeNull`,
|
|
15
|
-
* `toBeInstanceOf`, `toHaveLength`, `toEndWith`, `toMatchObject`, `.not` on each,
|
|
16
|
-
* and `rejects` / `resolves` for promises.
|
|
17
|
-
*
|
|
18
|
-
* Two differences from `bun:test`, stated so a failure is not misread:
|
|
19
|
-
*
|
|
20
|
-
* - `toEqual` is structural for arrays and plain objects, `Object.is` otherwise.
|
|
21
|
-
* A class instance only equals itself; `toBeInstanceOf` is the matcher for type
|
|
22
|
-
* identity. The ported tests compare plain data, which is why this is enough.
|
|
23
|
-
* - A failure throws `AssertionError` with the two values rendered, not Bun's
|
|
24
|
-
* coloured diff.
|
|
25
|
-
*/
|
|
26
|
-
/** Raised by every failing assertion. */
|
|
27
|
-
var AssertionError = class extends Error {
|
|
28
|
-
constructor(message) {
|
|
29
|
-
super(message);
|
|
30
|
-
this.name = "AssertionError";
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
function display(value) {
|
|
34
|
-
if (typeof value === "function") return `[Function ${value.name || "anonymous"}]`;
|
|
35
|
-
return inspect(value, {
|
|
36
|
-
depth: 3,
|
|
37
|
-
breakLength: 120,
|
|
38
|
-
maxArrayLength: 20
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
function messageOf(error) {
|
|
42
|
-
if (error instanceof Error) return `${error.name}: ${error.message}`;
|
|
43
|
-
return String(error);
|
|
44
|
-
}
|
|
45
|
-
/** Structural equality for the shapes the ported tests compare. */
|
|
46
|
-
function deepEqual(left, right) {
|
|
47
|
-
if (Object.is(left, right)) return true;
|
|
48
|
-
if (left instanceof Date || right instanceof Date) return left instanceof Date && right instanceof Date && left.getTime() === right.getTime();
|
|
49
|
-
if (Array.isArray(left) || Array.isArray(right)) {
|
|
50
|
-
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
51
|
-
return left.every((item, index) => deepEqual(item, right[index]));
|
|
52
|
-
}
|
|
53
|
-
if (left === null || right === null || typeof left !== "object" || typeof right !== "object") return false;
|
|
54
|
-
const leftProto = Object.getPrototypeOf(left);
|
|
55
|
-
if (leftProto !== Object.getPrototypeOf(right)) return false;
|
|
56
|
-
if (leftProto !== Object.prototype && leftProto !== null) return false;
|
|
57
|
-
const leftKeys = Object.keys(left);
|
|
58
|
-
const rightKeys = Object.keys(right);
|
|
59
|
-
if (leftKeys.length !== rightKeys.length) return false;
|
|
60
|
-
return leftKeys.every((key) => deepEqual(left[key], right[key]));
|
|
61
|
-
}
|
|
62
|
-
function contains(haystack, needle) {
|
|
63
|
-
if (typeof haystack === "string") return haystack.includes(String(needle));
|
|
64
|
-
if (Array.isArray(haystack)) return haystack.some((item) => deepEqual(item, needle));
|
|
65
|
-
if (haystack instanceof Set) return [...haystack].some((item) => deepEqual(item, needle));
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Whether `expected` is a partial description of `actual`.
|
|
70
|
-
*
|
|
71
|
-
* `bun:test`'s `toMatchObject` is recursive: every key in `expected` must exist and
|
|
72
|
-
* match, arrays match element by element with the same partial rule, and extra keys
|
|
73
|
-
* in `actual` are allowed. That is what lets an assertion name only the fields it
|
|
74
|
-
* cares about, which several ported cases rely on.
|
|
75
|
-
*/
|
|
76
|
-
function matchesObject(actual, expected) {
|
|
77
|
-
if (Array.isArray(expected)) {
|
|
78
|
-
if (!Array.isArray(actual) || actual.length !== expected.length) return false;
|
|
79
|
-
return expected.every((item, index) => matchesObject(actual[index], item));
|
|
80
|
-
}
|
|
81
|
-
if (expected instanceof Date) return deepEqual(actual, expected);
|
|
82
|
-
if (expected === null || typeof expected !== "object") return deepEqual(actual, expected);
|
|
83
|
-
if (actual === null || typeof actual !== "object") return false;
|
|
84
|
-
return Object.entries(expected).every(([key, value]) => matchesObject(actual[key], value));
|
|
85
|
-
}
|
|
86
|
-
function thrownBy(call) {
|
|
87
|
-
try {
|
|
88
|
-
call();
|
|
89
|
-
return { thrown: false };
|
|
90
|
-
} catch (error) {
|
|
91
|
-
return {
|
|
92
|
-
thrown: true,
|
|
93
|
-
error
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
function buildMatchers(actual, negate) {
|
|
98
|
-
/**
|
|
99
|
-
* Fail when `ok` is not what the matcher asked for.
|
|
100
|
-
*
|
|
101
|
-
* `negate` inverts the requirement, so `.not.toContain(x)` passes on a haystack
|
|
102
|
-
* that does not contain `x` and fails when it does.
|
|
103
|
-
*/
|
|
104
|
-
const check = (ok, matcher, expected) => {
|
|
105
|
-
if (ok === !negate) return;
|
|
106
|
-
const relation = negate ? `not to ${matcher}` : matcher;
|
|
107
|
-
throw new AssertionError(`expected ${display(actual)} ${relation} ${display(expected)}`);
|
|
108
|
-
};
|
|
109
|
-
const checkPlain = (ok, matcher) => {
|
|
110
|
-
if (ok === !negate) return;
|
|
111
|
-
throw new AssertionError(`expected ${display(actual)} ${negate ? `not to ${matcher}` : matcher}`);
|
|
112
|
-
};
|
|
113
|
-
const matchers = {
|
|
114
|
-
toBe(expected) {
|
|
115
|
-
check(Object.is(actual, expected), "toBe", expected);
|
|
116
|
-
},
|
|
117
|
-
toEqual(expected) {
|
|
118
|
-
check(deepEqual(actual, expected), "toEqual", expected);
|
|
119
|
-
},
|
|
120
|
-
toContain(expected) {
|
|
121
|
-
check(contains(actual, expected), "toContain", expected);
|
|
122
|
-
},
|
|
123
|
-
toMatch(expected) {
|
|
124
|
-
const text = String(actual);
|
|
125
|
-
check(typeof expected === "string" ? text.includes(expected) : expected.test(text), "toMatch", expected);
|
|
126
|
-
},
|
|
127
|
-
toThrow(expected) {
|
|
128
|
-
if (typeof actual !== "function") throw new AssertionError(`toThrow needs a function, received ${display(actual)}`);
|
|
129
|
-
const result = thrownBy(actual);
|
|
130
|
-
if (!result.thrown) {
|
|
131
|
-
checkPlain(false, "toThrow");
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (expected === void 0) {
|
|
135
|
-
checkPlain(true, "toThrow");
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const message = messageOf(result.error);
|
|
139
|
-
const ok = typeof expected === "string" ? message.includes(expected) : expected instanceof RegExp ? expected.test(message) : result.error instanceof expected;
|
|
140
|
-
check(ok, "toThrow", expected);
|
|
141
|
-
},
|
|
142
|
-
toBeNull() {
|
|
143
|
-
checkPlain(actual === null, "toBeNull");
|
|
144
|
-
},
|
|
145
|
-
toBeInstanceOf(expected) {
|
|
146
|
-
check(actual instanceof expected, "toBeInstanceOf", expected);
|
|
147
|
-
},
|
|
148
|
-
toHaveLength(expected) {
|
|
149
|
-
const length = actual?.length;
|
|
150
|
-
check(length === expected, "toHaveLength", expected);
|
|
151
|
-
},
|
|
152
|
-
toEndWith(expected) {
|
|
153
|
-
check(String(actual).endsWith(expected), "toEndWith", expected);
|
|
154
|
-
},
|
|
155
|
-
toMatchObject(expected) {
|
|
156
|
-
check(matchesObject(actual, expected), "toMatchObject", expected);
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
Object.defineProperties(matchers, {
|
|
160
|
-
not: {
|
|
161
|
-
get: () => buildMatchers(actual, !negate),
|
|
162
|
-
enumerable: true
|
|
163
|
-
},
|
|
164
|
-
rejects: {
|
|
165
|
-
get: () => buildAsyncMatchers(actual, "rejects", negate),
|
|
166
|
-
enumerable: true
|
|
167
|
-
},
|
|
168
|
-
resolves: {
|
|
169
|
-
get: () => buildAsyncMatchers(actual, "resolves", negate),
|
|
170
|
-
enumerable: true
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
return matchers;
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Compare against a settled promise.
|
|
177
|
-
*
|
|
178
|
-
* `rejects` reads the rejection reason and fails when the promise resolved;
|
|
179
|
-
* `resolves` is the mirror. Each matcher awaits the settlement, which is what
|
|
180
|
-
* makes `await expect(promise).rejects.toThrow(...)` wait before it compares.
|
|
181
|
-
*/
|
|
182
|
-
function buildAsyncMatchers(actual, mode, negate) {
|
|
183
|
-
if (typeof actual?.then !== "function") throw new AssertionError(`expected a promise, received ${display(actual)}`);
|
|
184
|
-
const wantRejection = mode === "rejects" !== negate;
|
|
185
|
-
const settle = async () => {
|
|
186
|
-
let resolved;
|
|
187
|
-
let rejected;
|
|
188
|
-
await Promise.resolve(actual).then((value) => {
|
|
189
|
-
resolved = { value };
|
|
190
|
-
}, (error) => {
|
|
191
|
-
rejected = { error };
|
|
192
|
-
});
|
|
193
|
-
if (wantRejection) {
|
|
194
|
-
if (resolved !== void 0) throw new AssertionError(`expected the promise to reject, it resolved to ${display(resolved.value)}`);
|
|
195
|
-
return rejected?.error;
|
|
196
|
-
}
|
|
197
|
-
if (rejected !== void 0) throw new AssertionError(`expected the promise to resolve, it rejected with ${messageOf(rejected.error)}`);
|
|
198
|
-
return resolved?.value;
|
|
199
|
-
};
|
|
200
|
-
/**
|
|
201
|
-
* Run one matcher against the settled value.
|
|
202
|
-
*
|
|
203
|
-
* `toThrow` and `toBeInstanceOf` both read the rejection reason, so a thrown
|
|
204
|
-
* reason is turned back into a thunk for `toThrow` rather than special-cased
|
|
205
|
-
* twice. `negate` is already folded into the settlement mode, so the inner
|
|
206
|
-
* matchers run un-negated.
|
|
207
|
-
*/
|
|
208
|
-
const wrap = async (name, expected) => {
|
|
209
|
-
const subject = await settle();
|
|
210
|
-
if (name === "toThrow") {
|
|
211
|
-
buildMatchers(() => {
|
|
212
|
-
throw subject;
|
|
213
|
-
}, false).toThrow(expected);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (name === "toBeNull") {
|
|
217
|
-
buildMatchers(subject, false).toBeNull();
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const matchers = buildMatchers(subject, false);
|
|
221
|
-
const matcher = matchers[name];
|
|
222
|
-
if (typeof matcher !== "function") throw new AssertionError(`unknown matcher ${name}`);
|
|
223
|
-
matcher.call(matchers, expected);
|
|
224
|
-
};
|
|
225
|
-
const matchers = {
|
|
226
|
-
toBe: (expected) => wrap("toBe", expected),
|
|
227
|
-
toEqual: (expected) => wrap("toEqual", expected),
|
|
228
|
-
toContain: (expected) => wrap("toContain", expected),
|
|
229
|
-
toMatch: (expected) => wrap("toMatch", expected),
|
|
230
|
-
toThrow: (expected) => wrap("toThrow", expected),
|
|
231
|
-
toBeNull: () => wrap("toBeNull"),
|
|
232
|
-
toBeInstanceOf: (expected) => wrap("toBeInstanceOf", expected),
|
|
233
|
-
toHaveLength: (expected) => wrap("toHaveLength", expected),
|
|
234
|
-
toEndWith: (expected) => wrap("toEndWith", expected),
|
|
235
|
-
toMatchObject: (expected) => wrap("toMatchObject", expected)
|
|
236
|
-
};
|
|
237
|
-
Object.defineProperty(matchers, "not", {
|
|
238
|
-
get: () => buildAsyncMatchers(actual, mode, !negate),
|
|
239
|
-
enumerable: true
|
|
240
|
-
});
|
|
241
|
-
return matchers;
|
|
242
|
-
}
|
|
243
|
-
const expect = Object.assign((actual) => buildMatchers(actual, false), {
|
|
244
|
-
not: (actual) => buildMatchers(actual, true),
|
|
245
|
-
rejects: (actual) => buildAsyncMatchers(actual, "rejects", false),
|
|
246
|
-
resolves: (actual) => buildAsyncMatchers(actual, "resolves", false)
|
|
247
|
-
});
|
|
248
|
-
//#endregion
|
|
249
|
-
//#region .runtime-sources/skills/poteto-mode/scripts/watch-pr/types.ts
|
|
250
|
-
function nonEmpty(items) {
|
|
251
|
-
return items.length === 0 ? null : [items[0], ...items.slice(1)];
|
|
252
|
-
}
|
|
253
|
-
function parsePrNumber(value, label = "PR number") {
|
|
254
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new Error(`${label} must be a positive integer`);
|
|
255
|
-
return value;
|
|
256
|
-
}
|
|
257
|
-
//#endregion
|
|
258
|
-
//#region .runtime-sources/skills/poteto-mode/scripts/watch-pr/github.ts
|
|
259
|
-
var WatcherQueryError = class extends Error {
|
|
260
|
-
failure;
|
|
261
|
-
constructor(failure) {
|
|
262
|
-
super(failure.detail);
|
|
263
|
-
this.name = "WatcherQueryError";
|
|
264
|
-
this.failure = failure;
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
var ChecksUnavailable = class extends WatcherQueryError {
|
|
268
|
-
constructor(detail) {
|
|
269
|
-
super({
|
|
270
|
-
kind: "checks-unavailable",
|
|
271
|
-
retryable: true,
|
|
272
|
-
detail
|
|
273
|
-
});
|
|
274
|
-
this.name = "ChecksUnavailable";
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
const firstLine = (value) => value.trim().split(/\r?\n/, 1)[0]?.slice(0, 240) ?? "";
|
|
278
|
-
function isBugbot(comment) {
|
|
279
|
-
if (comment === null) return false;
|
|
280
|
-
const author = (comment.authorLogin ?? "").toLowerCase();
|
|
281
|
-
const body = comment.body.toLowerCase();
|
|
282
|
-
return author.includes("bugbot") || author === "cursor" && [
|
|
283
|
-
"bugbot",
|
|
284
|
-
"cursor_automation_id",
|
|
285
|
-
"agentic security review",
|
|
286
|
-
"description start",
|
|
287
|
-
"severity"
|
|
288
|
-
].some((token) => body.includes(token));
|
|
289
|
-
}
|
|
290
|
-
function passKey(comment) {
|
|
291
|
-
if (comment === null) return null;
|
|
292
|
-
for (const pattern of [/RUN_ID:\s*([a-zA-Z0-9_.:-]+)/, /CURSOR_AUTOMATION_ID:\s*([a-zA-Z0-9_.:-]+)/]) {
|
|
293
|
-
const match = pattern.exec(comment.body);
|
|
294
|
-
if (match?.[1]) return match[1];
|
|
295
|
-
}
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
function buildReviewThreads(threads) {
|
|
299
|
-
const keys = /* @__PURE__ */ new Set();
|
|
300
|
-
let keyless = false;
|
|
301
|
-
for (const thread of threads) {
|
|
302
|
-
if (!isBugbot(thread.firstComment)) continue;
|
|
303
|
-
const key = passKey(thread.firstComment);
|
|
304
|
-
if (key === null) keyless = true;
|
|
305
|
-
else keys.add(key);
|
|
306
|
-
}
|
|
307
|
-
const passes = keys.size > 0 ? keys.size : keyless ? 1 : 0;
|
|
308
|
-
return threads.filter((thread) => !thread.resolved).map(({ id, firstComment }) => ({
|
|
309
|
-
id,
|
|
310
|
-
firstComment,
|
|
311
|
-
isBugbot: isBugbot(firstComment),
|
|
312
|
-
bugbotReviewPasses: passes
|
|
313
|
-
}));
|
|
314
|
-
}
|
|
315
|
-
async function resolveChecks(reader, context) {
|
|
316
|
-
const fast = await reader.checksFastPath(context);
|
|
317
|
-
const direct = fast.kind === "checks" ? nonEmpty(fast.checks) : null;
|
|
318
|
-
if (direct !== null) return {
|
|
319
|
-
source: "gh-pr-checks",
|
|
320
|
-
checks: direct
|
|
321
|
-
};
|
|
322
|
-
const checks = [];
|
|
323
|
-
let after = null;
|
|
324
|
-
do {
|
|
325
|
-
const page = await reader.checkRollupPage(context, after);
|
|
326
|
-
checks.push(...page.checks);
|
|
327
|
-
after = page.endCursor;
|
|
328
|
-
} while (after !== null);
|
|
329
|
-
const fallback = nonEmpty(checks);
|
|
330
|
-
if (fallback !== null) return {
|
|
331
|
-
source: "graphql-rollup",
|
|
332
|
-
checks: fallback
|
|
333
|
-
};
|
|
334
|
-
throw new ChecksUnavailable(`could not read PR checks: ${fast.kind === "unusable" ? `fast path exit=${fast.exitCode}; GraphQL rollup was empty${firstLine(fast.stderr) ? `; ${firstLine(fast.stderr)}` : ""}` : "fast path and GraphQL rollup were empty"}`);
|
|
335
|
-
}
|
|
336
|
-
async function resolveReviewThreads(reader, context) {
|
|
337
|
-
const threads = [];
|
|
338
|
-
let after = null;
|
|
339
|
-
do {
|
|
340
|
-
const page = await reader.reviewThreadPage(context, after);
|
|
341
|
-
threads.push(...page.threads);
|
|
342
|
-
after = page.endCursor;
|
|
343
|
-
} while (after !== null);
|
|
344
|
-
return buildReviewThreads(threads);
|
|
345
|
-
}
|
|
346
|
-
//#endregion
|
|
347
|
-
//#region .runtime-sources/skills/poteto-mode/scripts/watch-pr/policy.ts
|
|
348
|
-
function assessGitHubMerge(args) {
|
|
349
|
-
if (args.mergeStateStatus === "BLOCKED") {
|
|
350
|
-
if (args.headRollupState === "ERROR" || args.headRollupState === "FAILURE") return {
|
|
351
|
-
kind: "refused",
|
|
352
|
-
mergeStateStatus: args.mergeStateStatus,
|
|
353
|
-
headRollupState: args.headRollupState
|
|
354
|
-
};
|
|
355
|
-
return {
|
|
356
|
-
kind: "allowed",
|
|
357
|
-
basis: "rollup",
|
|
358
|
-
mergeStateStatus: args.mergeStateStatus,
|
|
359
|
-
headRollupState: args.headRollupState
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
return {
|
|
363
|
-
kind: "allowed",
|
|
364
|
-
basis: "merge-state",
|
|
365
|
-
mergeStateStatus: args.mergeStateStatus,
|
|
366
|
-
headRollupState: args.headRollupState
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
async function mergeAssessment(reader, facts) {
|
|
370
|
-
const commits = await reader.commitRollups(facts.context);
|
|
371
|
-
const headRollupState = facts.headRefOid === null ? null : commits.find((commit) => commit.oid === facts.headRefOid)?.state ?? null;
|
|
372
|
-
return {
|
|
373
|
-
hadPreviousPassingCi: commits.some((commit) => commit.oid !== facts.headRefOid && commit.state === "SUCCESS"),
|
|
374
|
-
github: assessGitHubMerge({
|
|
375
|
-
mergeStateStatus: facts.mergeStateStatus,
|
|
376
|
-
headRollupState
|
|
377
|
-
})
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
const AUTOMATION_TOKENS = [
|
|
381
|
-
"bugbot",
|
|
382
|
-
"security review",
|
|
383
|
-
"pr review automation",
|
|
384
|
-
"review automation"
|
|
385
|
-
];
|
|
386
|
-
async function readSnapshot(args) {
|
|
387
|
-
const facts = await args.reader.pullRequest(args.context);
|
|
388
|
-
if (facts.state === "MERGED" || facts.mergedAt !== null) return {
|
|
389
|
-
kind: "merged",
|
|
390
|
-
context: args.context,
|
|
391
|
-
facts
|
|
392
|
-
};
|
|
393
|
-
if (facts.state === "CLOSED") return {
|
|
394
|
-
kind: "closed",
|
|
395
|
-
context: args.context,
|
|
396
|
-
facts
|
|
397
|
-
};
|
|
398
|
-
const threads = await resolveReviewThreads(args.reader, args.context);
|
|
399
|
-
const checks = await resolveChecks(args.reader, args.context);
|
|
400
|
-
const failed = nonEmpty(checks.checks.filter((check) => check.kind === "failed"));
|
|
401
|
-
const pending = nonEmpty(checks.checks.filter((check) => check.kind === "pending"));
|
|
402
|
-
let ci;
|
|
403
|
-
if (failed === null && pending !== null && args.pendingHistory === "omit") ci = {
|
|
404
|
-
kind: "ci-pending",
|
|
405
|
-
source: checks.source,
|
|
406
|
-
all: checks.checks,
|
|
407
|
-
failed: [],
|
|
408
|
-
pending,
|
|
409
|
-
hadPreviousPassingCi: false
|
|
410
|
-
};
|
|
411
|
-
else {
|
|
412
|
-
const merge = await mergeAssessment(args.reader, facts);
|
|
413
|
-
const base = {
|
|
414
|
-
source: checks.source,
|
|
415
|
-
all: checks.checks,
|
|
416
|
-
hadPreviousPassingCi: merge.hadPreviousPassingCi
|
|
417
|
-
};
|
|
418
|
-
if (failed !== null) ci = {
|
|
419
|
-
...base,
|
|
420
|
-
kind: "ci-failing",
|
|
421
|
-
failed,
|
|
422
|
-
pending: pending ?? [],
|
|
423
|
-
github: merge.github
|
|
424
|
-
};
|
|
425
|
-
else if (merge.github.kind === "refused") ci = {
|
|
426
|
-
...base,
|
|
427
|
-
kind: "ci-github-rejected",
|
|
428
|
-
failed: [],
|
|
429
|
-
pending: pending ?? [],
|
|
430
|
-
github: merge.github
|
|
431
|
-
};
|
|
432
|
-
else if (pending !== null) ci = {
|
|
433
|
-
...base,
|
|
434
|
-
kind: "ci-pending",
|
|
435
|
-
failed: [],
|
|
436
|
-
pending
|
|
437
|
-
};
|
|
438
|
-
else ci = {
|
|
439
|
-
...base,
|
|
440
|
-
kind: "ci-clean",
|
|
441
|
-
failed: [],
|
|
442
|
-
pending: [],
|
|
443
|
-
github: merge.github
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
return {
|
|
447
|
-
kind: "open",
|
|
448
|
-
context: args.context,
|
|
449
|
-
facts,
|
|
450
|
-
threads,
|
|
451
|
-
ci,
|
|
452
|
-
reviewAutomationRunning: checks.checks.some((check) => check.kind === "pending" && AUTOMATION_TOKENS.some((token) => check.name.toLowerCase().includes(token)))
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
const conflictBlocker = (row) => row.kind === "open" && (row.facts.mergeable === "CONFLICTING" || row.facts.mergeStateStatus === "DIRTY" || row.facts.mergeStateStatus === "CONFLICTING") ? {
|
|
456
|
-
kind: "merge-conflicts",
|
|
457
|
-
pr: row.context,
|
|
458
|
-
facts: row.facts
|
|
459
|
-
} : null;
|
|
460
|
-
function threadBlocker(row) {
|
|
461
|
-
if (row.kind !== "open") return null;
|
|
462
|
-
const threads = nonEmpty(row.threads);
|
|
463
|
-
return threads === null ? null : {
|
|
464
|
-
kind: "review-threads",
|
|
465
|
-
pr: row.context,
|
|
466
|
-
threads
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
const ciBlocker = (row) => row.kind === "open" && (row.ci.kind === "ci-failing" || row.ci.kind === "ci-github-rejected") ? {
|
|
470
|
-
kind: "failing-checks",
|
|
471
|
-
pr: row.context,
|
|
472
|
-
ci: row.ci
|
|
473
|
-
} : null;
|
|
474
|
-
function gateReason(row, allowDraft) {
|
|
475
|
-
if (row.kind === "merged") return null;
|
|
476
|
-
if (row.kind === "closed") return "closed-without-merge";
|
|
477
|
-
if (row.facts.isDraft && !allowDraft) return "draft-pr";
|
|
478
|
-
return row.facts.reviewDecision === "CHANGES_REQUESTED" ? "changes-requested" : null;
|
|
479
|
-
}
|
|
480
|
-
function gateBlocker(row, allowDraft) {
|
|
481
|
-
const reason = gateReason(row, allowDraft);
|
|
482
|
-
return reason === null || reason === "draft-pr" && row.kind === "open" && row.ci.kind === "ci-pending" ? null : {
|
|
483
|
-
kind: "merge-gate",
|
|
484
|
-
pr: row.context,
|
|
485
|
-
reason
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
function readyContribution(row, allowDraft) {
|
|
489
|
-
if (row.kind === "merged") return {
|
|
490
|
-
kind: "merged-pr",
|
|
491
|
-
context: row.context,
|
|
492
|
-
mergedAt: row.facts.mergedAt
|
|
493
|
-
};
|
|
494
|
-
if (row.kind !== "open" || row.ci.kind !== "ci-clean" || row.threads.length !== 0 || conflictBlocker(row) !== null || gateReason(row, allowDraft) !== null) return null;
|
|
495
|
-
const reviewDecision = row.facts.reviewDecision;
|
|
496
|
-
if (reviewDecision === "CHANGES_REQUESTED") return null;
|
|
497
|
-
return {
|
|
498
|
-
kind: "ready-pr",
|
|
499
|
-
context: row.context,
|
|
500
|
-
proof: {
|
|
501
|
-
mergeability: "clear",
|
|
502
|
-
threads: [],
|
|
503
|
-
ci: row.ci,
|
|
504
|
-
gate: {
|
|
505
|
-
state: "OPEN",
|
|
506
|
-
reviewDecision,
|
|
507
|
-
draft: row.facts.isDraft ? "draft-allowed" : "not-draft"
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
function classifyPr(row, allowDraft = false) {
|
|
513
|
-
for (const blocker of [
|
|
514
|
-
conflictBlocker(row),
|
|
515
|
-
threadBlocker(row),
|
|
516
|
-
ciBlocker(row),
|
|
517
|
-
gateBlocker(row, allowDraft)
|
|
518
|
-
]) if (blocker !== null) return {
|
|
519
|
-
kind: "blocker",
|
|
520
|
-
blocker
|
|
521
|
-
};
|
|
522
|
-
if (row.kind === "open" && row.ci.kind === "ci-pending") return {
|
|
523
|
-
kind: "waiting",
|
|
524
|
-
frontier: row.context,
|
|
525
|
-
pending: row.ci.pending
|
|
526
|
-
};
|
|
527
|
-
const ready = readyContribution(row, allowDraft);
|
|
528
|
-
if (ready === null) throw new Error("snapshot has no classified decision");
|
|
529
|
-
return ready.kind === "merged-pr" ? {
|
|
530
|
-
kind: "merged",
|
|
531
|
-
pr: ready
|
|
532
|
-
} : {
|
|
533
|
-
kind: "ready",
|
|
534
|
-
pr: ready
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
function selectTierMajorStackDecision(rows, allowDraft = false) {
|
|
538
|
-
for (const tier of [
|
|
539
|
-
conflictBlocker,
|
|
540
|
-
threadBlocker,
|
|
541
|
-
ciBlocker
|
|
542
|
-
]) for (const row of rows) {
|
|
543
|
-
const blocker = tier(row);
|
|
544
|
-
if (blocker !== null) return {
|
|
545
|
-
kind: "blocker",
|
|
546
|
-
blocker
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
for (const row of rows) {
|
|
550
|
-
const blocker = gateBlocker(row, allowDraft);
|
|
551
|
-
if (blocker !== null) return {
|
|
552
|
-
kind: "blocker",
|
|
553
|
-
blocker
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
for (const row of rows) if (row.kind === "open" && row.ci.kind === "ci-pending") return {
|
|
557
|
-
kind: "waiting",
|
|
558
|
-
frontier: row.context,
|
|
559
|
-
pending: row.ci.pending
|
|
560
|
-
};
|
|
561
|
-
const prs = nonEmpty(rows.map((row) => readyContribution(row, allowDraft)).filter((row) => row !== null));
|
|
562
|
-
if (prs === null || prs.length !== rows.length) throw new Error("stack has no classified decision");
|
|
563
|
-
return {
|
|
564
|
-
kind: "clear",
|
|
565
|
-
prs
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
const queryBackoffSeconds = (interval, failures) => Math.min(Math.max(interval, 60) * 2 ** (failures - 1), 300);
|
|
569
|
-
function verdictFactory(clock, mode) {
|
|
570
|
-
let sequence = 0;
|
|
571
|
-
function stamp(payload, override) {
|
|
572
|
-
return {
|
|
573
|
-
schemaVersion: 1,
|
|
574
|
-
sequence: sequence += 1,
|
|
575
|
-
observedAt: clock.observedAt(),
|
|
576
|
-
mode: override ?? mode,
|
|
577
|
-
...payload
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
return stamp;
|
|
581
|
-
}
|
|
582
|
-
function blockerVerdict(stamp, blocker) {
|
|
583
|
-
switch (blocker.kind) {
|
|
584
|
-
case "merge-conflicts": return stamp({
|
|
585
|
-
kind: "BLOCKER",
|
|
586
|
-
terminal: true,
|
|
587
|
-
exitCode: 2,
|
|
588
|
-
blocker
|
|
589
|
-
});
|
|
590
|
-
case "review-threads": return stamp({
|
|
591
|
-
kind: "BLOCKER",
|
|
592
|
-
terminal: true,
|
|
593
|
-
exitCode: 3,
|
|
594
|
-
blocker
|
|
595
|
-
});
|
|
596
|
-
case "failing-checks": return stamp({
|
|
597
|
-
kind: "BLOCKER",
|
|
598
|
-
terminal: true,
|
|
599
|
-
exitCode: 4,
|
|
600
|
-
blocker
|
|
601
|
-
});
|
|
602
|
-
case "merge-gate": return stamp({
|
|
603
|
-
kind: "BLOCKER",
|
|
604
|
-
terminal: true,
|
|
605
|
-
exitCode: 6,
|
|
606
|
-
blocker
|
|
607
|
-
});
|
|
608
|
-
default: return blocker;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
function statusQueryVerdict(stamp, failures, failure) {
|
|
612
|
-
return stamp({
|
|
613
|
-
kind: "BLOCKER",
|
|
614
|
-
terminal: true,
|
|
615
|
-
exitCode: 7,
|
|
616
|
-
blocker: {
|
|
617
|
-
kind: "status-query",
|
|
618
|
-
failures,
|
|
619
|
-
failure
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
const deadlinePassed = (started, options, now) => options.timeout > 0 && now - started >= options.timeout;
|
|
624
|
-
async function pollUntilTerminal(args) {
|
|
625
|
-
let failures = 0;
|
|
626
|
-
const started = args.dependencies.clock.now();
|
|
627
|
-
while (true) {
|
|
628
|
-
let result;
|
|
629
|
-
try {
|
|
630
|
-
result = await args.step();
|
|
631
|
-
failures = 0;
|
|
632
|
-
} catch (error) {
|
|
633
|
-
if (!(error instanceof WatcherQueryError)) throw error;
|
|
634
|
-
failures += 1;
|
|
635
|
-
if (!error.failure.retryable || failures >= args.options.maxQueryErrors) return statusQueryVerdict(args.stamp, failures, error.failure);
|
|
636
|
-
const retryInSeconds = queryBackoffSeconds(args.options.interval, failures);
|
|
637
|
-
args.dependencies.emit(args.stamp({
|
|
638
|
-
kind: "RETRY",
|
|
639
|
-
terminal: false,
|
|
640
|
-
failure: error.failure,
|
|
641
|
-
consecutiveFailures: failures,
|
|
642
|
-
retryInSeconds
|
|
643
|
-
}));
|
|
644
|
-
if (deadlinePassed(started, args.options, args.dependencies.clock.now())) return args.stamp({
|
|
645
|
-
kind: "TIMEOUT",
|
|
646
|
-
terminal: true,
|
|
647
|
-
exitCode: 5,
|
|
648
|
-
reason: {
|
|
649
|
-
kind: "status-unavailable",
|
|
650
|
-
failure: error.failure
|
|
651
|
-
}
|
|
652
|
-
});
|
|
653
|
-
await args.dependencies.clock.sleep(retryInSeconds);
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
if (result.kind === "terminal") return result.verdict;
|
|
657
|
-
if (result.kind === "sleep") {
|
|
658
|
-
if (result.onDeadline !== void 0 && deadlinePassed(started, args.options, args.dependencies.clock.now())) return result.onDeadline();
|
|
659
|
-
await args.dependencies.clock.sleep(result.seconds);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
const createQueueState = (queue, now) => ({
|
|
664
|
-
queue,
|
|
665
|
-
snapshots: /* @__PURE__ */ new Map(),
|
|
666
|
-
work: {
|
|
667
|
-
kind: "whole-stack-sweep",
|
|
668
|
-
remaining: queue
|
|
669
|
-
},
|
|
670
|
-
nextSweepAt: now,
|
|
671
|
-
frontier: null,
|
|
672
|
-
lastWaitKey: null,
|
|
673
|
-
startedAt: now
|
|
674
|
-
});
|
|
675
|
-
const orderedRows = (state) => state.queue.flatMap((context) => {
|
|
676
|
-
const row = state.snapshots.get(context.number);
|
|
677
|
-
return row === void 0 ? [] : [row];
|
|
678
|
-
});
|
|
679
|
-
const activeRows = (state) => orderedRows(state).filter((row) => row.kind !== "merged");
|
|
680
|
-
function planQueue(state, now) {
|
|
681
|
-
if (state.work !== null) return state;
|
|
682
|
-
if (state.snapshots.size === 0 || now >= state.nextSweepAt) {
|
|
683
|
-
const remaining = nonEmpty(state.queue.filter((context) => state.snapshots.get(context.number)?.kind !== "merged"));
|
|
684
|
-
if (remaining !== null) return {
|
|
685
|
-
...state,
|
|
686
|
-
work: {
|
|
687
|
-
kind: "whole-stack-sweep",
|
|
688
|
-
remaining
|
|
689
|
-
}
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
const frontier = activeRows(state)[0]?.context;
|
|
693
|
-
return frontier === void 0 ? state : {
|
|
694
|
-
...state,
|
|
695
|
-
work: {
|
|
696
|
-
kind: "frontier-poll",
|
|
697
|
-
frontier
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
function applyQueueSnapshot(state, snapshot, now, options) {
|
|
702
|
-
if (state.work === null) throw new Error("queue has no read in flight");
|
|
703
|
-
const snapshots = new Map(state.snapshots);
|
|
704
|
-
snapshots.set(snapshot.context.number, snapshot);
|
|
705
|
-
const base = {
|
|
706
|
-
...state,
|
|
707
|
-
snapshots
|
|
708
|
-
};
|
|
709
|
-
if (state.work.kind === "frontier-poll") return {
|
|
710
|
-
state: {
|
|
711
|
-
...base,
|
|
712
|
-
work: null
|
|
713
|
-
},
|
|
714
|
-
completedSweepRows: null
|
|
715
|
-
};
|
|
716
|
-
const [head, ...tail] = state.work.remaining;
|
|
717
|
-
if (head.number !== snapshot.context.number) throw new Error("snapshot does not match sweep head");
|
|
718
|
-
const remaining = nonEmpty(tail);
|
|
719
|
-
if (remaining !== null) return {
|
|
720
|
-
state: {
|
|
721
|
-
...base,
|
|
722
|
-
work: {
|
|
723
|
-
kind: "whole-stack-sweep",
|
|
724
|
-
remaining
|
|
725
|
-
}
|
|
726
|
-
},
|
|
727
|
-
completedSweepRows: null
|
|
728
|
-
};
|
|
729
|
-
const rows = nonEmpty(state.queue.flatMap((context) => {
|
|
730
|
-
const row = snapshots.get(context.number);
|
|
731
|
-
return row === void 0 ? [] : [row];
|
|
732
|
-
}));
|
|
733
|
-
if (rows === null || rows.length !== state.queue.length) throw new Error("sweep completed without every snapshot");
|
|
734
|
-
return {
|
|
735
|
-
state: {
|
|
736
|
-
...base,
|
|
737
|
-
work: null,
|
|
738
|
-
nextSweepAt: now + options.sweepInterval
|
|
739
|
-
},
|
|
740
|
-
completedSweepRows: rows
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
function evaluateQueue(state, now, options) {
|
|
744
|
-
const active = activeRows(state);
|
|
745
|
-
if (active.length === 0) {
|
|
746
|
-
const merged = nonEmpty(orderedRows(state).flatMap((row) => row.kind === "merged" ? [{
|
|
747
|
-
kind: "merged-pr",
|
|
748
|
-
context: row.context,
|
|
749
|
-
mergedAt: row.facts.mergedAt
|
|
750
|
-
}] : []));
|
|
751
|
-
if (merged === null) throw new Error("empty queue cannot complete");
|
|
752
|
-
return {
|
|
753
|
-
kind: "complete",
|
|
754
|
-
state,
|
|
755
|
-
merged
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
const rows = nonEmpty(active);
|
|
759
|
-
if (rows === null) throw new Error("active queue cannot be empty");
|
|
760
|
-
const decision = selectTierMajorStackDecision(rows, options.allowDraft);
|
|
761
|
-
if (decision.kind === "blocker") return {
|
|
762
|
-
kind: "blocker",
|
|
763
|
-
state,
|
|
764
|
-
blocker: decision.blocker
|
|
765
|
-
};
|
|
766
|
-
const frontier = rows[0].context;
|
|
767
|
-
if (state.frontier !== null && state.frontier.number !== frontier.number) return {
|
|
768
|
-
kind: "advance",
|
|
769
|
-
state: {
|
|
770
|
-
...state,
|
|
771
|
-
frontier,
|
|
772
|
-
lastWaitKey: null
|
|
773
|
-
},
|
|
774
|
-
merged: state.frontier,
|
|
775
|
-
frontier,
|
|
776
|
-
remaining: active.length
|
|
777
|
-
};
|
|
778
|
-
if (deadlinePassed(state.startedAt, options, now)) return {
|
|
779
|
-
kind: "timeout",
|
|
780
|
-
state: {
|
|
781
|
-
...state,
|
|
782
|
-
frontier
|
|
783
|
-
},
|
|
784
|
-
frontier,
|
|
785
|
-
unmergedCount: active.length
|
|
786
|
-
};
|
|
787
|
-
const row = rows[0];
|
|
788
|
-
const pending = row.kind === "open" && row.ci.kind === "ci-pending" ? row.ci.pending : null;
|
|
789
|
-
const reason = pending === null ? {
|
|
790
|
-
kind: "merge-queue",
|
|
791
|
-
unmergedCount: active.length
|
|
792
|
-
} : {
|
|
793
|
-
kind: "pending-checks",
|
|
794
|
-
pending
|
|
795
|
-
};
|
|
796
|
-
const key = reason.kind === "pending-checks" ? `pending:${frontier.number}:${reason.pending.length}` : `queue:${frontier.number}:${reason.unmergedCount}`;
|
|
797
|
-
return {
|
|
798
|
-
kind: "waiting",
|
|
799
|
-
state: {
|
|
800
|
-
...state,
|
|
801
|
-
frontier,
|
|
802
|
-
lastWaitKey: key
|
|
803
|
-
},
|
|
804
|
-
frontier,
|
|
805
|
-
reason,
|
|
806
|
-
emit: state.lastWaitKey !== key
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
async function runQueued(args) {
|
|
810
|
-
let state = createQueueState(args.contexts, args.dependencies.clock.now());
|
|
811
|
-
const stamp = verdictFactory(args.dependencies.clock, "queued-stack");
|
|
812
|
-
args.dependencies.emit(stamp({
|
|
813
|
-
kind: "QUEUE",
|
|
814
|
-
terminal: false,
|
|
815
|
-
queue: args.contexts
|
|
816
|
-
}));
|
|
817
|
-
const step = async () => {
|
|
818
|
-
state = planQueue(state, args.dependencies.clock.now());
|
|
819
|
-
if (state.work === null) {
|
|
820
|
-
const complete = evaluateQueue(state, args.dependencies.clock.now(), args.options);
|
|
821
|
-
if (complete.kind !== "complete") throw new Error("queue has no work while active");
|
|
822
|
-
return {
|
|
823
|
-
kind: "terminal",
|
|
824
|
-
verdict: stamp({
|
|
825
|
-
kind: "COMPLETE",
|
|
826
|
-
terminal: true,
|
|
827
|
-
exitCode: 0,
|
|
828
|
-
queue: state.queue,
|
|
829
|
-
merged: complete.merged
|
|
830
|
-
})
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
const context = state.work.kind === "whole-stack-sweep" ? state.work.remaining[0] : state.work.frontier;
|
|
834
|
-
const snapshot = await readSnapshot({
|
|
835
|
-
reader: args.dependencies.reader,
|
|
836
|
-
context,
|
|
837
|
-
pendingHistory: "omit",
|
|
838
|
-
allowDraft: args.options.allowDraft
|
|
839
|
-
});
|
|
840
|
-
const applied = applyQueueSnapshot(state, snapshot, args.dependencies.clock.now(), args.options);
|
|
841
|
-
state = applied.state;
|
|
842
|
-
if (applied.completedSweepRows !== null) args.dependencies.emit(stamp({
|
|
843
|
-
kind: "STATUS",
|
|
844
|
-
terminal: false,
|
|
845
|
-
reason: "whole-stack-sweep",
|
|
846
|
-
rows: applied.completedSweepRows
|
|
847
|
-
}));
|
|
848
|
-
if (state.work !== null) return { kind: "continue" };
|
|
849
|
-
const evaluation = evaluateQueue(state, args.dependencies.clock.now(), args.options);
|
|
850
|
-
state = evaluation.state;
|
|
851
|
-
switch (evaluation.kind) {
|
|
852
|
-
case "complete": return {
|
|
853
|
-
kind: "terminal",
|
|
854
|
-
verdict: stamp({
|
|
855
|
-
kind: "COMPLETE",
|
|
856
|
-
terminal: true,
|
|
857
|
-
exitCode: 0,
|
|
858
|
-
queue: state.queue,
|
|
859
|
-
merged: evaluation.merged
|
|
860
|
-
})
|
|
861
|
-
};
|
|
862
|
-
case "blocker": return {
|
|
863
|
-
kind: "terminal",
|
|
864
|
-
verdict: blockerVerdict(stamp, evaluation.blocker)
|
|
865
|
-
};
|
|
866
|
-
case "advance":
|
|
867
|
-
args.dependencies.emit(stamp({
|
|
868
|
-
kind: "ADVANCE",
|
|
869
|
-
terminal: false,
|
|
870
|
-
merged: evaluation.merged,
|
|
871
|
-
frontier: evaluation.frontier,
|
|
872
|
-
remaining: evaluation.remaining
|
|
873
|
-
}));
|
|
874
|
-
return { kind: "continue" };
|
|
875
|
-
case "timeout": return {
|
|
876
|
-
kind: "terminal",
|
|
877
|
-
verdict: stamp({
|
|
878
|
-
kind: "TIMEOUT",
|
|
879
|
-
terminal: true,
|
|
880
|
-
exitCode: 5,
|
|
881
|
-
reason: {
|
|
882
|
-
kind: "queued-stack",
|
|
883
|
-
frontier: evaluation.frontier,
|
|
884
|
-
unmergedCount: evaluation.unmergedCount
|
|
885
|
-
}
|
|
886
|
-
})
|
|
887
|
-
};
|
|
888
|
-
case "waiting":
|
|
889
|
-
if (evaluation.emit) args.dependencies.emit(stamp({
|
|
890
|
-
kind: "WAITING",
|
|
891
|
-
terminal: false,
|
|
892
|
-
frontier: evaluation.frontier,
|
|
893
|
-
reason: evaluation.reason
|
|
894
|
-
}));
|
|
895
|
-
return {
|
|
896
|
-
kind: "sleep",
|
|
897
|
-
seconds: args.options.interval
|
|
898
|
-
};
|
|
899
|
-
default: return evaluation;
|
|
900
|
-
}
|
|
901
|
-
};
|
|
902
|
-
return pollUntilTerminal({
|
|
903
|
-
dependencies: args.dependencies,
|
|
904
|
-
options: args.options,
|
|
905
|
-
stamp,
|
|
906
|
-
step
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
//#endregion
|
|
910
|
-
//#region .runtime-sources/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts
|
|
911
|
-
function toRawThread(thread) {
|
|
912
|
-
return {
|
|
913
|
-
id: thread.id,
|
|
914
|
-
firstComment: thread.firstComment,
|
|
915
|
-
resolved: false
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
function passingCheck(name = "ci") {
|
|
919
|
-
return {
|
|
920
|
-
kind: "passed",
|
|
921
|
-
name,
|
|
922
|
-
reportedState: "SUCCESS",
|
|
923
|
-
description: "",
|
|
924
|
-
link: "",
|
|
925
|
-
workflow: ""
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
function pendingCheck(name = "ci") {
|
|
929
|
-
return {
|
|
930
|
-
kind: "pending",
|
|
931
|
-
name,
|
|
932
|
-
reportedState: "PENDING",
|
|
933
|
-
description: "",
|
|
934
|
-
link: "",
|
|
935
|
-
workflow: ""
|
|
936
|
-
};
|
|
937
|
-
}
|
|
938
|
-
function failedCheck(name = "ci") {
|
|
939
|
-
return {
|
|
940
|
-
kind: "failed",
|
|
941
|
-
name,
|
|
942
|
-
reportedState: "FAILURE",
|
|
943
|
-
description: "",
|
|
944
|
-
link: "",
|
|
945
|
-
workflow: ""
|
|
946
|
-
};
|
|
947
|
-
}
|
|
948
|
-
function fakeReader(options = {}) {
|
|
949
|
-
const calls = [];
|
|
950
|
-
const context = options.current ?? {
|
|
951
|
-
owner: "owner",
|
|
952
|
-
repo: "repo",
|
|
953
|
-
number: parsePrNumber(1)
|
|
954
|
-
};
|
|
955
|
-
const defaults = {
|
|
956
|
-
context,
|
|
957
|
-
mergeable: "MERGEABLE",
|
|
958
|
-
mergeStateStatus: "CLEAN",
|
|
959
|
-
reviewDecision: "APPROVED",
|
|
960
|
-
headRefOid: "head",
|
|
961
|
-
headRefName: "feature",
|
|
962
|
-
baseRefName: "main",
|
|
963
|
-
state: "OPEN",
|
|
964
|
-
mergedAt: null,
|
|
965
|
-
isDraft: false
|
|
966
|
-
};
|
|
967
|
-
let page = 0;
|
|
968
|
-
let threadPage = 0;
|
|
969
|
-
return {
|
|
970
|
-
calls,
|
|
971
|
-
async originRepo() {
|
|
972
|
-
calls.push("originRepo");
|
|
973
|
-
return options.origin === void 0 ? {
|
|
974
|
-
owner: "owner",
|
|
975
|
-
repo: "repo"
|
|
976
|
-
} : options.origin;
|
|
977
|
-
},
|
|
978
|
-
async currentPr(pr) {
|
|
979
|
-
calls.push("currentPr");
|
|
980
|
-
return {
|
|
981
|
-
...context,
|
|
982
|
-
number: pr ?? context.number
|
|
983
|
-
};
|
|
984
|
-
},
|
|
985
|
-
async pullRequest(requested) {
|
|
986
|
-
calls.push("pullRequest");
|
|
987
|
-
return {
|
|
988
|
-
...defaults,
|
|
989
|
-
...options.facts,
|
|
990
|
-
context: requested
|
|
991
|
-
};
|
|
992
|
-
},
|
|
993
|
-
async openPullRequests() {
|
|
994
|
-
calls.push("openPullRequests");
|
|
995
|
-
return options.openPullRequests ?? [];
|
|
996
|
-
},
|
|
997
|
-
async checksFastPath() {
|
|
998
|
-
calls.push("checksFastPath");
|
|
999
|
-
return options.fastPath ?? {
|
|
1000
|
-
kind: "checks",
|
|
1001
|
-
checks: [passingCheck()]
|
|
1002
|
-
};
|
|
1003
|
-
},
|
|
1004
|
-
async checkRollupPage(_requested, after) {
|
|
1005
|
-
calls.push(`checkRollupPage:${after ?? "null"}`);
|
|
1006
|
-
return options.rollupPages?.[page++] ?? {
|
|
1007
|
-
checks: [],
|
|
1008
|
-
endCursor: null
|
|
1009
|
-
};
|
|
1010
|
-
},
|
|
1011
|
-
async reviewThreadPage(_requested, after) {
|
|
1012
|
-
calls.push(`reviewThreadPage:${after ?? "null"}`);
|
|
1013
|
-
return options.threadPages?.[threadPage++] ?? {
|
|
1014
|
-
threads: (options.threads ?? []).map(toRawThread),
|
|
1015
|
-
endCursor: null
|
|
1016
|
-
};
|
|
1017
|
-
},
|
|
1018
|
-
async commitRollups() {
|
|
1019
|
-
calls.push("commitRollups");
|
|
1020
|
-
return options.commitRollups ?? [{
|
|
1021
|
-
oid: "head",
|
|
1022
|
-
state: "SUCCESS"
|
|
1023
|
-
}];
|
|
1024
|
-
}
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
//#endregion
|
|
1028
|
-
//#region .runtime-sources/skills/poteto-mode/scripts/watch-pr/policy.test.ts
|
|
1029
|
-
const context = (number) => ({
|
|
1030
|
-
owner: "owner",
|
|
1031
|
-
repo: "repo",
|
|
1032
|
-
number: parsePrNumber(number)
|
|
1033
|
-
});
|
|
1034
|
-
const options = {
|
|
1035
|
-
interval: 10,
|
|
1036
|
-
sweepInterval: 300,
|
|
1037
|
-
timeout: 0,
|
|
1038
|
-
maxQueryErrors: 5,
|
|
1039
|
-
allowDraft: false
|
|
1040
|
-
};
|
|
1041
|
-
describe("readiness truth table", () => {
|
|
1042
|
-
it("covers every specified row and every UNKNOWN rollup value", () => {
|
|
1043
|
-
for (const [mergeStateStatus, headRollupState, expected] of [
|
|
1044
|
-
[
|
|
1045
|
-
"BLOCKED",
|
|
1046
|
-
"FAILURE",
|
|
1047
|
-
"refused"
|
|
1048
|
-
],
|
|
1049
|
-
[
|
|
1050
|
-
"BLOCKED",
|
|
1051
|
-
"ERROR",
|
|
1052
|
-
"refused"
|
|
1053
|
-
],
|
|
1054
|
-
[
|
|
1055
|
-
"BLOCKED",
|
|
1056
|
-
"PENDING",
|
|
1057
|
-
"allowed"
|
|
1058
|
-
],
|
|
1059
|
-
[
|
|
1060
|
-
"UNSTABLE",
|
|
1061
|
-
"FAILURE",
|
|
1062
|
-
"allowed"
|
|
1063
|
-
],
|
|
1064
|
-
[
|
|
1065
|
-
"UNKNOWN",
|
|
1066
|
-
"ERROR",
|
|
1067
|
-
"allowed"
|
|
1068
|
-
],
|
|
1069
|
-
[
|
|
1070
|
-
"UNKNOWN",
|
|
1071
|
-
"EXPECTED",
|
|
1072
|
-
"allowed"
|
|
1073
|
-
],
|
|
1074
|
-
[
|
|
1075
|
-
"UNKNOWN",
|
|
1076
|
-
"FAILURE",
|
|
1077
|
-
"allowed"
|
|
1078
|
-
],
|
|
1079
|
-
[
|
|
1080
|
-
"UNKNOWN",
|
|
1081
|
-
"PENDING",
|
|
1082
|
-
"allowed"
|
|
1083
|
-
],
|
|
1084
|
-
[
|
|
1085
|
-
"UNKNOWN",
|
|
1086
|
-
"SUCCESS",
|
|
1087
|
-
"allowed"
|
|
1088
|
-
],
|
|
1089
|
-
[
|
|
1090
|
-
"UNKNOWN",
|
|
1091
|
-
null,
|
|
1092
|
-
"allowed"
|
|
1093
|
-
],
|
|
1094
|
-
[
|
|
1095
|
-
"CLEAN",
|
|
1096
|
-
"SUCCESS",
|
|
1097
|
-
"allowed"
|
|
1098
|
-
]
|
|
1099
|
-
]) expect(assessGitHubMerge({
|
|
1100
|
-
mergeStateStatus,
|
|
1101
|
-
headRollupState
|
|
1102
|
-
}).kind).toBe(expected);
|
|
1103
|
-
});
|
|
1104
|
-
it("turns a clean visible list plus GitHub refusal into an explicit CI blocker", async () => {
|
|
1105
|
-
const snapshot = await readSnapshot({
|
|
1106
|
-
reader: fakeReader({
|
|
1107
|
-
facts: { mergeStateStatus: "BLOCKED" },
|
|
1108
|
-
fastPath: {
|
|
1109
|
-
kind: "checks",
|
|
1110
|
-
checks: [passingCheck()]
|
|
1111
|
-
},
|
|
1112
|
-
commitRollups: [{
|
|
1113
|
-
oid: "head",
|
|
1114
|
-
state: "FAILURE"
|
|
1115
|
-
}]
|
|
1116
|
-
}),
|
|
1117
|
-
context: context(1),
|
|
1118
|
-
pendingHistory: "include",
|
|
1119
|
-
allowDraft: false
|
|
1120
|
-
});
|
|
1121
|
-
expect(snapshot.kind).toBe("open");
|
|
1122
|
-
if (snapshot.kind !== "open") throw new Error("expected open snapshot");
|
|
1123
|
-
expect(snapshot.ci.kind).toBe("ci-github-rejected");
|
|
1124
|
-
expect(classifyPr(snapshot)).toMatchObject({
|
|
1125
|
-
kind: "blocker",
|
|
1126
|
-
blocker: { kind: "failing-checks" }
|
|
1127
|
-
});
|
|
1128
|
-
});
|
|
1129
|
-
});
|
|
1130
|
-
describe("snapshot query planning", () => {
|
|
1131
|
-
it("does not query commit rollups while queued checks are pending", async () => {
|
|
1132
|
-
const reader = fakeReader({ fastPath: {
|
|
1133
|
-
kind: "checks",
|
|
1134
|
-
checks: [pendingCheck()]
|
|
1135
|
-
} });
|
|
1136
|
-
const snapshot = await readSnapshot({
|
|
1137
|
-
reader,
|
|
1138
|
-
context: context(2),
|
|
1139
|
-
pendingHistory: "omit",
|
|
1140
|
-
allowDraft: false
|
|
1141
|
-
});
|
|
1142
|
-
expect(snapshot.kind).toBe("open");
|
|
1143
|
-
if (snapshot.kind !== "open") throw new Error("expected open snapshot");
|
|
1144
|
-
expect(snapshot.ci.kind).toBe("ci-pending");
|
|
1145
|
-
expect(reader.calls).toEqual([
|
|
1146
|
-
"pullRequest",
|
|
1147
|
-
"reviewThreadPage:null",
|
|
1148
|
-
"checksFastPath"
|
|
1149
|
-
]);
|
|
1150
|
-
});
|
|
1151
|
-
it("queries rollups for settled and failed lists", async () => {
|
|
1152
|
-
const settled = fakeReader();
|
|
1153
|
-
await readSnapshot({
|
|
1154
|
-
reader: settled,
|
|
1155
|
-
context: context(3),
|
|
1156
|
-
pendingHistory: "omit",
|
|
1157
|
-
allowDraft: false
|
|
1158
|
-
});
|
|
1159
|
-
expect(settled.calls).toContain("commitRollups");
|
|
1160
|
-
const failed = fakeReader({ fastPath: {
|
|
1161
|
-
kind: "checks",
|
|
1162
|
-
checks: [failedCheck()]
|
|
1163
|
-
} });
|
|
1164
|
-
await readSnapshot({
|
|
1165
|
-
reader: failed,
|
|
1166
|
-
context: context(4),
|
|
1167
|
-
pendingHistory: "omit",
|
|
1168
|
-
allowDraft: false
|
|
1169
|
-
});
|
|
1170
|
-
expect(failed.calls).toContain("commitRollups");
|
|
1171
|
-
});
|
|
1172
|
-
it("short-circuits merged rows before threads and checks", async () => {
|
|
1173
|
-
const reader = fakeReader({ facts: {
|
|
1174
|
-
state: "MERGED",
|
|
1175
|
-
mergedAt: "2026-07-26T00:00:00Z"
|
|
1176
|
-
} });
|
|
1177
|
-
expect((await readSnapshot({
|
|
1178
|
-
reader,
|
|
1179
|
-
context: context(5),
|
|
1180
|
-
pendingHistory: "include",
|
|
1181
|
-
allowDraft: false
|
|
1182
|
-
})).kind).toBe("merged");
|
|
1183
|
-
expect(reader.calls).toEqual(["pullRequest"]);
|
|
1184
|
-
});
|
|
1185
|
-
});
|
|
1186
|
-
it("scans stacks tier-major so an upstack conflict outranks frontier CI", async () => {
|
|
1187
|
-
const decision = selectTierMajorStackDecision([await readSnapshot({
|
|
1188
|
-
reader: fakeReader({
|
|
1189
|
-
fastPath: {
|
|
1190
|
-
kind: "checks",
|
|
1191
|
-
checks: [failedCheck()]
|
|
1192
|
-
},
|
|
1193
|
-
commitRollups: [{
|
|
1194
|
-
oid: "head",
|
|
1195
|
-
state: "FAILURE"
|
|
1196
|
-
}]
|
|
1197
|
-
}),
|
|
1198
|
-
context: context(10),
|
|
1199
|
-
pendingHistory: "omit",
|
|
1200
|
-
allowDraft: false
|
|
1201
|
-
}), await readSnapshot({
|
|
1202
|
-
reader: fakeReader({ facts: { mergeable: "CONFLICTING" } }),
|
|
1203
|
-
context: context(11),
|
|
1204
|
-
pendingHistory: "omit",
|
|
1205
|
-
allowDraft: false
|
|
1206
|
-
})]);
|
|
1207
|
-
expect(decision).toMatchObject({
|
|
1208
|
-
kind: "blocker",
|
|
1209
|
-
blocker: {
|
|
1210
|
-
kind: "merge-conflicts",
|
|
1211
|
-
pr: { number: 11 }
|
|
1212
|
-
}
|
|
1213
|
-
});
|
|
1214
|
-
});
|
|
1215
|
-
it("attributes a stack wait to the PR whose checks are pending, not the bottom", async () => {
|
|
1216
|
-
const decision = selectTierMajorStackDecision([await readSnapshot({
|
|
1217
|
-
reader: fakeReader(),
|
|
1218
|
-
context: context(20),
|
|
1219
|
-
pendingHistory: "omit",
|
|
1220
|
-
allowDraft: false
|
|
1221
|
-
}), await readSnapshot({
|
|
1222
|
-
reader: fakeReader({ fastPath: {
|
|
1223
|
-
kind: "checks",
|
|
1224
|
-
checks: [pendingCheck("upstack-build")]
|
|
1225
|
-
} }),
|
|
1226
|
-
context: context(21),
|
|
1227
|
-
pendingHistory: "omit",
|
|
1228
|
-
allowDraft: false
|
|
1229
|
-
})]);
|
|
1230
|
-
expect(decision).toMatchObject({
|
|
1231
|
-
kind: "waiting",
|
|
1232
|
-
frontier: { number: 21 },
|
|
1233
|
-
pending: [{ name: "upstack-build" }]
|
|
1234
|
-
});
|
|
1235
|
-
});
|
|
1236
|
-
it("waits on a draft while checks are pending, then reports the draft gate", async () => {
|
|
1237
|
-
const pending = await readSnapshot({
|
|
1238
|
-
reader: fakeReader({
|
|
1239
|
-
facts: { isDraft: true },
|
|
1240
|
-
fastPath: {
|
|
1241
|
-
kind: "checks",
|
|
1242
|
-
checks: [pendingCheck()]
|
|
1243
|
-
}
|
|
1244
|
-
}),
|
|
1245
|
-
context: context(12),
|
|
1246
|
-
pendingHistory: "omit",
|
|
1247
|
-
allowDraft: false
|
|
1248
|
-
});
|
|
1249
|
-
expect(classifyPr(pending).kind).toBe("waiting");
|
|
1250
|
-
const settled = await readSnapshot({
|
|
1251
|
-
reader: fakeReader({ facts: { isDraft: true } }),
|
|
1252
|
-
context: context(12),
|
|
1253
|
-
pendingHistory: "omit",
|
|
1254
|
-
allowDraft: false
|
|
1255
|
-
});
|
|
1256
|
-
expect(classifyPr(settled)).toMatchObject({
|
|
1257
|
-
kind: "blocker",
|
|
1258
|
-
blocker: {
|
|
1259
|
-
kind: "merge-gate",
|
|
1260
|
-
reason: "draft-pr"
|
|
1261
|
-
}
|
|
1262
|
-
});
|
|
1263
|
-
});
|
|
1264
|
-
describe("queued-stack cadence", () => {
|
|
1265
|
-
async function openSnapshot(pr) {
|
|
1266
|
-
return readSnapshot({
|
|
1267
|
-
reader: fakeReader(),
|
|
1268
|
-
context: pr,
|
|
1269
|
-
pendingHistory: "omit",
|
|
1270
|
-
allowDraft: false
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
it("drops a sweep head only after its snapshot succeeds", async () => {
|
|
1274
|
-
const queue = [
|
|
1275
|
-
context(20),
|
|
1276
|
-
context(21),
|
|
1277
|
-
context(22)
|
|
1278
|
-
];
|
|
1279
|
-
let state = createQueueState(queue, 0);
|
|
1280
|
-
const first = await openSnapshot(queue[0]);
|
|
1281
|
-
state = applyQueueSnapshot(state, first, 0, options).state;
|
|
1282
|
-
expect(state.work).toMatchObject({
|
|
1283
|
-
kind: "whole-stack-sweep",
|
|
1284
|
-
remaining: [{ number: 21 }, { number: 22 }]
|
|
1285
|
-
});
|
|
1286
|
-
const second = await openSnapshot(queue[1]);
|
|
1287
|
-
state = applyQueueSnapshot(state, second, 60, options).state;
|
|
1288
|
-
expect(state.work).toMatchObject({
|
|
1289
|
-
kind: "whole-stack-sweep",
|
|
1290
|
-
remaining: [{ number: 22 }]
|
|
1291
|
-
});
|
|
1292
|
-
});
|
|
1293
|
-
it("resumes the sweep at the PR whose read failed", async () => {
|
|
1294
|
-
const middle = context(21);
|
|
1295
|
-
const base = fakeReader();
|
|
1296
|
-
let failNext = true;
|
|
1297
|
-
const timeline = [];
|
|
1298
|
-
const reader = {
|
|
1299
|
-
...base,
|
|
1300
|
-
async pullRequest(pr) {
|
|
1301
|
-
if (pr.number === middle.number && failNext) {
|
|
1302
|
-
failNext = false;
|
|
1303
|
-
timeline.push(`fail:${pr.number}`);
|
|
1304
|
-
throw new WatcherQueryError({
|
|
1305
|
-
kind: "command-exit",
|
|
1306
|
-
retryable: true,
|
|
1307
|
-
detail: "rate limited",
|
|
1308
|
-
code: 1
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
timeline.push(`read:${pr.number}`);
|
|
1312
|
-
return base.pullRequest(pr);
|
|
1313
|
-
}
|
|
1314
|
-
};
|
|
1315
|
-
let now = 0;
|
|
1316
|
-
let sleeps = 0;
|
|
1317
|
-
const running = runQueued({
|
|
1318
|
-
dependencies: {
|
|
1319
|
-
reader,
|
|
1320
|
-
clock: {
|
|
1321
|
-
now: () => now,
|
|
1322
|
-
observedAt: () => "2026-07-26T00:00:00.000Z",
|
|
1323
|
-
async sleep(seconds) {
|
|
1324
|
-
timeline.push("sleep");
|
|
1325
|
-
now += seconds;
|
|
1326
|
-
sleeps += 1;
|
|
1327
|
-
if (sleeps === 2) throw new Error("stop after resume proof");
|
|
1328
|
-
}
|
|
1329
|
-
},
|
|
1330
|
-
emit(verdict) {
|
|
1331
|
-
timeline.push(`emit:${verdict.kind}`);
|
|
1332
|
-
}
|
|
1333
|
-
},
|
|
1334
|
-
contexts: [
|
|
1335
|
-
context(20),
|
|
1336
|
-
middle,
|
|
1337
|
-
context(22)
|
|
1338
|
-
],
|
|
1339
|
-
options
|
|
1340
|
-
});
|
|
1341
|
-
await expect(running).rejects.toThrow("stop after resume proof");
|
|
1342
|
-
expect(timeline).toEqual([
|
|
1343
|
-
"emit:QUEUE",
|
|
1344
|
-
"read:20",
|
|
1345
|
-
"fail:21",
|
|
1346
|
-
"emit:RETRY",
|
|
1347
|
-
"sleep",
|
|
1348
|
-
"read:21",
|
|
1349
|
-
"read:22",
|
|
1350
|
-
"emit:STATUS",
|
|
1351
|
-
"emit:WAITING",
|
|
1352
|
-
"sleep"
|
|
1353
|
-
]);
|
|
1354
|
-
});
|
|
1355
|
-
it("emits a completed sweep only after its final successful snapshot", async () => {
|
|
1356
|
-
const queue = [context(30), context(31)];
|
|
1357
|
-
let state = createQueueState(queue, 0);
|
|
1358
|
-
const first = applyQueueSnapshot(state, await openSnapshot(queue[0]), 0, options);
|
|
1359
|
-
expect(first.completedSweepRows).toBeNull();
|
|
1360
|
-
state = first.state;
|
|
1361
|
-
const second = applyQueueSnapshot(state, await openSnapshot(queue[1]), 5, options);
|
|
1362
|
-
expect(second.completedSweepRows?.map((row) => Number(row.context.number))).toEqual([30, 31]);
|
|
1363
|
-
expect(second.state.nextSweepAt).toBe(305);
|
|
1364
|
-
});
|
|
1365
|
-
it("ADVANCE continues directly to the new frontier without sleeping", async () => {
|
|
1366
|
-
const one = context(40);
|
|
1367
|
-
const two = context(41);
|
|
1368
|
-
const base = fakeReader();
|
|
1369
|
-
const reads = /* @__PURE__ */ new Map();
|
|
1370
|
-
const timeline = [];
|
|
1371
|
-
const reader = {
|
|
1372
|
-
...base,
|
|
1373
|
-
async pullRequest(pr) {
|
|
1374
|
-
timeline.push(`read:${pr.number}`);
|
|
1375
|
-
const facts = await base.pullRequest(pr);
|
|
1376
|
-
const count = (reads.get(pr.number) ?? 0) + 1;
|
|
1377
|
-
reads.set(pr.number, count);
|
|
1378
|
-
return pr.number === one.number && count > 1 ? {
|
|
1379
|
-
...facts,
|
|
1380
|
-
state: "MERGED",
|
|
1381
|
-
mergedAt: "2026-07-26T00:00:00Z"
|
|
1382
|
-
} : facts;
|
|
1383
|
-
}
|
|
1384
|
-
};
|
|
1385
|
-
let now = 0;
|
|
1386
|
-
let sleeps = 0;
|
|
1387
|
-
const emitted = [];
|
|
1388
|
-
const running = runQueued({
|
|
1389
|
-
dependencies: {
|
|
1390
|
-
reader,
|
|
1391
|
-
clock: {
|
|
1392
|
-
now: () => now,
|
|
1393
|
-
observedAt: () => "2026-07-26T00:00:00.000Z",
|
|
1394
|
-
async sleep(seconds) {
|
|
1395
|
-
timeline.push("sleep");
|
|
1396
|
-
now += seconds;
|
|
1397
|
-
sleeps += 1;
|
|
1398
|
-
if (sleeps === 2) throw new Error("stop after advance proof");
|
|
1399
|
-
}
|
|
1400
|
-
},
|
|
1401
|
-
emit(verdict) {
|
|
1402
|
-
emitted.push(verdict);
|
|
1403
|
-
timeline.push(`emit:${verdict.kind}`);
|
|
1404
|
-
}
|
|
1405
|
-
},
|
|
1406
|
-
contexts: [one, two],
|
|
1407
|
-
options
|
|
1408
|
-
});
|
|
1409
|
-
await expect(running).rejects.toThrow("stop after advance proof");
|
|
1410
|
-
expect(emitted.some((event) => event.kind === "ADVANCE")).toBe(true);
|
|
1411
|
-
const firstSleep = timeline.indexOf("sleep");
|
|
1412
|
-
expect(timeline.slice(firstSleep, firstSleep + 5)).toEqual([
|
|
1413
|
-
"sleep",
|
|
1414
|
-
"read:40",
|
|
1415
|
-
"emit:ADVANCE",
|
|
1416
|
-
"read:41",
|
|
1417
|
-
"emit:WAITING"
|
|
1418
|
-
]);
|
|
1419
|
-
});
|
|
1420
|
-
it("deduplicates identical waits and schedules the next due sweep", async () => {
|
|
1421
|
-
const queue = [context(50)];
|
|
1422
|
-
let state = createQueueState(queue, 0);
|
|
1423
|
-
state = applyQueueSnapshot(state, await openSnapshot(queue[0]), 0, options).state;
|
|
1424
|
-
const first = evaluateQueue(state, 0, options);
|
|
1425
|
-
expect(first.kind).toBe("waiting");
|
|
1426
|
-
if (first.kind !== "waiting") throw new Error("expected waiting");
|
|
1427
|
-
expect(first.emit).toBe(true);
|
|
1428
|
-
const second = evaluateQueue(first.state, 10, options);
|
|
1429
|
-
expect(second.kind).toBe("waiting");
|
|
1430
|
-
if (second.kind !== "waiting") throw new Error("expected waiting");
|
|
1431
|
-
expect(second.emit).toBe(false);
|
|
1432
|
-
expect(planQueue(second.state, 300).work?.kind).toBe("whole-stack-sweep");
|
|
1433
|
-
});
|
|
1434
|
-
});
|
|
1435
|
-
it("uses the specified retry floor and cap", () => {
|
|
1436
|
-
expect(queryBackoffSeconds(1, 1)).toBe(60);
|
|
1437
|
-
expect(queryBackoffSeconds(1, 2)).toBe(120);
|
|
1438
|
-
expect(queryBackoffSeconds(60, 4)).toBe(300);
|
|
1439
|
-
});
|
|
1440
|
-
//#endregion
|
|
1441
|
-
export {};
|