Skip to content

Honor architecture.json filepath for prompts in nested .pddrc contexts#1971

Open
Serhan-Asad wants to merge 62 commits into
mainfrom
fix/arch-json-prompts-dir-prefix
Open

Honor architecture.json filepath for prompts in nested .pddrc contexts#1971
Serhan-Asad wants to merge 62 commits into
mainfrom
fix/arch-json-prompts-dir-prefix

Conversation

@Serhan-Asad

@Serhan-Asad Serhan-Asad commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Problem

get_pdd_file_paths() gives architecture.json.filepath tier-1 precedence for code outputs, but a nested .pddrc prompts_dir can strip filename prefixes retained by architecture entries. The original fallback fixed only repositories rooted at <project>/prompts, still missed canonical/custom prompt layouts, ignored exact example/test output templates, reparsed architecture on misses, and made uncontained architecture paths reachable by sync.

Downstream report: promptdriven/pdd_cloud#3203. Tracking issue: #1976.

Fix

  • Resolve prefix-stripped architecture names in the primary lookup without assuming the prompt root is named prompts.
  • Validate architecture prompt filenames before discovery; reject absolute, parent-traversal, backslash, and symlink-resolved prompt escapes.
  • Establish prompt ownership across configured sibling roots so a narrowed backend root cannot borrow a same-leaf frontend module.
  • Restrict filepath-stem compatibility matching to entries that are not themselves recognized prompt filenames.
  • Compare root-relative prompt identities without resolving caller-influenced paths, while preserving trusted root symlink aliases such as prompts -> pdd/prompts.
  • Keep architecture authoritative for code while honoring exact .pddrc outputs.example.path and outputs.test.path templates.
  • Reject unsafe architecture output paths before generation can use them.
  • Replace repeated whole-tree ownership scans with bounded, cached directory walks and retain one architecture parse per resolution.
  • Align the governing prompt, path-construction guide, Unreleased changelog, and drift metadata.

Undeclared deliverables still require a downstream architecture entry; core intentionally does not guess a non-template output path when no source-of-truth entry exists.

Validation

  • 509 resolver and backward-compatibility tests passed.
  • 244 affected module and metadata tests passed; 244 broader sync/architecture tests also passed.
  • New coverage includes missing internal prompts with unsafe architecture hints, sibling backend/frontend contexts, differently named prompt aliases, trusted prompt-root symlink aliases, custom roots, canonical normalization, exact code/example/test outputs, duplicate flat/nested basenames, unsafe output paths, and symlink escapes.
  • A 191-prompt differential sweep against pdd_cloud PR #3205 found zero unintended changes versus the prior PR head; the 32 intended downstream corrections remain intact and the seven expected downstream mismatches remain visible.
  • On a 3,001-prompt fixture, ten canonical ownership resolutions improved from about 1,108 ms on the prior head to about 142 ms; warm fixed calls are about 7 ms each.
  • compileall, Ruff critical selectors, and git diff --check passed.
  • Metadata now records the checkout version 0.0.301.dev0; local drift detection reports no prompt or example drift for the affected modules.

Closes #1976.

@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Companion downstream PR: promptdriven/pdd_cloud#3205 (adds the missing architecture.json entries + a drift gate; closes pdd_cloud#3203).

Comment thread pdd/sync_determine_operation.py Fixed
Comment thread pdd/sync_determine_operation.py Fixed
Comment thread pdd/sync_determine_operation.py Fixed
Comment thread pdd/sync_determine_operation.py Fixed
Comment thread pdd/sync_determine_operation.py Fixed
@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Addressed the three blockers from the latest review in 8c697609f (on top of ee0bf6427). Each fix has a load-bearing regression test (verified to fail when the fix is neutralized).

1. Direct fast-path symlink escape — containment was only on recursive candidates, so Step 1's direct/case-insensitive lookup returned the exact expected prompt even when it was a symlink to a file outside prompts_root (which update opens "w" and truncates). Containment now gates every _find_prompt_file return (direct, arch-hint join, glob fallback); in-root aliases are preserved.
Tests: test_find_prompt_file_direct_fast_path_rejects_escaping_symlink, test_find_prompt_file_preserves_in_root_symlink_alias.

2. Broad-root context selection depended on CWD_find_prompt_file loaded context_prefix via _find_pddrc_file() (CWD). With a project-level prompts root, two contexts sharing a leaf, and a parent/sibling CWD, no prefix loaded and the shallowest/lexicographic tie-break returned the wrong-context (backend) prompt for a context_override="frontend" request. The prefix lookup now anchors at the resolved prompt root.
Test: test_get_pdd_file_paths_broad_root_context_selection_from_parent_cwd.

3. Two-snapshot architecture raceget_pdd_file_paths now parses architecture.json once and threads that immutable snapshot (modules= param + _ARCH_MODULES_UNSET sentinel) through ambiguity, prompt discovery, and code-path selection, so a mid-resolution rewrite can't produce a torn prompt/code pair. Also removes the redundant parse from the perf note.
Test: test_get_pdd_file_paths_parses_architecture_once (asserts exactly 1 parse, was 3).

