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
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ jobs:
- name: Test Rust non-Cypher surface inventory
run: python3 scripts/ci/test-non-cypher-surface-gate.py

- name: Test Rust coverage ledger failure modes
run: python3 scripts/ci/test-rust-coverage-ledger.py

- name: Test Python and Node non-Cypher parity policy
run: python3 scripts/ci/check-binding-parity-policy.py

Expand Down
25 changes: 17 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ test-tck: ## Run TCK compliance tests via Rust BDD runner

# Multi-surface coverage thresholds (#742 §2). Override per surface as needed.
COVERAGE_FAIL_UNDER_RUST ?= 85
COVERAGE_FAIL_UNDER_RUST_PYTHON_ADAPTER ?= 80
COVERAGE_FAIL_UNDER_RUST_NODE_ADAPTER ?= 80
COVERAGE_FAIL_UNDER_PYTHON ?= 85
COVERAGE_FAIL_UNDER_NODE ?= 85
# Back-compat alias used by coverage-python.
Expand All @@ -88,15 +90,15 @@ coverage: ## Rust + Python + Node coverage with per-surface thresholds
@echo "━━━ Node JS surface coverage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@$(MAKE) coverage-node
@echo "━━━ Coverage complete ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "Rust: build/coverage-rust/ (lcov + html + summary)"
@echo "Rust: build/coverage-rust/ (ledger + merged lcov + core HTML + summary)"
@echo "Python: coverage.xml + htmlcov/"
@echo "Node: crates/graphforge-bindings-node/coverage/"
@echo "✅ All surfaces collected; thresholds enforced per surface"

coverage-python: ## Run unit tests with Python wrapper coverage (requires maturin develop)
@$(MAKE) _ensure-graphforge
uv run coverage erase
uv run pytest tests/unit \
uv run pytest tests/unit crates/graphforge-bindings-py/tests/cli_entrypoint.py \
-n $${PYTEST_WORKERS:-4} \
--cov=$(PYTHON_COVERAGE_SRC) --cov-branch \
--cov-report=term-missing \
Expand Down Expand Up @@ -137,10 +139,13 @@ check-coverage-python: ## Validate Python wrapper coverage (≥85% default)
(echo "❌ Python wrapper coverage below $(COVERAGE_FAIL_UNDER_PYTHON)%" && exit 1)
@echo "✅ Python wrapper coverage meets threshold"

check-coverage-rust: ## Validate Rust line coverage from llvm-cov summary (≥85% default)
@test -f build/coverage-rust/summary.txt || \
(echo "❌ Missing build/coverage-rust/summary.txt — run make coverage-rust first"; exit 1)
@COVERAGE_FAIL_UNDER_RUST=$(COVERAGE_FAIL_UNDER_RUST) bash scripts/check-coverage-rust.sh
check-coverage-rust: ## Validate core Rust (≥85%) and each native adapter (≥80%)
@test -f build/coverage-rust/ledger.json || \
(echo "❌ Missing build/coverage-rust/ledger.json — run make coverage-rust first"; exit 1)
@COVERAGE_FAIL_UNDER_RUST=$(COVERAGE_FAIL_UNDER_RUST) \
COVERAGE_FAIL_UNDER_RUST_PYTHON_ADAPTER=$(COVERAGE_FAIL_UNDER_RUST_PYTHON_ADAPTER) \
COVERAGE_FAIL_UNDER_RUST_NODE_ADAPTER=$(COVERAGE_FAIL_UNDER_RUST_NODE_ADAPTER) \
bash scripts/check-coverage-rust.sh

coverage-strict: ## Strict 90% coverage check for new features
@echo "Checking strict coverage (90%)..."
Expand Down Expand Up @@ -205,6 +210,7 @@ pre-push-fast: ## Run fast checks only — format, lint, type, security, docstr

pre-push: ## Run local policy checks plus multi-surface coverage thresholds
@$(MAKE) pre-push-fast
@uv run --no-sync python scripts/ci/test-rust-coverage-ledger.py
@echo "━━━ Coverage + thresholds (Rust + Python + Node) ━━━━━━━━━━━━━━━━━━━━━━━"
@$(MAKE) coverage
@echo "━━━ Public API BDD mutation sentinels ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Expand Down Expand Up @@ -242,8 +248,11 @@ cargo-test: ## Run all Rust workspace tests
# cargo install cargo-llvm-cov
# rustup component add llvm-tools-preview
# Prefer an isolated CARGO_TARGET_DIR when other builds are running (AGENTS.md).
coverage-rust: ## Workspace Rust coverage via cargo-llvm-cov (term + lcov + html under build/coverage-rust)
@COVERAGE_FAIL_UNDER_RUST=$(COVERAGE_FAIL_UNDER_RUST) bash scripts/coverage-rust.sh
coverage-rust: ## Core + same-SHA Python/Node adapter Rust coverage ledger
@COVERAGE_FAIL_UNDER_RUST=$(COVERAGE_FAIL_UNDER_RUST) \
COVERAGE_FAIL_UNDER_RUST_PYTHON_ADAPTER=$(COVERAGE_FAIL_UNDER_RUST_PYTHON_ADAPTER) \
COVERAGE_FAIL_UNDER_RUST_NODE_ADAPTER=$(COVERAGE_FAIL_UNDER_RUST_NODE_ADAPTER) \
bash scripts/coverage-rust.sh
@$(MAKE) check-coverage-rust

