|
| 1 | +# campaign-workflow Specification |
| 2 | + |
| 3 | +## Purpose |
| 4 | +TBD - created by archiving change add-campaign-workflow. Update Purpose after archive. |
| 5 | +## Requirements |
| 6 | +### Requirement: Campaigns have a validated local contract |
| 7 | +The system SHALL represent a research campaign as a closed-schema `Campaign.v1` YAML object under `.omv/campaigns/<id>.yaml` with a paired Markdown runbook at `.omv/campaigns/<id>.md`, and SHALL reject malformed, calendar-invalid, non-normalized, semantically inconsistent, or undeclared fields before returning or using the object. |
| 8 | + |
| 9 | +#### Scenario: Valid Campaign object |
| 10 | +- **WHEN** a campaign contains schema version 1, a safe id, target name, supported enum values, at least one normalized vulnerability class, and matching lanes |
| 11 | +- **THEN** campaign validation succeeds and returns the typed normalized object |
| 12 | + |
| 13 | +#### Scenario: Invalid Campaign object |
| 14 | +- **WHEN** a campaign YAML file is malformed or omits a required value |
| 15 | +- **THEN** campaign validation fails with the artifact path and actionable field errors |
| 16 | + |
| 17 | +#### Scenario: Noncanonical or undeclared Campaign data |
| 18 | +- **WHEN** a campaign contains uppercase unknown markers, whitespace-padded normalized text, an invented title, missing baseline safety boundaries, an impossible calendar timestamp, or an undeclared root, nested, or lane field |
| 19 | +- **THEN** campaign validation fails before list, show, or seed returns the object |
| 20 | + |
| 21 | +### Requirement: Campaign initialization is deterministic and conservative |
| 22 | +The system SHALL require a non-empty target and at least one vulnerability class; normalize text, ids, and vulnerability-class slugs; deduplicate classes while preserving first-seen order; and create one generic lane and deterministic finding id per class. Omitted optional values SHALL default to `mode: passive`, `goal.output: research-notes`, `budget.depth: standard`, `status: active`, `profile: generic`, and `unknown` for target version, source, ecosystem, and local reproduction. |
| 23 | + |
| 24 | +#### Scenario: Initialize with required flags |
| 25 | +- **WHEN** the user initializes a campaign with a target and a comma-separated vulnerability-class list |
| 26 | +- **THEN** the CLI writes one YAML source of truth and one Markdown runbook with deterministic lanes and finding ids |
| 27 | + |
| 28 | +#### Scenario: Normalize duplicate classes |
| 29 | +- **WHEN** vulnerability classes differ only by whitespace, case, or slug punctuation |
| 30 | +- **THEN** initialization preserves first-seen order and creates exactly one lane for each normalized class |
| 31 | + |
| 32 | +#### Scenario: Safe public defaults |
| 33 | +- **WHEN** the user initializes target `Acme` with vulnerability classes `XSS, xss` and omits every optional value |
| 34 | +- **THEN** the Campaign contains one `xss` lane and every documented safe default |
| 35 | + |
| 36 | +#### Scenario: Unknown version is omitted from derived id |
| 37 | +- **WHEN** target `Acme` has an omitted or explicit `unknown` version and no explicit id |
| 38 | +- **THEN** the derived campaign id is `acme` rather than `acme-unknown` |
| 39 | + |
| 40 | +#### Scenario: Unsafe explicit id |
| 41 | +- **WHEN** an explicit campaign id contains unsafe filename characters |
| 42 | +- **THEN** initialization fails before either Campaign artifact is written |
| 43 | + |
| 44 | +#### Scenario: Existing campaign is protected |
| 45 | +- **WHEN** either campaign artifact already exists and initialization does not include `--force` |
| 46 | +- **THEN** the CLI fails without overwriting either artifact |
| 47 | + |
| 48 | +#### Scenario: Concurrent initialization is serialized |
| 49 | +- **WHEN** two no-force initializers race to create the same Campaign id |
| 50 | +- **THEN** exactly one commits the YAML/runbook pair and the other fails without overwriting it |
| 51 | + |
| 52 | +#### Scenario: Dangling or external symlink is not followed |
| 53 | +- **WHEN** a Campaign destination is a symlink, including a dangling symlink or a symlink to an external file |
| 54 | +- **THEN** no-force initialization treats it as a collision and force replacement replaces only the directory entry without modifying the symlink target |
| 55 | + |
| 56 | +#### Scenario: Explicit overwrite |
| 57 | +- **WHEN** the user repeats initialization with `--force` |
| 58 | +- **THEN** the CLI replaces both campaign artifacts from the newly normalized Campaign object |
| 59 | + |
| 60 | +#### Scenario: Pair replacement rolls back on failure |
| 61 | +- **WHEN** staging or committing either Campaign artifact fails |
| 62 | +- **THEN** initialization leaves no partial new pair and restores any force-replaced artifact entries |
| 63 | + |
| 64 | +#### Scenario: Activity failure is non-fatal |
| 65 | +- **WHEN** the YAML/runbook pair commits but workspace activity cannot be appended |
| 66 | +- **THEN** initialization succeeds with a warning that names the activity error |
| 67 | + |
| 68 | +### Requirement: Campaign initialization supports safe interactive and non-interactive modes |
| 69 | +The system SHALL obtain missing required initialization values through an injectable prompt adapter only in an interactive terminal, and SHALL never prompt when `--no-interactive` or `--json` is present. |
| 70 | + |
| 71 | +#### Scenario: Interactive required values |
| 72 | +- **WHEN** an interactive user initializes without a target or vulnerability classes |
| 73 | +- **THEN** the prompt adapter supplies the missing required values before normalization and persistence |
| 74 | + |
| 75 | +#### Scenario: JSON is non-interactive |
| 76 | +- **WHEN** initialization includes `--json` but omits a required value |
| 77 | +- **THEN** the CLI returns an error without invoking the prompt adapter or writing an artifact |
| 78 | + |
| 79 | +#### Scenario: Successful JSON initialization |
| 80 | +- **WHEN** initialization supplies a target and vulnerability classes with `--json` but without `--no-interactive` |
| 81 | +- **THEN** the CLI never invokes the prompt adapter, writes the YAML/runbook pair, and emits exactly one JSON document |
| 82 | + |
| 83 | +#### Scenario: Non-TTY input is non-interactive |
| 84 | +- **WHEN** required values are missing and the command streams are not interactive terminals |
| 85 | +- **THEN** the CLI fails with the missing fields instead of waiting for input |
| 86 | + |
| 87 | +### Requirement: Campaigns can be listed and shown without a workspace index |
| 88 | +The system SHALL scan `.omv/campaigns/*.yaml` directly for list and show operations, sort campaign summaries by id, and SHALL NOT add Campaign records to `WorkspaceIndex`. |
| 89 | + |
| 90 | +#### Scenario: List campaigns |
| 91 | +- **WHEN** multiple valid campaign YAML files exist |
| 92 | +- **THEN** `omv campaign list` returns sorted summaries containing ids, targets, statuses, lane counts, and next actions |
| 93 | + |
| 94 | +#### Scenario: Duplicate Campaign source pair |
| 95 | +- **WHEN** both `<id>.yaml` and `<id>.yml` exist for the same Campaign id |
| 96 | +- **THEN** list and show fail with an actionable duplicate-source error instead of returning inconsistent identities |
| 97 | + |
| 98 | +#### Scenario: List missing campaign directory |
| 99 | +- **WHEN** `.omv/campaigns/` does not exist |
| 100 | +- **THEN** campaign listing returns an empty result without requiring an index rebuild |
| 101 | + |
| 102 | +#### Scenario: Show campaign as JSON |
| 103 | +- **WHEN** the user runs `omv campaign show <id> --json` |
| 104 | +- **THEN** stdout is one JSON document containing the parsed Campaign object and its artifact paths |
| 105 | + |
| 106 | +#### Scenario: Unknown ecosystem has an actionable prerequisite |
| 107 | +- **WHEN** init, list, show, or runbook rendering handles a Campaign whose target ecosystem is `unknown` |
| 108 | +- **THEN** its next action tells the user to set a supported ecosystem before running seed |
| 109 | + |
| 110 | +### Requirement: Campaign command aliases preserve canonical behavior |
| 111 | +The system SHALL make `omv first [flags]` an alias of `omv campaign init`, and SHALL map `omv first init|list|show|seed` to the corresponding canonical campaign subcommands while `omv campaign` defaults to `list`. |
| 112 | + |
| 113 | +#### Scenario: First without subcommand |
| 114 | +- **WHEN** the user runs `omv first --target acme --vuln xss --no-interactive` |
| 115 | +- **THEN** the same Campaign artifacts and result are produced as by the canonical `campaign init` command |
| 116 | + |
| 117 | +#### Scenario: Canonical command without subcommand |
| 118 | +- **WHEN** the user runs `omv campaign` |
| 119 | +- **THEN** the CLI performs the campaign list operation |
| 120 | + |
| 121 | +### Requirement: Campaign seeding creates hypotheses only |
| 122 | +The system SHALL validate the complete Campaign before writes, require a known Evidence-compatible ecosystem, create at most one valid candidate `Evidence.v1` file per lane, never overwrite an existing `.yaml` or `.yml` finding, and MUST NOT create ThreatMap, reproduction, verification, audit, proof-of-concept, or report artifacts. Seed SHALL have no force mode. |
| 123 | + |
| 124 | +#### Scenario: Seed campaign lanes |
| 125 | +- **WHEN** a valid campaign with a known Evidence-compatible ecosystem has unseeded lanes |
| 126 | +- **THEN** `omv campaign seed <id>` creates candidate findings containing only target identity, ecosystem, vulnerability class, and explicit unknown evidence fields |
| 127 | + |
| 128 | +#### Scenario: Existing YAML or YML finding is skipped |
| 129 | +- **WHEN** a lane's `.yaml` or `.yml` finding path already exists |
| 130 | +- **THEN** seeding preserves the existing file byte-for-byte and reports the lane as skipped |
| 131 | + |
| 132 | +#### Scenario: Unknown ecosystem blocks seeding |
| 133 | +- **WHEN** a campaign target ecosystem is `unknown` |
| 134 | +- **THEN** seeding fails before creating any finding and asks for an explicit supported ecosystem |
| 135 | + |
| 136 | +#### Scenario: Seed output has no Campaign coupling or proof artifacts |
| 137 | +- **WHEN** seeding completes |
| 138 | +- **THEN** created Evidence files contain the deterministically mapped `researcher_goal` and no `campaign_id`, tested version remains unknown, proof fields remain unknown, and no other lane artifact path exists |
| 139 | + |
| 140 | +#### Scenario: Partial seed failure is structured and retryable |
| 141 | +- **WHEN** one lane encounters an I/O error after other lanes were created or skipped |
| 142 | +- **THEN** the result reports created, skipped, and failed ids with messages, and rerunning remains idempotent |
| 143 | + |
| 144 | +### Requirement: Campaign profiles remain data-driven |
| 145 | +Every generated `Campaign.v1` SHALL use `profile: generic` and derive lanes solely from normalized user input. The CLI MUST NOT branch on target names. |
| 146 | + |
| 147 | +#### Scenario: Named target receives no built-in content |
| 148 | +- **WHEN** a user initializes a Zimbra campaign with only the `xss` vulnerability class |
| 149 | +- **THEN** the Campaign contains only the generic `xss` lane and no built-in Zimbra attack-surface claims |
| 150 | + |
0 commit comments