Nits: fixed both .[] jq diagnostics in agentic_arch_step13_fix_LLM.prompt to (.modules // .)[] (handles {"modules": [...]}); extended the governing sync_determine_operation prompt with all three invariants so a future regeneration preserves them; refreshed .pdd/meta so prompt/code/test hashes match on-disk (drift = 0).

Validation: 212 passed (resolver suite), 465 (path-resolution regressions), 698 (broader sync/agentic/drift); ruff-critical, compileall, and git diff --check clean.

Follow-up (not in this PR): the acknowledged test-file churn/deletion gate for the auto-heal pipeline remains unimplemented — it belongs in the heal driver, not this path-resolution change, and needs its own change + tests. Tracking it separately.

@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Addressed the three architecture-lookup blockers from the latest review in 6d604382a. Each has a load-bearing regression test (verified to fail when the fix is neutralized), reproduced first.

1. Architecture hint ignored context ownership — with a broad project prompts root and context_override="backend", _find_prompt_file Step 3 ran a bare-leaf architecture lookup without the resolved context, so it borrowed a sibling frontend/credits row and returned the frontend prompt (and frontend/credits.py) via the direct join before the context-aware recursive fallback. The hint now passes the resolved context, so _context_owned_filepath rejects an out-of-context row.
Test: test_get_pdd_file_paths_arch_hint_respects_context_from_broad_root.

2. Unsafe same-leaf row raised a spurious AmbiguousModuleError_architecture_module_choices counted an unsafe row (../../foo_Python.prompt, /tmp/…) alongside a valid foo_Python.prompt, producing two "distinct" targets and blocking a valid module. Unsafe filenames (absolute, .., backslash, Windows drive) are now excluded from ambiguity counting; a null/empty filename still flows to the filepath-stem branch.
Test: test_get_pdd_file_paths_unsafe_same_leaf_row_does_not_block_valid_module.

3. "filename": null crashed into a cwd-relative default — the case-insensitive and basename/language match loops called .lower() on module.get("filename", ""), which is None for a null value (the default only applies to a missing key). The AttributeError was swallowed by the broad get_pdd_file_paths fallback → cwd-relative foo.py instead of the architecture src/foo.py. All three sites now coerce with str(module.get("filename") or ""); the module resolves by filepath stem.
Test: test_get_pdd_file_paths_null_filename_uses_architecture_filepath.

Also extended the governing sync_determine_operation prompt with all three invariants and refreshed .pdd/meta (prompt/code/test hashes match on-disk, drift = 0).

Validation (the review noted a local pytest slice segfaulted with 139 in that environment — here the suites run clean): 215 passed (resolver suite), 796 passed (broader sync/agentic/path-resolution); ruff-critical, compileall, and git diff --check origin/main...HEAD all pass.

@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Addressed the three blockers from the latest review in ea6528c04. Each has a load-bearing regression test (reproduced first, then verified to fail when the fix is neutralized).

1. Flat legacy filenames bypassed context ownership — territory was applied to the leaf-match and filepath-stem borrows but not the basename+language or flat-filename match loops. A stale sibling row with a flat credits_Python.prompt filename → frontend/credits.py was borrowed by a backend resolution. _context_owned_filepath now gates those loops too.
Test: test_get_pdd_file_paths_flat_legacy_row_respects_context_territory.

2. Canonical lookup stayed CWD-dependent_get_filepath_from_architecture re-detected the context and stripped the basename prefix via a CWD-based .pddrc lookup. From a parent/sibling CWD with an absolute prompts root, backend/foo failed to align with the canonical backend/services/foo.py row → fell back to backend/functions/foo.py. It now prefers the caller's resolved context and anchors _relative_basename_for_context / _module_filepath_matches_basename / _prompt_basename_candidates at the architecture project root. (Both halves verified independently load-bearing.)
Test: test_get_pdd_file_paths_canonical_target_resolves_from_parent_cwd.

3. Unsafe OUTPUT rows still caused false ambiguity_architecture_module_choices validated the filename but added the raw filepath to the distinct set, so a valid foo → src/foo.py plus a same-filename foo → ../../outside/foo.py (or /tmp/…, D:/…) read as two targets and raised AmbiguousModuleError. Unsafe filepaths are now validated with the same containment used before generation and excluded from ambiguity counting.
Test: test_get_pdd_file_paths_unsafe_filepath_row_does_not_block_valid_module (parametrized ../.., /abs, D:/).

Also extended the governing sync_determine_operation prompt with all three invariants and refreshed .pdd/meta (prompt/code/test hashes match on-disk, drift = 0).

Validation (noting the earlier report that local pytest crashes before collection in that host — it runs clean here): 220 passed (resolver suite), 801 passed (broader sync/agentic/path-resolution); ruff-critical, compileall, and git diff --check origin/main...HEAD all pass.

@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Addressed the three correctness findings and the performance regression from the latest review in 56d757d5a. Each has a load-bearing regression test (reproduced first, then verified to fail when the fix is neutralized).

1. Context inference depended on CWD with no override_resolve_context_name_for_basename searched from the process CWD, so a path-qualified backend/foo run from the project's parent with no context_override missed the backend context → the canonical backend/services/foo.py fell back to backend/functions/foo.py. It now takes a pddrc_anchor; get_pdd_file_paths establishes the prompts-root anchor before resolving context and passes it.
Test: test_get_pdd_file_paths_context_inferred_from_parent_cwd_without_override.

2. Custom-root prompt discovery did not receive the anchor_find_prompt_file built its candidates and did path-qualified alignment via a CWD-based .pddrc lookup, so from a parent CWD with a custom root the prefix wasn't stripped: an existing specs/services/utils/foo_Python.prompt was missed and a duplicated specs/services/backend/utils/foo_Python.prompt returned. It now anchors both candidate building and the Step-4 alignment at the resolved prompts root (both points verified load-bearing).
Test: test_get_pdd_file_paths_custom_root_finds_existing_prompt_from_parent_cwd.

3. Exact example/test templates stayed CWD-dependentconstruct_paths_basename was stripped via a CWD-based lookup, so from a parent CWD a {category} template duplicated the prefix (backend/examples/backend/foo_example.py). Now stripped via the project-anchored lookup.
Test: test_get_pdd_file_paths_example_test_templates_not_duplicated_from_parent_cwd.

4. Performance regression (from the round-5 unsafe-filepath fix)_architecture_module_choices filesystem-resolved every module's filepath for containment before checking the filename match (the ~8x / 6,003-resolution slowdown you measured). Containment is now deferred until after the cheap filename/leaf/stem match, so only matching rows are resolved — 1 resolution instead of 3,001 in the repro. Correctness (unsafe filepaths excluded from ambiguity) is preserved.
Test: test_architecture_module_choices_defers_containment_to_matches (asserts ≤5 resolutions across 301 modules).

Also extended the governing prompt and refreshed .pdd/meta (drift = 0).

Validation (local pytest runs clean here despite the earlier pre-collection crash on that host): 224 passed (resolver suite), 819 passed (broader sync/agentic/path-resolution); ruff-critical, compileall, git diff --check origin/main...HEAD all pass. Re: the branch being behind main — origin/main has not touched sync_determine_operation.py since the merge-base, so the merge stays clean.

@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Addressed the three findings from the latest review in a993f8e3c (the round-6 perf regression stays fixed). Each has a load-bearing regression test (reproduced first, then verified to fail when neutralized).

1. New-module outputs resolved outside the subproject — the missing-prompt branch called construct_paths with no anchoring input (path_resolution_mode="cwd"), so from a parent CWD a new module's code/test resolved under the parent. It now passes the prompt path as a hint (so construct_paths finds the subproject .pddrc and resolves against it), and project-relative template outputs are anchored at the project root (_anchor_output_paths_at_project — the outputs.code template branch produced a relative backend/functions/foo.py that was otherwise CWD-resolved).
Test: test_get_pdd_file_paths_new_module_outputs_under_subproject_from_parent_cwd.

2. Non-architecture templates duplicated context prefixes — the existing-prompt template branch (no arch entry) recomputed the basename without the project anchor, and _overlay_configured_output_paths had the same omission, so from a parent CWD a {category} template produced backend/examples/backend/foo_example.py. Both now anchor.
Test: test_get_pdd_file_paths_non_architecture_templates_not_duplicated_from_parent_cwd.

3. Proven ownership was overridden by territory — good catch. Round 5's territory check was applied unconditionally, so a row whose physical prompt owner IS the resolved prompt was discarded when its authoritative target is intentionally shared (outside the context globs). _belongs_to_resolved_prompt now returns a three-state classification (ineligible / heuristic-eligible / proven); territory guards only heuristic-eligible rows. Subtlety I had to reconcile: a stale flat row (credits_Python.promptfrontend/credits.py) also proves ownership via a leaf collision, so a naive "skip territory when proven" would re-break round 5. The fix: a PROVEN row keeps a shared/no-context target but is still rejected when the target is owned by a sibling context.
Tests: test_..._proven_owner_honored_for_shared_target and test_..._proven_owner_still_rejects_sibling_context_target (locks in the round-5 behavior).

Extended the governing prompt and refreshed .pdd/meta (drift = 0).

Validation (local pytest runs clean here): 228 passed (resolver suite), 831 passed (broader sync/agentic/path-resolution); ruff-critical, compileall, git diff --check origin/main...HEAD all pass. origin/main still hasn't touched sync_determine_operation.py, so the branch-behind-main note remains a clean merge.

@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Addressed the three findings from the latest review in 3b4b5c39c. Each has a load-bearing regression test (reproduced first, then verified to fail when neutralized).

1. Existing-prompt template outputs stayed parent-CWD-relative — round 7 anchored only the missing-prompt branch. The existing-prompt template branch now anchors project-relative outputs at the subproject too (_anchor_output_paths_at_project), and that helper re-anchors only when the project root differs from the CWD — preserving the established relative-output contract when CWD == project (which an existing test asserts).
Test: test_get_pdd_file_paths_existing_prompt_template_anchored_from_parent_cwd.

2. Absolute sibling output paths defeated cross-context isolation_filepath_matches_context compared raw config prefixes against the project-relative architecture filepath, so an absolute generate_output_path never matched and a sibling context stopped owning its code (a stale flat backend row targeting frontend/ was then accepted). Absolute .pddrc paths/output values are now re-expressed relative to the project before comparison (an absolute value outside the project can never own a project-relative target).
Test: test_get_pdd_file_paths_absolute_sibling_output_path_still_isolates.

3. Flat architecture hints selected the wrong same-leaf prompt_resolve_prompt_path_from_architecture sorted duplicate recursive matches by shallowest/lexicographic without context, so a legacy flat filename matching the leaf in both backend/ and frontend/ returned the backend prompt for a frontend request while code resolved under frontend (a torn pair). It now prefers the resolving context's prefix (threaded from _find_prompt_file).
Test: test_get_pdd_file_paths_flat_hint_selects_requested_context_prompt.

Extended the governing prompt and refreshed .pdd/meta (drift = 0).

Validation (local pytest runs clean here): 231 passed (resolver suite), 834 passed (broader sync/agentic/path-resolution); ruff-critical, compileall, git diff --check origin/main...HEAD all pass. origin/main still hasn't touched sync_determine_operation.py, so the branch-behind-main note remains a clean merge.

@Serhan-Asad Serhan-Asad self-assigned this Jul 11, 2026
@Serhan-Asad Serhan-Asad requested a review from gltanaka July 11, 2026 03:00

@gltanaka gltanaka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR is needed: it fixes a real tier-1 resolution failure where nested .pddrc prompt roots strip an architecture filename prefix and make pdd sync fall back to the wrong code template. The normal path is well covered, and I independently verified python -m pdd sync commands/checkup --dry-run resolves prompts/commands/checkup_python.prompt to pdd/commands/checkup.py. The contributor also supplied substantial resolver, broader-suite, differential-sweep, and downstream CLI dry-run evidence.

One correctness blocker remains:

Blocking: context prefix matching is substring-based and can still return a sibling prompt. At pdd/sync_determine_operation.py:345, the new flat-architecture hint tie-break uses context_prefix in str(m.relative_to(prompts_root)). The same predicate remains at lines 1081 and 1115. This does not test path ownership. With contexts prompts/api and prompts/aapi, both containing credits_Python.prompt, a flat architecture row for api/credits.py, and context_override="api", get_pdd_file_paths() returns prompts/aapi/credits_Python.prompt. I reproduced this on head 639002eef; the code target remains in the requested api context, so resolution produces a torn cross-context prompt/code pair. An update can therefore modify the sibling prompt.

Please match exact path components/prefixes rather than substrings (ideally through one shared helper used by all three sites) and add a regression with overlapping context names such as api and aapi. Until that case is covered, the real end-to-end testing is strong but not sufficient to establish the cross-context safety claimed by this PR.

Serhan-Asad and others added 15 commits July 11, 2026 09:51
get_pdd_file_paths() resolves a prompt's code deliverable with
architecture.json's `filepath` as tier-1, overriding the `.pddrc` code
template. The arch.json lookup key was built relative to the `.pddrc`
context's `prompts_dir` (e.g. `prompts/backend`), which strips a leading
path segment that architecture.json `filename` fields retain (they are
stored relative to the repo `prompts/` root, e.g.
`backend/credits_Python.prompt`). The key `credits_Python.prompt` never
matched the stored `backend/credits_Python.prompt`, so tier-1 silently
missed and pdd fell back to the wrong `.pddrc` template
(`backend/functions/credits.py`) — for modules whose real code lives
elsewhere this yields a non-existent path and "Code file not found".

Fix: when the primary architecture.json lookup returns nothing, retry once
with the key computed relative to the repo `prompts/` root. Guarded by
`alt_lookup != prompt_filename_for_lookup` and `try/except ValueError`, so
it is a pure fallback — it only fires on a primary miss and no-ops for
repos whose prompt tree is not under `prompts/`, preserving existing
behavior (verified: 400+ resolver tests pass, purely additive).

Reported downstream: promptdriven/pdd_cloud#3203.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…air prompt

Address three review findings on the architecture.json.filepath precedence work:

1. Stale cross-context entries no longer redirect sync. A same-leaf prompt could
   borrow a sibling-context architecture entry (e.g. `frontend/credits.py`) when
   the sibling's prompt was deleted but its `architecture.json` row survived —
   `if not owners: return True` treated the missing owner as eligible and the leaf
   fallback matched by filename leaf, silently overwriting another context's code.
   Heuristic leaf/filepath-stem borrows are now restricted to filepaths within the
   resolving prompt's `.pddrc` context territory (its `paths` globs / output
   locations) via `_context_owned_filepath`; exact filename matches are unaffected,
   and non-context projects keep prior behavior (permit when no territory derives).

