← Back to ledger

Ledger entry

Sequence 4629 · bugstack · action.proposed

Cryptographic proof

Hash
e9e8b197bf202932cb38aabf49ec01f97e176dbdc74373b3dd9fda54ce5ef27d
Previous hash
a39360fcc8da211f3e380d85e0184038bb9cddc86556735b9f901d2f4a86830d
Ed25519 signature
u2LF7c3/6hlqe3odYxlcatZFhTeJ9hz9LtcT0v5mdkEsh2FYFoJ0tK0vH5B/P+1S0wXDWTGUNIeOX+Hy5I1mAw==
Signing public key (SPKI, base64)
MCowBQYDK2VwAyEAxFhLvd6zMtQx3v6VarlyTyW+m8JUK8asT736IYWEsus=
Published at
2026-08-17T20:54:09.207Z
Verify independently (no BugStack software)

The signature is Ed25519 over the hash above (as UTF-8). Fetch the org's keys at /org/bugstack/key — or use the key shown on this page — and run:

node -e '
const { createPublicKey, verify } = require("crypto");
const key = createPublicKey({ key: Buffer.from("<signing public key>", "base64"), type: "spki", format: "der" });
console.log(verify(null, Buffer.from("<hash>", "utf8"), key, Buffer.from("<signature>", "base64")));
'

Scrubbed payload