bench-traversal: ## Run the #767 traversal scaling benchmark (release, manual; see benchmarks/traversal_scaling.md)
Expand Down
133 changes: 133 additions & 0 deletions crates/graphforge-bindings-node/tests/composite_transaction.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,136 @@ test("Node exposes one composite publish entrypoint with no inference helpers",
assert.equal(typeof forge.publishCompositeKnowledgeTransaction, "undefined");
assert.equal(typeof forge.inferCompositeParticipants, "undefined");
});

test("every explicit composite participant family crosses the Node adapter", async () => {
const project = mkdtempSync(
join(tmpdir(), "gf-composite-participants-node-"),
);
try {
const forge = new GraphForge(project);
await enableCapabilities(forge);
const operationUuid = "018f0f4e-7b8c-7000-8000-00000000e001";
const provenanceUuid = "018f0f4e-7b8c-7000-8000-00000000e002";
const assertionUuid = "018f0f4e-7b8c-7000-8000-00000000e003";
const confidenceUuid = "018f0f4e-7b8c-7000-8000-00000000e004";
const reasoningUuid = "018f0f4e-7b8c-7000-8000-00000000e005";
const groupUuid = "018f0f4e-7b8c-7000-8000-00000000e006";
const before = projectDigest(project);
assert.throws(
() =>
forge.publishCompositeTransaction({
contractVersion: 1,
operationUuid,
graphMutations: [
{
kind: "create_node",
nodeUuid: "018f0f4e-7b8c-7000-8000-00000000e010",
label: "Person",
properties: { name: "Grace" },
},
],
knowledge: {
confidenceAssessments: [
{
confidenceUuid,
assertionUuid,
policy: "conservative_min",
value: 0.5,
provenanceUuid,
recordedAtMicros: RECORDED_AT,
},
],
confidenceInputs: [
{
confidenceUuid,
inputConfidenceUuid: "018f0f4e-7b8c-7000-8000-00000000e007",
inputValue: 0.5,
ordinal: 0,
},
],
evidence: [
{
evidenceUuid: "018f0f4e-7b8c-7000-8000-00000000e008",
assertionUuid,
sourceUuid: "018f0f4e-7b8c-7000-8000-00000000e009",
sourceKind: "document",
role: "supports",
weight: 0.8,
provenanceUuid,
recordedAtMicros: RECORDED_AT,
},
],
reasoning: [
{
reasoningUuid,
assertionUuid,
kind: "methodological_note",
contentFormat: "text/plain",
content: Buffer.from("explicit participant conversion"),
provenanceUuid,
recordedAtMicros: RECORDED_AT,
},
],
assertionSupersessions: [
{
supersessionUuid: "018f0f4e-7b8c-7000-8000-00000000e00a",
priorAssertionUuid: assertionUuid,
replacementAssertionUuid:
"018f0f4e-7b8c-7000-8000-00000000e00b",
statusEventUuid: "018f0f4e-7b8c-7000-8000-00000000e00c",
reasoningUuid,
provenanceUuid,
recordedAtMicros: RECORDED_AT,
},
],
hypothesisGroups: [
{
groupUuid,
questionKey: "which hypothesis",
provenanceUuid,
recordedAtMicros: RECORDED_AT,
},
],
hypothesisMembership: [
{
membershipEventUuid: "018f0f4e-7b8c-7000-8000-00000000e00d",
operationUuid,
groupUuid,
assertionUuid,
action: "added",
reasoningUuid,
provenanceUuid,
recordedAtMicros: RECORDED_AT,
},
],
hypothesisSelection: [
{
selectionEventUuid: "018f0f4e-7b8c-7000-8000-00000000e00e",
operationUuid,
groupUuid,
selectedAssertionUuid: assertionUuid,
reasoningUuid,
provenanceUuid,
recordedAtMicros: RECORDED_AT,
},
],
assertionValidity: [
{
validityEventUuid: "018f0f4e-7b8c-7000-8000-00000000e00f",
assertionUuid,
validFromMicros: 1,
validToMicros: 2,
reasoningUuid,
provenanceUuid,
recordedAtMicros: RECORDED_AT,
},
],
},
}),
(error) => error?.code === "GF_NOT_FOUND",
);
assert.equal(projectDigest(project), before);
} finally {
rmSync(project, { recursive: true, force: true });
}
});
141 changes: 141 additions & 0 deletions crates/graphforge-bindings-py/tests/composite_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,17 @@ def check_composite_transaction(project: Path) -> None:
)
assert project_digest(project) == before

