@bugmole/cli 0.4.0 → 0.6.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 +18 -4
- package/package.json +7 -4
- package/scripts/bugmole-admin.mjs +194 -0
- package/scripts/bugmole-admin.test.mjs +62 -0
- package/scripts/bugmole.test.ts +94 -2
- package/scripts/bugmole.ts +290 -43
- package/scripts/ensure-playwright.cjs +87 -0
- package/scripts/ensure-playwright.test.ts +36 -0
- package/scripts/sync-byok.d.mts +2 -0
- package/scripts/sync-byok.mjs +18 -0
- package/scripts/sync-launcher-version.cjs +24 -0
- package/scripts/sync-launcher-version.test.ts +28 -0
- package/scripts/sync-plan-catalog.d.mts +3 -0
- package/scripts/sync-plan-catalog.mjs +16 -6
- package/spec/domain_rules.yaml +139 -0
- package/spec/roles.yaml +20 -0
- package/spec/test-case-results.schema.json +20 -4
- package/spec/test-cases.schema.json +131 -16
- package/src/billing/plan-catalog.test.ts +82 -2
- package/src/billing/plan-catalog.ts +212 -9
- package/src/integrations/jira.ts +264 -0
- package/src/mcp/roles-and-review.test.ts +111 -0
- package/src/mcp/server.ts +358 -22
- package/src/mcp/write-test-cases.test.ts +68 -0
- package/src/registry/control-plane-client.ts +121 -6
- package/src/registry/migrations/0034_signup_attribution.sql +19 -0
- package/src/registry/migrations/0035_domain_verify_file.sql +6 -0
- package/src/registry/migrations/0036_task_approval.sql +11 -0
- package/src/registry/migrations/0037_spec_proposals.sql +27 -0
- package/src/registry/migrations/0038_local_worker_seen.sql +5 -0
- package/src/registry/migrations/0039_project_secrets.sql +37 -0
- package/src/registry/migrations/0040_roles_and_task_review.sql +31 -0
- package/src/registry/migrations/0041_jira_integration.sql +75 -0
- package/src/registry/migrations/0042_subscription_gaps.sql +10 -0
- package/src/registry/migrations/0043_workspace_feature_overrides.sql +17 -0
- package/src/registry/migrations/0044_project_identity.sql +31 -0
- package/src/registry/project-identity.test.ts +99 -0
- package/src/registry/project-identity.ts +206 -0
- package/src/registry/roles.test.ts +66 -0
- package/src/registry/roles.ts +201 -0
- package/src/registry/task-scheduling.test.ts +94 -0
- package/src/registry/task-scheduling.ts +199 -2
- package/src/registry/test-case-revisions.test.ts +57 -0
- package/src/registry/test-case-revisions.ts +132 -0
- package/src/registry-worker/ai/routes.ts +3 -3
- package/src/registry-worker/artifacts.ts +23 -4
- package/src/registry-worker/billing/billing-core.test.ts +1 -1
- package/src/registry-worker/billing/checkout-routes.ts +53 -4
- package/src/registry-worker/billing/enforcement.ts +67 -12
- package/src/registry-worker/billing/paypal/api.ts +14 -0
- package/src/registry-worker/billing/paypal/client.ts +9 -0
- package/src/registry-worker/billing/paypal/provider.ts +10 -1
- package/src/registry-worker/billing/paypal.test.ts +108 -1
- package/src/registry-worker/billing/plan-gaps.test.ts +216 -0
- package/src/registry-worker/billing/provider.ts +8 -0
- package/src/registry-worker/billing/routes.ts +2 -1
- package/src/registry-worker/billing/subscriptions.ts +114 -4
- package/src/registry-worker/core.ts +24 -0
- package/src/registry-worker/devices/policy.ts +4 -5
- package/src/registry-worker/devices/routes.ts +8 -8
- package/src/registry-worker/domains/domains.test.ts +64 -1
- package/src/registry-worker/domains/routes.ts +32 -15
- package/src/registry-worker/domains.ts +40 -2
- package/src/registry-worker/feature-access.ts +165 -0
- package/src/registry-worker/feature-flags/admin.ts +300 -0
- package/src/registry-worker/feature-flags/feature-flags.test.ts +270 -0
- package/src/registry-worker/feature-flags/routes.ts +102 -0
- package/src/registry-worker/features.ts +11 -0
- package/src/registry-worker/feedback/feedback.test.ts +47 -0
- package/src/registry-worker/feedback/routes.ts +2 -2
- package/src/registry-worker/feedback/website.ts +77 -0
- package/src/registry-worker/flags.ts +80 -18
- package/src/registry-worker/github/checks.ts +4 -4
- package/src/registry-worker/hooks.ts +9 -0
- package/src/registry-worker/identity/oidc.test.ts +103 -0
- package/src/registry-worker/identity/oidc.ts +176 -0
- package/src/registry-worker/index.ts +494 -173
- package/src/registry-worker/jira/connection.ts +111 -0
- package/src/registry-worker/jira/jira.test.ts +404 -0
- package/src/registry-worker/jira/routes.ts +432 -0
- package/src/registry-worker/jira/workflow.ts +577 -0
- package/src/registry-worker/jobs/retention.ts +21 -5
- package/src/registry-worker/mcp/tools.ts +2 -1
- package/src/registry-worker/notifications/alerts.ts +4 -4
- package/src/registry-worker/notifications/notifications.test.ts +11 -0
- package/src/registry-worker/notifications/routes.ts +9 -10
- package/src/registry-worker/notifications/teams.ts +6 -2
- package/src/registry-worker/org/routes.test.ts +24 -1
- package/src/registry-worker/org/routes.ts +9 -8
- package/src/registry-worker/projects/identity.ts +194 -0
- package/src/registry-worker/projects/inactivity.ts +162 -0
- package/src/registry-worker/projects/projects.test.ts +283 -0
- package/src/registry-worker/proposals/proposals.test.ts +80 -0
- package/src/registry-worker/proposals/routes.ts +183 -0
- package/src/registry-worker/roles/roles.test.ts +84 -0
- package/src/registry-worker/roles/routes.ts +141 -0
- package/src/registry-worker/runner/dispatch.ts +1 -0
- package/src/registry-worker/runner/routes.ts +22 -5
- package/src/registry-worker/runner/runner.test.ts +59 -0
- package/src/registry-worker/runner/tokens.ts +12 -1
- package/src/registry-worker/secrets/crypto.ts +135 -0
- package/src/registry-worker/secrets/routes.ts +296 -0
- package/src/registry-worker/secrets/secrets.test.ts +237 -0
- package/src/registry-worker/signup/attribution.test.ts +147 -0
- package/src/registry-worker/signup/attribution.ts +194 -0
- package/src/registry-worker/signup/policy.ts +2 -2
- package/src/registry-worker/signup/routes.ts +12 -3
- package/src/registry-worker/sso/membership.ts +4 -2
- package/src/registry-worker/sso/routes.ts +24 -8
- package/src/registry-worker/sso/sso.test.ts +3 -2
- package/src/registry-worker/task-approval.test.ts +67 -0
- package/src/registry-worker/task-resume.test.ts +196 -0
- package/src/registry-worker/task-review.test.ts +121 -0
- package/src/runtime/ai-exploration.test.ts +34 -1
- package/src/runtime/ai-exploration.ts +97 -3
- package/src/runtime/appium-driver.ts +7 -1
- package/src/runtime/apply-proposals.test.ts +74 -0
- package/src/runtime/apply-proposals.ts +41 -0
- package/src/runtime/cloud-secrets.test.ts +201 -0
- package/src/runtime/cloud-secrets.ts +210 -0
- package/src/runtime/config-validate.ts +12 -9
- package/src/runtime/cursor-driver-run.ts +14 -3
- package/src/runtime/discovery-task.test.ts +26 -1
- package/src/runtime/discovery-task.ts +87 -7
- package/src/runtime/executor.ts +18 -10
- package/src/runtime/explorer.test.ts +32 -0
- package/src/runtime/explorer.ts +48 -0
- package/src/runtime/flow-language.ts +43 -8
- package/src/runtime/init-wizard.ts +112 -6
- package/src/runtime/journey-editor.ts +78 -2
- package/src/runtime/journey-graph.test.ts +60 -0
- package/src/runtime/local-registry-stub.test.ts +64 -0
- package/src/runtime/local-registry-stub.ts +255 -7
- package/src/runtime/local-vault.ts +65 -0
- package/src/runtime/pipeline.test.ts +23 -0
- package/src/runtime/pipeline.ts +61 -20
- package/src/runtime/planner.test.ts +24 -1
- package/src/runtime/planner.ts +88 -23
- package/src/runtime/playwright-driver.test.ts +164 -0
- package/src/runtime/playwright-driver.ts +286 -15
- package/src/runtime/project-identity.test.ts +161 -0
- package/src/runtime/project-identity.ts +232 -0
- package/src/runtime/project-roles.test.ts +107 -0
- package/src/runtime/project-roles.ts +158 -0
- package/src/runtime/propose-cli.ts +66 -0
- package/src/runtime/record-run-verdicts.test.ts +58 -0
- package/src/runtime/record-run-verdicts.ts +38 -4
- package/src/runtime/reporter.ts +1 -1
- package/src/runtime/reset.ts +2 -0
- package/src/runtime/roles-cli.test.ts +47 -0
- package/src/runtime/roles-cli.ts +60 -0
- package/src/runtime/run-job.test.ts +21 -0
- package/src/runtime/run-job.ts +8 -0
- package/src/runtime/run-once.ts +68 -5
- package/src/runtime/scenario-matrix.test.ts +41 -0
- package/src/runtime/scenario-matrix.ts +98 -0
- package/src/runtime/secret-driver.ts +77 -0
- package/src/runtime/secret-redaction.ts +69 -0
- package/src/runtime/secret-sources.test.ts +442 -0
- package/src/runtime/secret-sources.ts +176 -0
- package/src/runtime/secrets-cli.ts +146 -0
- package/src/runtime/serve-worker.ts +64 -6
- package/src/runtime/site-discovery.test.ts +181 -4
- package/src/runtime/site-discovery.ts +248 -29
- package/src/runtime/vault-federation.test.ts +37 -0
- package/src/runtime/vault-federation.ts +128 -0
- package/src/runtime/web-suite.test.ts +65 -0
- package/src/runtime/web-suite.ts +8 -2
- package/src/storage/create-object-store.ts +5 -1
- package/src/storage/object-store.ts +12 -1
- package/src/storage/test-case-results.test.ts +64 -0
- package/src/storage/test-case-results.ts +49 -0
- package/src/vendor/byok.ts +371 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Pins the unscoped `bugmole` launcher (packages/bugmole) to the @bugmole/cli
|
|
3
|
+
// version being released, so `npx bugmole` always installs that exact CLI.
|
|
4
|
+
// The publish workflow runs this right before publishing the launcher.
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
|
|
8
|
+
function syncLauncher(root = path.resolve(__dirname, "..")) {
|
|
9
|
+
const cli = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
10
|
+
const launcherPath = path.join(root, "packages", "bugmole", "package.json");
|
|
11
|
+
const launcher = JSON.parse(fs.readFileSync(launcherPath, "utf8"));
|
|
12
|
+
if (cli.name !== "@bugmole/cli") throw new Error(`Expected the root package to be @bugmole/cli, found ${cli.name}`);
|
|
13
|
+
launcher.version = cli.version;
|
|
14
|
+
launcher.dependencies = { ...launcher.dependencies, "@bugmole/cli": cli.version };
|
|
15
|
+
fs.writeFileSync(launcherPath, `${JSON.stringify(launcher, null, 2)}\n`);
|
|
16
|
+
return { version: launcher.version, dependency: launcher.dependencies["@bugmole/cli"] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { syncLauncher };
|
|
20
|
+
|
|
21
|
+
if (require.main === module) {
|
|
22
|
+
const result = syncLauncher();
|
|
23
|
+
console.log(`bugmole launcher pinned to @bugmole/cli ${result.dependency}`);
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { syncLauncher } = require("./sync-launcher-version.cjs") as { syncLauncher: (root: string) => { version: string; dependency: string } };
|
|
11
|
+
|
|
12
|
+
test("the bugmole launcher is released at the CLI's version and installs exactly that CLI", () => {
|
|
13
|
+
const root = mkdtempSync(path.join(tmpdir(), "launcher-"));
|
|
14
|
+
mkdirSync(path.join(root, "packages", "bugmole"), { recursive: true });
|
|
15
|
+
writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "@bugmole/cli", version: "0.5.2" }));
|
|
16
|
+
writeFileSync(path.join(root, "packages", "bugmole", "package.json"), JSON.stringify({ name: "bugmole", version: "0.4.0", bin: { bugmole: "bin/bugmole.cjs" }, dependencies: { "@bugmole/cli": "^0.4.0" } }));
|
|
17
|
+
assert.deepEqual(syncLauncher(root), { version: "0.5.2", dependency: "0.5.2" });
|
|
18
|
+
const launcher = JSON.parse(readFileSync(path.join(root, "packages", "bugmole", "package.json"), "utf8"));
|
|
19
|
+
assert.equal(launcher.version, "0.5.2");
|
|
20
|
+
assert.equal(launcher.dependencies["@bugmole/cli"], "0.5.2");
|
|
21
|
+
assert.deepEqual(launcher.bin, { bugmole: "bin/bugmole.cjs" }, "everything else is left alone");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("the real launcher's bin matches its package name, so npx bugmole knows what to run", () => {
|
|
25
|
+
const launcher = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "packages", "bugmole", "package.json"), "utf8"));
|
|
26
|
+
assert.equal(launcher.name, "bugmole");
|
|
27
|
+
assert.ok(launcher.bin.bugmole, "npx picks the bin named after the package when there are several");
|
|
28
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Copies the
|
|
2
|
-
// without importing files from outside
|
|
3
|
-
//
|
|
1
|
+
// Copies files the dashboard shares with the CLI and registry into
|
|
2
|
+
// apps/qa, which deliberately builds without importing files from outside
|
|
3
|
+
// it. Run after editing a source below; the root test suite fails if a copy
|
|
4
|
+
// drifts.
|
|
4
5
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
|
|
@@ -8,9 +9,18 @@ const root = path.resolve(import.meta.dirname, "..");
|
|
|
8
9
|
export const catalogSource = path.join(root, "src/billing/plan-catalog.ts");
|
|
9
10
|
export const catalogCopy = path.join(root, "apps/qa/src/lib/plan-catalog.ts");
|
|
10
11
|
export const COPY_HEADER = "// GENERATED by `npm run sync:catalog` from src/billing/plan-catalog.ts — edit that file instead.\n";
|
|
12
|
+
export const scenarioSource = path.join(root, "src/runtime/scenario-matrix.ts");
|
|
13
|
+
export const scenarioCopy = path.join(root, "apps/qa/src/lib/scenario-matrix.ts");
|
|
14
|
+
export const SCENARIO_COPY_HEADER = "// GENERATED by `npm run sync:catalog` from src/runtime/scenario-matrix.ts — edit that file instead.\n";
|
|
15
|
+
|
|
16
|
+
const COPIES = [
|
|
17
|
+
[catalogSource, catalogCopy, COPY_HEADER],
|
|
18
|
+
[scenarioSource, scenarioCopy, SCENARIO_COPY_HEADER],
|
|
19
|
+
];
|
|
11
20
|
|
|
12
21
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
13
|
-
const source
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
for (const [source, copy, header] of COPIES) {
|
|
23
|
+
await writeFile(copy, header + await readFile(source, "utf8"));
|
|
24
|
+
console.log(`Wrote ${path.relative(root, copy)}`);
|
|
25
|
+
}
|
|
16
26
|
}
|
package/spec/domain_rules.yaml
CHANGED
|
@@ -22,3 +22,142 @@ domain_rules:
|
|
|
22
22
|
- name: screenshot_evidence_manifest
|
|
23
23
|
description: Browser evidence is manifest-backed.
|
|
24
24
|
rule: "Screenshots live under qa/screenshots and the current manifest lives at qa/browser-evidence.json."
|
|
25
|
+
|
|
26
|
+
- name: agent_task_approval_mode
|
|
27
|
+
description: Each project has an approval mode for agent tasks, default auto.
|
|
28
|
+
rule: "manual holds every task until a project manager approves it; auto starts discovery and investigation tasks by themselves and holds code and ui tasks; extreme holds nothing. A task records approvedAt and approvedBy (a person, or auto:<mode>)."
|
|
29
|
+
|
|
30
|
+
- name: workers_take_only_approved_tasks
|
|
31
|
+
description: Nothing runs before it is approved.
|
|
32
|
+
rule: "Claiming a task and dispatching it to Bugmole Cloud both require approvedAt; claiming an unapproved task answers 409 'Task is waiting for approval'. Approving a cloud task hands it to a runner."
|
|
33
|
+
|
|
34
|
+
- name: worker_approval_is_stricter_never_looser
|
|
35
|
+
description: A worker can demand more approval than its project, never less.
|
|
36
|
+
rule: "With bugmole serve --approval <mode> (BUGMOLE_APPROVAL), a worker skips tasks its own mode would hold unless a person approved them; it never takes a task the project holds."
|
|
37
|
+
|
|
38
|
+
- name: test_case_revisions
|
|
39
|
+
description: A test case is never edited in place.
|
|
40
|
+
rule: "Changing a test case adds its next revision and marks the replaced one obsolete (status obsolete, supersededBy, obsoleteReason revised); retiring marks the current one obsolete (obsoleteReason retired). At most one revision of a caseId is active. Obsolete revisions don't run or count toward coverage and keep their results; a result records caseRevision, so a new revision is untested until a run judges it."
|
|
41
|
+
|
|
42
|
+
- name: test_case_proposals
|
|
43
|
+
description: Changes to test cases arrive as proposals.
|
|
44
|
+
rule: "Agents (bugmole_propose_test_case), developers (bugmole propose) and dashboard users propose add, change or retire with a summary and the revision they change. The approval mode decides acceptance: a project manager in manual and auto, on arrival in extreme. A proposal whose base revision is no longer current becomes outdated instead of overwriting. Bugmole storage is updated by the registry on accept; a project's own storage by bugmole serve or the proposing CLI."
|
|
45
|
+
|
|
46
|
+
- name: test_case_results_history
|
|
47
|
+
description: Recording test case results keeps the earlier ones.
|
|
48
|
+
rule: "Every result write (bugmole_write_test_case_results or a run's verdicts) replaces projects/<id>/test-case-results/<caseSetId>.json and also writes projects/<id>/test-case-history/<caseSetId>/<recordedAt>-<runId>.json. The dashboard reads the newest ten history files per case set; a case's history counts only verdicts for its own revision and the current journey revision. Blocked and needs_setup are neither passes nor failures; a case is flaky when it both passed and failed in that window."
|
|
49
|
+
|
|
50
|
+
- name: resume_never_strands_a_task
|
|
51
|
+
description: Resuming a task puts it where something will run it, or says why it can't.
|
|
52
|
+
rule: "POST /api/tasks/<id>/resume hands a cloud task (executionMode cloud) to the dispatcher. A task with executionMode self on a project that runs in the cloud (storage_mode managed or default_execution_mode cloud) is refused with 409 code needs_local_worker unless a machine's API key listed the project's tasks within LOCAL_WORKER_FRESH_MS (projects.local_worker_seen_at); canRunInCloud says a discovery can be resumed with executionMode cloud, which passes the same checks as starting on Bugmole Cloud."
|
|
53
|
+
|
|
54
|
+
- name: resume_keeps_approval_semantics
|
|
55
|
+
description: Resuming never skips the approval step the project requires.
|
|
56
|
+
rule: "A person's approval (approvedBy not auto:) survives a resume; an automatic or missing one is decided again by the project's current approval mode, and a task that waits for approval again is not dispatched until approved."
|
|
57
|
+
|
|
58
|
+
- name: completion_review_follows_approval_mode
|
|
59
|
+
description: A finished AI task waits for a person when the project's approval mode asks for it.
|
|
60
|
+
rule: "When a worker reports completed, the registry sets status in_review instead when the project's approval mode would hold that task type: manual reviews every task, auto reviews code and ui tasks, extreme none (completionStatus in src/registry/task-scheduling.ts). The response carries inReview: true and a message. A task in review is not claimable or resumable, a repeated completed is a no-op, and any other worker update answers 409. Tasks that were completed before review existed stay completed."
|
|
61
|
+
|
|
62
|
+
- name: only_a_person_reviews
|
|
63
|
+
description: Accepting or sending back a task is a person's call.
|
|
64
|
+
rule: "POST /api/tasks/<id>/accept (status completed, completed_at, reviewed_by, reviewed_at, optional review_note) and /send-back (a note is required; status queued, attempts 0, lease and worker cleared, review_note kept, approval kept) need project.manage and refuse API keys and runners with 403, so an agent cannot accept its own work. Both are audited (task.review_accepted, task.review_sent_back). The pipeline pauses while any task is in review."
|
|
65
|
+
|
|
66
|
+
- name: roles_are_suggested_then_confirmed
|
|
67
|
+
description: Bugmole suggests roles; a person decides which are used.
|
|
68
|
+
rule: "project_roles rows are suggested, confirmed or rejected, with source (sign_in_wall, role_gated_route, role_gated_menu, spec, persona, agent, person) and evidence. Anyone with project.write may suggest (evidence required); a person with project.manage adding a role confirms it. Only a person with project.manage confirms, renames, rejects or deletes; API keys and runners get 403. Suggesting a role a person already decided on changes nothing. Every change is audited (role.suggested, role.created, role.confirmed, role.rejected, role.updated, role.deleted)."
|
|
69
|
+
|
|
70
|
+
- name: only_confirmed_roles_are_planned
|
|
71
|
+
description: Plans and test case sets are written for confirmed roles only.
|
|
72
|
+
rule: "bugmole_plan and bugmole_write_test_cases refuse an actor that is not confirmed, naming the confirmed roles. A roles.yaml entry without status is confirmed; the registry's decision overrides roles.yaml. bugmole_write_spec writes any role an agent adds or promotes in roles.yaml as status suggested (a rejected one stays rejected). The pipeline pauses while a flow's only missing actors are suggested roles."
|
|
73
|
+
|
|
74
|
+
- name: project_secrets_are_write_only
|
|
75
|
+
description: Sign-in secrets for cloud runs are stored per project and never read back.
|
|
76
|
+
rule: "Project managers (project.manage) set, replace and delete project secrets in the dashboard; API keys and runners cannot write them. No route returns a value to a browser or an API key: the list carries name, environmentId, createdBy, updatedBy, createdAt, updatedAt and lastUsedAt only. Set, replace, delete and release are audited with names, never values."
|
|
77
|
+
|
|
78
|
+
- name: project_secrets_encryption
|
|
79
|
+
description: Values are encrypted per project and bound to where they belong.
|
|
80
|
+
rule: "Values are AES-256-GCM ciphertext under a per-project key derived with HKDF-SHA256 from the registry secret PROJECT_SECRETS_KEY (info bugmole:project-secret:<projectId>), with additional data <projectId>|<environmentId>|<name> and a key_version for rotation. Without PROJECT_SECRETS_KEY the secrets routes answer 503 and nothing is stored."
|
|
81
|
+
|
|
82
|
+
- name: project_secrets_released_only_to_cloud_runner
|
|
83
|
+
description: Only a Bugmole Cloud runner reads values, for the job it holds.
|
|
84
|
+
rule: "GET /api/runner/runs/:id/secrets and /api/runner/tasks/:id/secrets answer only the run token of that job, while the job is running on cloud:<id>. Project-wide values are replaced by the run environment's own values; explorations get project-wide values. Self-hosted workers never receive stored values and keep reading BUGMOLE_FLOW_SECRET_<NAME> from their own environment."
|
|
85
|
+
|
|
86
|
+
- name: runner_secrets_are_redacted
|
|
87
|
+
description: A secret value never leaves a worker in a log, result or report.
|
|
88
|
+
rule: "Every secret a worker types or is released is replaced with [secret] in logs, step results, failure context, manifests, text uploads and registry updates. A flow that types a secret records no Playwright trace. A cloud runner holds released values in its environment as BUGMOLE_FLOW_SECRET_<NAME> only while the job runs."
|
|
89
|
+
|
|
90
|
+
- name: cloud_exploration_signs_in_first
|
|
91
|
+
description: A project's sign-in flow runs before cloud exploration.
|
|
92
|
+
rule: "When a project has a sign-in flow, cloud exploration runs it first and explores in its session; the flow references secrets by name and saving a flow that contains a stored secret's value is refused. A flow that uses a secret the project doesn't have fails the exploration with a message naming the missing secret."
|
|
93
|
+
|
|
94
|
+
- name: coverage_counts_only_checked_work
|
|
95
|
+
description: Covered means a run checked it and it passed; everything else is listed as a gap.
|
|
96
|
+
rule: "A test case is an assumption until a run records a result for its current revision, and an assumption never counts as covered. Overview lists what isn't covered from data the dashboard already loads (apps/qa/src/lib/coverage-gaps.ts): pages or journeys with no test or case, journeys whose cases are all obsolete, tests never run, tests or cases whose latest result failed or was blocked, and what the last exploration couldn't reach (a sign-in wall, pages it couldn't open, links it skipped, its page limit). Each gap names its reason and a next action; a gap is listed only when a record shows it."
|
|
97
|
+
|
|
98
|
+
- name: estimates_before_starting
|
|
99
|
+
description: Starting a run or exploration says how long it usually takes, or that there is no estimate yet.
|
|
100
|
+
rule: "Before a test run starts, the estimate is the median of that test's last ten finished runs; without one it is estimated from the test's step count, then from other tests in the project; with none of these it says there is no estimate yet. Explorations use earlier ones of the same kind, or a first-time default. Estimates come from apps/qa/src/lib/eta.ts and are never shown as a promise."
|
|
101
|
+
|
|
102
|
+
- name: jira_credentials_are_write_only
|
|
103
|
+
description: A project's Jira API token and webhook secret are stored sealed and never read back.
|
|
104
|
+
rule: "Only project managers (project.manage) signed in to the dashboard connect, update, pause and disconnect Jira (PUT/DELETE /api/projects/:id/jira); API keys and runners cannot. The token is verified against Jira (myself and the project) before anything is stored, then sealed with AES-256-GCM under a key derived by HKDF-SHA256 from PROJECT_SECRETS_KEY with info bugmole:jira:<projectId> and additional data <projectId>|jira|<connectionId>|<field>. No response, audit entry or log line contains the token or webhook secret; the webhook secret is returned once, at connect or rotation. The site must be https://<name>.atlassian.net."
|
|
105
|
+
|
|
106
|
+
- name: jira_plan_and_tickets
|
|
107
|
+
description: Bugmole proposes its test plan as one Jira issue and keeps one ticket per approved test case.
|
|
108
|
+
rule: "POST /api/projects/:id/jira/plan creates one Task labelled bugmole-plan from the project's pending test case proposals (the same pending set is not posted twice); when every proposal on it is decided in Bugmole, the issue gets a comment and moves to a Done-category status. Accepted proposals (and, on request for Bugmole storage, existing active cases) get one Task each labelled bugmole-test plus a per-case label bugmole-case-<sha256(projectId|caseSetId|caseId)[0:16]>; jira_issue_links maps (project_id, case_set_id, case_id) to the issue key and revision. Creation is claimed per case and searches Jira by the case label first, so a lost write never creates a second ticket."
|
|
109
|
+
|
|
110
|
+
- name: jira_results_move_tickets
|
|
111
|
+
description: Run results move tickets to Done or Blocked and mention the people on failed ones.
|
|
112
|
+
rule: "When a run of a project with linked tickets ends, the outbox reports it once (jira-report:<runId>). Case verdicts come from the run's test-case-results record (latest or history) on Bugmole storage; a case-scoped retest without readable verdicts is judged by the run's own status. Passed moves the ticket to a Done-category transition and removes bugmole-blocked; failed or blocked moves it to a status named Blocked, or adds bugmole-blocked when the workflow has none, and comments with ADF mentions of the assignee and reporter (never Bugmole's own account) and the run's evidence link; needs_setup only comments."
|
|
113
|
+
|
|
114
|
+
- name: jira_mention_retests_one_case
|
|
115
|
+
description: Mentioning Bugmole on a ticket retests only that ticket's case.
|
|
116
|
+
rule: "POST /api/jira/webhook?connection=<id> accepts a delivery only with the connection's secret, compared in constant time, as X-Hub-Signature sha256 HMAC of the body, an X-Bugmole-Webhook-Secret header, or a token query parameter; accepted and rejected deliveries are audited with actor webhook jira:<connectionId>. A comment_created event on a linked ticket in the connected project, not written by Bugmole's own account, that contains the mention keyword or mentions Bugmole's account queues a run of the case's journey plan with idempotency key jira:<connectionId>:<issueKey>:<commentId>, trigger_source jira and runs.case_scope_json {caseSetId, caseIds:[caseId], jiraIssueKey}. Workers judge only scoped cases and merge them into the latest results, so other cases and tickets keep their state."
|
|
117
|
+
|
|
118
|
+
- name: plan_limits_on_every_way_in
|
|
119
|
+
description: Project and Free-workspace limits apply however a project is created, including CLI device pairing.
|
|
120
|
+
rule: "POST /api/projects/register (dashboard or API key) and CLI device pairing (POST /api/device-authorizations/claim) refuse a new project past the plan's project limit with 402 plan_limit, carrying suggestedPlan (the smallest plan with room), upgradeUrl (/billing?workspace=<id>&plan=<plan>) and billingUrl (absolute, from DASHBOARD_URL). Pairing never creates a second Free workspace: when the person already owns one, the project goes there, under that workspace's limit. A refused pairing is stored on the attempt (status refused, refusal_json) and returned by the CLI's poll, so `bugmole init` prints the message and the billing link instead of failing. Re-pairing an existing project is not a new project; pairing to a project the person does not manage is refused with 409 project_exists."
|
|
121
|
+
|
|
122
|
+
- name: keep_plan_before_period_end
|
|
123
|
+
description: A cancellation or scheduled plan change can be undone until the paid period ends.
|
|
124
|
+
rule: "POST /api/workspaces/:id/billing/resume (billing.manage, audited) clears cancel_at_period_end and any scheduled change while the provider's subscription is still live, activating it first when PayPal only suspended it. Cancelling at PayPal is final (a CANCELLED subscription cannot be activated), so otherwise it answers checkout_required and starts a checkout for the same plan and interval beginning at the period end, so nothing is charged twice. After the period ends it is refused with period_ended."
|
|
125
|
+
|
|
126
|
+
- name: choose_projects_within_limit
|
|
127
|
+
description: Projects past the plan's limit are read-only, and an owner chooses which stay active.
|
|
128
|
+
rule: "applyProjectLimit keeps projects with kept_at first, then the oldest, and locks the rest (locked_at); nothing is deleted. PUT /api/workspaces/:id/projects/active (workspace.manage, audited as billing.projects_chosen) sets kept_at on at most the plan's limit of the workspace's own projects and re-applies the limit, so later plan changes keep the choice."
|
|
129
|
+
|
|
130
|
+
- name: history_cutoff_everywhere
|
|
131
|
+
description: Run history follows the plan wherever the dashboard reads it, and retention follows the entitlements rollout.
|
|
132
|
+
rule: "Once limits are enforced, finished runs older than the plan's historyDays are hidden from the runs list, from GET /api/runs/:id (404 history_expired) and from run evidence under projects/<id>/runs/<runId>/ in the artifacts list and object routes (evidence of an unknown run is judged by its upload time). The daily retention job deletes the same runs and evidence only when FEATURE_ENTITLEMENTS enforces; in shadow or off it only logs how many it would delete."
|
|
133
|
+
|
|
134
|
+
- name: one_upsell_for_locked_features
|
|
135
|
+
description: Plan-gated features and limits show one upsell with the plan that includes them.
|
|
136
|
+
rule: "Dashboard pages render <LockedFeature feature enabled requiredPlan> (\"Available on Team and Business\", See plans -> /billing?workspace=&plan=) instead of their own copy, and a 402 plan_limit shows <PlanLimitNotice> with the suggested plan. Plan features reach it through one adapter (featureAccessFromUsage over GET /usage limits.features)."
|
|
137
|
+
- name: feature_access_single_evaluator
|
|
138
|
+
description: One function decides whether a workspace may use a feature, and the dashboard reads the same answer.
|
|
139
|
+
rule: "src/registry-worker/feature-access.ts effectiveFeatures evaluates every FEATURES entry in a fixed order and the first layer that decides wins: the global FEATURE_<NAME> kill switch (off or shadow → flag_off), beta rollout (FEATURE_<NAME>=beta needs a workspace_flags opt-in → beta_not_enabled), an unexpired workspace_feature_overrides row (→ workspace_override), then the plan-catalog features with the contract overrides on workspace_plans (→ plan or enterprise_override). Limits are entitlementsFor(plan, overrides), the same values enforcement reads. GET /api/workspaces/:id/features returns the evaluated features (enabled, reason, requiredPlan), limits (limit, used, unlimited) and plan to any workspace member; the dashboard hides features blocked by rollout or an operator and locks, with the required plan, those an upgrade unlocks."
|
|
140
|
+
|
|
141
|
+
- name: feature_flags_match_the_catalog
|
|
142
|
+
description: Every priced feature has exactly one flag.
|
|
143
|
+
rule: "FEATURE_DEFS in src/registry-worker/flags.ts maps each flag to at most one plan-catalog features key; every catalog features key has exactly one flag, and flags without one are listed in OPERATIONAL_FEATURES (entitlements, cloud_runners, billing, feedback, mcp). A test fails when either side drifts."
|
|
144
|
+
|
|
145
|
+
- name: platform_admin_routes
|
|
146
|
+
description: Only platform operators change overrides and contract plans, and every change is audited.
|
|
147
|
+
rule: "/api/platform/… routes require Authorization Bearer PLATFORM_ADMIN_TOKEN compared in constant time (SHA-256 of both sides); they answer 503 when the secret is unset and 401 for a missing or wrong token, and never accept dashboard sessions or API keys. The dashboard proxy never forwards them. Setting or clearing a feature override or beta opt-in, and setting a contract plan, writes an audit_log row with actor system:platform_admin, the x-bugmole-operator name, the new and previous values and the note. A contract plan is enterprise or free only, is refused with 409 while a provider subscription is active, re-applies the project limit and runs the plan-change hooks."
|
|
148
|
+
|
|
149
|
+
- name: one_app_one_project
|
|
150
|
+
description: A project is one app, identified by its domain or its repository and app folder; the same app in a workspace is the same project.
|
|
151
|
+
rule: "src/registry/project-identity.ts normalizes an identity: domain:<host> (lowercase, no port, no leading www., never a private or local host) for an app on the internet, repo:<host>/<path>#<appPath> for a git remote (scheme, user, password, token, port and .git dropped; lowercase host; path lowercased on github.com, gitlab.com, bitbucket.org, dev.azure.com and codeberg.org) plus the app's folder ('.' for the root). The CLI computes it (bugmole init, pairing, bugmole serve) and sends only the normalized, credential-free form; the registry normalizes again and stores projects.identity_kind, identity_key, identity_hash (SHA-256 of the key) and identity_origin, unique per workspace among undeleted projects. POST /api/projects/register with an identity (or a public appUrl) already in the workspace answers 200 {existing: true, project} instead of creating one; CLI pairing joins a project with the same identity in any workspace where the person is an owner, admin or member (poll returns its projectId, the key is scoped to it, the project and the person's role are unchanged). A project without an identity takes its first public environment base URL's domain. POST /api/projects/:id/identity sets it once; a different app is 409 identity_mismatch, one already in the workspace 409 identity_exists."
|
|
152
|
+
|
|
153
|
+
- name: monorepo_apps_are_separate_projects
|
|
154
|
+
description: Every app in a repository is its own project and counts toward the project limit.
|
|
155
|
+
rule: "The CLI finds web apps (framework dependencies, Django, Rails, Laravel, a static index.html) and mobile apps (Expo, React Native, Capacitor, Ionic, Flutter, Android com.android.application, iOS .xcodeproj/.xcworkspace), treating an npm/yarn/pnpm workspace, Turborepo, Nx or Lerna root as a container rather than an app. The app is the deepest one containing the working directory, the one named by --app-path, or the repository's only app; with several and no choice it sends no identity and asks for --app-path rather than collapsing apps into one project. With no web or mobile app it prints that data and AI projects are out of scope."
|
|
156
|
+
|
|
157
|
+
- name: free_identity_in_use
|
|
158
|
+
description: On Free, an app that already belongs to someone else's Free workspace can't be added to another Free workspace.
|
|
159
|
+
rule: "Creating a project (register, sign-up onboarding, CLI pairing) or giving one a domain through an environment in a Free workspace (no plan row, plan free, or a canceled plan) is refused with 409 identity_in_use when an undeleted, active (inactive_at IS NULL) project with the same identity_hash is in another Free workspace the person doesn't own. The message names the app, not the other workspace, and says to ask its owner for an invitation or upgrade (billingUrl for Team). Team, Business and Enterprise workspaces are never refused. It follows FEATURE_ENTITLEMENTS: enforced refuses, shadow logs, off allows. The one-Free-workspace-per-person rule is unchanged."
|
|
160
|
+
|
|
161
|
+
- name: inactive_projects_pause
|
|
162
|
+
description: Projects idle past the plan's inactiveProjectDays are paused until a member activates them.
|
|
163
|
+
rule: "inactiveProjectDays is a plan-catalog limit (Free 30, Team/Business/Enterprise null = never), on the pricing page as 'Projects paused after inactivity' and in GET /api/workspaces/:id/features limits. The daily job (jobs at 17 3 * * *, projects/inactivity.ts) takes a project's last activity as the latest of created_at, activated_at, local_worker_seen_at (a machine or MCP client with an API key listing tasks), runs requested/completed, agent tasks requested/heartbeat/completed, plan_drafts.updated_at, journey_revisions.created_at and non-system audit_log rows; past the limit it sets projects.inactive_at and audits project.paused. Enforced only when FEATURE_ENTITLEMENTS enforces; shadow logs what it would pause; off does nothing. A paused project is readable, refuses new runs, run claims, new tasks and task claims with 409 project_inactive {activateUrl}, and doesn't count toward the project limit, usage or Billing. POST /api/projects/:id/activate (project member with project.write, audited project.activated) clears inactive_at and sets activated_at, and answers 402 plan_limit when the workspace's active projects already fill the plan. The dashboard shows a Paused banner with Activate; bugmole serve prints the activation link."
|
package/spec/roles.yaml
CHANGED
|
@@ -28,3 +28,23 @@ roles:
|
|
|
28
28
|
permissions:
|
|
29
29
|
- "projects:read"
|
|
30
30
|
- "keys:write"
|
|
31
|
+
- "proposals:create"
|
|
32
|
+
|
|
33
|
+
- name: project_manager
|
|
34
|
+
description: Workspace owner or admin (project.manage), always a person. Everything a user can do, plus approving agent tasks the project's approval mode holds back, accepting or sending back finished tasks it holds for review, confirming, renaming or rejecting suggested roles, changing the project's approval mode and where runs go, and setting the project's sign-in flow and write-only secrets for cloud runs (never reading a value back).
|
|
35
|
+
permissions:
|
|
36
|
+
- "projects:read"
|
|
37
|
+
- "keys:write"
|
|
38
|
+
- "tasks:approve"
|
|
39
|
+
- "tasks:review"
|
|
40
|
+
- "roles:decide"
|
|
41
|
+
- "proposals:decide"
|
|
42
|
+
- "project_settings:write"
|
|
43
|
+
- "project_secrets:write"
|
|
44
|
+
|
|
45
|
+
- name: worker
|
|
46
|
+
description: A bugmole serve process, coding-agent driver or Bugmole Cloud runner acting with a project API key or run token. Claims queued work and reports results; takes only approved agent tasks, and with --approval / BUGMOLE_APPROVAL only those its own mode would allow. Its completed may become in_review for a person, and it may suggest roles but never confirm one or review a task. Only a Bugmole Cloud runner, with the run token of the job it holds, reads the project's secret values (project_secrets:release); API-key workers never do.
|
|
47
|
+
permissions:
|
|
48
|
+
- "tasks:claim"
|
|
49
|
+
- "runs:claim"
|
|
50
|
+
- "roles:suggest"
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"results"
|
|
12
12
|
],
|
|
13
13
|
"properties": {
|
|
14
|
-
"schemaVersion": {
|
|
14
|
+
"schemaVersion": {
|
|
15
|
+
"const": 1
|
|
16
|
+
},
|
|
15
17
|
"caseSetId": {
|
|
16
18
|
"type": "string",
|
|
17
19
|
"minLength": 1,
|
|
@@ -36,24 +38,38 @@
|
|
|
36
38
|
"items": {
|
|
37
39
|
"type": "object",
|
|
38
40
|
"additionalProperties": false,
|
|
39
|
-
"required": [
|
|
41
|
+
"required": [
|
|
42
|
+
"caseId",
|
|
43
|
+
"status",
|
|
44
|
+
"detail"
|
|
45
|
+
],
|
|
40
46
|
"properties": {
|
|
41
47
|
"caseId": {
|
|
42
48
|
"type": "string",
|
|
43
49
|
"description": "Must be a caseId present in the referenced case set."
|
|
44
50
|
},
|
|
45
51
|
"status": {
|
|
46
|
-
"enum": [
|
|
52
|
+
"enum": [
|
|
53
|
+
"passed",
|
|
54
|
+
"failed",
|
|
55
|
+
"blocked",
|
|
56
|
+
"needs_setup"
|
|
57
|
+
],
|
|
47
58
|
"description": "\"failed\" means the check ran and the expectation did not hold. \"blocked\" means it could not be run at all. Do not use one for the other: a regression hidden behind a setup problem is how a broken app looks healthy."
|
|
48
59
|
},
|
|
49
60
|
"detail": {
|
|
50
61
|
"type": "string",
|
|
51
62
|
"minLength": 1,
|
|
52
|
-
"description": "What was actually observed
|
|
63
|
+
"description": "What was actually observed \u2014 the status code, the element that was or was not there, the assertion that failed. Required, including for a pass: a verdict nobody can review is not evidence."
|
|
53
64
|
},
|
|
54
65
|
"evidence": {
|
|
55
66
|
"type": "string",
|
|
56
67
|
"description": "Artifact key for the screenshot or capture backing this verdict."
|
|
68
|
+
},
|
|
69
|
+
"caseRevision": {
|
|
70
|
+
"type": "integer",
|
|
71
|
+
"minimum": 1,
|
|
72
|
+
"description": "The revision of the case this verdict judged. A newer revision is untested until a run records a verdict for it."
|
|
57
73
|
}
|
|
58
74
|
}
|
|
59
75
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://app.bugmole.com/schemas/test-cases.schema.json",
|
|
4
4
|
"title": "Bugmole Test Case Assumptions",
|
|
5
|
-
"description": "Candidate test cases derived from a discovered journey. These are assumptions: what the agent believes each step should do, grounded in nodes and edges that actually exist in the journey graph. They are not yet verified
|
|
5
|
+
"description": "Candidate test cases derived from a discovered journey. These are assumptions: what the agent believes each step should do, grounded in nodes and edges that actually exist in the journey graph. They are not yet verified \u2014 a run is what turns an assumption into evidence.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": [
|
|
@@ -13,30 +13,88 @@
|
|
|
13
13
|
"cases"
|
|
14
14
|
],
|
|
15
15
|
"properties": {
|
|
16
|
-
"schemaVersion": {
|
|
16
|
+
"schemaVersion": {
|
|
17
|
+
"const": 1
|
|
18
|
+
},
|
|
17
19
|
"caseSetId": {
|
|
18
20
|
"type": "string",
|
|
19
21
|
"minLength": 1,
|
|
20
22
|
"pattern": "^[a-zA-Z0-9._-]+$"
|
|
21
23
|
},
|
|
22
|
-
"projectId": {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
24
|
+
"projectId": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
"journeyId": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"minLength": 1
|
|
30
|
+
},
|
|
31
|
+
"journeyRevision": {
|
|
32
|
+
"type": "integer",
|
|
33
|
+
"minimum": 1
|
|
34
|
+
},
|
|
35
|
+
"actor": {
|
|
36
|
+
"type": "string"
|
|
37
|
+
},
|
|
38
|
+
"generatedAt": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"format": "date-time"
|
|
41
|
+
},
|
|
42
|
+
"scenarioExclusions": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"description": "Scenario categories that don't apply, with the reason, instead of a silent gap (a step with no input has nothing to validate; an app with one role has no permission scenarios). Without nodeId an exclusion covers the whole journey.",
|
|
45
|
+
"items": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"required": [
|
|
49
|
+
"category",
|
|
50
|
+
"reason"
|
|
51
|
+
],
|
|
52
|
+
"properties": {
|
|
53
|
+
"category": {
|
|
54
|
+
"enum": [
|
|
55
|
+
"happy_path",
|
|
56
|
+
"validation",
|
|
57
|
+
"error_state",
|
|
58
|
+
"permission",
|
|
59
|
+
"edge_case"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
"nodeId": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"minLength": 1,
|
|
65
|
+
"description": "Must be a node in the referenced journey."
|
|
66
|
+
},
|
|
67
|
+
"reason": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"minLength": 1
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
27
74
|
"cases": {
|
|
28
75
|
"type": "array",
|
|
29
76
|
"items": {
|
|
30
77
|
"type": "object",
|
|
31
78
|
"additionalProperties": false,
|
|
32
|
-
"required": [
|
|
79
|
+
"required": [
|
|
80
|
+
"caseId",
|
|
81
|
+
"title",
|
|
82
|
+
"nodeId",
|
|
83
|
+
"intent",
|
|
84
|
+
"steps",
|
|
85
|
+
"expected",
|
|
86
|
+
"confidence"
|
|
87
|
+
],
|
|
33
88
|
"properties": {
|
|
34
89
|
"caseId": {
|
|
35
90
|
"type": "string",
|
|
36
91
|
"minLength": 1,
|
|
37
92
|
"pattern": "^[a-zA-Z0-9._-]+$"
|
|
38
93
|
},
|
|
39
|
-
"title": {
|
|
94
|
+
"title": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"minLength": 1
|
|
97
|
+
},
|
|
40
98
|
"nodeId": {
|
|
41
99
|
"type": "string",
|
|
42
100
|
"minLength": 1,
|
|
@@ -47,39 +105,96 @@
|
|
|
47
105
|
"minLength": 1,
|
|
48
106
|
"description": "Optional; when present must be an edge in the referenced journey."
|
|
49
107
|
},
|
|
50
|
-
"route": {
|
|
108
|
+
"route": {
|
|
109
|
+
"type": "string"
|
|
110
|
+
},
|
|
51
111
|
"intent": {
|
|
52
112
|
"type": "string",
|
|
53
113
|
"minLength": 1,
|
|
54
114
|
"description": "What this case is trying to prove, in one sentence."
|
|
55
115
|
},
|
|
116
|
+
"category": {
|
|
117
|
+
"enum": [
|
|
118
|
+
"happy_path",
|
|
119
|
+
"validation",
|
|
120
|
+
"error_state",
|
|
121
|
+
"permission",
|
|
122
|
+
"edge_case"
|
|
123
|
+
],
|
|
124
|
+
"description": "What kind of scenario this case is: happy_path (works for the intended user), validation (bad or missing input is rejected), error_state (backend failure, network error or no data is handled), permission (a user without access is kept out), edge_case (limits and unusual but valid input). Every step needs a happy_path case; each other category needs a case somewhere in the journey or a scenarioExclusions entry saying why it doesn't apply."
|
|
125
|
+
},
|
|
56
126
|
"steps": {
|
|
57
127
|
"type": "array",
|
|
58
128
|
"minItems": 1,
|
|
59
|
-
"items": {
|
|
129
|
+
"items": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"minLength": 1
|
|
132
|
+
}
|
|
60
133
|
},
|
|
61
134
|
"expected": {
|
|
62
135
|
"type": "array",
|
|
63
136
|
"minItems": 1,
|
|
64
|
-
"items": {
|
|
137
|
+
"items": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"minLength": 1
|
|
140
|
+
},
|
|
65
141
|
"description": "Observable outcomes. Each should be checkable, not a restatement of the step."
|
|
66
142
|
},
|
|
67
143
|
"confidence": {
|
|
68
|
-
"enum": [
|
|
144
|
+
"enum": [
|
|
145
|
+
"high",
|
|
146
|
+
"medium",
|
|
147
|
+
"low"
|
|
148
|
+
],
|
|
69
149
|
"description": "How sure the agent is that this expectation is correct rather than guessed."
|
|
70
150
|
},
|
|
71
151
|
"basis": {
|
|
72
152
|
"type": "array",
|
|
73
|
-
"items": {
|
|
74
|
-
|
|
153
|
+
"items": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"minLength": 1
|
|
156
|
+
},
|
|
157
|
+
"description": "What this assumption was derived from \u2014 source paths, observed controls, domain rules. An assumption with no basis should be low confidence."
|
|
75
158
|
},
|
|
76
159
|
"blockedReason": {
|
|
77
160
|
"type": "string",
|
|
78
161
|
"minLength": 1,
|
|
79
162
|
"description": "Set when the case cannot be asserted yet; recorded rather than silently dropped."
|
|
163
|
+
},
|
|
164
|
+
"revision": {
|
|
165
|
+
"type": "integer",
|
|
166
|
+
"minimum": 1,
|
|
167
|
+
"description": "Which revision of this case this is; 1 when absent. A change never edits a case in place: it adds the next revision."
|
|
168
|
+
},
|
|
169
|
+
"status": {
|
|
170
|
+
"enum": [
|
|
171
|
+
"active",
|
|
172
|
+
"obsolete"
|
|
173
|
+
],
|
|
174
|
+
"description": "Only the active revision runs and counts toward coverage; obsolete revisions are kept with their past results. Absent means active."
|
|
175
|
+
},
|
|
176
|
+
"supersededBy": {
|
|
177
|
+
"type": "integer",
|
|
178
|
+
"minimum": 2,
|
|
179
|
+
"description": "The revision that replaced this one (absent when the case was retired)."
|
|
180
|
+
},
|
|
181
|
+
"obsoletedAt": {
|
|
182
|
+
"type": "string",
|
|
183
|
+
"format": "date-time"
|
|
184
|
+
},
|
|
185
|
+
"obsoleteReason": {
|
|
186
|
+
"enum": [
|
|
187
|
+
"revised",
|
|
188
|
+
"retired"
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
"proposalId": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"description": "The accepted proposal that created this revision."
|
|
80
194
|
}
|
|
81
195
|
}
|
|
82
|
-
}
|
|
196
|
+
},
|
|
197
|
+
"description": "A case may appear once per revision; at most one revision of a caseId is active."
|
|
83
198
|
}
|
|
84
199
|
}
|
|
85
200
|
}
|