Skip to content

feat(llm): openai-cli backend — semantic extraction through the locally authenticated Codex CLI - #2981

Closed
Azeem1985 wants to merge 17 commits into
Graphify-Labs:v8from
TelB-io:feat/openai-cli-backend
Closed

feat(llm): openai-cli backend — semantic extraction through the locally authenticated Codex CLI#2981
Azeem1985 wants to merge 17 commits into
Graphify-Labs:v8from
TelB-io:feat/openai-cli-backend

Conversation

@Azeem1985

Copy link
Copy Markdown
Contributor

What

A sibling of the existing claude-cli backend for operators whose OpenAI access is a ChatGPT subscription (Codex CLI OAuth), not an API key: graphify extract . --backend openai-cli.

Mechanics (field-proven implementation, measured ~308 docs/hour at concurrency 6 on an 8 GB box): codex exec --skip-git-repo-check --json --sandbox read-only, prompt via stdin (a real chunk exceeds Linux MAX_ARG_STRLEN as an argv entry), result via -o file, empty-graph refusal so a hollow reply cannot trigger a bisect storm. Two per-call config pins via codex's own -c mechanism: mcp_servers={} (each exec otherwise spawns the user's configured MCP servers per call — measured 160–300 MB each, more than the extraction itself) and model_reasoning_effort (GRAPHIFY_OPENAI_CLI_EFFORT). Model via GRAPHIFY_OPENAI_CLI_MODEL. The extract credential gate treats a present codex binary as the credential, mirroring claude-cli.

Tests

tests/test_openai_cli_backend.py (argv contract, env overrides, missing-binary error); full suite 4,812 passed.

Live verification

One-document extraction on a real box: 9 nodes / 10 edges landed, 98,519 in / 4,733 out tokens, $0 (subscription).

Azeem added 2 commits August 23, 2026 21:24
…ly authenticated Codex CLI

Sibling of the existing claude-cli backend, for operators whose OpenAI access is a ChatGPT
subscription (Codex CLI OAuth), not an API key. Ported from a field-proven implementation
(measured ~308 docs/hour at concurrency 6 on an 8 GB box): codex exec --skip-git-repo-check
--json --sandbox read-only, prompt via stdin (argv would hit Linux MAX_ARG_STRLEN), result
via -o file, empty-graph refusal to avoid hollow-bisect storms. Two per-call config pins:
mcp_servers={} (each exec otherwise spawns the user's configured MCP servers per call —
measured ~160-300 MB each, more than the extraction itself) and model_reasoning_effort
(GRAPHIFY_OPENAI_CLI_EFFORT, default ultra). Model via GRAPHIFY_OPENAI_CLI_MODEL (default
gpt-5.6-sol). Tests: argv contract, env overrides, missing-binary error.
…the credential (OAuth), mirroring claude-cli

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 2 change(s) alter behavior, breaking input(s) attached.

Behavior changes: \_\_getattr\_\_ changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_\_getattr\_\_ behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"name":"'save\_query\_result'"\}, the old code produced raises AttributeError but the new code produces \<function save\_query\_result at 0x7f958d8fe340\>. Paste that input straight into a regression test.

Behavior changes: run\_benchmark changes behavior, here is the input that shows it.

