Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,28 @@ release below.
test queries recognize JavaScript/TypeScript `.test`/`.spec` and `__tests__`
conventions and import relationships without claiming execution coverage.

- Graphify search readiness now comes from the installed query reader, not
from the lifecycle alone (#1029). `context-graph build`, `refresh`, `status`
and `connection-status` read the published generation the same way a query
does. They report `search` and a metadata-only `query_reader` verdict beside
the generation state, and exit non-zero or withhold `authorization` unless
the reader can consume the generation. So `status`, `connection-status` and
a real query agree. A known provider/reader mismatch, such as a `doc_ref`
generation meeting the `v1.4.2` reader, reports `reader_incompatible`. Its
remediation names the installed Code Mower and the release that reads it,
and does not call the generation `unreadable`. A generation from an
unreviewed provider gets a rebuild-or-upgrade remediation. Reviewed means
the exact distribution and version (`graphifyy==0.9.58`), not the version
alone. Unknown
node types from the reviewed release still fail closed, and no verdict
carries graph content, targets or local paths. Query summaries now report
`generation_completeness` and `query_completeness` separately. A bounded
answer that stopped at a budget, depth or document limit, or crossed an
ambiguous relationship, stays `available` and usable, discloses
`provider_has_more`, `unresolved_entities` or `document_limit`, and does not
mark the generation incomplete. An answer whose only omission is
`unresolved_entities` is `partial` too. Intended for `v1.5.0` together with #1007.

## 1.4.2 — published

- `code-mower board service` manages a persistent local Board: render a
Expand Down
33 changes: 31 additions & 2 deletions docs/context-graph-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,33 @@ written.
`partial` exists because of the requeue defect recorded in the evaluation: a
fast incremental repeat is not proof that the graph is complete.

### Search readiness is a separate verdict

The states above describe the *generation*: present, intact, complete, and
bound to the revision. They do not say whether the installed query reader can
consume it. A generation can be `current` and complete and still carry a
provider node type the installed reader predates. The `v1.4.2` reader, for
example, refuses the `doc_ref` nodes the pinned extractor emits. Since #1029,
`build`, `refresh`, `status` and `connection-status` run the query's own read
over the published generation and report the result beside the state, never in
place of it:

| `search` | `query_reader.reader` | Meaning | Next action |
| --- | --- | --- | --- |
| `available` | `compatible` | The reader consumed the generation; a query can answer from it. | none |
| `unavailable` | `not_checked` | The generation is not usable; `reason` repeats its state. | build or refresh |
| `unavailable` | `not_checked` | `connection-status` only: the connection is disconnected (`reason: disconnected`), so the graph is not read. | reconnect |
| `unavailable` | `incompatible` | Known provider/reader mismatch. `remediation` names the installed Code Mower, the generation's provider release and the required Code Mower release, when one is known. | upgrade Code Mower, or rebuild with a reviewed provider release |
| `unavailable` | `unreadable` | The reader has no account of what the generation contains, for example an unknown node type from the reviewed provider release. Fails closed. | treat as a defect; do not work around it |

`status` and `build` exit zero only when the generation is usable **and**
`search` is `available`. `connection-status` reports `authorization:
available` under the same condition. The verdict is metadata only: fixed
reasons, release numbers and the Code Mower vocabulary for a known node type.
It never contains graph content, a query target, an unknown type's spelling, or
a local path. A reader upgrade does not require a rebuild. The published
generation stays valid, and the new reader reads it as it is.

## Commands

```
Expand All @@ -595,8 +622,10 @@ code-mower context-graph remove [--show-local-paths]
code-mower context-graph doctor [--pin-file PIN]
```

`status` exits non-zero when the graph is not usable, so a script can branch on
it. `build` and `refresh` do the same, and for the same reason: a provider that
`status` exits non-zero when the graph is not usable or the installed reader
cannot search it (see
[Search readiness is a separate verdict](#search-readiness-is-a-separate-verdict)),
so a script can branch on it. `build` and `refresh` do the same, and for the same reason: a provider that
admitted an incomplete run has published a generation `status` will call
`partial` and refuse, so the build prints `partial` and exits non-zero rather
than describing it as `current` for as long as it takes to ask again.
Expand Down
34 changes: 34 additions & 0 deletions docs/context-graph-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,17 @@ arrived after `v1.4.2` was published and are on `main` for the next appropriate
release. In the published package a `doc_ref` node is an unknown type and the
JavaScript/TypeScript conventions are not recognized.

From #1029, a reader that meets `doc_ref` without supporting it reports that
node type as a known provider/reader mismatch and names Code Mower `1.5.0` as
the release that reads it. It does not call the generation `unreadable`. Other
unknown node types stay unexplained refusals. When the generation was built by
a provider this reader was not reviewed against, an unknown node type is
reported as `reader_incompatible` with a rebuild-or-upgrade remediation.
Reviewed means an exact requirement, distribution and version together
(`graphifyy==0.9.58` today, the distribution compared in its normalized
spelling), so another distribution published at `0.9.58` is not reviewed.
Either way, search is unavailable until the reader and the generation agree.

Each traversal is symbol-first: a target resolves to the symbols carrying that
name, and only a target that names no symbol at all is read as a path. Each is
breadth-first over adjacency sorted by the full relationship identity described
Expand Down Expand Up @@ -346,6 +357,22 @@ Confidence maps the provider's own qualification onto the contract's vocabulary:
| `inferred` | `inferred` | derived, not read directly |
| `ambiguous` | `unknown` | resolved to more than one candidate |

A partial packet is still an available answer. When a relationship budget, the
depth limit, the seed bound, or the policy's document limit stops a query, or
the query crosses an ambiguous relationship or leaves a target unresolved, the
status stays `available` and
`dependent_work` stays `usable`. The omissions say what was left out and why.
The query summary reports two completeness claims separately, so a bounded
answer is never read as a broken build:

| Summary field | Describes |
| --- | --- |
| `generation_completeness` | the published build's own `complete`/`partial`, from its manifest |
| `query_completeness` | this answer: `partial` whenever an omission above applies, including `unresolved_entities` alone |

`completeness` keeps its earlier meaning, the packet's completeness, and
equals `query_completeness`.

`ambiguous` also raises `unresolved_entities` on the packet, so a recipient sees
the uncertainty at the packet level and not only per document. A target name
that matches more than one definition does the same.
Expand Down Expand Up @@ -376,6 +403,13 @@ feature.
| `required: true` | `required_unavailable` | `paused` | 1 |
| `required: false` | `optional_unavailable` | `usable` | 0 |

A usable generation that the installed reader cannot consume follows the same
table. Its `reason` is `reader_incompatible` when the mismatch is a known one,
and the summary carries the same `remediation` and `next_action` that
`status` and `connection-status` report. It is `unreadable` when the reader
has no account of the content, which stays closed. See
[search readiness](context-graph-lifecycle.md#search-readiness-is-a-separate-verdict).

The words match `context_prepare`, so a caller branches on one vocabulary.
Optional unavailable context is not a degraded answer — there is no packet at
all, and the next action is to carry on with ordinary repository tools.
Expand Down
6 changes: 5 additions & 1 deletion docs/current-state-and-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,11 @@ release order. Merged fixes count as on main until a later published package is
verified to contain them; #935/#973 and the phase-3 Board PRs are now verified
in the published `v1.4.2` artifact, while the merged Graphify compatibility
fixes in [PR #1007](https://github.com/codemower-ai/code-mower/pull/1007) are on
main awaiting the next appropriate release.
main awaiting the next appropriate release. They are intended for `v1.5.0`
together with the #1029 search-readiness check. That check makes `status` and
`connection-status` report `search` from the installed query reader, so a
current generation the reader cannot consume is reported as a reader mismatch
with an upgrade action rather than as searchable.

Each child issue should produce one reviewable PR with one branch writer,
independent current-head review, the normal gate, and package-level validation.
Expand Down
26 changes: 24 additions & 2 deletions docs/graphify-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,28 @@ code-mower context-graph status --json
it. A provider run that admitted an incomplete census publishes a generation
`status` calls `partial` and refuses, rather than describing it as `current`.

The rest of this step, up to step 4, is **post-`v1.4.2`** and describes
current `main` (#1029, intended for `v1.5.0`). `status` also reports `search`:
whether the installed query reader can consume the generation. A `current`
generation with `search: unavailable` exits non-zero. Read
`query_reader.next_action`:

- `reader: incompatible` is a known provider/reader mismatch. The
`remediation` block names your installed Code Mower and the release that
reads the generation. Upgrade Code Mower; the generation needs no rebuild.
If it names an unreviewed provider instead (another release, or another
distribution at the same version), refresh with the pinned
`graphifyy` `0.9.58`.
- `reader: unreadable` means the generation holds something no reader has an
account of. It fails closed on purpose; report it rather than working around
it.

The published `v1.4.2` package has no such check. There, `status` and
`connection-status` can report a usable graph and `search: available` while
the first dependency query fails with `unreadable` on a generation containing
`doc_ref` nodes. That query failure is this mismatch; upgrading to the release
containing #1007 and #1029 resolves it.

**4. Register the graph as a local context connection.**

```bash
Expand Down Expand Up @@ -231,8 +253,8 @@ ramp-up above -- acquisition, the separate contained offline build, and steps 1
through 7 -- describe that published package. The paragraphs above that are
explicitly marked post-`v1.4.2` describe current `main` instead: the
language-extras and runtime-ownership paragraphs under
[Separate acquisition environment](#separate-acquisition-environment) are the
only ones so marked today.
[Separate acquisition environment](#separate-acquisition-environment) and the
search-readiness text in step 3 are the only ones so marked today.

[PR #1007](https://github.com/codemower-ai/code-mower/pull/1007) has since
merged to `main` with further real-pilot compatibility fixes: a bounded 16 MiB
Expand Down
51 changes: 41 additions & 10 deletions src/code_mower/context_graph_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,21 @@ def _write_packet(destination: Path, packet: dict) -> None:
raise


def _readiness(repository: Path, root: Path | None, report: lifecycle.GenerationStatus) -> dict:
"""The query reader's verdict on the published generation, metadata only."""
return query.search_readiness(lifecycle.GraphStateRoot(repository, root=root), report)


def _search_text(readiness: dict) -> str:
"""The readiness lines appended to the lifecycle's own status text."""
lines = [f" search: {readiness['search']} (query reader: {readiness['reader']})"]
if readiness.get("detail") and readiness["reader"] != "not_checked":
lines.append(f" reader: {readiness['detail']}")
if readiness.get("next_action"):
lines.append(f" next: {readiness['next_action']}")
return "\n".join(lines) + "\n"


def _emit(payload: dict, *, as_json: bool, text: str) -> None:
if as_json:
print(json.dumps(payload, indent=2, sort_keys=True))
Expand Down Expand Up @@ -210,23 +225,39 @@ def main(argv=None) -> int:
manifest=manifest,
detail="" if complete else "local graph build was incomplete; refresh it",
)
summary = {"status": "published", "usable": published.usable, **manifest.shareable_summary()}
_emit(summary, as_json=args.json, text=lifecycle.render_status_text(published))
# Publishing an unusable generation is a reportable condition, not
# a crash: exit non-zero for the same reason ``status`` does, so a
# script does not have to re-ask to find out what it just built.
return 0 if published.usable else 1
# Asked of the generation just published, not of whatever ``status``
# would find later: the manifest is the one this build wrote.
readiness = _readiness(args.repo_path, args.state_dir, published)
summary = {"status": "published", "usable": published.usable, **manifest.shareable_summary(),
"search": readiness["search"], "query_reader": readiness}
_emit(summary, as_json=args.json,
text=lifecycle.render_status_text(published) + _search_text(readiness))
# Publishing an unusable or unsearchable generation is a reportable
# condition, not a crash: exit non-zero for the same reason
# ``status`` does, so a script does not have to re-ask to find out
# what it just built.
searchable = readiness["search"] == query.SEARCH_AVAILABLE
return 0 if published.usable and searchable else 1
if args.command == "status":
report = lifecycle.graph_status(
args.repo_path,
root=args.state_dir,
revision=args.revision,
require_complete=not args.allow_partial,
)
_emit(report.shareable_summary(), as_json=args.json, text=lifecycle.render_status_text(report))
# A non-current graph is a normal, reportable condition, not a
# command failure; exit 1 so a script can branch on usability.
return 0 if report.usable else 1
# ``state``/``usable`` stay the generation's own verdict -- current,
# complete, bound to this revision -- and ``search`` is whether the
# installed reader can answer from it. They are different facts, so
# they are different fields; the exit code needs both.
readiness = _readiness(args.repo_path, args.state_dir, report)
summary = {**report.shareable_summary(), "search": readiness["search"],
"query_reader": readiness}
_emit(summary, as_json=args.json,
text=lifecycle.render_status_text(report) + _search_text(readiness))
# A non-current or unsearchable graph is a normal, reportable
# condition, not a command failure; exit 1 so a script can branch.
searchable = readiness["search"] == query.SEARCH_AVAILABLE
return 0 if report.usable and searchable else 1
if args.command == "query":
authorization = _load_authorization(args.authorization)
outcome = query.graph_context(
Expand Down
31 changes: 25 additions & 6 deletions src/code_mower/context_graph_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,15 @@ def saved_state(value: Any, name: str) -> dict[str, Any]:
("repository_root", "repositories", "recipients")})}


def _summary(state: Mapping[str, Any]) -> dict[str, Any]:
def _summary(state: Mapping[str, Any], *, search: str = "available") -> dict[str, Any]:
"""The connection's shape. ``search`` is the provider capability by default.

``connect`` and ``disconnect`` report the capability the connection offers;
``status`` passes the observed readiness instead, because that is the one
report an operator reads to decide whether a query will work now.
"""
return {"schema": "code_mower.contextConnectionSummary.v1", "provider": PROVIDER,
"kind": CONNECTION_KIND, "status": state["state"], "search": "available",
"kind": CONNECTION_KIND, "status": state["state"], "search": search,
"memory": "unavailable", "credential_storage": "none"}


Expand Down Expand Up @@ -154,10 +160,23 @@ def status(store: ContextStore, name: str, *, root: Path | None = None,
"""Report the connection and the graph behind it, without minting evidence."""
with store.locked(name) as locked:
state = saved_state(locked.read(), name)
report = lifecycle.graph_status(Path(state["repository_root"]), root=root, revision=revision)
return {**_summary(state), "graph": report.shareable_summary(),
"authorization": "available" if state["state"] == "verified" and report.usable
else "unavailable"}
repository = Path(state["repository_root"])
report = lifecycle.graph_status(repository, root=root, revision=revision)
# The query's own read, not the lifecycle's verdict alone: a current,
# complete generation this reader cannot consume must not be reported as
# searchable and then fail on the first question asked of it. A
# disconnected connection cannot search whatever the reader says, so the
# graph artifact is not opened for it.
verified = state["state"] == "verified"
if verified:
readiness = query.search_readiness(lifecycle.GraphStateRoot(repository, root=root), report)
else:
readiness = query.reader_not_checked("disconnected")
search = readiness["search"]
return {**_summary(state, search=search), "graph": report.shareable_summary(),
"query_reader": readiness,
"authorization": "available" if verified and report.usable
and search == query.SEARCH_AVAILABLE else "unavailable"}
Comment on lines +163 to +179

@gitar-bot gitar-bot Bot Sep 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Performance: connection.status always runs full read_graph even when unverified

In context_graph_connection.py's status() (lines 158-174), query.search_readiness(...) is called unconditionally before checking verified = state["state"] == "verified". search_readiness performs a full read_graph when the generation is usable: opening the tar artifact, extracting and reading up to MAX_GRAPH_BYTES (64 MiB), parsing JSON, and building the whole CodeGraph. When the connection is disconnected, search is forced to SEARCH_UNAVAILABLE regardless (readiness["search"] if verified else query.SEARCH_UNAVAILABLE), so all of that work is thrown away. This is a real, reachable cost on every disconnected connection's status check, not just a corner case. Move the search_readiness call after the verified check so it only runs when its result can matter, or short-circuit it when not verified.

Skip the expensive read_graph-backed readiness check entirely when the connection is not verified, since its result is discarded anyway.:

repository = Path(state["repository_root"])
report = lifecycle.graph_status(repository, root=root, revision=revision)
verified = state["state"] == "verified"
if verified:
    readiness = query.search_readiness(lifecycle.GraphStateRoot(repository, root=root), report)
    search = readiness["search"]
else:
    readiness = {"schema": query.READINESS_SCHEMA, "search": query.SEARCH_UNAVAILABLE,
                 "reader": "not_checked", "reason": "disconnected"}
    search = query.SEARCH_UNAVAILABLE
return {**_summary(state, search=search), "graph": report.shareable_summary(),
        "query_reader": readiness,
        "authorization": "available" if verified and report.usable
        and search == query.SEARCH_AVAILABLE else "unavailable"}

Was this helpful? React with 👍 / 👎



def _published(state: Mapping[str, Any], *, root: Path | None,
Expand Down
Loading
Loading