A snapshot: read at commit 707a04c7ce8 on 2026-10-18T00:06:01Z by majordomus 0.29.0. Nothing on this page is read again. See it live
test/cases/94_use_cases.sh
An object of kind test
What it is
- URI
majordomus://test/test/cases/94_use_cases.sh- Kind
test- Identity
test/cases/94_use_cases.sh- Path
test/cases/94_use_cases.sh- Provenance
- declared
- Media type
text/plain- As a capability
- test.test/cases/94_use_cases.sh
Front matter
{}Content
# majordomus-covers: usecase
# majordomus-negative: usecase
# claims: use-case-coverage, use-case-evidence, use-case-impact
# The executable use-case system, in a repository `init` wrote: the sections exist with the
# taxonomy; a use case is one file, discovered, validated against everything it names, run
# against the real tool with the evidence recorded and normalised; coverage is computed
# from the registry and gated by the policy; a draft scaffolded for a gap runs but never
# counts; impact traces a changed file to the use cases and cases it affects; and every
# broken reference, failed step and stale expectation is refused with the entity named.
. "$ROOT/test/lib.sh"
"$MJ" init >/dev/null; "$MJ" update >/dev/null
git add -A >/dev/null && git commit -qm install
UC=.ai/repo/use-cases
# --- the sections are seeded and empty; coverage is advisory in a fresh policy
[ -f "$UC/taxonomy.yaml" ] || { echo " init seeded no taxonomy"; exit 1; }
[ -f .ai/repo/applications/README.md ] || { echo " init seeded no applications section"; exit 1; }
expect_exit 0 "$MJ" usecase list
expect_grep '^use cases: 0 in \.ai/repo/use-cases$'
expect_exit 0 "$MJ" usecase coverage
expect_grep '^command +doctor +0 +0 +0 +gap +advisory$'
expect_grep 'required gap\(s\)'
expect_exit 0 "$MJ" usecase coverage --check
expect_exit 2 "$MJ" usecase bogus
expect_grep "unknown subcommand 'bogus'"
expect_exit 12 "$MJ" usecase show nosuch
expect_grep "no use case 'nosuch'"
# --- one use case: a file with a scenario section, nothing else to register
cat > "$UC/see-the-version.md" <<'MD'
---
id: see-the-version
kind: use-case
title: 'See which version runs'
summary: 'Print the version and refuse an unknown option.'
category: adoption
status: active
target: guaranteed
actors: [operator]
difficulty: basic
commands: [version]
doctrines: []
claims: [exit-code-contract]
responsibilities: []
applications: []
---
# Situation
Which tool is this?
# Scenario
```yaml
setup: bare
given:
- 'nothing installed'
steps:
- id: print
run: ['version']
note: 'the version string'
expect:
exit: 0
stdout_contains: ['^majordomus [0-9]+\.[0-9]+\.[0-9]+$']
- id: refuse
run: ['version', '--no-such-option']
note: 'a usage error'
expect:
exit: 2
stdout_contains: ['unknown option']
then:
- 'the version needs no repository'
```
# Outcome
The version, from anywhere.
MD
# the repository has no claims file or fixtures of its own: the claim resolves against the
# distribution's? No — a use case names what this repository declares; give it a claims
# file and let the distribution's fixtures (setup scripts) serve the scenario
mkdir -p docs
printf 'version: 1\nstatuses:\n - id: guaranteed\n meaning: Deterministic.\nclaims:\n - id: exit-code-contract\n claim: Exit codes mean one thing\n source: docs/CLI.md\n implementation: bin/majordomus\n test: test/cases/00.sh\n status: guaranteed\n responsibility: none\n' > docs/CLAIMS.yaml
# a guaranteed claim of one of the tool's responsibilities, and an MCP tool the executable's
# registry projects: coverage counts both, and neither is named by any use case yet
printf ' - id: health-is-reported\n claim: Doctor reports the health\n source: docs/CLI.md\n implementation: lib/doctor.sh\n test: test/cases/00.sh\n status: guaranteed\n responsibility: doctor\n' >> docs/CLAIMS.yaml
mkdir -p docs/generated
printf '{"capabilities":[{"id":"status.read","tool": "majordomus_status"}]}\n' > docs/generated/registry.json
expect_exit 0 "$MJ" usecase coverage
expect_grep '^claim +health-is-reported +0 +0 +0 +gap +advisory$'
expect_grep '^mcp_tool +majordomus_status +0 +0 +0 +gap +advisory$'
# the claim with responsibility none is not a coverage target at all
expect_no_grep 'exit-code-contract'
git add -A >/dev/null && git commit -qm "one use case"
expect_exit 0 "$MJ" usecase validate
expect_grep 'usecase validate: 0 failure'
expect_exit 0 "$MJ" usecase list
expect_grep '^see-the-version +adoption +active +scenario +version'
expect_exit 0 "$MJ" usecase run see-the-version
expect_grep '^see-the-version +pass +2 step'
EV=.ai/local/state/../evidence/use-cases/see-the-version.json
[ -f "$EV" ] || { echo " no evidence written"; exit 1; }
grep -q '"result":"pass"' "$EV" || { echo " evidence does not record the pass"; cat "$EV"; exit 1; }
grep -q '"command":"majordomus version --no-such-option"' "$EV" || { echo " evidence does not carry the command"; exit 1; }
grep -q '"expected_exit":2' "$EV" || { echo " evidence does not carry the expected exit"; exit 1; }
# the evidence is normalised: no absolute path of the scenario repository, no timestamp
grep -q "$T" "$EV" && { echo " evidence leaks the scenario path"; exit 1; }
expect_exit 0 "$MJ" usecase show see-the-version
expect_grep '^--- evidence: .*\(pass\)'
# the tracked tree is untouched by a run
[ -z "$(git status --porcelain)" ] || { echo " usecase run changed the tree"; git status --porcelain; exit 1; }
# coverage moved
expect_exit 0 "$MJ" usecase coverage
expect_grep '^command +version +1 +1 +1 +covered'
expect_exit 0 "$MJ" usecase coverage --json
expect_grep '"id":"version","use_cases":1,"executable":1,"evidence":1,"status":"covered"'
# --- a failing expectation fails the run, naming the step and the reason; evidence says fail
sed -i.bak "s/stdout_contains: \['unknown option'\]/stdout_contains: ['everything is fine']/" "$UC/see-the-version.md"; rm -f "$UC/see-the-version.md.bak"
expect_exit 10 "$MJ" usecase run see-the-version
expect_grep '^see-the-version +FAIL +expected /everything is fine/'
grep -q '"result":"fail"' "$EV" || { echo " evidence does not record the failure"; exit 1; }
git checkout -q -- "$UC/see-the-version.md"
sed -i.bak "s/^ exit: 2$/ exit: 0/" "$UC/see-the-version.md"; rm -f "$UC/see-the-version.md.bak"
expect_exit 10 "$MJ" usecase run see-the-version
expect_grep 'expected exit 0, got 2'
git checkout -q -- "$UC/see-the-version.md"
# --- and the same failure through --json, which is the form every generator reads.
# The count of failures is a fact about the run, not about how it is printed. This lived
# only in the text branch once, so `--json` reported "failed":0 and exited 0 however the
# scenarios went — and scripts/generate-site-data relies on exactly that exit code to
# refuse to publish a demonstration that does not hold. A green exit there did not stop
# the site; it silently lowered the use case's maturity instead, which is the difference
# between knowing something is broken and not knowing anything about it.
sed -i.bak "s/stdout_contains: \['unknown option'\]/stdout_contains: ['everything is fine']/" "$UC/see-the-version.md"; rm -f "$UC/see-the-version.md.bak"
expect_exit 10 "$MJ" usecase run --json see-the-version
"$MJ" usecase run --json see-the-version > run.json 2>/dev/null || true
jq -e '.failed == 1 and .ran == 1' run.json >/dev/null \
|| { echo " the JSON run does not report its own failure"; cat run.json; exit 1; }
jq -e '[.results[] | select(.result != "pass")] | length == 1' run.json >/dev/null \
|| { echo " the failing scenario is not in the JSON results"; cat run.json; exit 1; }
git checkout -q -- "$UC/see-the-version.md"
# a passing run still exits 0 and says so, so the fix did not make --json fail always
expect_exit 0 "$MJ" usecase run --json see-the-version
"$MJ" usecase run --json see-the-version > run.json 2>/dev/null
jq -e '.failed == 0 and .ran == 1 and (.results[0].result == "pass")' run.json >/dev/null \
|| { echo " a passing scenario is not reported as passing in JSON"; cat run.json; exit 1; }
# --- every broken reference is refused with the entity and the relation named
probe() { # description, sed expression, expected pattern
sed -i.bak "$2" "$UC/see-the-version.md"; rm -f "$UC/see-the-version.md.bak"
expect_exit 10 "$MJ" usecase validate || { echo " probe '$1' was not refused"; git checkout -q -- "$UC/see-the-version.md"; exit 1; }
expect_grep "$3" || { echo " probe '$1' failed for another reason"; git checkout -q -- "$UC/see-the-version.md"; exit 1; }
git checkout -q -- "$UC/see-the-version.md"
}
probe "unknown command" "s/^commands: \[version\]/commands: [version, nosuchcmd]/" "names command 'nosuchcmd', which bin\/majordomus does not dispatch"
probe "unknown doctrine" "s/^doctrines: \[\]/doctrines: [majordomus.nosuch]/" "names doctrine 'majordomus.nosuch', which no rule declares"
probe "unknown claim" "s/^claims: \[exit-code-contract\]/claims: [nosuch-claim]/" "names claim 'nosuch-claim'"
probe "unknown category" "s/^category: adoption/category: nosuch/" "category 'nosuch' is not in taxonomy.yaml"
probe "unknown setup" "s/^setup: bare/setup: nosuch/" "names setup 'nosuch'"
probe "step not listed" "s/run: \['version', '--no-such-option'\]/run: ['doctor']/" "step 'refuse' runs 'doctor', which the use case does not list under commands"
probe "unknown key" "s/^difficulty: basic/difficulty: basic\ncolour: red/" "unknown key.s.: colour"
probe "no situation" "s/^# Situation/# Setting/" "body has no '# Situation' heading"
probe "unknown application" "s/^applications: \[\]/applications: [nosuch-app]/" "names application 'nosuch-app', which does not exist"
# a guaranteed target with no scenario is refused: a guarantee needs executable evidence
awk '/^# Scenario$/{skip=1} /^# Outcome$/{skip=0} !skip' "$UC/see-the-version.md" > "$UC/x.md" && mv "$UC/x.md" "$UC/see-the-version.md"
expect_exit 10 "$MJ" usecase validate
expect_grep 'targets guaranteed and has no scenario'
git checkout -q -- "$UC/see-the-version.md"
# the id is the file name, and a duplicate id is refused
cp "$UC/see-the-version.md" "$UC/copy.md"
expect_exit 10 "$MJ" usecase validate
expect_grep "id 'see-the-version' is not the file name"
expect_grep "declared twice"
rm -f "$UC/copy.md"
# doctor applies the same validation under the catalogue doctrine, and coverage under its own
expect_exit 0 "$MJ" usecase validate
sed -i.bak "s/^category: adoption/category: nosuch/" "$UC/see-the-version.md"; rm -f "$UC/see-the-version.md.bak"
"$MJ" doctor > "$T/doctor.out" 2>&1 || true
grep -q "FAIL use-case see-the-version — category 'nosuch'" "$T/doctor.out" || { echo " doctor does not apply the use-case validation"; grep 'use-case\|catalogue' "$T/doctor.out"; exit 1; }
git checkout -q -- "$UC/see-the-version.md"
# --- an application must name its use cases back, and a use case its applications
cat > .ai/repo/applications/solo.md <<'MD'
---
id: solo
kind: application
title: 'Solo'
summary: 'One person.'
status: active
fits_when:
- 'one person'
does_not_fit_when:
- 'a team'
use_cases: [see-the-version]
doctrines: []
responsibilities: []
---
# Context
One person and one repository.
MD
expect_exit 10 "$MJ" usecase validate
expect_grep "application names use case 'see-the-version', which does not name it back"
sed -i.bak "s/^applications: \[\]/applications: [solo]/" "$UC/see-the-version.md"; rm -f "$UC/see-the-version.md.bak"
expect_exit 0 "$MJ" usecase validate
git add -A >/dev/null && git commit -qm "an application"
# --- the policy gates coverage: required turns a gap into a failure of doctor, check and finish
sed -i.bak 's/^ commands: advisory$/ commands: required/' .ai/repo/policy.yaml; rm -f .ai/repo/policy.yaml.bak
"$MJ" update >/dev/null
expect_exit 10 "$MJ" usecase coverage --check
expect_grep 'required gap\(s\)'
"$MJ" doctor > "$T/doctor.out" 2>&1 || true
grep -q 'FAIL use-case command doctor — gap: 0 use case(s) name it, 0 run it; the policy requires an executable use case \[reproduce: majordomus usecase scaffold --for command:doctor\]' "$T/doctor.out" \
|| { echo " doctor does not fail a required gap with the scaffold named"; tail -5 "$T/doctor.out"; exit 1; }
# the finish contract carries the key once the policy lists it
sed -i.bak 's/^ - note_present$/ - note_present\n - use_cases_covered/' .ai/repo/policy.yaml; rm -f .ai/repo/policy.yaml.bak
"$MJ" update >/dev/null
git add -A >/dev/null && git -c core.hooksPath=/dev/null commit -qm "require coverage"
"$MJ" start "cover it" --scope lib >/dev/null
# check applies the same gate as doctor: a required gap fails it with the scaffold named
expect_exit 10 "$MJ" check
expect_grep 'FAIL use-case +command doctor — gap: 0 use case\(s\) name it, 0 run it; the policy requires an executable use case'
printf '# Objective\no\n# Current State\nc\n# Next Action\nn\n' | "$MJ" handover >/dev/null
expect_exit 10 "$MJ" finish --outcome completed --verify-command "test -d .ai"
expect_grep 'FAIL use-case'
expect_grep 'refused'
# --- a scaffold closes nothing by itself: it is a draft that runs and does not count
expect_exit 0 "$MJ" usecase scaffold --for command:doctor --dry-run
expect_grep 'would write: \.ai/repo/use-cases/doctor-draft\.md'
[ ! -f "$UC/doctor-draft.md" ] || { echo " --dry-run wrote"; exit 1; }
expect_exit 0 "$MJ" usecase scaffold --for command:doctor
expect_grep 'wrote: \.ai/repo/use-cases/doctor-draft\.md'
grep -q '^status: draft$' "$UC/doctor-draft.md" || { echo " the scaffold is not a draft"; exit 1; }
grep -q '^target: advisory$' "$UC/doctor-draft.md" || { echo " the scaffold targets a guarantee"; exit 1; }
grep -q '^commands: \[doctor\]$' "$UC/doctor-draft.md" || { echo " the scaffold does not name its command"; exit 1; }
expect_exit 0 "$MJ" usecase validate
expect_exit 0 "$MJ" usecase run doctor-draft
expect_grep '^doctor-draft +pass'
expect_exit 10 "$MJ" usecase coverage --check
expect_grep '^command +doctor +0 +0 +0 +gap +required'
expect_exit 0 "$MJ" usecase scaffold --for command:doctor
expect_grep '^exists: '
expect_exit 0 "$MJ" usecase scaffold --missing --dry-run
expect_grep 'would write: \.ai/repo/use-cases/init-draft\.md'
# made active, with the narrative and its claims, it counts
sed -i.bak 's/^status: draft$/status: active/; s/^title: .*/title: '"'"'Check the health'"'"'/; s/^summary: .*/summary: '"'"'Doctor.'"'"'/' "$UC/doctor-draft.md"; rm -f "$UC/doctor-draft.md.bak"
sed -i.bak 's/^claims: \[.*\]$/claims: []/; s/^responsibilities: \[.*\]$/responsibilities: []/' "$UC/doctor-draft.md"; rm -f "$UC/doctor-draft.md.bak"
expect_exit 0 "$MJ" usecase validate
expect_exit 0 "$MJ" usecase coverage
expect_grep '^command +doctor +1 +1 +1 +covered +required'
# the claim and the MCP tool are still named gaps: nothing active names them
expect_grep '^claim +health-is-reported +0 +0 +0 +gap +advisory$'
expect_grep '^mcp_tool +majordomus_status +0 +0 +0 +gap +advisory$'
# naming them from the executable use case closes both, and the tally says so
sed -i.bak 's/^claims: \[\]$/claims: [health-is-reported]\nmcp_tools: [majordomus_status]/' "$UC/doctor-draft.md"; rm -f "$UC/doctor-draft.md.bak"
expect_exit 0 "$MJ" usecase validate
expect_exit 0 "$MJ" usecase coverage
expect_grep '^claim +health-is-reported +1 +1 +1 +covered +advisory$'
expect_grep '^mcp_tool +majordomus_status +1 +1 +1 +covered +advisory$'
# the policy gates the other classes too: a claim gap under claims: required fails --check
sed -i.bak 's/^claims: \[health-is-reported\]$/claims: []/' "$UC/doctor-draft.md"; rm -f "$UC/doctor-draft.md.bak"
cp .ai/repo/policy.yaml "$T/policy.required"
sed -i.bak 's/^ commands: required$/ commands: advisory/' .ai/repo/policy.yaml; rm -f .ai/repo/policy.yaml.bak
expect_exit 0 "$MJ" usecase coverage --check
expect_grep '^claim +health-is-reported +0 +0 +0 +gap +advisory$'
sed -i.bak 's/^ claims: advisory$/ claims: required/' .ai/repo/policy.yaml; rm -f .ai/repo/policy.yaml.bak
expect_exit 10 "$MJ" usecase coverage --check
expect_grep '^claim +health-is-reported +0 +0 +0 +gap +required$'
expect_grep ', 1 required gap\(s\)'
cp "$T/policy.required" .ai/repo/policy.yaml
sed -i.bak 's/^claims: \[\]$/claims: [health-is-reported]/' "$UC/doctor-draft.md"; rm -f "$UC/doctor-draft.md.bak"
# --- impact: a changed file names the use cases, scenarios and cases it reaches
git add -A >/dev/null && git -c core.hooksPath=/dev/null commit -qm "doctor use case"
printf '\n# touched\n' >> "$UC/see-the-version.md"
expect_exit 0 "$MJ" usecase impact --base HEAD
expect_grep '^ use cases see-the-version'
expect_grep '^ scenarios see-the-version'
expect_grep '^next: majordomus usecase run see-the-version'
git checkout -q -- "$UC/see-the-version.md"
expect_exit 0 "$MJ" usecase impact --base HEAD --json
expect_grep '"use_cases":\[\]'
# an implementation file names its command, the behavioural cases that declare they cover
# it, and through the command the use cases and scenarios that run it
mkdir -p lib test/cases
printf '# the doctor implementation of this fixture\n' > lib/doctor.sh
printf '# majordomus-covers: doctor\n' > test/cases/01_doctor.sh
printf '# majordomus-covers: version\n' > test/cases/02_version.sh
git add -A >/dev/null && git -c core.hooksPath=/dev/null commit -qm "an implementation and its cases"
printf '# touched\n' >> lib/doctor.sh
expect_exit 0 "$MJ" usecase impact --base HEAD
expect_grep '^ commands doctor$'
expect_grep '^ cases test/cases/01_doctor\.sh$'
expect_grep '^ use cases doctor-draft$'
expect_grep '^ scenarios doctor-draft$'
expect_exit 0 "$MJ" usecase impact --base HEAD --json
expect_grep '"commands":\["doctor"\],"rules":\[\],"use_cases":\["doctor-draft"\],"scenarios":\["doctor-draft"\],"cases":\["test/cases/01_doctor\.sh"\]'
git checkout -q -- lib/doctor.sh
# a rule file names the rule by its identity, read from its front matter, not its path
RULE=.ai/repo/rules/vendor/majordomus/rules/use-case-coverage.v1.md
[ -f "$RULE" ] || { echo " the fixture has no vendored use-case-coverage rule"; exit 1; }
printf '\n' >> "$RULE"
expect_exit 0 "$MJ" usecase impact --base HEAD
expect_grep '^ rules majordomus\.use-case-coverage$'
expect_grep '^ commands none$'
# ...and the behavioural case the rule names as its proof is affected by changing the rule
expect_grep '^ cases test/cases/94_use_cases\.sh$'
git checkout -q -- "$RULE"
# --- the evidence the site shows is the evidence the tool recorded. The site generator runs
# in a scratch copy of this tool's tree (never the real site/), with the catalogue pruned
# to the use cases whose scenarios walk the lifecycle and refuse something, which is the
# least the generator accepts; it executes them itself and embeds what they recorded.
# The challenge site/data/challenge.toml names is kept too: the generator refuses a
# challenge whose use case the catalogue does not have, and README.md's first transcript
# must be one of its recorded steps.
command -v jq >/dev/null || { echo " jq absent; the site half is not asserted"; exit 1; }
SF="$T/site-fixture"
fixture_repo "$SF" AGENTS.md docs site/data/marketing.toml site/content-src
KEEP='know-which-tool-is-running hand-work-between-sessions run-several-workers-at-once carry-a-blocker-across-a-handover catch-an-agent-that-says-it-is-done'
for f in "$SF"/.ai/repo/use-cases/*.md "$SF"/.ai/repo/applications/*.md; do
grep -q '^kind: context$' "$f" && continue
case " $KEEP " in *" $(basename "$f" .md) "*) sed -i.bak 's/^applications: \[.*\]$/applications: []/' "$f"; rm -f "$f.bak" ;; *) rm -f "$f" ;; esac
done
# the identity is the fixture's own: a runner has no global git identity, and a commit that
# borrows the developer's passes on a laptop and fails on CI
( cd "$SF" && git init -q && git config user.email case94@example.com && git config user.name case94 \
&& git add -A >/dev/null && git -c core.hooksPath=/dev/null commit -qm fixture ) || exit 1
expect_exit 0 "$SF/scripts/generate-site-data" --out "$T/site-data" \
|| { echo " the site generator refused the pruned catalogue"; exit 1; }
CAT="$T/site-data/catalogue.json"
expect_file "$CAT"
for u in $KEEP; do
EVF="$SF/.ai/local/evidence/use-cases/$u.json"
expect_file "$EVF" || { echo " the generator did not execute the scenario of $u"; exit 1; }
jq -S --arg u "$u" '.use_cases[] | select(.id == $u) | .evidence' "$CAT" > "$T/shown.json"
jq -S 'del(.steps[].timing)' "$EVF" > "$T/recorded.json"
cmp -s "$T/shown.json" "$T/recorded.json" \
|| { echo " the site does not show the evidence the tool recorded for $u"; diff "$T/recorded.json" "$T/shown.json" | head -20; exit 1; }
done
# and that evidence is the executed scenario, step by step: exit and output as observed
jq -e '.use_cases[] | select(.id == "know-which-tool-is-running") | .evidence
| .result == "pass" and (.steps | length) == 2
and .steps[0].exit == 0 and (.steps[0].output | test("^majordomus [0-9]+\\.[0-9]+\\.[0-9]+$"))
and .steps[1].exit == 2 and .steps[1].expected_exit == 2 and (.steps[1].output | test("unknown option"))
and all(.steps[]; .result == "pass" and (.assertions | length) > 0)' "$CAT" >/dev/null \
|| { echo " the site's evidence is not the executed scenario"; jq '.use_cases[] | select(.id == "know-which-tool-is-running") | .evidence' "$CAT"; exit 1; }
grep -q "$T" "$CAT" && { echo " the site data leaks the case directory path"; exit 1; }
# the page carries the scenario it demonstrates, and maturity is observed from it: the scenario
# lives in the body's `# Scenario` section, and a generator that read only the front matter
# showed every use case of this repository as `described`, with no steps, while
# `usecase run` executed the same scenarios and passed them
for u in $KEEP; do
jq -e --arg u "$u" '.use_cases[] | select(.id == $u)
| (.scenario.steps | length) > 0 and .maturity != "described"' "$CAT" >/dev/null \
|| { echo " the site shows $u without the scenario it runs"; jq --arg u "$u" '.use_cases[] | select(.id == $u) | {maturity, scenario}' "$CAT"; exit 1; }
done
# --- every reference the migrated catalogue of this tree makes resolves, and its scenarios run:
# the repository's own use cases against its own tool (dogfooding), one of them at least
( cd "$ROOT" && expect_exit 0 "$MJ" usecase validate && expect_grep 'usecase validate: 0 failure' ) || exit 1
( cd "$ROOT" && expect_exit 0 "$MJ" usecase run know-which-tool-is-running && expect_grep '^know-which-tool-is-running +pass' ) || exit 1
( cd "$ROOT" && expect_exit 0 "$MJ" usecase coverage --check ) || exit 1