You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A user story whose linked prompt lives in a subdirectory of the prompts root can never be validated in pdd detect --stories --scope-manifest mode. It always fails closed with scope:MANIFEST_MISMATCH before any provider call.
This is self-inflicted: pdd story link / pdd story add write the story metadata in a form that the manifest pre-flight gate cannot resolve, and manifest mode rejects the flag that would otherwise point the resolver at the right directory. Every prompt under prompts/<subdir>/ is affected, which today means prompts/core/, prompts/commands/, and any new package-style prompt folder.
Found while adding story coverage for a prompt split under prompts/conformance/ (PR #2374).
Reproduction
Self-contained, no network, no provider key:
importjson, os, tempfilefrompathlibimportPathfrompdd.commands.analysisimport_load_scope_manifest, _scope_manifest_metadata_matchesSTORY="""<!-- pdd-story-prompts: {refs} --># User Story: demo## StoryAs a user, I can do the thing, so that the benefit follows."""defbuild(root: Path, refs: str) ->Path:
(root/"prompts"/"conformance").mkdir(parents=True)
(root/"prompts"/"conformance"/"demo_python.prompt").write_text("% demo\n")
(root/"user_stories"/"contracts").mkdir(parents=True)
(root/"user_stories"/"story__demo.md").write_text(STORY.format(refs=refs))
(root/"user_stories"/"contracts"/"demo.contract.md").write_text(
"# Contract: demo\n\n## Covers\n- AC1: the thing\n")
manifest=root/"scope.json"manifest.write_text(json.dumps({
"schema_version": "pdd.detect.stories.scope.v1",
"stories": [{
"story": "user_stories/story__demo.md",
"contract": "user_stories/contracts/demo.contract.md",
"prompts": ["prompts/conformance/demo_python.prompt"],
}],
}))
returnmanifestforlabel, refsin [
("basename (what `pdd story link` writes)", "demo_python.prompt"),
("repo-relative path (manual edit)", "prompts/conformance/demo_python.prompt"),
]:
withtempfile.TemporaryDirectory() astmp:
root=Path(tmp).resolve()
manifest=build(root, refs)
os.chdir(root)
ok=_scope_manifest_metadata_matches(_load_scope_manifest(manifest))
print(f"{label:44}{'accepted'ifokelse'REJECTED -> scope:MANIFEST_MISMATCH'}")
The caller resolves the prompt paths ({pf.resolve() for pf in linked_prompt_paths}, _upsert_story_prompt_metadata line 332) but does not resolve prompts_dir. When the prompts root is a symlink — as it is in this repo, prompts -> pdd/prompts — relative_to raises and the code silently falls back to the basename:
prompts (symlink, as passed) -> ValueError => falls back to 'gate_errors_python.prompt'
prompts resolved -> conformance/gate_errors_python.prompt
The basename is lossy for anything in a subdirectory, and the fallback is silent.
B. The manifest pre-flight gate never looks in a subdirectory
_scope_manifest_metadata_matches (pdd/commands/analysis.py:192-195) tries exactly two candidates:
For demo_python.prompt both are <root>/demo_python.prompt, which does not exist. No prompts root is consulted, and there is no recursive search — so a bare basename for a subdirectory prompt can never match.
pdd/story_detection_result.py:343-347 has a third candidate (prompts_dir / Path(prompt_ref).name), but that is the post-evaluation resolver, it is still not recursive, and in manifest mode prompts_dir is pinned to ..
C. No escape hatch
pdd/commands/analysis.py:403 rejects the one flag that could redirect the resolver:
Error: --scope-manifest cannot be combined with --stories-dir or --prompts-dir.
So the user cannot work around B from the CLI at all.
Impact
Scope-manifest mode is the documented path for hosted and monorepo validation (docs/generating_user_stories.md, Step 6), and it is precisely the layout — nested prompt packages — where directory discovery is "too broad" and a manifest is most needed.
It fails closed, so it is not a silent-wrong-answer bug. But it is indistinguishable from a genuine scope violation, and the message ("Story prompt metadata does not match the exact scope manifest") does not hint that a basename/subdirectory mismatch is the cause.
The only workaround is to hand-edit the <!-- pdd-story-prompts: ... --> comment to a repo-relative path, which the next pdd story link will silently revert.
Suggested fix
Smallest correct change is A: resolve prompts_dir before relative_to, so the metadata keeps conformance/demo_python.prompt instead of degrading to a basename.
B — make the pre-flight resolver consult the prompts root and match a bare basename against a unique recursive hit, failing closed only when the basename is genuinely ambiguous. That also makes existing stories with basename metadata work without rewriting them.
C — either allow --prompts-dir with --scope-manifest (it narrows rather than widens the authorization boundary), or say in the MANIFEST_MISMATCH message which references failed to resolve and what spelling was expected.
A regression test with a prompt in a subdirectory. The existing coverage appears to use flat prompts/ layouts only, which is why this was not caught.
Related
Basename-keyed story linking bites elsewhere too: coverage_contracts._story_links_prompt matches on basename with the directory already discarded by _prompt_basename, so two prompts sharing a basename in different packages (prompts/core/errors_python.prompt vs prompts/conformance/errors_python.prompt) cross-count each other's story evidence with no path-qualified escape. Worth considering together if the linking layer is revisited — a fix to A alone does not address it.
Summary
A user story whose linked prompt lives in a subdirectory of the prompts root can never be validated in
pdd detect --stories --scope-manifestmode. It always fails closed withscope:MANIFEST_MISMATCHbefore any provider call.This is self-inflicted:
pdd story link/pdd story addwrite the story metadata in a form that the manifest pre-flight gate cannot resolve, and manifest mode rejects the flag that would otherwise point the resolver at the right directory. Every prompt underprompts/<subdir>/is affected, which today meansprompts/core/,prompts/commands/, and any new package-style prompt folder.Found while adding story coverage for a prompt split under
prompts/conformance/(PR #2374).Reproduction
Self-contained, no network, no provider key:
Actual:
Expected: both accepted. The two spellings name the same file.
Root cause
Two defects that only bite in combination.
A. Metadata degrades to a bare basename
_prompt_reference_for_metadata(pdd/user_story_tests.py:316-321):The caller resolves the prompt paths (
{pf.resolve() for pf in linked_prompt_paths},_upsert_story_prompt_metadataline 332) but does not resolveprompts_dir. When the prompts root is a symlink — as it is in this repo,prompts -> pdd/prompts—relative_toraises and the code silently falls back to the basename:The basename is lossy for anything in a subdirectory, and the fallback is silent.
B. The manifest pre-flight gate never looks in a subdirectory
_scope_manifest_metadata_matches(pdd/commands/analysis.py:192-195) tries exactly two candidates:For
demo_python.promptboth are<root>/demo_python.prompt, which does not exist. No prompts root is consulted, and there is no recursive search — so a bare basename for a subdirectory prompt can never match.pdd/story_detection_result.py:343-347has a third candidate (prompts_dir / Path(prompt_ref).name), but that is the post-evaluation resolver, it is still not recursive, and in manifest modeprompts_diris pinned to..C. No escape hatch
pdd/commands/analysis.py:403rejects the one flag that could redirect the resolver:So the user cannot work around B from the CLI at all.
Impact
docs/generating_user_stories.md, Step 6), and it is precisely the layout — nested prompt packages — where directory discovery is "too broad" and a manifest is most needed.<!-- pdd-story-prompts: ... -->comment to a repo-relative path, which the nextpdd story linkwill silently revert.Suggested fix
Smallest correct change is A: resolve
prompts_dirbeforerelative_to, so the metadata keepsconformance/demo_python.promptinstead of degrading to a basename.Worth doing alongside it:
--prompts-dirwith--scope-manifest(it narrows rather than widens the authorization boundary), or say in theMANIFEST_MISMATCHmessage which references failed to resolve and what spelling was expected.prompts/layouts only, which is why this was not caught.Related
coverage_contracts._story_links_promptmatches on basename with the directory already discarded by_prompt_basename, so two prompts sharing a basename in different packages (prompts/core/errors_python.promptvsprompts/conformance/errors_python.prompt) cross-count each other's story evidence with no path-qualified escape. Worth considering together if the linking layer is revisited — a fix to A alone does not address it.