2. Windows drive-qualified metadata is rejected. `D:/x` / `D:x` are relative to
   `PurePosixPath` (so they passed the absolute/`..` checks) but escape the repo
   when joined on Windows. Both `_safe_architecture_prompt_filename` and
   `_contained_architecture_code_path` now reject any `PureWindowsPath(raw).drive`.

3. Realign `agentic_arch_step13_fix_LLM.prompt` with Issue #617. Its STEP A
   diagnostics and cleanup checklist still labeled nested prompts and slash-bearing
   architecture filenames as WRONG / "no slashes", contradicting the same prompt's
   path-mirroring rules and letting the repair agent flatten the layout this change
   relies on. Diagnostics, the Strategy-A delete guard, and checklist items 4/6 now
   state that filenames MUST mirror filepath and nested prompts under a configured
   `prompts_dir` are correct.

Adds regression tests for the stale-sibling borrow and drive-qualified rejection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ext leak

Follow-up review of head ba6df21 (auto-heal bot commit). Address all three
merge-blockers plus the governing-prompt durability gap:

1. Prompt discovery could escape through a same-leaf FILE symlink. rglob does not
   descend into symlinked directories (so the existing directory-symlink test did
   not cover this), but it DOES yield a file symlink whose is_file() follows the
   link. _find_prompt_file Steps 3b/3c/4 returned such a candidate; an update then
   writes through it, overwriting a file outside the repo. Every recursively
   discovered candidate must now resolve inside prompts_root
   (_prompt_candidate_within_root), mirroring _resolve_prompt_path_from_architecture.

