Skip to content
Back to skills

Spec Enforce Ac

ASecurity

SDD spec AC quality and splitting enforcement — the canonical cross-agent contract. Runs AC granularity triggers 1-10, run-on safety net, per-AC verbosity cap, Constraints conciseness, actor coherence, sub-bullets ban, splitting by actor/sub-feature/concern, accretion guard, chain enforcement, mechanism leakage. Invoked conditionally by spec-enforce when diff touches any AC bullet OR any Constraints bullet OR scope=all.

  • 25 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 18, 2026
developmentjavascriptgojavagitapisecuritydocumentation

Works with

  • api

Security analysis

A100/100

Scanned October 18, 2026

npx -y skills add nikolanovoselec/codeflare --skill spec-enforce-ac --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Spec Enforce Ac?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Spec Enforce Ac
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nikolanovoselec-spec-enforce-ac/badge)](https://www.skillsdirectory.com/skills/nikolanovoselec-spec-enforce-ac)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: spec-enforce-ac
description: SDD spec AC quality and splitting enforcement — the canonical cross-agent contract. Runs AC granularity triggers 1-10, run-on safety net, per-AC verbosity cap, Constraints conciseness, actor coherence, sub-bullets ban, splitting by actor/sub-feature/concern, accretion guard, chain enforcement, mechanism leakage. Invoked conditionally by spec-enforce when diff touches any AC bullet OR any Constraints bullet OR scope=all.
version: 4.0.0
---

# Spec Enforcement — AC quality and splitting

This skill enforces the rules that operate on Acceptance Criteria bullets and REQ-splitting decisions. Invoked by `spec-enforce` (the spine) when the diff touches any AC bullet or when scope=all.

## Inputs

- `purpose`: `review` | `clean` (inherited from parent — `review` reports only; `clean` applies mechanical edits allowed by mode)
- `diff`: git diff against base
- `scope`: `all` | `diff`
- `mode`: `interactive` | `auto` | `unleashed`
- `layout`: `nested` | `flat` (auto-detected by parent `spec-enforce`)

**Scope contract.** Consume the parent's already-resolved in-scope REQ set; do not reconstruct the diff. Under `scope=diff`, inspect only changed AC/Constraint hunks plus the complete parent REQ needed to judge coherence, cap, and numbering; follow references only when a proposed split/renumber would invalidate them. Under `scope=all`, inspect every AC and Constraint.

**Layout-awareness.** Cross-cutting REQ extraction and new-domain scaffolding paths resolve per the detected layout:
- Nested: new domain files land in `sdd/spec/{domain}.md`
- Flat: new domain files land in `sdd/{domain}.md` (legacy)

The AC granularity rules, splitting mechanics, and accretion guard are layout-invariant; only the target file paths change.

## Output

Returns findings array + auto-fix actions (per mode) — each finding names the REQ, AC, firing trigger, severity, cross-reference blast radius, and proposed smallest action. Inspect each parent REQ once and stop after every changed AC has a disposition. Writes evidence-count rows back to the spine's manifest:
- `Acceptance criteria + AC granularity + REQ accretion guard`: `ran (N REQs, K diff hunks, M findings)`
- `Per-AC verbosity + Constraints conciseness`: `ran (N REQs, M findings)`
- `Actor coherence`: `ran (N REQs, M findings)`
- `Sub-bullets in ACs banned`: `ran (N REQs, M findings)`
- `Cross-cutting concerns get own REQ family`: `ran (N REQs, M findings)`
- `Concern-boundary split`: `ran (N REQs, M findings)`
- `Mechanism leakage in AC bullets`: `ran (N REQs, M findings)`

## Acceptance criteria

### Cap and basic shape

- Each AC is **binary pass/fail**, testable in principle.
- **AC count cap is binding.** 3-5 ACs typical. 6-7 normal for feature-rich surfaces. Beyond 7, the rule fires regardless of axis.

  | AC count | Single actor + single concern | + actor mixing OR cross-cutting concern |
  |---|---|---|
  | <=7 | OK | Mixing rule fires regardless of count |
  | 8-10 | MEDIUM `ac-count-over-cap`. Auto-fix: attempt sibling merge, else **Splitting by sub-feature**. | HIGH - split by actor or concern. |
  | >10 | HIGH `ac-count-binding-cap-exceeded`. Auto-fix mandatory: **Splitting by sub-feature** runs even when no actor/concern axis exists. | HIGH - split immediately. |

  **AC-count is NOT the per-REQ line-length soft warning. Do not conflate them.** The "REQ length guidance" table in `spec-enforce` (`<=25` OK / `26-50` LOW / ...) measures *body line count* and is a never-auto-fixed soft warning. The AC-count cap above measures *number of ACs* and is MEDIUM/HIGH with mandatory auto-fix. A REQ with 8 ACs is `ac-count-over-cap` MEDIUM (act on it) even when its line count is in the LOW band. Reporting an 8-10-AC REQ as "LOW, soft-limit, deferred" is the conflation bug: it is itself a HIGH `ac-count-misclassified-as-deferrable`.

  **No silent downgrade.** Once `ac-count-over-cap`/`ac-count-binding-cap-exceeded` fires, the manifest MUST record, per offending REQ, the concrete action taken: `merged ACx+ACy`, `split -> REQ-..., REQ-...`, or `escalated -> .review-queue.md (reason)`. "deferred", "LOW", "soft limit", or "looked intentional" are NOT valid dispositions for a fired AC-count finding. Downgrading any fired MEDIUM/HIGH finding to LOW/deferred without one of those three dispositions is HIGH `finding-downgraded-to-skip`.

  **Cross-ref-density pre-check (binding before any merge/renumber/split).** ACs are frequently referenced by number elsewhere. BEFORE renumbering or merging ACs, grep the whole repo for by-number references to this REQ's ACs: `grep -rn "REQ-X-NNN AC[0-9]" sdd/ documentation/` PLUS the REQ's own `@test` anchor descriptions (`-> ACn`, `ACn/ACm`) and any `decisions/README.md` `ACn` mentions. Every hit is a cross-ref that a renumber breaks. A merge/split that does not rewrite ALL of them in the same commit corrupts the spec<->test<->doc graph and is forbidden. If the blast radius is large, prefer **Splitting by sub-feature** (which moves whole ACs to a new REQ ID, changing fewer numbers) over an in-place merge that renumbers the tail; record the full hit list in the commit body's AC-mapping table. A blind in-place renumber that leaves any by-number ref dangling is HIGH `ac-renumber-orphaned-crossref`.

- **No sub-bullets in ACs.** A sub-bulleted AC (`a./b./c.` or indented `-`) is conjunction-stuffing.
- Avoid "should" - use "must" or describe the observable outcome.
- Avoid vague terms ("responsive", "fast") - specify the criterion.

### Granularity — one behaviour per AC (binding)

**An AC MUST encode exactly one observable behaviour. If an AC can be split into >=2 distinct test names, it MUST be split.** Hard triggers below; any one fires the rule. Agent MUST attempt a split and only abandon when the AC encodes one behaviour with sentences 2..N as pure rationale.

1. **Sentence count >= 3** in one AC. Sentence = run ending `.`/`!`/`?` + whitespace + capital. Escape: sentences 2..N are rationale (`so`/`so that`/`because`) AND share the subject AND introduce no new contractual verb.
2. **Word count > 130** in a single-sentence AC. Split at the load-bearing conjunction.
3. **Word count > 80 AND sentence count >= 2**.
4. **Subject shift across sentences**: sentence 1 subject != sentence 2 subject. Two subjects = two enforcement sites = two ACs.
5. **Multi-site enforcement phrase**: literal tokens `applies the same`, `also applies`, `is enforced both`, `the same X defensively`. Each names >=2 sites.
6. **Tie-break or appendage after `;`**: `; among ...`, `; within ...`, `; tie-break ...`, `; with ...` attaching a new rule to a primary rule.
7. **Multi-path contract**: `regardless of (path|order|whether)` + substantive contract clause: one positive-path AC and one negative-path AC.
8. **Transform + downstream effect** in one bullet: transform (canonicalisation, normalisation, parsing, validation) AND effect (idempotency, dedup, cache hit, retry).
9. **>=2 distinct verb phrases joined by `and` / `then` / `before`** with own subjects or distinct objects (outside comma-enumerations).
10. **Reviewer can write >=2 test-name candidates** exercising distinct code paths. Borderline tiebreaker.

Binding examples:
- VALID (escape): "Articles older than 48 hours are dropped before LLM summarisation so stale items do not consume LLM budget." Sentence 2 is rationale, same subject. No split.
- INVALID (trigger 1): "Articles older than 48 hours are dropped before LLM summarisation. Candidates with no parsable date fall back to ingestion time and are kept." Sentence 2 introduces a new positive-path contract. Split.
- INVALID (trigger 4): "On settings save, any submitted tag triggers an INSERT. The discovery cron applies the same short-circuit defensively." Subject shifts; two sites. Split.

Severity: MEDIUM `ac-multi-behaviour`. Auto-fix in `auto`/`unleashed`: split at the boundary named by the firing trigger; preserve every clause; never silently drop. If post-split count exceeds 7, chain-enforcement binds Splitting by sub-feature in the same pass.

### Run-on AC bullets (length safety net)

Residual catch-net for single-behaviour ACs that slipped past triggers 1-10: >150 words OR >=3 semicolons outside comma-separated enumerations. MEDIUM `ac-run-on`. Auto-fix: split at conjunctions, preserving every clause. Granularity fires first.

### Per-AC verbosity cap (rationale belongs in Intent / changes.md / ADRs)

The granularity triggers permit a rationale tail (`so`/`because`) on a single-behaviour AC, and the run-on net only catches >150 words. Between them sits the common bloat the line-count gauge also misses: a 40-90 word AC that states one behaviour but pads it with embedded justification ("rather than X, which would...", "the SDK reads false because...", "so a Y can never be mistaken for Z"). That rationale is not contractual - it duplicates the Intent and the `sdd/changes.md` / ADR narrative, and an AC is a pass/fail assertion, not a place to explain WHY.

Detection (per AC, counting prose words only - exclude trailing `<!-- @impl/@test -->` anchors and markdown link URLs): **prose word count > 45** = MEDIUM `ac-verbose`. The rationale-tail escape from the granularity rule does NOT exempt this: an escape-qualified rationale sentence that pushes the AC past 45 words still fires `ac-verbose`.

Auto-fix in `auto`/`unleashed`: trim the AC to the bare testable assertion. The stripped rationale, if it is not already in the REQ's Intent, moves to the `sdd/changes.md` entry for the change (one sentence) or, for a design tradeoff, to the relevant ADR - never back into the AC. If the over-length came from two behaviours rather than rationale, defer to the granularity split instead. Reporting an `ac-verbose` AC as "fine / single-behaviour / acceptable" without trimming is `finding-downgraded-to-skip` (HIGH), per the no-silent-downgrade rule.

### Constraints conciseness (one terse boundary per bullet)

`**Constraints:**` bullets are boundaries (invariants, limits, and accepted-residual one-liners), NOT a place for why-narratives. The same rationale that is banned from ACs is banned here: "for the same reason X follows Y", "a DO eviction mid-shutdown would discard...", multi-sentence explanations of mechanism or history. The full rationale lives in `sdd/changes.md` and `documentation/decisions/README.md`; a Constraint may carry at most a one-clause pointer (`... (rationale in [ADxx](...))`).

Detection (per Constraints bullet, prose words excluding cross-ref link URLs):
- **bullet > 45 words** OR **>= 2 sentences** OR contains a rationale-narrative connective chaining a justification (`because`, `so that`, `for the same reason`, `rather than ... which would`, `the same ... follows`, `and for the same reason`) = MEDIUM `constraint-bloat` on that bullet.
- **any single bullet > 80 words**, OR the whole `**Constraints:**` section prose > 150 words = HIGH `constraint-section-bloat` (the paragraph-bible failure mode).

Auto-fix in `auto`/`unleashed`: reduce each bullet to its boundary statement; move the stripped rationale to the `sdd/changes.md` entry or the relevant ADR (with a one-clause pointer left in the bullet only if a reader needs the reference). Never delete a genuine boundary, only the narration around it. A Constraint that is purely narration with no boundary (pure "we tried X then Y" history) is deleted, not trimmed (that belongs in git history per `spec-driven-development` "What is NOT a requirement"). Reporting a fired `constraint-bloat`/`constraint-section-bloat` as LOW/intentional/acceptable is `finding-downgraded-to-skip` (HIGH).

Writes evidence row to the spine manifest: `Per-AC verbosity + Constraints conciseness: ran (N REQs, M findings)`.

### Actor coherence (one actor per REQ)

Every REQ declares a single actor in `Applies To:`. **Every AC must describe behaviour of that same actor.** When an AC describes a different actor, the REQ is incoherent at the actor level.

Detection: parse the first 8 words of each AC for an actor keyword (`user`, `admin`, `operator`, `visitor`, `guest`, role names from `sdd/README.md`). Subject differs from `Applies To:`: finding.

Severity: HIGH when 2+ ACs target a different actor. MEDIUM when 1 AC targets a different actor.

Auto-fix: split the REQ along the actor axis. The AC subjects declare the boundary mechanically; SAFE refactor, not JUDGMENT.

### Sub-bullets banned

Any indented list item (`   a.`, `   - `, `   1.`) under a numbered AC is conjunction-stuffing that bypasses the run-on rule. Each sub-bullet is an independent behaviour.

Severity: MEDIUM. Auto-fix: promote each sub-bullet to its own AC at the parent level. If the resulting count exceeds the <=7 cap, splitting rules apply.

## Splitting

### Chain enforcement (binding)

When Granularity, Concern-boundary, or Sub-bullets fires, the agent MUST complete the chain in one auto-fix pass: granulate, check resulting AC count, if >7 run `Splitting by sub-feature`, emit sibling REQs each <=7 ACs. Committing a granulated-but-unsplit REQ with >7 ACs is itself a HIGH finding `chain-not-completed`.

Worked outcomes:
- REQ with 8 packed ACs: Granularity yields ~22 single-behaviour ACs, cap binds, Splitting by sub-feature, 3-4 sibling REQs each with 3-5 ACs.
- REQ with 7 multi-concern ACs: Concern-boundary detects 3 operationally distinct clusters, 3 sibling REQs with 2-3 ACs each.
- Deprecated REQ deletion: clauses not already covered by successor are folded into successor in the same commit; `sdd/changes.md` records the deletion.

### Cross-cutting concerns get their own REQ family

Cross-cutting concerns (rate limiting, CSRF, audit logging, security headers, auth gating, caching, retry/backoff) apply across many features. When a feature REQ's ACs encode such a policy, it infects every feature REQ on that surface.

Detection: AC bullets with policy-shape language (`Every <route family> is rate-limited`, `Every response carries`, `All <verb> endpoints reject`) in a feature REQ.

Severity: MEDIUM when 1-2 policy-shape ACs appear. HIGH when policy ACs dominate (>=3 of <=7 ACs).

Auto-fix: extract the policy ACs to a new policy REQ in the appropriate cross-cutting domain. Feature REQ keeps one AC referencing the policy REQ by ID.

**Deterministic target-domain rule (unleashed):**

| Concern keyword | Target domain file | Domain ID prefix |
|---|---|---|
| rate limit / throttle / 429 | `sdd/rate-limits.md` | `REQ-RATE-` |
| CSP / security header / CSRF policy | `sdd/security-policy.md` | `REQ-SEC-` |
| audit log / observability / metrics / tracing | existing `sdd/observability.md` | `REQ-OPS-` |
| cache / Cache-Control / CDN | `sdd/cache-policy.md` | `REQ-CACHE-` |
| retry / backoff / circuit breaker | `sdd/resilience.md` | `REQ-RES-` |

New domain file scaffolded with standard header. First extracted REQ gets `REQ-{PREFIX}-001`. Default catch-all when no keyword matches: `sdd/policies.md` with prefix `REQ-POL-`.

### Concern-boundary split (sub-feature trigger below the numeric cap)

A single-actor REQ whose ACs span >=2 distinct sub-features MUST split, **regardless of AC count**. Adds a concern-boundary trigger to the existing cap-based and actor/cross-cutting axes; fires when the REQ is structurally two REQs even at <=7 ACs.

Detection (both must hold): (1) lexical clustering of AC first-clause subjects yields >=2 clusters, each with >=2 ACs; (2) clusters describe operationally distinct sub-jobs (different verb families).

Severity: MEDIUM `concern-boundary-split-required`. Auto-fix in `auto`/`unleashed`: apply **Splitting by sub-feature** mechanics even without numeric cap trip.

A 9-AC REQ where every AC is one job from different angles is NOT a split target; clusters must be operationally distinct.

### Accretion guard (diff-level check)

The structural rules catch a REQ that is **already** bloated. The accretion guard catches the diff that is **about to** bloat it.

Detection runs against the diff:

1. **AC addition introducing a new actor**: HIGH regardless of count. Propose: move to the actor-appropriate REQ.
2. **AC addition introducing a cross-cutting concern**: HIGH regardless of count. Propose: extract to a cross-cutting REQ.
3. **AC addition pushing count past 7** (single-actor + single-concern): MEDIUM `ac-count-over-cap`. Propose: sibling merge, else **Splitting by sub-feature**.
4. **AC addition pushing count past 10**: HIGH `ac-count-binding-cap-exceeded`. Split is mandatory.
5. **AC extension grows sub-bullets**: MEDIUM. Propose: promote each sub-bullet.
6. **AC extension grows past 150 words** OR **multi-behaviour added in diff**: MEDIUM. Propose: split per AC-granularity rule.

The accretion guard is fail-loud: it names the diff hunk, the violated rule, the proposed split target, and (when mechanical) the auto-fix preview.

### Splitting by actor or concern (SAFE refactor)

Splitting by **prose semantics** stays under JUDGMENT. Splitting by **actor** or by **cross-cutting concern** is mechanical: the AC subject declares the boundary; resulting REQs carry the same ACs verbatim under a new header.

SAFE when ALL hold:
1. Split axis is actor OR cross-cutting concern.
2. No AC needs rewriting; each moves verbatim or with whitespace-only edits.
3. Each resulting REQ has a coherent single actor and a coherent single job.
4. Tests and doc cross-refs update by ID renaming alone.

Mechanics: original REQ ID stays with the largest coherent piece; new REQs get next free IDs (cross-cutting in the cross-cutting domain). Update **every** cross-ref in the same commit. The complete cross-ref set (not just REQ bodies + `documentation/` backlinks + `sdd/changes.md`) is: (1) other REQs citing old AC numbers, (2) `documentation/lanes/**` + `documentation/decisions/README.md` by-number AC mentions (`REQ-X-NNN ACn`), (3) this REQ's own `<!-- @test: ... -> ACn -->` and `<!-- @impl: ... -->` anchor descriptions, (4) the moved ACs' inline `@impl` comments travel WITH the AC to its new REQ, (5) `sdd/changes.md` entries that name the moved ACs by number. Commit body MUST include an AC-mapping table (old REQ ACn -> new REQ ACm). After the rewrite, re-grep `REQ-X-NNN AC[0-9]` and the new REQ ID across `sdd/` + `documentation/`; any dangling old-number ref is HIGH `ac-renumber-orphaned-crossref` and blocks the commit. Commit: `[spec-reviewer] split: REQ-X-NNN by actor/concern -> ...`. Tests NOT renamed in the same commit; substring matching keeps coverage green.

### Splitting by sub-feature (binding-cap safety net)

When AC count cap binds (>10, or 8-10 with no axis) AND no actor or cross-cutting axis exists, **Splitting by sub-feature** is the deterministic fallback. Also fires from the **Concern-boundary split** rule above when clusters are operationally distinct even at <=7 ACs.

Cluster identification: tokenise each AC's first 12 words (content-words only); cluster greedily; two ACs share cluster when they share >=2 tokens (Jaccard >=0.25). Dominant cluster keeps the original REQ ID; remaining clusters become new REQs; singletons join the dominant. If all ACs land in one cluster: **median split**; ACs 1..N/2 stay; N/2+1..N become a sibling REQ with `-extended` suffix, Intent copied verbatim.

Mechanics: original REQ ID stays with dominant cluster; new REQs get next free IDs in same domain. Each new REQ inherits parent's `Applies To:`, `Constraints:`, `Priority:`, `Verification:` verbatim; Intent rewritten per sub-feature; parent-child via `Dependencies:` (NOT Notes). Cross-refs update in same commit; commit body MUST include AC-mapping table. Commit: `[spec-reviewer] split: REQ-X-NNN by sub-feature -> ...`. Tests NOT renamed.

SAFE when ALL hold:
1. No AC rewritten; every AC moves verbatim.
2. Each resulting REQ has <=7 ACs.
3. Clustering boundary was identifiable OR median fallback applied.

If 1 or 2 fails: emit HIGH `sub-feature-split-cannot-mechanize`; don't edit.

## Mechanism leakage in AC bullets

An AC describes WHAT the user observes, not HOW. Move to `documentation/`: cookie attributes (`HttpOnly`, `SameSite=Lax`), vendor-prefix headers (`Cf-Access-Jwt-Assertion`), internal middleware names, HTTP method+path enumerations in non-API REQs, internal query params, cache directive strings, crypto algorithm names.

A user observes "JavaScript on the page cannot read the session token", not `HttpOnly`.

Severity: MEDIUM. Auto-fix: rewrite AC to user-observable consequence; move mechanism to the relevant `documentation/` lane file with a backlink.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.