Skip to content

GitHub issues review: proposal + fixes for #176, #181 (scan), #178 aliases, #173 ownership guard - #182

Open
ohjonathan wants to merge 7 commits into
mainfrom
claude/ontos-github-issues-review-s88zbm
Open

GitHub issues review: proposal + fixes for #176, #181 (scan), #178 aliases, #173 ownership guard#182
ohjonathan wants to merge 7 commits into
mainfrom
claude/ontos-github-issues-review-s88zbm

Conversation

@ohjonathan

Copy link
Copy Markdown
Owner

Summary

Reviews all ten open issues (#149, #158, #165, #173#178, #181) against the v5.0.2 codebase — every verdict adversarially verified, every bug claim reproduced end-to-end — and lands the four highest-value remediation slices plus a consolidated disposition/design document (docs/specs/project-ontos-github-issues-review-2026-07-proposal.md).

Changes

Related Issues

Fixes #176. Advances #181, #178, #173 (slice checkboxes; issues stay open per their remaining scope). Design dispositions recorded for #174, #175, #177, #165; board-maintenance plan for #158/#149 in proposal §3.9.

Note: per plan §11.8, #176 closes only after a released artifact passes the reproduction — auto-close on merge is earlier than that gate; reopen or defer closure if you want to hold to it strictly.

Checklist

  • Tests pass (1613 passed; baseline was 1568)
  • Ontos activation/validation checked (ontos activate: usable, 234 docs, 0 errors, 0 warnings; ontos doctor: 10 passed, 0 failed)
  • Documentation updated (Ontos_Manual config section, proposal spec)
  • CHANGELOG.md updated (Unreleased section with behavior notes)

Testing

Full suite green at every commit. New/rewritten coverage: §11.2 acceptance matrix for bare deps (tests/core/test_graph.py::TestBareRootFileDependencies), nested-archive skip semantics (tests/io/test_scan_skip_patterns.py), alias config validation + repair round-trips incl. CLI end-to-end (tests/core/test_config_phase3.py, tests/core/test_frontmatter_repair.py, tests/commands/test_agentic_activation_resilience.py), ownership + clone-equivalence regression (tests/core/test_instruction_ownership.py). End-to-end manual verification: issue #176's exact scenario flips from broken_link/exit 1 to allowlisted external_file_dependency/exit 0 across activate, doctor, and link-check; a hand-authored AGENTS.md survives ontos maintain and ontos map --sync-agents byte-identical.

🤖 Generated with Claude Code

https://claude.ai/code/session_019poEo7Q6nsQkayuZ7v5dP5


Generated by Claude Code

claude added 5 commits July 16, 2026 23:45
A depends_on entry such as 'pyproject.toml' (no separator, no .md suffix)
bypassed filesystem resolution entirely in _resolve_depends_on_path and was
reported as a hard broken_link even when the file existed at the workspace
root and was listed in validation.allowed_external_dependency_paths, while
'./pyproject.toml' resolved as an external file dependency.

Bare tokens now get a stricter probe than explicit path spellings:

- active document IDs keep deterministic precedence (unchanged);
- only existing regular files count — a bare token matching a directory
  stays a broken link;
- the existing symlink-containment and loaded-path/inode collision logic
  applies unchanged;
- when the workspace-root and declaring-doc candidates name two different
  files, resolution fails closed instead of picking by ordering accident;
- the allowlist matches the resolved workspace-relative path, so bare and
  ./ spellings classify identically.

Explicit-path behavior, including its characterized handling of
directories, is byte-for-byte unchanged. All consumers (activate, doctor,
map, link-check, hooks, MCP) share this resolver via build_graph.

Acceptance matrix from the v5 remediation plan §11.2 covered in
tests/core/test_graph.py::TestBareRootFileDependencies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019poEo7Q6nsQkayuZ7v5dP5
The default skip pattern 'archive/*' was matched with a right-anchored,
segment-local Path.match plus a full-path fnmatch, so it only excluded
files DIRECTLY under an archive/ directory. The nested layout that
'ontos consolidate' itself writes by default (docs/archive/logs/...) was
scanned straight back into the live graph — the opposite of the archive
contract — and every nested file under node_modules/ (e.g. package
READMEs one level down) leaked into scans the same way.

scan_documents now additionally matches each skip pattern against every
path-segment suffix with fnmatch (where '*' crosses '/'), so directory
patterns exclude the whole subtree. Basename patterns such as
'_template.md' stay exact-match only, and a file literally named
archive.md is still scanned.

Behavior note: repositories with nested archive layouts (or markdown
under node_modules/) will correctly see lower document counts; library
scope now excludes .ontos-internal/archive/ like every other scope.

Found while reviewing GitHub issue #181 — this slice delivers the
issue's core 'archive out of the live graph' premise, which was not
actually true for the layout consolidate produces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019poEo7Q6nsQkayuZ7v5dP5
…mapping (#178)

Mature repositories carry project vocabulary ('runbook',
'provider_limited_fallback_complete', the ubiquitous agent-emitted
'in-progress') that the hard-coded repair tables cannot map, leaving
maintain --fix-frontmatter-enums reporting invalid values with zero
repairable — and one unresolved value blocks the entire apply.

- New [frontmatter.aliases.type] / [frontmatter.aliases.status] tables in
  .ontos.toml declare explicit project mappings. Validation is fail-closed
  at config load: targets must be canonical enum values ('unknown' is never
  a valid target, so chains/cycles are impossible by construction), alias
  keys must not themselves be canonical, keys are lowercased with
  case-normalized duplicates rejected.
- Built-in 'in-progress' -> 'in_progress' repair (v5 remediation plan
  §11.4): the canonical status existed but its most common spelling had no
  mapping.
- Config aliases win over built-ins; every repair edit now reports its
  mapping source ('built-in' | 'config') in dry-run/apply JSON.
- Threaded through both consumers of build_enum_repair_plan: maintain
  --fix-frontmatter-enums and doctor --frontmatter.
- Manual documents the new tables and the forward-compatibility caveat:
  older installs hard-reject unknown config sections, so repositories
  adopting [frontmatter] should pin [ontos].required_version.

Existing guarantees (plan-first deterministic JSON, original_type/
original_status preservation, formatting/BOM/CRLF safety, clean-git guard,
refusing partial applies) are reused unchanged.

Deferred per the issue review: custom types extending canonical types and
custom statuses with lifecycle classes — a cross-cutting refactor of the
str-Enum normalization boundary (~98 call sites) that original_* retention
does not require.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019poEo7Q6nsQkayuZ7v5dP5
…ess (#173)

AGENTS.md freshness was a pure mtime comparison in both doctor and
maintain. That signal is reproducibly wrong across clones, worktrees, and
touch-only changes: two checkouts of the same tree report opposite
freshness depending on checkout write order. Worse, maintain's sync task
acted on the false signal with a forced regeneration — a plain
'ontos maintain' in a fresh clone replaced a fully hand-authored AGENTS.md
with generated content (only a .bak survived).

- classify_instruction_ownership() distinguishes ontos_owned (generation
  header present), managed_block (USER CUSTOM markers only), and
  user_managed (neither). Unreadable files fail closed to user_managed.
- maintain's sync task, its staleness condition, and map --sync-agents all
  refuse to touch a file that is not ontos_owned, with an explicit
  'run ontos agents --force to adopt' message. Explicit adoption via
  'ontos agents --force' is unchanged.
- Staleness for Ontos-owned files is now decided by rendering the expected
  content and comparing through the existing timestamp-normalizing
  semantic comparator — content, not mtime. Identical trees now agree on
  freshness at any absolute path or device.
- The touch_on_unchanged hack (touching AGENTS.md to game the mtime
  signal) is retired with the signal it gamed; a semantic no-op leaves
  bytes AND metadata untouched.
- doctor reports ownership instead of false staleness for non-Ontos files.

Mtime-pinned tests rewritten to pin the content contract, including a
clone-equivalence regression test mirroring the reproduced failure.

Remainder of #173 (portable graph/config digests, activation receipts) is
designed in docs/specs/project-ontos-github-issues-review-2026-07-proposal.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019poEo7Q6nsQkayuZ7v5dP5
…ted artifacts

- docs/specs/project-ontos-github-issues-review-2026-07-proposal.md:
  verdicts and dispositions for all ten open issues (#149, #158, #165,
  #173-#178, #181) — every verdict adversarially verified against v5.0.2
  code with end-to-end reproductions of the bug claims — plus designs for
  the deferred slices (#175 map --check/merge-check, #177 wikilink
  namespaces, #174 finding IDs and findings diff, #173 digest core,
  #181 archive manifest/verify, #165 audit registry) and the cross-cutting
  contract constraints binding future work.
- CHANGELOG.md: Unreleased entries for the four implemented slices with
  behavior notes.
- Session log per the Ontos session-end ritual; Ontos_Context_Map.md and
  AGENTS.md regenerated via ontos map --sync-agents (activation is clean:
  234 docs, 0 errors, 0 warnings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019poEo7Q6nsQkayuZ7v5dP5

Copy link
Copy Markdown
Owner Author

Review outcome: request changes

I reviewed the full issue set, the merged v5 remediation plan, the implementation at 0acd0bf, CI, the added tests, and adversarial cases beyond the committed fixtures.

The direction is valuable and the PR fixes real bugs, but I do not think PR #182 should merge as-is. Several safety, portability, and compatibility claims fail under adversarial testing.

Blocking findings

  1. P1 — The scan fix can exclude an entire repository

The new skip matcher walks suffixes of the absolute filesystem path and applies fnmatch. A checkout beneath an ancestor directory named archive, node_modules, pycache, or another configured skip name can therefore scan zero documents.

Reproduced:

  • Base scans /…/archive/project/docs/keep.md.
  • PR head returns no documents.
  • End-to-end activation changes from usable to E_ACTIVATION_UNUSABLE.

It also silently makes custom patterns recursive: reviews/.md excludes reviews/team/nested.md, and reviews//draft.md can exclude deeper paths than requested.

Code:

def _matches_path_suffix(md_file: Path, pattern: str) -> bool:
"""Match a skip pattern against every path-segment suffix.
(#181) ``Path.match`` is right-anchored and segment-local, so a directory
pattern such as the default ``archive/*`` only matched files DIRECTLY
under an ``archive/`` directory — the nested layout that
``ontos consolidate`` itself writes (``docs/archive/logs/…``) was scanned
straight back into the live graph, as was every nested file under
``node_modules/*``. Matching the pattern against each segment suffix with
``fnmatch`` (where ``*`` crosses ``/``) makes a directory pattern exclude
the whole subtree while leaving basename patterns like ``_template.md``
exact-match only.
"""
normalized = pattern.replace("\\", "/")
parts = md_file.parts
return any(
fnmatch("/".join(parts[start:]), normalized)
for start in range(len(parts))
)

Match only workspace- or scan-root-relative paths. Define segment-local * semantics and explicit recursive syntax such as /**.

  1. P1 — AGENTS.md ownership remains unsafe

Any line beginning “Generated by Ontos v” anywhere in a file is accepted as proof of whole-file Ontos ownership.

I reproduced a hand-authored AGENTS.md containing that text as an example being classified ontos_owned and selected for implicit replacement. A backup helps recovery, but does not meet the “never overwrite user-owned instructions” contract.

Code:

_GENERATED_HEADER_TIMESTAMP_RE = re.compile(
r"^(Generated by Ontos v.+ on )\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC$"
)
_GENERATED_BY_ONTOS_RE = re.compile(r"^Generated by Ontos v", re.MULTILINE)
_USER_CUSTOM_MARKER = "<!-- USER CUSTOM -->"
# (#173) Ownership classes for instruction files. Only `ontos_owned` files
# may be regenerated implicitly (maintain sync task, map --sync-agents);
# everything else requires an explicit `ontos agents --force`.
OWNERSHIP_ONTOS_OWNED = "ontos_owned"
OWNERSHIP_MANAGED_BLOCK = "managed_block"
OWNERSHIP_USER_MANAGED = "user_managed"
OWNERSHIP_MISSING = "missing"
def classify_instruction_ownership(path: Path) -> str:
"""(#173) Classify who owns an instruction file.
Returns one of:
- ``ontos_owned``: carries the ``Generated by Ontos v…`` header — the
whole file is an Ontos-generated artifact (its USER CUSTOM section is
preserved on regeneration).
- ``managed_block``: no generation header, but contains USER CUSTOM
markers — a hand-maintained file that embeds Ontos-managed content.
- ``user_managed``: neither marker — Ontos did not write this file and
must never overwrite it implicitly.
- ``missing``: the file does not exist.
Unreadable files classify as ``user_managed`` (fail closed: when
ownership cannot be proven, Ontos does not own it).
"""
if not path.exists():
return OWNERSHIP_MISSING
try:
content = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return OWNERSHIP_USER_MANAGED
if _GENERATED_BY_ONTOS_RE.search(content):
return OWNERSHIP_ONTOS_OWNED

Require an exact, fixed-position, versioned ownership sentinel plus structural validation. USER CUSTOM markers should not imply Ontos ownership; real managed blocks need separate ONTOS MANAGED START/END markers.

  1. P1 — Worktree freshness is still branch-dependent

Generated instructions embed the current branch, while semantic comparison normalizes only timestamps.

At identical HEAD:

  • The original branch was fresh.
  • A feature-branch worktree was stale.
  • Routine maintain queued an AGENTS.md rewrite.

The freshness API also reads the passed repository root but regenerates expected content from the process CWD, allowing cross-repository false staleness.

Code:

result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True,
text=True,
timeout=5,
cwd=repo_root,
)
if result.returncode == 0:
stats["current_branch"] = result.stdout.strip()

Comparator:
def is_agents_semantically_stale(
repo_root: Path,
scope: Optional[str] = None,
) -> bool:
"""(#173) Decide AGENTS.md staleness by content, not mtime.
Renders the content Ontos would write right now (preserving the existing
USER CUSTOM section) and compares it against the file through the
timestamp-normalizing semantic comparator. Two checkouts of the same
tree therefore agree on freshness regardless of checkout write order,
clone time, or touch-only changes — the mtime signal was reproducibly
wrong across clones and worktrees.
Raises whatever ``generate_agents_content`` raises; callers surface the
failure instead of guessing.
"""
agents_path = repo_root / "AGENTS.md"
existing = agents_path.read_text(encoding="utf-8")
expected = generate_agents_content(existing, scope=scope)
return _without_generated_instruction_timestamps(
existing
) != _without_generated_instruction_timestamps(expected)

Branch, HEAD, dirty state, root basename, and timestamps should not participate in portable committed artifact bytes. Keep checkout binding in activation output or a separate receipt.

  1. P2 — Alias preservation and older-client compatibility claims are false

A configured alias with a quoted # value can corrupt the preserved original value. For example, status: "qa#blocked" was repaired successfully but produced original_status: '"qa' rather than qa#blocked, because the lexical extractor treats # inside the quoted YAML scalar as a comment.

Code:

def _apply_file_edits(path: Path, edits: Sequence[EnumRepairEdit]) -> str:
original = read_utf8_for_mutation(path)
parsed, _ = parse_frontmatter_content(original.removeprefix("\ufeff"))
if not parsed:
return original
updates: Dict[str, Any] = {}
for edit in sorted(edits, key=lambda item: item.field):
if edit.new_value is None:
continue
if edit.original_field not in parsed:
match = re.search(
rf"(?m)^[ \t]*{re.escape(edit.field)}[ \t]*:[ \t]*([^#\r\n]*?)"
rf"[ \t]*(?:#[^\r\n]*)?\r?$",
original,
)
raw_value = match.group(1).strip() if match else edit.old_value
if len(raw_value) >= 2 and raw_value[0] == raw_value[-1] and raw_value[0] in {'\"', "'"}:
raw_value = raw_value[1:-1]
updates[edit.original_field] = raw_value
updates[edit.field] = edit.new_value

Default config serialization also always emits an empty [frontmatter.aliases] table. Ontos v5.0.2 rejects that section even when aliases are unused. Adding required_version >=5.1 does not help because the old client rejects the unknown section before checking the version.

Config serialization:

@dataclass
class FrontmatterConfig:
"""[frontmatter] section.
(#178) ``aliases`` holds workspace-declared enum-repair mappings keyed by
field name (``type``/``status``); each table maps a project alias to a
canonical DocumentType/DocumentStatus value. Keys are normalized to
lowercase at load time and validated fail-closed: targets must be
canonical (never ``unknown``) and keys must not themselves be canonical
values, so alias chains and cycles are impossible by construction.
"""
aliases: Dict[str, Dict[str, str]] = field(default_factory=dict)
@dataclass
class OntosConfig:
"""Root configuration object."""
ontos: OntosSection = field(default_factory=OntosSection)
paths: PathsConfig = field(default_factory=PathsConfig)
scanning: ScanningConfig = field(default_factory=ScanningConfig)
validation: ValidationConfig = field(default_factory=ValidationConfig)
workflow: WorkflowConfig = field(default_factory=WorkflowConfig)
hooks: HooksConfig = field(default_factory=HooksConfig)
mcp: McpConfig = field(default_factory=McpConfig)
frontmatter: FrontmatterConfig = field(default_factory=FrontmatterConfig)
def default_config() -> OntosConfig:
"""Create default configuration."""
return OntosConfig()
def config_to_dict(config: OntosConfig) -> dict:
"""Convert config dataclass to dict for TOML serialization."""
return asdict(config)

There is also a contract conflict: configured aliases can override built-ins—for example in-progress to complete—while the issue acceptance language calls for conflicts to fail closed.

For a patch release, ship only the built-in alias. Move workspace-configured vocabulary to v5.1 after designing a compatible extension mechanism.

  1. P2 — [Bug] Bare workspace-root files bypass dependency path resolution #176 ambiguity still produces false diagnostics

The core bare-root dependency bug is fixed, but ambiguity is collapsed into the same null result as “missing.” The caller then says a dependency does not exist and recommends creating it even when two candidates exist. A loaded-document match can also return before inspecting the second candidate.

Code:

resolved.relative_to(workspace_root_resolved)
except ValueError:
continue
resolved_doc_id, ambiguous = loaded_paths.resolve(candidate, resolved)
if resolved_doc_id is not None:
return resolved_doc_id, None, None
if ambiguous:
# A case-only or physical-file collision cannot be resolved
# safely. Fall through to a broken dependency rather than
# selecting an arbitrary document or misclassifying it as an
# out-of-scope file.
return None, None, None
if explicit_path:
if candidate.exists():
return None, candidate, resolved
continue
# Bare-token probe: regular files only. A bare token matching a
# directory (e.g. 'docs') stays a broken link — it is far more
# likely a typo'd document ID than an intentional directory edge.
if candidate.is_file():
identity: object = resolved
try:
stat_result = candidate.stat()
identity = (stat_result.st_dev, stat_result.st_ino)
except (OSError, ValueError):
pass
if identity not in bare_seen:
bare_seen.add(identity)
bare_matches.append((candidate, resolved))
if not explicit_path and len(bare_matches) == 1:
candidate, resolved = bare_matches[0]
return None, candidate, resolved
# Zero bare matches -> broken link; two distinct files for the same bare
# token -> fail-closed ambiguity (also broken link) per #176.
return None, None, None

Return a typed resolution result such as document, external_file, missing, ambiguous, escape, or unsupported. Ambiguity diagnostics should list relative candidates and require an explicit path.

  1. P2 — The broader freshness problem remains unresolved

MCP cache freshness still treats (mtime_ns, size) as authoritative.

I rewrote a file with same-length content and restored its original mtime. MCP retained revision 1 and served the old content.

Code:

def _is_stale(self, state: SnapshotCacheState) -> bool:
current_doc_paths = self._scan_canonical_document_keys()
if current_doc_paths != state.tracked_doc_path_keys:
return True
for key in state.tracked_doc_path_keys:
rel_path = key.split("::", 1)[1]
current = self._stat_fingerprint(self.workspace_root / rel_path)
if current != state.fingerprints.get(key):
return True
current_extra_fingerprints = self._describes_fingerprints(
state.snapshot,
state.tracked_doc_path_keys,
)
current_extra_fingerprints.update(self._validation_input_fingerprints())
tracked_extra_keys = set(state.fingerprints) - set(state.tracked_doc_path_keys)
if tracked_extra_keys != set(current_extra_fingerprints):
return True
for key, current in current_extra_fingerprints.items():
if current != state.fingerprints.get(key):
return True
return False

Adding digests to generated artifacts is insufficient while another surface continues using mutable filesystem metadata as its source of truth.

Claim verification

Claim Result
All current GitHub issues inventoried Verified: exactly ten
#149 has 20 of 21 original findings resolved Verified
#176 core bare-root bug fixed Verified end-to-end
#176 fully fails closed with trustworthy diagnostics Not verified; ambiguity remains wrong
#181 prevents intended nested archive leakage Verified
#181 is portable across checkout locations Disproved
Built-in and configured alias happy paths Verified
Alias application always preserves original values Disproved
Older clients receive a useful version-pin error Disproved
Ordinary plain user instruction files fail closed Verified
Non-Ontos files can never be implicitly replaced Disproved
Touch-only changes do not cause staleness Verified
Worktrees no longer cause staleness Disproved
Managed-block synchronization is implemented Not implemented; classified and skipped
Frozen paths remain diagnosed but unedited False; skip_patterns removes them from diagnosis
Full PR suite 1,613 passed, 6 warnings
Added tests 45 over base’s 1,568 collected tests
GitHub CI All six checks passing at review time
Diff hygiene git diff --check passed

The passing suite is useful evidence, but it does not cover the checkout-location, different-branch worktree, quoted-# scalar, older-client parser-order, or same-size/same-mtime cache cases above.

Corrections to the issue proposal

The merged remediation plan explicitly limits the v5.0.3 slice more narrowly:

### 11.5 Explicit exclusions
v5.0.3 does not include:
- custom vocabulary configuration;
- a new public document model;
- body namespaces or path policy;
- stable finding IDs or baselines;
- generated-artifact ownership changes;
- archive deletion or migration;
- distributed coordination; or
- removal of #149 compatibility names or isolation of the non-log names beyond
the mandatory delegation slice in §11.3.

Recommended architecture

The deeper opportunity is to stop treating map generation, doctor, MCP, merge checking, portfolio discovery, and agent instructions as separate filesystem readers.

Create one immutable WorkspaceAnalysis snapshot containing:

  • A stable configured workspace ID
  • Coherently captured source bytes
  • Effective configuration, vocabulary, policy, and rule digests
  • Graph and finding digests
  • Explicit multiplicity and provenance
  • Separate Git/checkout binding
  • A portable session or handoff receipt

Map, doctor, MCP, audit, portfolio, and merge-check should become pure consumers of that snapshot.

For multi-repository and multi-device use, portable catalogs should be content-addressed and support references such as ontos://workspace-id/document-id. Absolute paths, checkout basenames, branches, mtimes, and dirty state belong in local binding metadata—not semantic identity.

Recommended disposition

Split this into three PRs:

  1. v5.0.3 correctness patch: corrected [Bug] Bare workspace-root files bypass dependency path resolution #176 resolution, workspace-relative [FEATURE] External archive backends for integrity-bound cold storage #181 matcher, built-in alias only, and narrowly scoped instruction safety if the release plan is explicitly amended.
  2. v5.1 semantic foundation: configured vocabulary, true frozen repair scopes, canonical findings, receipts, and authoritative registry.
  3. Revised architecture proposal: merge safety, catalogs, external archives, cross-repo references, and coherent snapshot consumption.

The PR contains worthwhile work, particularly the core #176 fix and intended nested archive fix, but the blockers above should be addressed before merge.

claude added 2 commits July 17, 2026 05:58
All five code findings from the review reproduced and are fixed with
regression tests pinning each reproduction:

1 (P1, scan): the segment-suffix skip matcher walked ABSOLUTE path
  suffixes, so a checkout under an ancestor directory named archive/
  node_modules/__pycache__ scanned zero documents, and fnmatch's
  separator-crossing '*' silently made custom patterns recursive
  (reviews/*.md excluded reviews/team/nested.md). Rewritten: patterns
  match contiguous runs of workspace-relative (or scan-root-relative)
  segments, '*' is segment-local, '/**' is explicit subtree syntax, and
  ancestors outside the repository never participate.

2 (P1, ownership): 'Generated by Ontos v' anywhere in a file conferred
  ownership, so a hand-authored file quoting the phrase was eligible for
  implicit replacement. Ownership now requires the byte-exact versioned
  header line in its structural template position (file head, preceded
  only by known template head lines); anything else fails closed to
  user_managed with the explicit --force adoption path.

3 (P1, staleness): the current branch is checkout-local state and is now
  normalized out of the semantic comparison, so two worktrees at the same
  HEAD agree on freshness; generate_agents_content and
  is_agents_semantically_stale take an explicit repo_root so freshness is
  never computed against the process CWD's repository.

4 (P2, aliases): original_* extraction is quote-aware ('#' inside a
  quoted scalar is content; unquoted '#' opens a comment only after
  whitespace; malformed quoting falls back to the parsed value);
  config_to_dict omits an empty [frontmatter] section so defaults stay
  readable by <=5.0.2; a configured alias redefining a built-in mapping
  to a different target is a fail-closed config conflict; the manual's
  compatibility wording corrected (old clients reject the unknown section
  before required_version is evaluated).

5 (P2, #176 ambiguity): bare-token resolution returns a typed result,
  both candidate bases are evaluated before deciding (a loaded-doc match
  cannot shadow a different file at the other base), and ambiguity emits
  an explicit diagnostic naming the workspace-relative candidates
  (reason: ambiguous_bare_candidates) instead of a false 'does not
  exist'.

Finding 6 (MCP mtime-fingerprint cache freshness) is acknowledged and
tracked as the #173 remainder — it must land batched with the pinned
freshness_mode contract and MCP schema/parity updates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019poEo7Q6nsQkayuZ7v5dP5
Records the adversarial-review dispositions in the proposal spec (fixed
findings 1-5, tracked finding 6, and accepted corrections to the #174/
#175/#177/#181/#158 designs including the finding_key/finding_id split,
merge_safe=false on source conflicts, the unresolved-wikilink downgrade
hazard, manifest-vs-catalog scope, and the #158 custody-rule amendment),
corrects the frozen-paths claim in place, and updates the Unreleased
changelog entries to match the hardened behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019poEo7Q6nsQkayuZ7v5dP5

Copy link
Copy Markdown
Owner Author

Thanks for the adversarial review — all six findings verified as real. I reproduced each one before fixing. Status as of a086db2 (full suite: 1,629 passing; every reproduction is now a pinned regression test):

Fixed in 78cdc92:

  1. Scan matcher (P1) — patterns now match contiguous runs of workspace-relative (or scan-root-relative) segments; ancestors outside the repository never participate (your /…/archive/project/ case scans normally). * is segment-local, so reviews/*.md no longer excludes reviews/team/nested.md; explicit subtree intent is dir/**. Nested-archive exclusion is preserved.
  2. Ownership sentinel (P1) — ownership now requires the byte-exact versioned header line in its structural template position (file head, preceded only by known template head lines). Your repro — the phrase quoted as an example after user prose — classifies user_managed and is skipped; the genuinely generated file still classifies ontos_owned. USER CUSTOM markers alone never confer ownership (that class was already write-protected; the docstring now says so explicitly). Real ONTOS MANAGED START/END blocks remain future [Feature] Content-addressed activation receipts and portable AGENTS/map freshness #173 work.
  3. Branch/CWD staleness (P1) — the | Branch | row is normalized out of the semantic comparison (worktrees at identical HEAD agree; pinned by a git worktree regression test), and generate_agents_content/is_agents_semantically_stale now take an explicit repo_root (pinned by a cross-repository CWD test).
  4. Alias safety (P2)original_* extraction is quote-aware (status: "qa#blocked" round-trips exactly; unquoted # only opens a comment after whitespace; malformed quoting falls back to the parsed value). config_to_dict omits an empty [frontmatter] section, so defaults and fresh init configs stay readable by ≤ 5.0.2. Redefining a built-in mapping to a different target (in-progress = "complete") is now a fail-closed config conflict. The manual's required_version claim was corrected — you're right that the unknown-section error fires first; the documented guidance is now "upgrade every device before adopting the section."
  5. [Bug] Bare workspace-root files bypass dependency path resolution #176 ambiguity (P2) — resolution returns a typed result; both candidate bases are evaluated before deciding (a loaded-doc match can't shadow a different file at the other base), and ambiguity emits an explicit diagnostic listing the workspace-relative candidates with reason: ambiguous_bare_candidates — no more false "does not exist."

Acknowledged, not in this PR:

  1. MCP cache freshness (P2) — agreed, and deliberately out of scope here: freshness_mode is a pinned contract, so switching the cache to content-based invalidation must land batched with the digest core and mcp/schemas.py/parity updates in one change. Recorded as the [Feature] Content-addressed activation receipts and portable AGENTS/map freshness #173 remainder in the proposal addendum.

Proposal corrections — all accepted and recorded in the spec's new Review Addendum (§5): the finding_key/finding_id split with explicit multiplicity for #174 (and baseline support treated as part of the deliverable), merge_safe=false on source conflicts for #175, the unresolved-wikilink downgrade hazard for #177, manifest≠catalog and the historical-clone-size correction for #181, the #158 custody-rule amendment, and the frozen-paths claim fixed in place.

Left to you: the three-PR split and release scoping (this branch keeps each concern in separate commits so it can be split or cherry-picked either way — e.g. a strict-§11.5 v5.0.3 could take baa1896 + the scan/ambiguity/safety fixes and drop the config-alias surface from 73c4f8c), and whether to amend "Fixes #176" given §11.8's released-artifact closure gate.


Generated by Claude Code

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.

[Bug] Bare workspace-root files bypass dependency path resolution

2 participants