2. Context isolation failed when resolution ran outside the project CWD.
   _context_owned_filepath loaded .pddrc from the process CWD and failed open, so a
   parent/sibling CWD with an absolute prompts root re-opened the stale
   sibling-context borrow (backend -> frontend/credits.py). It now anchors the
   .pddrc lookup at the project root (architecture.json's directory).

3. The auto-heal checkpoint (ba6df21) regenerated the resolver test suite,
   collapsing it from 5,990 lines / 154 functions to 438 / 16 and dropping both the
   new security tests and longstanding regression coverage. Restore the full suite
   and refresh .pdd/meta so its prompt/code/test hashes match the on-disk files
   (drift = 0, computed via the module's own calculate_sha256 / calculate_prompt_hash)
   — the stale test_hash left by my previous commit is what tripped the destructive
   heal. NOTE: a durable fix still needs a test-file churn/deletion gate in the heal
   pipeline so a regeneration can never silently delete a hand-maintained suite;
   that belongs in the heal driver and is filed as a follow-up.

Also update the governing sync_determine_operation prompt to require containment on
every recursive candidate, CWD-independent .pddrc anchoring, and Windows
drive-qualified rejection, so a future regeneration preserves these invariants.

Adds load-bearing regression tests for the file-symlink escape and the parent-CWD
context isolation (both verified to fail without the fix). Fixes the trailing-
whitespace nit. Full suite: 208 passed; broader sync/agentic suites: 460 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-break, single arch snapshot

Third review round on head ee0bf64. Address the three new blockers plus the jq nit:

1. Direct fast path could return an escaping file symlink. Containment was applied
   only to recursive candidates; the Step 1 direct/case-insensitive lookup returned
   the exact expected prompt even when it was a symlink to a file OUTSIDE prompts_root,
   which a later update opens with "w" and truncates. Containment now gates every
   _find_prompt_file return (Step 1 direct, the architecture-hint join, and the glob
   fallback). In-root aliases (target inside the resolved root) are preserved.

2. Broad-root context selection depended on the process CWD. _find_prompt_file loaded
   context_prefix via _find_pddrc_file() (CWD). With a project-level prompts root, two
   contexts owning a same-leaf prompt, and a parent/sibling CWD, no prefix loaded and
   the shallowest/lexicographic tie-break returned the WRONG-context (backend) prompt
   for a context_override="frontend" request. The prefix lookup now anchors at the
   resolved prompt root.

3. Nested discovery read two architecture snapshots. Prompt discovery parsed
   architecture.json (via _find_prompt_file Step 3) and get_pdd_file_paths parsed it
   again for the code path; an atomic rewrite between them pairs a prompt from the old
   registry with a code target from the new one (torn pair). get_pdd_file_paths now
   parses ONCE and threads that immutable module snapshot through ambiguity detection,
   prompt discovery, and code-path selection (also removes the redundant parse).

Nit: the architecture-repair prompt's jq diagnostics used `.[]`, which fails for the
supported {"modules": [...]} format and misreports "None found"; use `(.modules // .)[]`.

Also extends the governing sync_determine_operation prompt with all three invariants,
refreshes .pdd/meta so prompt/code/test hashes match on-disk (drift = 0), and adds
load-bearing regression tests (each verified to fail without its fix), including an
in-root-alias-preserved test and a single-parse assertion.

Full suite 212 passed; path-resolution regressions 465; broader sync/agentic/drift 698.

FOLLOW-UP (still unimplemented, needs its own change in the heal pipeline): a
test-file churn/deletion gate so auto-heal can never silently replace a hand-maintained
suite. Tracked separately; not in this path-resolution PR's scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fourth review round on head 8c69760. Three architecture-lookup bugs:

1. Architecture HINT ignored context ownership. With a broad project prompts root and
   context_override="backend", `_find_prompt_file` Step 3 ran a bare-leaf architecture
   lookup WITHOUT the resolved context, so it borrowed a sibling `frontend/credits`
   row and returned the frontend prompt (and frontend/credits.py) via the direct join,
   before the context-aware recursive fallback. The hint now passes the resolved
   context so `_context_owned_filepath` rejects a row whose filepath is out of context.

2. Unsafe same-leaf row raised a spurious AmbiguousModuleError. `_architecture_module_choices`
   counted an unsafe row (`../../foo_Python.prompt`, `/tmp/...`) alongside a valid
   `foo_Python.prompt`, producing two "distinct" targets and blocking a legitimate
   module. Unsafe filenames (absolute, parent traversal, backslash, Windows drive) are
   now excluded from ambiguity counting; a null/empty filename still flows to the
   filepath-stem branch.

3. `"filename": null` crashed into a cwd-relative default. Case-insensitive and
   basename/language match loops called `.lower()` on `module.get("filename", "")`,
   which is `None` for a null value (the default only applies to a MISSING key). The
   AttributeError was swallowed by the broad get_pdd_file_paths fallback, returning a
   cwd-relative `foo.py` instead of the architecture `src/foo.py`. All three sites now
   coerce with `str(module.get("filename") or "")`, so the module resolves by filepath
   stem.

Extends the governing sync prompt with all three invariants, refreshes .pdd/meta
(prompt/code/test hashes match on-disk, drift = 0), and adds three load-bearing
regression tests (each verified to fail without its fix).

Full suite 215 passed; broader sync/agentic/path-resolution 796 passed; ruff-critical,
compileall, and git diff --check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t, unsafe-filepath ambiguity

Fifth review round on head 6d60438. Three deeper variants of the context/metadata
issues:

1. Flat legacy filenames bypassed context ownership. Territory checks were on the
   leaf-match and filepath-stem borrows, but NOT the basename+language or flat-filename
   match loops. A stale sibling row with a flat `credits_Python.prompt` filename
   pointing at `frontend/credits.py` was borrowed by a backend resolution (backend sync
   -> frontend code). `_context_owned_filepath` now gates those loops too.

2. Canonical lookup stayed CWD-dependent. `_get_filepath_from_architecture` re-detected
   the context from the CWD and stripped the basename prefix via a CWD-based `.pddrc`
   lookup. From a parent/sibling CWD with an absolute prompts root, a path-qualified
   `backend/foo` failed to align with the canonical `backend/services/foo.py` row and
   fell back to the default `backend/functions/foo.py`. It now prefers the caller's
   resolved context and anchors `_relative_basename_for_context` /
   `_module_filepath_matches_basename` / `_prompt_basename_candidates` at the
   architecture project root.

3. Unsafe OUTPUT rows still caused false ambiguity. `_architecture_module_choices`
   validated the filename but added the raw filepath to the distinct-target set, so a
   valid `foo -> src/foo.py` row plus a same-filename `foo -> ../../outside/foo.py` (or
   `/tmp/...`, `D:/...`) read as two targets and raised AmbiguousModuleError. Unsafe
   filepaths are now validated with the same containment used before generation and
   excluded from ambiguity counting.

Extends the governing prompt with all three invariants, refreshes .pdd/meta (drift = 0),
and adds five load-bearing regression tests (each verified to fail without its fix; both
halves of #2 — resolved-context preference and project-anchored .pddrc — checked
independently).

Full suite 220 passed; broader sync/agentic/path-resolution 801 passed; ruff-critical,
compileall, git diff --check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y perf regression

Sixth review round on head ea6528c. Three deeper CWD-dependency variants plus the
performance regression round 5 introduced:

1. Context inference still depended on CWD with NO override. `_resolve_context_name_for_basename`
   searched from the process CWD, so a path-qualified `backend/foo` run from the project's
   parent (no context_override) missed the backend context and the canonical
   `backend/services/foo.py`, falling back to `backend/functions/foo.py`. It now accepts a
   `pddrc_anchor`; get_pdd_file_paths establishes the prompts-root anchor BEFORE resolving
   context and passes it.

2. Custom-root prompt discovery did not receive the anchor. `_find_prompt_file` built its
   basename candidates and did path-qualified alignment via a CWD-based `.pddrc` lookup, so
   from a parent CWD with a custom prompt root the context prefix was not stripped — an
   existing `specs/services/utils/foo_Python.prompt` was missed and a duplicated
   `specs/services/backend/utils/foo_Python.prompt` returned. It now anchors both candidate
   building and the Step-4 alignment at the resolved prompts root.

3. Exact example/test templates stayed CWD-dependent. `construct_paths_basename` was stripped
   via a CWD-based lookup, so from a parent CWD a `{category}` template duplicated the prefix
   (`backend/examples/backend/foo_example.py`). It now strips via the project-anchored lookup.

4. PERF (regression from round 5): `_architecture_module_choices` filesystem-resolved EVERY
   module's filepath for containment before checking the filename match — ~8x slower on a
   3,001-module architecture (6,003 resolutions). Containment is now deferred until after the
   cheap filename/leaf/stem match, so only matching rows are resolved (1 instead of 3,001 in
   the repro).

Extends the governing prompt, refreshes .pdd/meta (drift = 0), and adds four load-bearing
regression tests (each verified to fail without its fix; F2 needed BOTH anchor points
neutralized, confirming both are load-bearing; PERF asserts <=5 containment resolutions
across 301 modules).

Full suite 224 passed; broader sync/agentic/path-resolution 819 passed; ruff-critical,
compileall, git diff --check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n ownership over territory

Seventh review round on head 56d757d. Three findings; the round-6 perf regression is
confirmed fixed.

1. New-module outputs resolved outside a subproject. The missing-prompt branch called
   construct_paths with no anchoring input (path_resolution_mode="cwd"), so from a parent
   CWD a new module's code/test resolved under the parent. It now passes the prompt path
   as a hint (construct_paths finds the subproject .pddrc and resolves against it), and
   project-relative template outputs are anchored at the project root
   (_anchor_output_paths_at_project) — the template branch produced a relative
   backend/functions/foo.py that was otherwise CWD-resolved.

2. Non-architecture templates duplicated context prefixes. The existing-prompt template
   branch (no arch entry) recomputed the basename without the project anchor, and
   _overlay_configured_output_paths had the same omission, so from a parent CWD a
   {category} template produced backend/examples/backend/foo_example.py. Both now anchor.

3. Proven ownership was overridden by territory. Round 5 added territory to the
   basename+language / flat / leaf / filepath-stem borrows, but it was applied
   unconditionally — discarding a row whose physical prompt owner IS the resolved prompt
   when its authoritative code target is intentionally shared (outside the context globs),
   falling back and risking a duplicate. `_belongs_to_resolved_prompt` now returns a
   three-state classification (ineligible / heuristic-eligible / proven); territory guards
   only heuristic-eligible rows. A PROVEN row keeps a shared/no-context target but is still
   rejected when the target is owned by a SIBLING context — reconciling round 7 (honor
   shared) with round 5 (reject a stale frontend row leaf-colliding with a backend prompt).

Extends the governing prompt, refreshes .pdd/meta (drift = 0), and adds four load-bearing
regression tests (each verified to fail without its fix), including an explicit
proven-owner-still-rejects-sibling-context guard so the reconciliation can't silently
regress.

Full suite 228 passed; broader sync/agentic/path-resolution 831 passed; ruff-critical,
compileall, git diff --check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… flat-hint context

Eighth review round on head a993f8e. Three follow-ups to the round-7 output/ownership
work:

1. Existing-prompt template outputs stayed relative to the parent CWD. Round 7 anchored
   only the missing-prompt template branch; the existing-prompt branch returned
   project-relative paths, so from a parent CWD an existing prompt's example/test
   resolved under the parent. It now anchors too (via _anchor_output_paths_at_project),
   and that helper re-anchors ONLY when the project root differs from the CWD — so the
   established relative-output contract (CWD == project) is preserved.

2. Absolute sibling output paths defeated cross-context isolation.
   _filepath_matches_context compared raw config prefixes against the project-relative
   architecture filepath, so an absolute generate_output_path never matched and a sibling
   context stopped owning its code — a stale flat backend row targeting frontend/ was then
   accepted. Absolute .pddrc paths/output values are now re-expressed relative to the
   project (an absolute value outside the project can never own a project-relative target).

3. Flat architecture hints selected the wrong same-leaf prompt.
   _resolve_prompt_path_from_architecture sorted duplicate recursive matches by
   shallowest/lexicographic without context, so a legacy flat filename matching the leaf
   in both backend/ and frontend/ returned the backend prompt for a frontend request while
   code resolved under frontend (a torn pair). It now prefers the resolving context's
   prefix (threaded from _find_prompt_file).

Extends the governing prompt, refreshes .pdd/meta (drift = 0), and adds three load-bearing
regression tests (each verified to fail without its fix).

Full suite 231 passed; broader sync/agentic/path-resolution 834 passed; ruff-critical,
compileall, git diff --check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Serhan-Asad Serhan-Asad force-pushed the fix/arch-json-prompts-dir-prefix branch from 639002e to f5bce25 Compare July 11, 2026 16:52
Serhan-Asad and others added 21 commits July 11, 2026 17:07
…t hint; R7/R10 coverage

Round-7 codex review (gpt-5.6-sol). Four findings addressed.

CODE (MAJOR) — ambiguity counting (_architecture_module_choices) compared filepath
stems case-sensitively while resolution is case-insensitive, so two case-variant
outputs undercounted and an ambiguous bare module fell through instead of raising.
Now case-insensitive (matches the round-6 resolution fix). Regression:
test_get_pdd_file_paths_case_variant_stem_outputs_raise_ambiguous.

CODE (MAJOR) — the architecture-hint prompt lookup applied the context_prefix filter
only to its recursive matches; the direct join and the final lexical fallback returned
without it, so a flat/stale hint could pair a wrong-context prompt with the requested
context's code. Every hint return path now enforces _prompt_path_has_context_prefix
when a context prefix is active (covered by the existing context-isolation suite).

PROMPT (MAJOR) — the resolved-symlink containment of architecture OUTPUT paths had no
contract rule. R7 now states it explicitly (a code filepath that resolves outside the
project after following symlinks is discarded) and maps
test_get_pdd_file_paths_rejects_symlink_architecture_escape.

PROMPT (MINOR) — mapped R10 to test_get_pdd_file_paths_rejects_degenerate_basename
(dot/trailing/duplicate-separator cases) alongside the whitespace/control test.

Refreshed both fingerprints + run report (313 passing). No drift. Resolver 313 passed /
2 skipped; architecture_sync 147; construct_paths+backward_compat+agentic_architecture 172.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…coverage

Round-8 codex review (gpt-5.6-sol). Three findings addressed.

CODE (MAJOR) — _project_relative left relative context globs un-normalized, so a
"./frontend/**" paths glob did not match the normalized project-relative architecture
filepath and sibling territory was silently missed (the prefixes branch already
stripped "./"; the globs branch did not). Relative values are now normalized via
PurePosixPath. Regression: test_filepath_matches_context_normalizes_dot_slash_glob.
(The reported Windows case-insensitive-drive comparison is a separate, non-reproducible
edge on POSIX; not changed.)

PROMPT (MINOR) — R2 had been narrowed until it merely restated R5/R6's sibling-target
prohibition while its coverage cited prompt-selection tests. R2 now states its distinct
obligation — the architecture hint must select a prompt aligned with the resolving
context and basename so prompt and code resolve under the same context — leaving
code-target ownership to R5/R6.

PROMPT/TEST (MINOR) — R13's only test exercised null; added a parametrized
test_get_pdd_file_paths_non_string_filename_uses_filepath covering number/bool/list/
object filenames (all resolve by filepath stem) and mapped it.

Refreshed both fingerprints + run report (319 passing). No drift. Resolver 319 passed /
2 skipped; architecture_sync 147; construct_paths+backward_compat+agentic_architecture 172.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round-9 codex review (gpt-5.6-sol). Two findings addressed; one assessed non-actionable.

CODE (MAJOR) — PROVEN ownership did not require a UNIQUE physical prompt owner, so a
flat/same-leaf architecture filename matching distinct prompts in two context roots was
classified as proven for each, letting both contexts claim one shared code target. It
now stays territory-guarded (ELIGIBLE) unless exactly one physical owner matches.
Load-bearing regression: test_get_pdd_file_paths_flat_same_leaf_in_two_roots_not_proven
(fails without the guard). Full suite unaffected.

PROMPT (MINOR) — R14 pinned the existence/level/timing/representation of an internal
INFO log record. Reworded to the observable log-safety behavior only: no raw,
unvalidated caller/architecture path value in any log; validated values are logged only
in escaped, normalized form.

NOT CHANGED — the reported "generate_output_path relocation is checked before relocation"
finding targets the resolving context's own trusted .pddrc output config; relocating a
bare filepath into a context's configured output dir is intended, and re-running
sibling-territory checks on a context's own config would reject valid configurations.
The untrusted-metadata threat model (architecture.json) is already validated.

Refreshed both fingerprints + run report (320 passing). No drift. Resolver 320 passed /
2 skipped; architecture_sync 147; construct_paths+backward_compat+agentic_architecture 172.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round-10 codex review (gpt-5.6-sol) — single MINOR finding. Requirement #6 still named
private `_find_prompt_file`/`construct_paths` and duplicated the Instruction #3 pointer.
Reworded to a purely behavioral statement: the resolution obligations are the stable
contract rules R1-R15 (enforced by <coverage>), with implementation strategy
unconstrained. No code/behavior change; sync_determine_operation prompt_hash refreshed;
no drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…owner + R14 contracts

Round-11 codex review (gpt-5.6-sol). Three findings addressed.

CODE (MAJOR) — territory matching compared drive-qualified Windows paths (and their
components) case-sensitively, so a drive/directory casing difference between .pddrc and
the resolved project root hid sibling ownership on Windows (a reproduced probe returned
False for a physically matching sibling root). _filepath_matches_context now folds case
throughout when the project root is a Windows path; POSIX roots stay case-sensitive
(unchanged). Regression: test_filepath_matches_context_windows_paths_are_case_insensitive.

PROMPT (MAJOR) — the <vocabulary> "proven-owner row" definition now states the unique-
physical-owner condition the round-9 code enforces (a flat/same-leaf name matching
prompts in more than one context root is heuristic, not proven), and R6 coverage now
maps the round-9 regression test_get_pdd_file_paths_flat_same_leaf_in_two_roots_not_proven.

PROMPT (MINOR) — R14 promised "normalized" logging of accepted values, but accepted
paths are logged escaped (via %r), not normalized. R14 now states the actually-
implemented guarantee: no raw unvalidated value in any log; validated values rendered
safely (escaped) so control/newline/ANSI content cannot forge or split a log line.

Refreshed both fingerprints + run report (321 passing). No drift. Resolver 321 passed /
2 skipped; architecture_sync 147; construct_paths+backward_compat+agentic_architecture 172.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rpen R8/Instruction-3

Round-12 codex review (gpt-5.6-sol). Three findings addressed.

CODE (MAJOR) — when construct_paths raises while resolving a NEW module, the convention
fallback built code/example/test paths relative to the process CWD, so a new module
resolved from a parent/sibling CWD landed its artifacts under the wrong root. The
fallback now anchors at the resolved subproject (.pddrc dir) when it differs from CWD,
staying relative when they coincide (legacy contract preserved). Regression:
test_get_pdd_file_paths_construct_paths_failure_anchors_fallback_at_subproject.

PROMPT (MAJOR) — R8 said "fail closed" for every escape, but its cited tests split:
an escaping DISCOVERY candidate is discarded and resolution continues with fallback
(rejects_symlink_prompt_discovery_escape), while the resolving context's OWN expected
prompt escaping hard-fails (nested_escaping_symlink_is_hard_failure). R8 now states both
behaviors distinctly, matching all its coverage tests.

PROMPT (MINOR) — Instruction #3 still pinned `construct_paths`, contradicting
Requirement 6's "implementation strategy unconstrained". Reworded to the observable
return shape + template/legacy-fallback behavior, no internal-call pin.

Refreshed both fingerprints + run report (322 passing). No drift. Resolver 322 passed /
2 skipped; architecture_sync 147; construct_paths+backward_compat+agentic_architecture 172.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nonicality

Round-13 codex review (gpt-5.6-sol) — single MINOR finding. The <vocabulary> "valid
output" definition cited R7-R10, but R8 governs prompt paths and R10 governed only
caller basenames, so architecture output canonicality was ambiguous and uncovered.

- "valid output" now names the checks that actually apply to output filepaths: R7
  (relative/contained/no-traversal/backslash/drive/symlink-escape), R9 (portable
  components), R10 (canonical spelling); R8 is noted as prompt-only.
- R10 now explicitly covers architecture output filepaths (the _contained_architecture_code_path
  `as_posix() != raw` canonicality check), and R11's ambiguity reference is R7/R9/R10.
- Added test_contained_architecture_code_path_rejects_noncanonical (dot segments,
  duplicate/trailing separators, no-components) and mapped it to R10.

No code change; sync_determine_operation prompt_hash + run report (328 passing) refreshed;
no drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ging)

Round-14 codex review (gpt-5.6-sol) — two MINOR coverage-mapping findings.

- R6 coverage now includes test_get_pdd_file_paths_proven_owner_honored_for_shared_target,
  the positive case verifying a proven owner keeps an unowned/shared target (its
  coverage previously listed only boundary/rejection cases).
- R14 coverage now maps the existing basename log-injection test
  (test_get_pdd_file_paths_validates_before_logging_raw_input) and a new
  test_get_pdd_file_paths_validates_language_before_logging (control/newline/ANSI-bearing
  language never reaches an INFO record before rejection), so all four R14 input sources
  have a negative logging test.

No code change; sync_determine_operation prompt_hash + run report (332 passing) refreshed;
no drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… for R12

Round-15 codex review (gpt-5.6-sol) — two MINOR findings.

- R10 now also covers non-canonical architecture prompt FILENAMES (HEAD's
  _safe_architecture_prompt_filename already rejects ./foo_Python.prompt, a//..., trailing
  slash, etc.); added test_safe_architecture_prompt_filename_rejects_noncanonical and
  mapped it to R10.
- R12's coverage now cites only the behavioral no-torn-pair test, not the parse-count
  mechanism test (which enforced a specific implementation). test_get_pdd_file_paths_parses_architecture_once
  remains in the suite as an implementation regression (per the earlier human review) but
  is no longer the rule's evidence.

No code change; sync_determine_operation prompt_hash + run report refreshed; no drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ot siblings

Round-16 codex review (gpt-5.6-sol). One MAJOR, one MINOR.

CODE (MAJOR) — the proven-owner check projected the resolved prompt's relative path
across EVERY prompt root, so a uniquely-named SIBLING prompt sitting at the same relative
path under a different context root could be misclassified as the resolved prompt's
proven owner and lend its shared code target across contexts. The projection is now
restricted to roots that ALIAS the resolving prompt root (resolve to the same directory);
sibling roots are excluded. Resolving the root directories (not the caller-influenced
prompt path) preserves the prior no-filesystem-sink hardening. Full proven-owner /
sibling / alias suite unchanged (484 passed).

PROMPT (MINOR) — R11 pinned the internal ordering ("before any prompt or fallback
resolution"), which is not observable. Reworded to the observable outcome: raise
AmbiguousModuleError rather than returning any resolved path or generating.

Refreshed both fingerprints + run report. No drift. Resolver 484-suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…alified ambiguity

Round-17 codex review (gpt-5.6-sol). Three findings addressed.

CODE (MAJOR) — a DISCOVERED architecture.json that is present but unreadable/malformed
was silently downgraded to an empty registry, so resolution proceeded at convention
fallback paths instead of the authoritative registered target. It now fails closed with
a new MalformedArchitectureError (an AmbiguousModuleError subclass, so every sync entry
point that already propagates that error fails fast); a genuine delete race
(FileNotFoundError after discovery) still degrades to no-registry. Regression:
test_get_pdd_file_paths_malformed_architecture_json_fails_closed.

CODE (MAJOR) — ambiguity detection returned [] for path-qualified basenames, but a
qualified basename matches by path-SUFFIX so more than one distinct valid output can
align (app/login/page -> app/login/page.py AND src/app/login/page.py) and was resolved
by architecture row order. _architecture_module_choices now collects distinct valid
suffix-aligned outputs for qualified basenames so the caller raises AmbiguousModuleError;
a single/zero match keeps canonical resolution unchanged (all issue_1677 tests pass).
Regression: test_get_pdd_file_paths_path_qualified_two_suffix_aligned_outputs_raise_ambiguous.

PROMPT (MINOR) — R7 said unsafe metadata causes configured-output fallback; it now says
the offending row is discarded, resolution continues with the other valid architecture
rows, and configured outputs are used only when no valid row remains (consistent with R11).

Refreshed both fingerprints + run report. No drift. 695 passed across resolver + arch +
issue_1677 + construct_paths + backward_compat + agentic_architecture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… filenames from qualified ambiguity

Round-18 codex review (gpt-5.6-sol). Three findings addressed (follow-ons to round 17).

CODE (MAJOR) — the round-17 fail-closed handled unparseable JSON but valid JSON with an
UNSUPPORTED schema (a top-level scalar, or a dict whose ``modules`` is not a list) still
flowed through extract_modules as an empty registry -> convention fallback. Schema is now
validated: a bare list or a dict are accepted (a dict MAY omit ``modules`` = legitimately
empty), but a scalar or a non-list ``modules`` raises MalformedArchitectureError.
Regressions: parametrized malformed test (scalar / non-list modules) +
test_get_pdd_file_paths_empty_registry_dict_is_not_malformed (empty dict is NOT malformed).

CODE (MAJOR) — the round-17 path-qualified ambiguity count did not exclude rows with an
unsafe architecture FILENAME (resolution already treats them ineligible), so an invalid
row could inflate the count and falsely block a valid mapping. The qualified branch now
skips unsafe-filename rows (matching the bare branch). Regression:
test_get_pdd_file_paths_path_qualified_unsafe_filename_row_does_not_block.

PROMPT (MAJOR) — R11 said "bare basename" only while its coverage cited a path-qualified
test. R11 now covers both: a bare basename (two rows -> distinct outputs) and a
path-qualified basename (two rows' outputs both path-suffix-align), with unsafe rows
(invalid filename OR output) excluded.

Refreshed both fingerprints + run report. No drift. 528 passed across resolver + arch +
issue_1677.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ry before containment

Round-19 codex review (gpt-5.6-sol). Two addressed; one declined with rationale.

CODE (MAJOR) — the round-18 schema validation accepted a modules list containing
non-object entries, which extract_modules silently discards, letting a corrupted registry
fall through to convention paths. A modules list with any non-object entry now raises
MalformedArchitectureError. Regression: parametrized malformed test gains the
`{"modules": [{...}, 42]}` case.

CODE (MINOR, perf) — the recursive prompt fallback resolved symlink containment for EVERY
*.prompt in the tree before checking the filename leaf. It now filters by the cheap
filename leaf first and pays the containment resolve only for the handful of leaf-matching
candidates (expected leaves computed once). Behavior-preserving (all nested/symlink tests
green); drops filesystem I/O proportional to the whole prompt tree.

NOT CHANGED — the "malformed .pddrc -> permissive territory" finding: a malformed .pddrc
yields no valid territory configuration, so resolution degrades to no-context-isolation
exactly as an absent .pddrc does (the "sibling" context it references is itself defined by
the file that will not parse). This graceful degradation is applied consistently at ~10
_load_pddrc_config call sites; failing closed at only the territory site would be
inconsistent, and repo-wide fail-closed on any .pddrc syntax error is out of scope.

Refreshed both fingerprints + run report. No drift. 529 passed across resolver + arch +
issue_1677.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…put vocab

Round-20 codex review (gpt-5.6-sol). Two findings addressed.

CODE (MAJOR) — path-qualified ambiguity detection matched outputs against the RAW
basename while final resolution matches against the context-relative basename (context
prefix stripped). A context-prefixed qualified basename could therefore have two
suffix-aligned outputs evade AmbiguousModuleError and resolve by row order.
_architecture_module_choices now takes the resolved context and uses the same
context-relative basename + anchor as resolution (only the fail-fast call passes it; the
example/test-stem call is unchanged). Regression:
test_get_pdd_file_paths_context_prefixed_qualified_ambiguity_uses_stripped_basename.

PROMPT (MINOR) — the "valid output" vocabulary attributed backslash / Windows-drive
rejection to R7, contradicting R7's own text (those belong to R9). The vocabulary now
scopes R7 to relative/contained/traversal/symlink-escape and points backslash/drive/
device/control rejection to R9.

Refreshed both fingerprints + run report. No drift. 530 passed across resolver + arch +
issue_1677.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… unparseable .pddrc; align unsafe hard-fail

Round-21 codex review (gpt-5.6-sol). Four findings addressed.

CODE (MAJOR) — ambiguity detection stripped the architecture filepath BEFORE containment
validation, so a trailing-space / noncanonical row (which resolution rejects) was
canonicalized and counted, falsely conflicting with a valid row. Both ambiguity branches
now validate the RAW filepath. Regression:
test_get_pdd_file_paths_trailing_space_output_does_not_block_valid_row.

CODE (MAJOR) — a present-but-unparseable .pddrc was swallowed into territory_config=None,
and _context_owned_filepath treats "no config" as permissive, so a heuristic borrow
targeting a sibling context was permitted (fail open). Territory is now marked
_TERRITORY_MALFORMED (distinct from absent), and a heuristic (non-proven) borrow is denied
when territory cannot be parsed; a proven explicit mapping is still honored. R5 documents
this. Regression: test_get_pdd_file_paths_malformed_pddrc_denies_heuristic_borrow.

CODE (MINOR) — the recursive prompt fallback hard-failed on an escaping same-leaf symlink
even under an UNRELATED directory that does not align with a path-qualified basename. The
unsafe hard-fail set is now filtered by the same basename-directory alignment as the safe
matches. Regression:
test_get_pdd_file_paths_unaligned_escaping_symlink_does_not_block_qualified_creation.

PROMPT (MINOR) — R14 pinned "escaped" as the logging mechanism; reworded to the observable
requirement (no logged value may let control/newline/ANSI content forge or split a record,
however achieved).

Refreshed both fingerprints + run report. No drift. 533 passed across resolver + arch +
issue_1677.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…established

Round-22 codex review (gpt-5.6-sol). Two findings addressed.

CODE (MAJOR) — when context_override is omitted and the basename does not encode a
context, resolved_context_name is None; _filepath_owned_by_other_context returns False for
a None context and _context_owned_filepath is permissive, so a FOREIGN heuristic row
(matched by filepath stem, not naming this module) targeting a named sibling context was
borrowed — pairing the prompt with sibling-context code (repro confirmed). Now, when the
resolving context cannot be established, a heuristic borrow whose target lies in ANY named
context is denied (fail closed) via new _filepath_in_named_context — UNLESS the row names
this module (its filename leaf equals the requested prompt filename, e.g. a new module with
no physical prompt yet), which is still honored, as are proven mappings and unowned/shared
targets. Regression:
test_get_pdd_file_paths_no_override_none_context_denies_foreign_sibling_borrow. (An initial
broad fix that derived the context from the prompt root was reverted — it returned the
catch-all `default` and broke a cross-directory proven layout.)

PROMPT (MINOR) — Instruction 3 duplicated Requirement 6's R1-R15 statement; it now carries
only the public interface + return shape and points to Requirement 6 for the contract.

Refreshed both fingerprints + run report. No drift. 497 passed across resolver + arch;
full resolver suite 350.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fail to active root

Round-23 codex review (gpt-5.6-sol). Three findings addressed.

CODE (MAJOR) — an exact architecture row that NAMES the requested module but whose prompt
is not created yet (new module), with a safe shared/unowned target, was rejected because
eligibility required context ownership or a physical proven owner — so sync fell back to a
convention path. _borrow_ownership_ok now honors a row whose filename exactly matches the
requested prompt filename (its own explicit mapping); sibling-owned and unsafe targets were
already rejected earlier, so this cannot cross contexts. Regression:
test_get_pdd_file_paths_exact_missing_prompt_row_shared_target_honored.

CODE (MAJOR) — an escaping same-leaf symlink in an UNRELATED auxiliary prompt root made
_architecture_prompt_owner's all_contained False and invalidated a unique contained owner
in the ACTIVE root, silently replacing its authoritative mapping with fallback paths. The
containment verdict is now scoped to the ACTIVE root (auxiliary-root escapes are ignored
when the active root supplies the owner; the active context's own expected prompt escaping
still hard-fails). Regression:
test_get_pdd_file_paths_auxiliary_root_symlink_escape_does_not_invalidate_active_owner.

PROMPT (MINOR) — defined "row that names this module" and "unowned/shared target" in
<vocabulary> (observable filename-match / context-territory criteria) so R5's exceptions
are unambiguous.

Refreshed both fingerprints + run report. No drift. 499 passed across resolver + arch;
full resolver suite 352.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…st qualified ambiguity

Round-24 codex review (gpt-5.6-sol). Two addressed; one declined with rationale.

CODE (MAJOR) — an absolute custom prompt root OUTSIDE any project made architecture.json
and .pddrc discovery start from that external root, silently missing the caller's project's
authoritative mapping. Config/architecture discovery now falls back to the project (CWD)
when the prompt root finds no config up-tree; a nested subproject root (which does find its
own config) still anchors at itself. Regression:
test_get_pdd_file_paths_external_absolute_prompt_root_finds_project_architecture.

CODE (MINOR, perf) — the path-qualified ambiguity branch resolved containment on disk for
every valid-looking row before the basename-suffix/extension checks. It now does the cheap
pure-string checks first and pays the filesystem containment resolve only for matching rows
(raw-filepath validation preserved). Behavior-preserving; issue_1677 ambiguity tests green.

NOT CHANGED — the "context-qualified filename not recognized as this module" finding: the
proposed leaf comparison over-matches a stale SIBLING row (frontend/credits_Python.prompt
for a backend request) and re-opens the cross-context borrow (breaks
does_not_borrow_stale_sibling_context_entry / context_isolation_holds_from_parent_cwd).
Correctly distinguishing an own context-qualified row from a sibling requires establishing
the resolving context, which is None in the reported scenario — the exact-match comparison
is retained as the safe choice.

Refreshed both fingerprints + run report. No drift. 537 passed across resolver + arch +
issue_1677; full resolver suite 353.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…anchor in arch branch; trim vocab

Round-25 codex review (gpt-5.6-sol). Three findings addressed.

CODE (MAJOR) — path-qualified ambiguity counted every suffix-aligned filepath even when the
row's PROMPT filename named a DIFFERENT module, raising a false AmbiguousModuleError before
selection could choose the uniquely named row. It now mirrors selection eligibility: a row
counts only if its filename names this module OR its filename is null/non-prompt
(filepath-stem eligible); a prompt filename naming a different module is skipped. Regression:
test_get_pdd_file_paths_qualified_foreign_named_suffix_row_no_false_ambiguity.

CODE (MAJOR) — the round-24 config_anchor fix missed the architecture branch's and other
.pddrc lookups (example/test destinations, new/existing-prompt template branches), so an
external absolute prompt root still resolved those from the external root, silently replacing
project-configured destinations with defaults. All config .pddrc discovery now threads
config_anchor (the anchor-selection check itself stays on the prompt root).

PROMPT (MINOR) — the <vocabulary> "row that names this module" / "unowned-shared target"
entries embedded ownership BEHAVIOR (honoring prompt-less rows, allowing shared targets) that
R5/R6 also state. Vocabulary is now identity-only; R6 carries the "a row that names this
module (even with no physical prompt yet) keeps an unowned/shared target unless sibling-owned"
permission.

Refreshed both fingerprints + run report. No drift. 537 passed across resolver + arch +
issue_1677; full resolver suite 354.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on); end-to-end R10 coverage

Round-26 codex review (gpt-5.6-sol). Two findings addressed.

CODE (MAJOR) — a whitespace-only architecture filename bypassed the ambiguity unsafe-filter
(the guard required filename.strip() to be truthy) and was counted, even though selection
treats it as INELIGIBLE (_safe_architecture_prompt_filename fails) — a false
AmbiguousModuleError could block a valid uniquely-named module. Both ambiguity branches now
skip a non-empty string filename that fails validation (whitespace included); an EMPTY string
and null/non-string stay filepath-stem-eligible, matching selection and R13. Regression:
test_get_pdd_file_paths_whitespace_filename_row_does_not_inflate_ambiguity.

PROMPT/TEST (MINOR) — R10's coverage cited only private-helper tests for architecture filename
and output-path canonicality. Added an end-to-end get_pdd_file_paths test asserting a
noncanonical output (dot segment) is rejected and resolution falls back to a canonical path,
and mapped it to R10.

Refreshed both fingerprints + run report. No drift. 538 passed across resolver + arch +
issue_1677; full resolver suite 356.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Coordination note from the #1998 stack orchestrator (stack/e2e-staging-v2).

Overlap detected: both this PR and #1998 modify pdd/sync_determine_operation.py — specifically the get_pdd_file_paths path-resolution area (and its prompt/test):

These are distinct, non-duplicative concerns, but they touch the same function and will conflict at merge to main for whichever lands second. Neither reimplements the other, so no duplicated implementation to unwind — just a textual merge resolution.

Flagging so the manager sequences the two and the second-lander resolves the get_pdd_file_paths conflict cleanly rather than either side silently reverting the other. Happy to rebase #1998's hunk onto #1971 (or vice-versa) once landing order is fixed.

Serhan-Asad and others added 3 commits July 12, 2026 11:39
Independent review found R7-R10 containment was applied only to
architecture.json code filepaths, not to .pddrc-derived output
destinations. A .pddrc generate_output_path / example_output_path /
test_output_path / outputs.<artifact>.path with parent traversal (or an
away-pointing absolute path) let get_pdd_file_paths return code/example/
test targets outside the project, and the non-architecture fallback even
mkdir'd a probe directory out of tree during --dry-run resolution.

Add UnsafeOutputPathError + a multi-root project-boundary containment
(_resolution_containment_roots / _output_path_within_project /
_ensure_output_within_project). Every get_pdd_file_paths return is routed
through _finalize_output_paths, and the temp-probe mkdir/touch is skipped
when the code target escapes, so a configured output that resolves outside
every legitimate boundary fails closed instead of writing out of tree. The
boundary is the set of legit anchors (config/prompt-root anchor, CWD, and
the .pddrc/architecture project root) so parent/sibling-CWD layouts that
anchor outputs at CWD are still honored.

Prompt: add observable contract rule R16 + "project boundary" vocabulary +
R16 coverage. Refresh sync_determine_operation .pdd/meta fingerprints
(drift = 0). Regression: 4 escape scenarios (arch example/test, arch
generate, arch outputs template, non-arch generate + no out-of-tree mkdir),
an in-project positive control, and two real `pdd sync --dry-run` Click
CliRunner boundary tests (nested-context resolution + escaping-.pddrc
negative control) covering the get_pdd_file_paths-only test gap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y (R16)

Independent round-2 review (gpt-5.6-sol xhigh) found three residual
trust-boundary gaps on top of the R16 containment landed in dfd737d:

1. BLOCKER — SyncLock built its lock filename from raw basename/language and
   is acquired BEFORE get_pdd_file_paths validates them, so a traversal-bearing
   language (e.g. "/../../tmp/victim") could mkdir/touch/unlink a .lock file
   out of tree. Lock names are now built from a sanitized, separator-free token
   (_safe_lock_component) and asserted under the locks dir.

2. BLOCKER/HIGH — .pddrc output destinations (generate/example/test_output_path
   and outputs.<artifact>.path templates) received containment only, so a
   parent-traversal ("../victim/", CWD-dependent) or a non-portable destination
   (reserved device, NTFS ADS colon, drive marker, control char) was accepted.
   They now get the same portable/canonical validation as architecture code
   filepaths, applied to the RAW configured value up front
   (_reject_unsafe_pddrc_output_config, before any branch-local `except
   ValueError` can swallow the error and before Path.resolve() normalizes away a
   `..`) — making the trust boundary CWD-independent. Multi-root containment
   remains as defense in depth.

3. R16 prompt pointers now read R1-R16, and the negative CLI control asserts the
   run is not-ok with an out-of-tree path-resolution failure (not just absence
   of a stray file).

Also regenerate both units' .pdd/meta fingerprints and run reports so the
committed provenance matches the tree exactly (metadata_sync include-dep hash,
prompt hashes, run-report test hashes); `pdd sync <unit> --dry-run` reports zero
drift for both. Adds lock-confinement, non-portable/traversal .pddrc, and mutable
entrypoint regression tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Serhan-Asad and others added 4 commits July 12, 2026 13:02
… prompt (R16/R8/R17)

Independent round-3 review (gpt-5.6-sol xhigh) found the round-2 R16 fix
still had reachable trust-boundary gaps; all addressed:

1. BLOCKER — `.pddrc outputs.prompt.path` could override the discovered prompt
   with a foreign/absolute path; `_finalize_output_paths` only validated
   code/example/test. It now also holds the returned prompt to the prompts root
   (R8), resolving both sides so an in-root symlink alias is preserved.

2. BLOCKER — the multi-root containment admitted the process CWD, so a benign
   relative `.pddrc` output (e.g. `generate_output_path: "sibling-output/"`, no
   `..`) resolved from a PARENT cwd escaped the governing project. Output
   authority now comes solely from PROVENANCE: `_governing_output_root` returns
   the single governing `.pddrc`/`architecture.json` directory, CWD-anchored
   outputs are RE-ANCHORED under it (`_reanchor_output_to_root`) before the
   containment check, and the probe mkdir is skipped for anything outside it.

3. HIGH — a nearer descendant `.pddrc` selected by construct_paths bypassed the
   early raw gate, and a missing-prompt broad `except Exception` swallowed the
   containment error. `_ensure_output_within_root` now also rejects non-portable
   components on the RESOLVED path (catches a nearer-config `CON/` that survives
   resolve() on POSIX), and `except AmbiguousModuleError: raise` guards precede
   the missing-prompt and construct_paths broad fallbacks so an out-of-tree
   target fails closed instead of degrading to an unvalidated path.

Prompt doctrine: add observable R17 (lock confinement) and map the lock tests to
it (was mislabeled R2); state R2 as the observable obligation, not the sanitizer
regex; update pointers to R1-R17; redefine the "project boundary" vocabulary as
the single provenance-based root (no CWD authority). Regenerate both units'
fingerprints + run reports (drift 0). Adds outputs.prompt.path, parent-CWD
re-anchor, nearer-.pddrc, and fail-closed missing-prompt regression tests.

Rejected round-3 low finding (remove the architecture parse-count test): the
manager explicitly asked to keep it as an implementation regression for R12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…utputs (R16)

Round-4 independent review (gpt-5.6-sol xhigh) follow-ups:

- HIGH: an explicit absolute `.pddrc` output pointing outside the governing
  project was silently re-anchored INTO a wrong in-project location by the
  round-3 re-anchor. Absolute configured values that resolve outside the project
  root now fail closed at the raw gate (`_configured_output_escapes_root`); only
  relative values (indistinguishable from a legitimate CWD expansion once
  resolved) are re-anchored.
- HIGH: control/format characters in a resolved output component are now rejected
  by the resolved-path check too (`_contains_disallowed_path_text`), covering a
  value that reaches a sink via a nearer descendant `.pddrc` construct_paths
  selected.
- Doctrine: R16 restated as the observable rejection outcome (no "RAW before
  resolve" sequencing); R1 now defines the supported architecture.json shapes so
  "malformed" is unambiguous.

Adds absolute-escape and control-component regression tests; fingerprints
regenerated (drift 0 both units).

Rejected (documented): the "external prompt-root appoints its own project
authority" finding is the intended nested-subproject behavior (Requirement 6 /
R3 / R7, covered by test_get_pdd_file_paths_external_absolute_prompt_root_finds_
project_architecture) — a caller pointing prompts_dir at a tree with its own
config anchors there by design, and it is not an out-of-tree escape. The
metadata-deletion wording the reviewer flagged in Requirement 1 vs 3.7 is
pre-existing on main and belongs to the conflict/metadata subsystem, not this
PR's path-resolution scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…back (R16)

Round-5 independent review (gpt-5.6-sol xhigh) found one blocker (its other
two candidate findings — substring context selection and torn arch snapshot —
were confirmed non-issues):

- BLOCKER: a truthy NON-STRING `.pddrc` output value (e.g. `generate_output_path:
  123`) slipped past `_configured_output_string_is_unsafe` (which treated every
  non-string as "absent"), then raised `AttributeError` in `str`-only path
  handling; the outermost `except Exception` returned a basename convention path
  resolved against the (parent) CWD WITHOUT the finalizer — an out-of-project
  write target. Now a present non-string value is malformed and fails closed, and
  the outer last-resort fallback is routed through `_finalize_output_paths` too
  (re-anchored under the governing root + contained), so no exception-driven
  fallback can return an uncontained path.

Adds non-string generate_output_path (parent-CWD) and non-string outputs template
regression tests; fingerprints regenerated (drift 0 both units).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ompts_dir (R16)

Round-6 independent review (gpt-5.6-sol xhigh) follow-ups:

- P1: discovered `test_files` (globbed siblings handed to the test runner) were
  re-anchored but not contained, so a `test_<name>*.py` symlink pointing outside
  the project could be executed. `_finalize_output_paths` now drops any
  test_files entry that resolves outside the governing root (keeping the
  validated primary test path), so an out-of-project file is never returned.
- P1: a nearer descendant `.pddrc` (governing the resolved prompt's own subtree)
  was not raw-validated — its `generate_output_path: "safe/../src/"` normalized
  the `..` away instead of failing closed. The finalizer now re-validates the
  `.pddrc` nearest the resolved prompt (R16 rejects `..` even when it normalizes
  back in-project).
- P2: a present non-string `.pddrc` `prompts_dir` degraded (via AttributeError in
  the string-only prefix extractor) to a wrong convention path; it now fails
  closed in the up-front gate.
- Made the mutable-entrypoint lock test load-bearing (captures the constructed
  SyncLock path and asserts confinement, independent of cleanup timing).

Adds test_files-symlink, nearer-.pddrc-traversal, and non-string-prompts_dir
regression tests; fingerprints regenerated (drift 0 both units).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Honor architecture.json filepath for prompts in nested .pddrc contexts

4 participants