try:
forge.publish_composite_transaction(
operation_uuid="018f0f4e-7b8c-7000-8000-00000000e020",
graph_mutations=[{"kind": "unknown_mutation"}],
)
except TypeError as exc:
assert "unknown composite graph mutation kind" in str(exc)
else:
raise SystemExit("expected an unknown composite mutation TypeError")
assert project_digest(project) == before

# Invalid identity (nil operation UUID) stays in Rust validation.
expect_code(
"GF_VALIDATION",
Expand Down Expand Up @@ -358,10 +369,140 @@ def check_strict_ontology_composite(project: Path) -> None:
assert project_digest(project) == before


def check_all_explicit_participant_conversions(project: Path) -> None:
"""Every documented participant family crosses the thin adapter."""
project.mkdir()
forge = g.GraphForge(str(project))
enable_capabilities(forge)
operation = "018f0f4e-7b8c-7000-8000-00000000e001"
provenance = "018f0f4e-7b8c-7000-8000-00000000e002"
assertion = "018f0f4e-7b8c-7000-8000-00000000e003"
confidence = "018f0f4e-7b8c-7000-8000-00000000e004"
reasoning = "018f0f4e-7b8c-7000-8000-00000000e005"
group = "018f0f4e-7b8c-7000-8000-00000000e006"
before = project_digest(project)

# These rows are structurally valid at the language boundary but deliberately
# reference an absent provenance event. Rust must reject the entire request
# without publishing the graph mutation.
expect_code(
"GF_NOT_FOUND",
lambda: forge.publish_composite_transaction(
operation_uuid=operation,
graph_mutations=[
{
"kind": "create_node",
"node_uuid": "018f0f4e-7b8c-7000-8000-00000000e010",
"label": "Person",
"properties": {"name": "Grace"},
}
],
knowledge={
"confidence_assessments": [
{
"confidence_uuid": confidence,
"assertion_uuid": assertion,
"policy": "conservative_min",
"value": 0.5,
"provenance_uuid": provenance,
"recorded_at_micros": RECORDED_AT,
}
],
"confidence_inputs": [
{
"confidence_uuid": confidence,
"input_confidence_uuid": "018f0f4e-7b8c-7000-8000-00000000e007",
"input_value": 0.5,
"ordinal": 0,
}
],
"evidence": [
{
"evidence_uuid": "018f0f4e-7b8c-7000-8000-00000000e008",
"assertion_uuid": assertion,
"source_uuid": "018f0f4e-7b8c-7000-8000-00000000e009",
"source_kind": "document",
"role": "supports",
"weight": 0.8,
"provenance_uuid": provenance,
"recorded_at_micros": RECORDED_AT,
}
],
"reasoning": [
{
"reasoning_uuid": reasoning,
"assertion_uuid": assertion,
"kind": "methodological_note",
"content_format": "text/plain",
"content": b"explicit participant conversion",
"provenance_uuid": provenance,
"recorded_at_micros": RECORDED_AT,
}
],
"assertion_supersessions": [
{
"supersession_uuid": "018f0f4e-7b8c-7000-8000-00000000e00a",
"prior_assertion_uuid": assertion,
"replacement_assertion_uuid": "018f0f4e-7b8c-7000-8000-00000000e00b",
"status_event_uuid": "018f0f4e-7b8c-7000-8000-00000000e00c",
"reasoning_uuid": reasoning,
"provenance_uuid": provenance,
"recorded_at_micros": RECORDED_AT,
}
],
"hypothesis_groups": [
{
"group_uuid": group,
"question_key": "which hypothesis",
"provenance_uuid": provenance,
"recorded_at_micros": RECORDED_AT,
}
],
"hypothesis_membership": [
{
"membership_event_uuid": "018f0f4e-7b8c-7000-8000-00000000e00d",
"operation_uuid": operation,
"group_uuid": group,
"assertion_uuid": assertion,
"action": "added",
"reasoning_uuid": reasoning,
"provenance_uuid": provenance,
"recorded_at_micros": RECORDED_AT,
}
],
"hypothesis_selection": [
{
"selection_event_uuid": "018f0f4e-7b8c-7000-8000-00000000e00e",
"operation_uuid": operation,
"group_uuid": group,
"selected_assertion_uuid": assertion,
"reasoning_uuid": reasoning,
"provenance_uuid": provenance,
"recorded_at_micros": RECORDED_AT,
}
],
"assertion_validity": [
{
"validity_event_uuid": "018f0f4e-7b8c-7000-8000-00000000e00f",
"assertion_uuid": assertion,
"valid_from": 1,
"valid_to": 2,
"reasoning_uuid": reasoning,
"provenance_uuid": provenance,
"recorded_at_micros": RECORDED_AT,
}
],
},
),
)
assert project_digest(project) == before
Comment thread
coderabbitai[bot] marked this conversation as resolved.


if __name__ == "__main__":
check_no_inference_helpers()
with tempfile.TemporaryDirectory(prefix="gf-composite-py-") as directory:
check_composite_transaction(Path(directory))
check_all_explicit_participant_conversions(Path(directory) / "participants")
onto_root = Path(directory) / "strict"
onto_root.mkdir()
project = onto_root / "project"
Expand Down
Loading
Loading