{
  "kind": "action.proposed",
  "action": {
    "id": "fa48f9c7-196d-4285-8537-6be6f48a33ca",
    "type": "open_pull_request",
    "orgId": "2e77b814-3c5d-4b48-965f-8e5475b92ba7",
    "status": "proposed",
    "target": {
      "repo": {
        "branch": "main",
        "repoFullName": "bugstackai/bugstack",
        "repositoryId": "c15eb17a-bf5f-4ded-8476-c309a3c705e7"
      },
      "type": "open_pull_request",
      "baseBranch": "main",
      "headBranch": "bugstack/deploy-fix/b12f7893-1785861871329"
    },
    "payload": {
      "body": "## 🤖 Deploy failure fix\n\nTest mock's zero-arg downloadSnapshot breaks mock.calls[0][0] typing\n\n### What failed\n- **Service**: bugstack-worker-tester (build_ended, deploy `dep-d9p1cjvlk1mc73a4aivg`)\n- **Commit range**: last green `532dbc540c` → failed `11b2b9ff17`\n- **Breaking commit**: `9aa4563bd3`\n\n### Root cause\nThe `github()` test helper in apps/workers/src/__tests__/cadence-scan.test.ts wraps `downloadSnapshot` in `vi.fn(async () => ...)` with no declared parameter, even though the real `downloadSnapshot` port takes an `{owner, repo, ref?, maxFileBytes?}` argument. Because the mocked function's inferred parameter list is empty, TypeScript infers `mock.calls[number]` as the empty tuple type `[]`, so `gh.downloadSnapshot.mock.calls[0]![0]` fails to typecheck (TS2493: tuple `[]` has no element at index 0), which cascades into `call` being typed as possibly `undefined` at every subsequent `call.xxx` access (TS18048) — exactly the 5 errors in the build log (lines 137-141). This is a test-authoring bug in code added by this branch, not an infra/config issue.\n\n### What changed\nGave the `github()` helper's `downloadSnapshot` mock an explicit (unused) parameter matching the real port signature (`{owner, repo, ref?, maxFileBytes?}`) so vi.fn infers a non-empty parameter tuple. This makes `gh.downloadSnapshot.mock.calls[0]` a 1-tuple, so indexing `[0]` type-checks and every subsequent `call.ref` / `call.maxFileBytes` / `call.owner` / `call.repo` access resolves to a defined type, clearing both the TS2493 and TS18048 errors without touching any other test or the module under test.\n\n<details><summary>Build/deploy log excerpt</summary>\n\n```\n#11 10.48 apps/dashboard postinstall: ✔ Generated Prisma Client (v5.22.0) to ./../../node_modules/.pnpm/@[email-redacted]@5.22.0/node_modules/@prisma/client in 126ms\n#11 10.13 apps/dashboard postinstall: Prisma schema loaded from ../../services/error-service/prisma/schema.prisma\n#11 8.196 apps/dashboard postinstall$ prisma generate\n#11 7.995 \n#11 7.995 + vitest 2.1.9\n#11 7.995 + typescript 5.9.3\n#11 7.995 + turbo 2.9.14\n#11 7.995 + @bugstack/cadence 0.0.0 <- packages/cadence\n#11 7.995 devDependencies:\n#11 7.995 \n#11 7.995 + pg 8.21.0\n#11 7.995 dependencies:\n#11 7.995 \n#11 7.387 Progress: resolved 840, reused 0, downloaded 837, added 840, done\n#11 7.241 Progress: resolved 840, reused 0, downloaded 836, added 839\n#11 6.241 Progress: resolved 840, reused 0, downloaded 832, added 834\n#11 5.535 \n#11 5.535    ╰───────────────────────────────────────────────╯\n#11 5.535    │                                               │\n#11 5.535    │   To update, run: corepack use [email-redacted].0   │\n#11 5.535    │     Changelog: https://pnpm.io/v/11.20.0      │\n#11 5.535    │     Update available! 10.29.3 → 11.20.0.      │\n#11 5.535    │                                               │\n#11 5.535    ╭───────────────────────────────────────────────╮\n#11 5.535 \n#11 5.238 Progress: resolved 840, reused 0, downloaded 824, added 287\n#11 4.238 Progress: resolved 840, reused 0, downloaded 556, added 173\n#11 3.217 Progress: resolved 840, reused 0, downloaded 305, added 79\n#11 2.213 Progress: resolved 840, reused 0, downloaded 0, added 0\n#11 1.618 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n```\n\n</details>\n\n### Confidence: high\n\n---\n_Generated by BugStack's deploy-failure playbook. Review before merging — auto-merge is gated on CI + governance._",
      "type": "open_pull_request",
      "files": [
        {
          "path": "apps/workers/src/__tests__/cadence-scan.test.ts",
          "content": "// apps/workers — the server-side cadence scan.\n//\n// The distinction these tests exist to protect: \"this repo is CLEAN\" and \"I\n// could not look at this repo\" must never collapse into the same outcome. The\n// whole org rides one member's OAuth token, so a repo that token cannot see is\n// invisible — and reporting invisible as clean is a false all-clear, the most\n// expensive kind of wrong answer a scanner can give.\n\nimport { describe, expect, it, vi } from \"vitest\";\n\nimport { scanOneRepo, type ScanTarget } from \"../cadence-scan\";\n\nconst TARGET: ScanTarget = {\n  orgId: \"org-1\",\n  repositoryId: \"repo-1\",\n  repoFullName: \"acme/svc\",\n  defaultBranch: \"main\",\n};\n\nconst OFFENDING = `import { getDb } from \"@bugstack/db\";\nconst REAP_MS = 60 * 1000;\nsetInterval(reap, REAP_MS);\n`;\n\nconst NEON_ENV = \"DATABASE_URL=postgres://[email-redacted]/app\";\n\n/**\n * The mocked `downloadSnapshot` must declare the same parameter the real port\n * takes (`{owner, repo, ref?, maxFileBytes?}`), even though this helper never\n * reads it. `vi.fn(async () => ...)` with a zero-arg function makes\n * TypeScript infer `mock.calls[number]` as the EMPTY tuple `[]`, so\n * `mock.calls[0][0]` fails to typecheck (TS2493) and every downstream\n * `call.xxx` access on that undefined-typed result cascades into TS18048.\n */\nfunction github(files: Record<string, string>) {\n  return {\n    downloadSnapshot: vi.fn(\n      async (_input: { owner: string; repo: string; ref?: string; maxFileBytes?: number }) =>\n        Object.entries(files).map(([path, text]) => ({ path, size: text.length, text })),\n    ),\n  };\n}\n\nfunction deps() {\n  const raised: Array<Record<string, unknown>> = [];\n  return {\n    raised,\n    raiseSignal: async (sig: Record<string, unknown>) => {\n      raised.push(sig);\n    },\n    today: () => \"2026-08-04\",\n  };\n}\n\ndescribe(\"scanOneRepo\", () => {\n  it(\"raises ONE signal for a repo with findings, in the playbook's shape\", async () => {\n    const d = deps();\n    const out = await scanOneRepo(\n      TARGET,\n      github({ \".env.example\": NEON_ENV, \"src/jobs.ts\": OFFENDING }),\n      d as never,\n    );\n\n    expect(out).toEqual({ kind: \"raised\", findings: 1 });\n    expect(d.raised).toHaveLength(1);\n\n    const sig = d.raised[0]!;\n    expect(sig.kind).toBe(\"anomaly\");\n    // Load-bearing: REPO_REQUIRING_KINDS gates fixer dispatch on a linked\n    // repository, so a null here ends the loop at \"tell a human\".\n    expect(sig.repositoryId).toBe(\"repo-1\");\n\n    const payload = sig.payload as Record<string, unknown>;\n    // The SAME discriminator the CI bridge emits — one playbook, two entry\n    // points.\n    expect(payload.playbook).toBe(\"cadence\");\n    expect(payload.source).toBe(\"server_side_cadence_scan\");\n    expect(payload.suggestedAgents).toEqual([\"fixer\"]);\n    expect(String(payload.summary)).toContain(\"acme/svc\");\n    expect(String(payload.summary)).toContain(\"neon\");\n  });\n\n  it(\"dedups to one signal per repo per DAY\", async () => {\n    // The scan is whole-tree with no base ref to diff against, so every pass\n    // re-reports every standing violation. Without a stable per-day key the\n    // same finding would open a fresh incident daily.\n    const files = { \".env.example\": NEON_ENV, \"src/jobs.ts\": OFFENDING };\n    const a = deps();\n    await scanOneRepo(TARGET, github(files), a as never);\n    const b = deps();\n    await scanOneRepo(TARGET, github(files), b as never);\n    expect(a.raised[0]!.externalId).toBe(b.raised[0]!.externalId);\n    expect(a.raised[0]!.externalId).toBe(\"cadence-scan:repo-1:2026-08-04\");\n  });\n\n  it(\"distinguishes CLEAN from COULD-NOT-LOOK\", async () => {\n    // Clean: the token saw the repo and there was nothing to report.\n    const clean = await scanOneRepo(\n      TARGET,\n      github({\n        \".env.example\": NEON_ENV,\n        \"src/ok.ts\": 'import { getDb } from \"@bugstack/db\";\\nalignedInterval(1800000, t);',\n      }),\n      deps() as never,\n    );\n    expect(clean).toEqual({ kind: \"skipped\", reason: \"clean\" });\n\n    // Not visible: the org's single OAuth token cannot see this repo. This is\n    // NOT an all-clear and must never be reported as one.\n    const blind = await scanOneRepo(\n      TARGET,\n      {\n        downloadSnapshot: async () => {\n          throw new Error(\"Not Found\");\n        },\n      },\n      deps() as never,\n    );\n    expect(blind).toMatchObject({ kind: \"skipped\", reason: \"not_visible\" });\n    expect((blind as { detail?: string }).detail).toContain(\"Not Found\");\n  });\n\n  it(\"stays silent on a repo with no idle-suspend database\", async () => {\n    const out = await scanOneRepo(\n      TARGET,\n      github({\n        \".env.example\": \"DATABASE_URL=postgres://[email-redacted]:5432/app\",\n        \"src/jobs.ts\": OFFENDING,\n      }),\n      deps() as never,\n    );\n    expect(out).toEqual({ kind: \"skipped\", reason: \"not_armed\" });\n  });\n\n  it(\"skips honestly when the org has no GitHub connector\", async () => {\n    const out = await scanOneRepo(TARGET, null, deps() as never);\n    expect(out).toEqual({ kind: \"skipped\", reason: \"no_connector\" });\n  });\n\n  it(\"asks for an explicit ref and a per-file cap\", async () => {\n    // downloadSnapshot defaults ref to \"\" — an empty path segment, not the\n    // default branch. And an uncapped snapshot of a large repo is how this\n    // worker OOMs on a 2GB box.\n    const gh = github({ \".env.example\": NEON_ENV, \"src/jobs.ts\": OFFENDING });\n    await scanOneRepo(TARGET, gh, deps() as never);\n    const call = gh.downloadSnapshot.mock.calls[0]![0];\n    expect(call.ref).toBe(\"main\");\n    expect(call.maxFileBytes).toBeGreaterThan(0);\n    expect(call.owner).toBe(\"acme\");\n    expect(call.repo).toBe(\"svc\");\n  });\n\n  it(\"tolerates null file text without throwing\", async () => {\n    // Anything past maxFileBytes comes back as text:null.\n    const gh = {\n      downloadSnapshot: async () => [\n        { path: \"src/big.ts\", size: 999_999, text: null },\n        { path: \".env.example\", size: 40, text: NEON_ENV },\n        { path: \"src/jobs.ts\", size: OFFENDING.length, text: OFFENDING },\n      ],\n    };\n    const out = await scanOneRepo(TARGET, gh, deps() as never);\n    expect(out).toEqual({ kind: \"raised\", findings: 1 });\n  });\n});\n"
        }
      ],
      "title": "🤖 Deploy fix: Test mock's zero-arg downloadSnapshot breaks mock.calls[0][0] typing"
    },
    "reasoning": "The `github()` test helper in apps/workers/src/__tests__/cadence-scan.test.ts wraps `downloadSnapshot` in `vi.fn(async () => ...)` with no declared parameter, even though the real `downloadSnapshot` port takes an `{owner, repo, ref?, maxFileBytes?}` argument. Because the mocked function's inferred parameter list is empty, TypeScript infers `mock.calls[number]` as the empty tuple type `[]`, so `gh.downloadSnapshot.mock.calls[0]![0]` fails to typecheck (TS2493: tuple `[]` has no element at index 0), which cascades into `call` being typed as possibly `undefined` at every subsequent `call.xxx` access (TS18048) — exactly the 5 errors in the build log (lines 137-141). This is a test-authoring bug in code added by this branch, not an infra/config issue.\n\nGave the `github()` helper's `downloadSnapshot` mock an explicit (unused) parameter matching the real port signature (`{owner, repo, ref?, maxFileBytes?}`) so vi.fn infers a non-empty parameter tuple. This makes `gh.downloadSnapshot.mock.calls[0]` a 1-tuple, so indexing `[0]` type-checks and every subsequent `call.ref` / `call.maxFileBytes` / `call.owner` / `call.repo` access resolves to a defined type, clearing both the TS2493 and TS18048 errors without touching any other test or the module under test.",
    "approvedAt": null,
    "executedAt": null,
    "incidentId": "3ef9f0c3-46fb-4882-bb1d-3a63ffe644a0",
    "proposedAt": "2026-08-04T16:44:31.335Z",
    "autonomyRuleId": null,
    "governanceDecision": "propose_and_auto_merge_on_test_pass",
    "proposedByAgentRunId": "6cec2fc8-d087-4455-9418-c223e839a0f6"
  },
  "evaluatorResults": [
    {
      "verdict": {
        "result": "pass"
      },
      "structured": {
        "fileWarn": 20,
        "lineWarn": 500,
        "fileCount": 1,
        "fileLimit": 50,
        "lineCount": 166,
        "lineLimit": 2000
      },
      "evaluatorId": "diff_size"
    },
    {
      "verdict": {
        "result": "pass"
      },
      "structured": {
        "note": "no riskClass on input",
        "riskClass": null,
        "environment": "prod"
      },
      "evaluatorId": "risk_class"
    },
    {
      "verdict": {
        "result": "pass"
      },
      "structured": {
        "override": false,
        "actionType": "open_pull_request",
        "environment": "prod"
      },
      "evaluatorId": "environment_floor"
    },
    {
      "verdict": {
        "result": "pass"
      },
      "structured": {
        "actionType": "open_pull_request",
        "environment": "prod"
      },
      "evaluatorId": "action_kind"
    },
    {
      "verdict": {
        "result": "pass"
      },
      "structured": {
        "confidence": "high"
      },
      "evaluatorId": "confidence"
    },
    {
      "verdict": {
        "result": "pass"
      },
      "structured": {
        "intact": true
      },
      "evaluatorId": "signature_chain"
    },
    {
      "verdict": {
        "result": "pass"
      },
      "structured": {
        "note": "no tests metadata; type has a downstream gate"
      },
      "evaluatorId": "test_coverage"
    },
    {
      "verdict": {
        "result": "pass"
      },
      "structured": {
        "note": "reproduction contract gates fixer merges only",
        "applicable": false
      },
      "evaluatorId": "reproduction"
    },
    {
      "verdict": {
        "result": "pass"
      },
      "structured": {
        "note": "no blastRadius metadata",
        "stub": true
      },
      "evaluatorId": "blast_radius"
    },
    {
      "verdict": {
        "result": "pass"
      },
      "structured": {
        "note": "no licenses metadata",
        "stub": true
      },
      "evaluatorId": "license_compliance"
    },
    {
      "verdict": {
        "result": "pass"
      },
      "structured": {
        "note": "no branchProtection metadata",
        "stub": true
      },
      "evaluatorId": "branch_protection"
    }
  ],
  "governanceDecision": {
    "reason": "Per-repo autonomy level: autopilot.",
    "decision": "propose_and_auto_merge_on_test_pass",
    "matchedRuleId": null,
    "matchedRulePriority": null
  }
}

Redaction metadata

{
  "patternMatches": [
    "email",
    "email"
  ],
  "redactedFields": [],
  "scrubberVersion": "1.0.0"
}