The verifier found a concrete input on which run\_benchmark behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"graph\_path":"None","corpus\_words":"None","questions":"None"\}, the old code produced raises TypeError but the new code produces \{'corpus\_tokens': 991400, 'corpus\_words': 743550, 'nodes': 14871, 'edges': 27533, 'avg\_query\_tokens': 6343, 'reduction\_ratio': 156\.3, 'per\_question': \[\{'question': 'how does authentication…. Paste that input straight into a regression test.


Graphify review — findings

Adds full CI/CD and repo-hygiene infrastructure: GitHub Actions workflows for testing (ci.yml), PyPI trusted publishing (publish.yml), and self-graph release assets (release-graph.yml), plus a skillgen-check pre-commit hook enforcing that generated skill artifacts stay in sync with tools/skillgen/ fragments. Adds .dockerignore, .gitattributes (Linguist HTML vendoring), FUNDING.yml, AGENTS.md, and expands .gitignore to cover generated outputs while whitelisting the committed graphify/skills/ bundles and skillgen core fragments.

Worth a look

  • Dedup leaves hyperedges pointing at removed node idsgraphify/build.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Python matrix is not applied to test interpreter.github/workflows/ci.yml:65 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Unpinned publishing action runs with OIDC token minting permission.github/workflows/publish.yml:55 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • README documents invalid cluster-only subcommandREADME.md · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • README contradicts query logging defaultREADME.md · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 14548 functions depend on the 14548 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 474 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: build_from_json() — 185 callers, 18 callees
  • new: detect() — 108 callers, 15 callees
  • new: deduplicate_entities() — 63 callers, 21 callees
  • new: build_merge() — 46 callers, 14 callees
  • new: save_semantic_cache() — 55 callers, 11 callees
  • new: _extract_generic() — 18 callers, 24 callees
  • …and 216 more — each is listed as a finding

Verification — 14548 functions in the blast radius were not formally verified this run (proofs are advisory here).

Health delta baseline: last indexed commit b2cd362 (diverged from this PR's base — delta is approximate).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 14548 function(s) in the blast radius were not formally verified this run

Formal verification

Behavior changes: \_\_getattr\_\_ changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_\_getattr\_\_ behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"name":"'save\_query\_result'"\}, the old code produced raises AttributeError but the new code produces \<function save\_query\_result at 0x7f958d8fe340\>. Paste that input straight into a regression test.

Behavior changes: run\_benchmark changes behavior, here is the input that shows it.

The verifier found a concrete input on which run\_benchmark behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"graph\_path":"None","corpus\_words":"None","questions":"None"\}, the old code produced raises TypeError but the new code produces \{'corpus\_tokens': 991400, 'corpus\_words': 743550, 'nodes': 14871, 'edges': 27533, 'avg\_query\_tokens': 6343, 'reduction\_ratio': 156\.3, 'per\_question': \[\{'question': 'how does authentication…. Paste that input straight into a regression test.

No difference found (not proven): No behavior difference found in main (not a proof).

The verifier ran both versions of main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_cross\_community\_surprises (not a proof).

The verifier ran both versions of \_cross\_community\_surprises on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_cross\_file\_surprises (not a proof).

The verifier ran both versions of \_cross\_file\_surprises on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_file\_category (not a proof).

The verifier ran both versions of \_file\_category on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_is\_file\_node.

The verifier did not have enough to check \_is\_file\_node, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_surprise\_score.

The verifier did not have enough to check \_surprise\_score, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in god\_nodes (not a proof).

The verifier ran both versions of god\_nodes on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify graph\_diff.

The verifier did not have enough to check graph\_diff, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: the input domain has 25 values but only 5 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

Could not verify: Could not verify suggest\_questions.

The verifier did not have enough to check suggest\_questions, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly ValueError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in \_query\_subgraph\_tokens (not a proof).

The verifier ran both versions of \_query\_subgraph\_tokens on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify print\_benchmark.

The verifier did not have enough to check print\_benchmark, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly KeyError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build.

The verifier did not have enough to check build, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build\_from\_json.

The verifier did not have enough to check build\_from\_json, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify cache\_dir.

The verifier did not have enough to check cache\_dir, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify cached\_files.

The verifier did not have enough to check cached\_files, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify check\_semantic\_cache.

The verifier did not have enough to check check\_semantic\_cache, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify clear\_cache.

The verifier did not have enough to check clear\_cache, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify file\_hash.

The verifier did not have enough to check file\_hash, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify load\_cached.

The verifier did not have enough to check load\_cached, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify save\_cached.

The verifier did not have enough to check save\_cached, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify save\_semantic\_cache.

The verifier did not have enough to check save\_semantic\_cache, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in \_split\_community (not a proof).

The verifier ran both versions of \_split\_community on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify cluster.

The verifier did not have enough to check cluster, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: non-vacuity: domain too small (only 2 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

…ket mcp_servers={} does not work

Codex merges config overrides into the table rather than replacing it, so `-c mcp_servers={}` leaves
every server enabled — verified with Codex's own reader: `codex mcp list -c mcp_servers={}` still
shows 'enabled', while `-c mcp_servers.<name>.enabled=false` shows 'disabled' (`enabled` is a real
field in `codex mcp get`). Measured cost of the ineffective pin during a live extraction: four MCP
servers spawned by one `codex exec`, ~152 MB each.

Now the backend asks Codex for its own server list (`codex mcp list --json`) and adds one
`-c mcp_servers.<name>.enabled=false` per server — no hardcoded names, best-effort (older Codex or no
config: extraction proceeds unchanged). Tests cover both servers disabled, the empty-config case, and
that the blanket form is gone.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 2 change(s) alter behavior, breaking input(s) attached.

Behavior changes: \_\_getattr\_\_ changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_\_getattr\_\_ behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"name":"'save\_query\_result'"\}, the old code produced raises AttributeError but the new code produces \<function save\_query\_result at 0x7fccec4ee340\>. Paste that input straight into a regression test.

Behavior changes: run\_benchmark changes behavior, here is the input that shows it.

The verifier found a concrete input on which run\_benchmark behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"graph\_path":"None","corpus\_words":"None","questions":"None"\}, the old code produced raises TypeError but the new code produces \{'corpus\_tokens': 991800, 'corpus\_words': 743850, 'nodes': 14877, 'edges': 27545, 'avg\_query\_tokens': 6343, 'reduction\_ratio': 156\.4, 'per\_question': \[\{'question': 'how does authentication…. Paste that input straight into a regression test.


Graphify review — findings

Adds CI/CD and repo hygiene scaffolding: GitHub Actions workflows for tests, skillgen drift checks, PyPI trusted publishing, and a self-graph release asset. Introduces .dockerignore, .gitattributes (Linguist HTML vendoring), .pre-commit-config.yaml with the skillgen guard and ruff, AGENTS.md, and expands .gitignore for graphify outputs and local benchmark scripts.

Worth a look

  • Manual dispatch can publish arbitrary ref to PyPI.github/workflows/publish.yml:54 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Default container starts unauthenticated HTTP service on all interfacesDockerfile:25 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • PR CI runs untrusted code without read-only token permissions.github/workflows/ci.yml:4 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Manual publish can run without release tag/version guard.github/workflows/publish.yml:13 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Publish job drops contents permission needed by checkout.github/workflows/publish.yml:30 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 14554 functions depend on the 14554 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 474 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: build_from_json() — 185 callers, 18 callees
  • new: detect() — 108 callers, 15 callees
  • new: deduplicate_entities() — 63 callers, 21 callees
  • new: build_merge() — 46 callers, 14 callees
  • new: save_semantic_cache() — 55 callers, 11 callees
  • new: _extract_generic() — 18 callers, 24 callees
  • …and 216 more — each is listed as a finding

Verification — 14554 functions in the blast radius were not formally verified this run (proofs are advisory here).

Health delta baseline: last indexed commit b2cd362 (diverged from this PR's base — delta is approximate).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 14554 function(s) in the blast radius were not formally verified this run

Formal verification

Behavior changes: \_\_getattr\_\_ changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_\_getattr\_\_ behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"name":"'save\_query\_result'"\}, the old code produced raises AttributeError but the new code produces \<function save\_query\_result at 0x7fccec4ee340\>. Paste that input straight into a regression test.

Behavior changes: run\_benchmark changes behavior, here is the input that shows it.

The verifier found a concrete input on which run\_benchmark behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"graph\_path":"None","corpus\_words":"None","questions":"None"\}, the old code produced raises TypeError but the new code produces \{'corpus\_tokens': 991800, 'corpus\_words': 743850, 'nodes': 14877, 'edges': 27545, 'avg\_query\_tokens': 6343, 'reduction\_ratio': 156\.4, 'per\_question': \[\{'question': 'how does authentication…. Paste that input straight into a regression test.

No difference found (not proven): No behavior difference found in main (not a proof).

The verifier ran both versions of main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_cross\_community\_surprises (not a proof).

The verifier ran both versions of \_cross\_community\_surprises on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_cross\_file\_surprises (not a proof).

The verifier ran both versions of \_cross\_file\_surprises on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_file\_category (not a proof).

The verifier ran both versions of \_file\_category on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_is\_file\_node.

The verifier did not have enough to check \_is\_file\_node, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_surprise\_score.

The verifier did not have enough to check \_surprise\_score, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in god\_nodes (not a proof).

The verifier ran both versions of god\_nodes on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify graph\_diff.

The verifier did not have enough to check graph\_diff, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: the input domain has 25 values but only 5 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

Could not verify: Could not verify suggest\_questions.

The verifier did not have enough to check suggest\_questions, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly ValueError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in \_query\_subgraph\_tokens (not a proof).

The verifier ran both versions of \_query\_subgraph\_tokens on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify print\_benchmark.

The verifier did not have enough to check print\_benchmark, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly KeyError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build.

The verifier did not have enough to check build, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build\_from\_json.

The verifier did not have enough to check build\_from\_json, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify cache\_dir.

The verifier did not have enough to check cache\_dir, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify cached\_files.

The verifier did not have enough to check cached\_files, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify check\_semantic\_cache.

The verifier did not have enough to check check\_semantic\_cache, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify clear\_cache.

The verifier did not have enough to check clear\_cache, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify file\_hash.

The verifier did not have enough to check file\_hash, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify load\_cached.

The verifier did not have enough to check load\_cached, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify save\_cached.

The verifier did not have enough to check save\_cached, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify save\_semantic\_cache.

The verifier did not have enough to check save\_semantic\_cache, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in \_split\_community (not a proof).

The verifier ran both versions of \_split\_community on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify cluster.

The verifier did not have enough to check cluster, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: non-vacuity: domain too small (only 2 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

@Azeem1985
Azeem1985 changed the base branch from main to v8 August 23, 2026 22:36
@Azeem1985

Copy link
Copy Markdown
Contributor Author

Re-targeted from main to v8 (the development branch — my mistake on opening). The earlier automated reviews ran against main, so they diffed the whole v8↔main divergence (100 files / ~280k lines); the __getattr__ and run_benchmark behavior deltas and the CI/Dockerfile findings come from that divergence, not from this change. Actual change: 4 files, +398/-5 — graphify/llm.py (the openai-cli backend + per-server MCP disable), graphify/cli.py (extract credential gate treats a present codex binary as the credential, mirroring claude-cli), graphify/__main__.py (help text), tests/test_openai_cli_backend.py (5 tests). Full suite on the branch: 4,812 passed, 50 skipped. Live: a one-document extraction produced 9 nodes / 10 edges, 98,519 in / 4,733 out tokens.

Azeem and others added 14 commits August 24, 2026 08:21
Two `graphify extract` runs (or an extract racing a watcher/hook rebuild)
on one graphify-out/ interleave cache saves and clobber graph.json. extract
now takes the same advisory flock _rebuild_code takes, immediately after
the output dir exists, and holds it to the end of the command. A contended
run prints the holder's PID and waits up to GRAPHIFY_LOCK_TIMEOUT seconds
(default 600) via a new bounded-wait mode on _rebuild_lock (the kernel only
offers skip or wait-forever, so the deadline is polled in userspace), then
exits 1 instead of hanging forever behind a wedged rebuild. The lock is
acquired exactly once — flock is per-open-file-description, so re-entry in
the same process would self-deadlock — and SystemExit unwinds the context
manager, so the unlink-on-release contract holds on every exit path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… on a second backend

When every semantic chunk fails on the primary backend (missing SDK
package, bad key, an outage), a configured fallback backend retries the
same still-uncached files once instead of failing the build. Nothing is
cache-saved for a zero-success pass, so the retry covers exactly the
files the primary failed on. --model belongs to the primary backend and
is not forwarded, the fallback name is validated before any API spend,
and only a zero-success retry keeps the all-chunks-failed exit 1.
GRAPHIFY_FALLBACK_BACKEND sets the default; the CLI flag wins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…doc changes

Instead of only writing the needs_update flag, `graphify watch --semantic`
runs `graphify extract` on the watched root when a debounced batch contains
surviving non-code files. The extract runs as a subprocess on purpose:
fcntl.flock is held per open file description, so in-process re-entry of
_rebuild_lock would self-deadlock, while a child process serializes cleanly
on graphify-out/.rebuild.lock against hook-driven rebuilds.

A failed extract falls back to _notify_only so the user never loses the
flag + /graphify --update instruction; a successful run clears the flag
(extract itself never touches it — only _rebuild_code does). --backend and
--fallback-backend are forwarded to the extract subprocess and rejected
without --semantic rather than being a silent no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The push sinks ran one MERGE query per node and per edge, so pushing a
large graph to a non-local server spent nearly all its time on round
trips. Rows are now grouped by sanitized node label / relationship type
(those are interpolated into the Cypher text and cannot be parameters)
and sent in UNWIND batches of --batch-size rows (default 100, ~100x
fewer round trips). Each row carries exactly the old per-entry params
and UNWIND processes rows in order, so the MERGE/SET upsert semantics -
including idempotent re-runs - are unchanged. A non-positive or
non-integer --batch-size is rejected on parse, before any connection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review addition: the no-double-spend edge — when the primary backend got at
least one semantic chunk through, --fallback-backend must NOT re-dispatch
the whole uncached set on the second backend. Locks the succeeded == 0
trigger condition in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
extract: hold the per-repo rebuild lock for the whole pipeline
# Conflicts:
#	CHANGELOG.md
#	graphify/cli.py
extract: --fallback-backend retries a zero-success semantic pass on a second backend
watch: --semantic runs LLM extraction automatically when doc/paper/image files change
export: batched UNWIND push for neo4j/falkordb (--batch-size)
# Conflicts:
#	graphify/cli.py
…etail, and turn-usage accounting; changelog

The argv-contract tests said what we send; these say what we accept back:
the -o JSON becomes the result dict with usage from the last turn.completed
JSONL event (cached input not double-counted), a non-zero exit or a missing/
empty -o file raises with stderr AND the JSONL stdout tail preserved (HTTP
errors surface only there), and an empty-but-valid graph raises instead of
feeding the hollow-response bisect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.


Graphify review — findings

Adds an openai-cli backend that runs semantic extraction and community labeling through the locally authenticated Codex CLI (stdin-fed prompts, read-only sandbox, per-server MCP disabling, turn.completed usage parsing, forced-serial by default), and gates it on a present codex binary like claude-cli. Adds graphify extract --fallback-backend/GRAPHIFY_FALLBACK_BACKEND for a one-shot retry of failed chunks on a second backend, and makes extract hold the per-repo rebuild lock across its whole pipeline. Extends graphify watch --semantic to auto-run extraction as a locked subprocess on doc/image changes, and batches export neo4j|falkordb --push into UNWIND queries with a --batch-size flag (default 100).

Worth a look

  • --no-cluster path now builds and writes clustered graphgraphify/cli.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • --global-merge no longer merges into global graphgraphify/cli.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • AST extraction and downstream logic moved under if needs_llm: blockgraphify/cli.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Default extract no longer writes analysis/manifest or final outputgraphify/cli.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Extract finalization is guarded by global_mergegraphify/cli.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1356 functions depend on the 617 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _rebuild_code() — 98 callers, 50 callees
  • new: deduplicate_entities() — 63 callers, 21 callees
  • new: build_merge() — 46 callers, 14 callees
  • new: extract_files_direct() — 17 callers, 21 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: _call_claude_cli() — 31 callers, 9 callees
  • new: dispatch_command() — 2 callers, 121 callees
  • new: _call_llm() — 11 callers, 21 callees
  • …and 24 more — each is listed as a finding

Verification — 1356 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 1292 function(s) in the blast radius were not formally verified this run

· 4 grounded finding(s) anchored inline below; 28 more finding(s) on lines outside this diff (see the check run).

yield rows[start:start + batch_size]


def push_to_neo4j(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionpush_to_neo4j()

6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/llm.py
return " | ".join(parts) or "(no stderr or stdout)"


def _call_claude_cli(user_message: str, max_tokens: int = 8192, *, deep_mode: bool = False, images: list[_ImageRef] | None = None) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_call_claude_cli()

fans out to 9 callees (efferent coupling); 31 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/llm.py
return args


def _call_openai_cli(user_message: str, max_tokens: int = 8192, *, deep_mode: bool = False, images: list[_ImageRef] | None = None) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_call_openai_cli()

fans out to 9 callees (efferent coupling); 11 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/watch.py


def watch(watch_path: Path, debounce: float = 3.0) -> None:
def watch(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionwatch()

fans out to 8 callees (efferent coupling); 6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@Azeem1985

Copy link
Copy Markdown
Contributor Author

Closing this in favour of #3073, which is the same change on a branch cut from v85 files instead of the 11 this one shows.

This PR was opened from a branch that also carried unrelated work from our fork, so its diff was 11 files of the backend plus the whole of our fork's in-flight work. That is my mistake in how I cut the branch, and it made this unreviewable. Nothing about the proposed change has altered; only the packaging.

#3073 has the full rationale and measurements. Sorry for the extra noise, and thank you for your patience.

@Azeem1985 Azeem1985 closed this Aug 25, 2026
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.

1 participant