> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# AC Verification & Testing Quality

> How an acceptance criterion or user story becomes verified, and the testing-quality rules that keep "verified" honest.

**Status:** active · **Added:** 2026-06-02 · **Origin:** HR-44/WENO spec-workflow dogfood

This doc defines how an acceptance criterion (AC) or user story becomes
**`verified`**, and the testing-quality rules that keep "verified" honest. It
exists because a dogfood found the spec pipeline could mark a feature shipped
while it was unproven — or broken.

***

## 1. The problem this fixes

The spec pipeline (create → research → plan → tasks → compliance) is
**authoring-only**: it writes ACs as `status: planned, draft: true` and nothing
ever promotes them. Corpus-wide that left **0 of 4,559 ACs verified**. Worse,
every Proliant/WENO integration suite **skipped by default**
(`describe.skipIf(!hasLocalSupabase)`), so a never-executed implementation read
as green. "Done" was defined as *artifacts exist*, not *capability proven*.

Standing up the live stack and running the suites for real (2026-06-02) found
**three genuine defects** behind the green-by-skip — see
[`specs/hr/reviews/HR-44-DOGFOOD-FINDINGS-2026-06-02.md`](https://github.com/Encore-OS/encoreos/blob/development/specs/hr/reviews/HR-44-DOGFOOD-FINDINGS-2026-06-02.md).

***

## 2. The `@verifies` backlink + the gate

A test declares the AC/US it proves with a JSDoc tag in its source:

```ts theme={null}
/**
 * @verifies HR-44 AC-10 AC-11
 * @verifies CL-06-EN-23 AC-1
 */
```

`scripts/specs/verify-acs.mjs` (npm `spec:verify-acs`) finds those backlinks,
runs exactly the backing tests, and promotes an item to `verified` **only** when
they actually pass:

> **Promotion rule (strict):** ≥1 backing file AND, across them,
> `passed > 0 AND failed == 0 AND skipped == 0`. **A skip is not a pass.**

* ACs (frontmatter): `status: planned, draft: true` → `status: verified, draft: false` + `verified_by: [tests]`, `verified_at`.
* User stories (`- **US-N** [planned] …` body items): `[planned]` → `[verified]`.

Commands:

```bash theme={null}
npm run spec:verify-acs -- --spec HR-44            # dry-run report
npm run spec:verify-acs -- --spec HR-44 --write    # apply promotions
npm run spec:verify-acs -- --spec HR-44 --enforce  # CI: fail on unbacked `verified` claim
npm run spec:verify-acs:scan                        # corpus coverage scorecard
```

The terminal AC state `verified` was added to `ListItemStatus`
(`scripts/specs/schema/types.ts`); it is distinct from `done` (**implemented**) —
code can ship `done` while no test proves the AC.

### Verifying integration-only ACs

Some ACs are only provable by an integration test that needs a served stack. The
gate counts **what actually passes in the current environment** — run it with the
live stack up (see §4). Such ACs verify for real; an AC whose only proof is a
skip stays `planned`. Don't backlink an AC to a test that can't pass unattended
unless you accept it will only verify on a stack-equipped runner (incl. CI with
the stack).

***

## 3. Skip-reason taxonomy — stop green-by-skip

Every intentional skip must declare **why**, via `tests/utils/skip-reasons.ts`:

| Category                             | Meaning                                                            | Counts as coverage?                                 |
| ------------------------------------ | ------------------------------------------------------------------ | --------------------------------------------------- |
| `CAPABILITY_ABSENT(specRef, reason)` | Feature missing or **broken** vs the real system. RED-in-disguise. | **Never.** The AC stays `planned`. Must be tracked. |
| `ENV_ABSENT(reason)`                 | Capability exists; this runner lacks stack/creds.                  | Legitimate; passes where the substrate is present.  |
| *(untagged skip)*                    | Nobody recorded why.                                               | Dangerous — flagged `UNCLASSIFIED`.                 |

Example — the F3 employee-sync bug is parked honestly, not hidden:

```ts theme={null}
it.skip(
  CAPABILITY_ABSENT('HR-44 AC-2',
    'employee pull auto-create broken vs hr_employees/pf_profiles schema — ' +
    'see HR-44-DOGFOOD-FINDINGS-2026-06-02.md'),
  async () => { /* exact repro retained; un-skips to RED when fixed */ },
);
```

`scripts/testing/scan-skip-reasons.mjs` (npm `test:skip-scan`) reports skips by
category and, with `--enforce`, fails when any `CAPABILITY_ABSENT` or
`UNCLASSIFIED` skip exists — so capability gaps are tracked and every skip must
say why.

***

## 4. Running the live verification substrate

Integration tests need the local stack **and** served edge functions. On the DGX
box the stack is the `encoreos` Docker project (REST `54321`, DB `54322`).
`.env.local` already carries `SUPABASE_SERVICE_ROLE_KEY` + `SUPABASE_URL`, so the
harness goes live automatically. For Proliant, the edge functions must be served
with the fixture-transport flag:

```bash theme={null}
# serve from the ROOT checkout (dir basename must match the stack's Docker
# project name 'encoreos'); hot-reloads on function edits
PROLIANT_FIXTURE_TRANSPORT=1 \
  npx supabase functions serve --workdir /home/jbloom/projects/encoreos --no-verify-jwt
```

Then `npm run test:integration -- tests/integration/hr/proliant/` runs for real.

***

## 5. Quality rules (carry forward)

1. **A unit test that asserts a *mapper's output* is not proof the row inserts.**
   F3 shipped because `employee-mapper.test.ts` asserted a flat `{first_name,…}`
   shape the `hr_employees` table rejects. Assert against the **real schema** (an
   integration insert), or label the unit test as logic-only.
2. **An integration suite that skips by default is not coverage.** It must either
   run in CI (served stack) or its ACs stay `planned`. Never let `skipIf` masquerade as green.
3. **System/cron paths must be tested under the service-role identity.** F2
   (`triggered_by: 'service-role'` → uuid 500) only appears when the function is
   invoked as the service role — exactly how cron and integration tests call it.
4. **Teardown is test code too.** F1 (cleanup referencing a non-existent column)
   was invisible because the suite never reached teardown. Run teardown live.
5. **`verified` requires a passing test, full stop.** `--enforce` in CI makes a
   regression (a `verified` AC whose test breaks or starts skipping) fail the build.
