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
18 changes: 18 additions & 0 deletions crates/graphforge-discovery/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
GraphForge
Copyright 2026 Curate Labs Inc.

GraphForge v0.5.0 and later are licensed under the Apache License, Version 2.0.
Previously published v0.4.0 and earlier artifacts remain under the MIT License
shipped with those versions.

The openCypher Technology Compatibility Kit material under tests/tck/ is
third-party material distributed under its own Apache License 2.0 and NOTICE.
Dependencies and other third-party materials remain under their respective
licenses and are not relicensed as part of GraphForge.

A machine-generated inventory of third-party Rust dependency license texts for
binary redistributions (Python wheels, native Node addons, and the CLI) is in
legal/THIRD_PARTY_NOTICES.md. Published binary packages include a copy of that
file. Regenerate it with `make third-party-notices` after dependency changes.
Rust dependency SPDX allowlisting is enforced with `cargo deny check licenses`
using deny.toml.
18 changes: 18 additions & 0 deletions crates/graphforge-observability/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
GraphForge
Copyright 2026 Curate Labs Inc.

GraphForge v0.5.0 and later are licensed under the Apache License, Version 2.0.
Previously published v0.4.0 and earlier artifacts remain under the MIT License
shipped with those versions.

The openCypher Technology Compatibility Kit material under tests/tck/ is
third-party material distributed under its own Apache License 2.0 and NOTICE.
Dependencies and other third-party materials remain under their respective
licenses and are not relicensed as part of GraphForge.

A machine-generated inventory of third-party Rust dependency license texts for
binary redistributions (Python wheels, native Node addons, and the CLI) is in
legal/THIRD_PARTY_NOTICES.md. Published binary packages include a copy of that
file. Regenerate it with `make third-party-notices` after dependency changes.
Rust dependency SPDX allowlisting is enforced with `cargo deny check licenses`
using deny.toml.
18 changes: 18 additions & 0 deletions crates/graphforge-portable-oci/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
GraphForge
Copyright 2026 Curate Labs Inc.

GraphForge v0.5.0 and later are licensed under the Apache License, Version 2.0.
Previously published v0.4.0 and earlier artifacts remain under the MIT License
shipped with those versions.

The openCypher Technology Compatibility Kit material under tests/tck/ is
third-party material distributed under its own Apache License 2.0 and NOTICE.
Dependencies and other third-party materials remain under their respective
licenses and are not relicensed as part of GraphForge.

A machine-generated inventory of third-party Rust dependency license texts for
binary redistributions (Python wheels, native Node addons, and the CLI) is in
legal/THIRD_PARTY_NOTICES.md. Published binary packages include a copy of that
file. Regenerate it with `make third-party-notices` after dependency changes.
Rust dependency SPDX allowlisting is enforced with `cargo deny check licenses`
using deny.toml.
32 changes: 17 additions & 15 deletions docs/development/publication-order.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Publication and recovery order (v0.5.2)

GraphForge v0.5.2 is one 27-node release: 18 crates.io crates, one PyPI
GraphForge v0.5.2 is one 29-node release: 20 crates.io crates, one PyPI
project, five native npm packages, the npm main package, CLI, and agent skills.
[ADR 0017](../adr/0017-unified-release-version.md) forbids a registry-specific
version. Existing v0.5.0 tags, records, supplements, and published packages are
Expand Down Expand Up @@ -129,17 +129,19 @@ The finite order is generated by `scripts/ci/crate-publish-plan.py`:
5. `graphforge-observability`
6. `graphforge-ast`
7. `graphforge-knowledge`
8. `graphforge-ontology`
8. `graphforge-portable-oci`
9. `graphforge-provenance`
10. `graphforge-ir`
11. `graphforge-plan`
12. `graphforge-storage`
13. `graphforge-rel`
14. `graphforge-search`
15. `graphforge-cypher`
16. `graphforge-exec`
17. `graphforge-api`
18. `graphforge-cli`
10. `graphforge-value`
11. `graphforge-cypher`
12. `graphforge-ontology`
13. `graphforge-ir`
14. `graphforge-plan`
15. `graphforge-storage`
16. `graphforge-rel`
17. `graphforge-search`
18. `graphforge-exec`
19. `graphforge-api`
20. `graphforge-cli`

Each invocation validates the retained `.crate` checksum before `cargo publish`.
After an accepted write it observes the public version once. A verified result
Expand All @@ -149,13 +151,13 @@ may unlock the next crate; pending or unsafe truth stops the finite loop.

The final job uses `if: always()` and records every lane conclusion, including
failure, cancellation, timeout, and skip. When the candidate is available it
re-observes all three registries and produces one stable 27-node summary. Job
re-observes all three registries and produces one stable 29-node summary. Job
history is operator context only; registry state and the next safe actions come
from the immutable candidate plus live registry truth.

If candidate preflight failed before a manifest was available, reconciliation
still emits all 27 node identities as `indeterminate` and identifies the
candidate blocker. The workflow is green only when all 27 nodes are publicly
still emits all 29 node identities as `indeterminate` and identifies the
candidate blocker. The workflow is green only when all 29 nodes are publicly
`verified`. The summary is retained for 30 days and contains no credentials,
headers, cookies, tokens, or raw registry bodies.

Expand Down Expand Up @@ -195,4 +197,4 @@ boundary explicit. Maintainers still make these decisions:
v0.5.1 tag and GitHub Release;
- decide any registry-specific yank/deprecation if reconciliation finds a
conflict; and
- close the human release tracker only after the 27-node summary is complete.
- close the human release tracker only after the 29-node summary is complete.
2 changes: 1 addition & 1 deletion docs/development/release-artifact-record.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ Before a write the lane persists a sanitized immutable attempt record; after a
successful registry response it persists an accepted receipt and performs one
public observation. Later recovery runs load both, so cancellation, timeout,
or propagation delay cannot become permission for a second write. There is no polling loop. The `always()`
reconciliation job then observes all 27 nodes and combines those states with
reconciliation job then observes all 29 nodes and combines those states with
job conclusions for operator context. See
[`publication-order.md`](publication-order.md) for the recovery and human stop
decisions.
13 changes: 9 additions & 4 deletions scripts/ci/clean-env-verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,26 @@
RELEASE_RECORD_SCHEMAS = (RELEASE_RECORD_SCHEMA, RELEASE_CANDIDATE_SCHEMA)
DEFAULT_VERSION = "0.5.2"
DEFAULT_DOCS_BASE = "https://docs.graphforge.sh"
# crates.io publish set and order; must equal `crate-publish-plan.py list`.
# scripts/ci/test-crate-publish-plan.py fails when this drifts from the plan.
DEFAULT_CRATES = (
"graphforge-core",
"graphforge-value",
"graphforge-discovery",
"graphforge-filesystem",
"graphforge-io",
"graphforge-observability",
"graphforge-ast",
"graphforge-knowledge",
"graphforge-ontology",
"graphforge-portable-oci",
"graphforge-provenance",
"graphforge-value",
"graphforge-cypher",
"graphforge-ontology",
"graphforge-ir",
"graphforge-plan",
"graphforge-storage",
"graphforge-io",
"graphforge-rel",
"graphforge-search",
"graphforge-cypher",
"graphforge-exec",
"graphforge-api",
"graphforge-cli",
Expand Down
13 changes: 9 additions & 4 deletions scripts/ci/release_candidate_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,26 @@
"indeterminate": "authoritative registry truth cannot be classified safely",
"failed": "a deterministic local or registry operation failed",
}
# crates.io publish set and order; must equal `crate-publish-plan.py list`.
# scripts/ci/test-crate-publish-plan.py fails when this drifts from the plan.
CRATES = (
"graphforge-core",
"graphforge-value",
"graphforge-discovery",
"graphforge-filesystem",
"graphforge-io",
"graphforge-observability",
"graphforge-ast",
"graphforge-knowledge",
"graphforge-ontology",
"graphforge-portable-oci",
"graphforge-provenance",
"graphforge-value",
"graphforge-cypher",
"graphforge-ontology",
"graphforge-ir",
"graphforge-plan",
"graphforge-storage",
"graphforge-io",
"graphforge-rel",
"graphforge-search",
"graphforge-cypher",
"graphforge-exec",
"graphforge-api",
"graphforge-cli",
Expand Down
5 changes: 4 additions & 1 deletion scripts/ci/test-clean-env-verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
sys.modules[SPEC.name] = cev # required for dataclasses under Python 3.9
SPEC.loader.exec_module(cev)

assert len(cev.DEFAULT_CRATES) == 17
assert len(cev.DEFAULT_CRATES) == 20
assert "graphforge-value" in cev.DEFAULT_CRATES
# #1373: these three were in the publish plan but missing from the inventory.
for crate_name in ("graphforge-discovery", "graphforge-observability", "graphforge-portable-oci"):
assert crate_name in cev.DEFAULT_CRATES
assert cev.DEFAULT_CRATES[0] == "graphforge-core"
assert cev.DEFAULT_CRATES[-1] == "graphforge-cli"
assert cev.LANE_ISSUES["cargo"] == 185
Expand Down
39 changes: 39 additions & 0 deletions scripts/ci/test-crate-publish-plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ def load_module():
return module


def load_named_module(path: Path):
"""Import a scripts/ module by path (names contain hyphens)."""
name = path.stem.replace("-", "_")
spec = importlib.util.spec_from_file_location(name, path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module


def run(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
Expand Down Expand Up @@ -98,4 +109,32 @@ def run(*args: str) -> subprocess.CompletedProcess[str]:
assert commands[0].startswith("cargo publish -p graphforge-core ")
assert commands[-1].startswith("cargo publish -p graphforge-cli ")

# --- Release inventories must not diverge from the publish plan (#1373) -------
# Every hand-maintained crates.io inventory is compared against the plan here so
# that adding a crate cannot silently leave a release gate behind.
INVENTORIES = (
(ROOT / "scripts" / "ci" / "release_candidate_manifest.py", "CRATES"),
(ROOT / "scripts" / "ci" / "clean-env-verify.py", "DEFAULT_CRATES"),
(ROOT / "scripts" / "verify_package_licenses.py", "CARGO_PUBLISH_CRATES"),
)

for script, attribute in INVENTORIES:
inventory_module = load_named_module(script)
inventory = tuple(getattr(inventory_module, attribute))
assert inventory == tuple(names), (
f"{script.relative_to(ROOT)}:{attribute} diverges from "
f"crate-publish-plan.py list; expected {list(names)}, found {list(inventory)}"
)

license_check = load_named_module(ROOT / "scripts" / "license_check.py")
license_dirs = {path.name for path in license_check.CARGO_PACKAGE_DIRS}
missing_license_dirs = sorted(set(names) - license_dirs)
assert not missing_license_dirs, (
f"scripts/license_check.py:CARGO_PACKAGE_DIRS lacks publishable crates: {missing_license_dirs}"
)

missing_notice = sorted(name for name in names if not (ROOT / "crates" / name / "NOTICE").is_file())
assert not missing_notice, f"publishable crates without a NOTICE file: {missing_notice}"


print("crate-publish-plan tests passed")
4 changes: 3 additions & 1 deletion scripts/ci/test-release-candidate.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,9 @@ def main() -> None:
root = Path(temp)
manifest_path, artifacts, manifest = create_candidate(root)
validated = release_candidate.validate(manifest_path, artifacts, SHA, VERSION)
assert len(validated["nodes"]) == 26
assert len(validated["nodes"]) == 1 + len(manifest_module.NPM_PACKAGES) + len(
manifest_module.CRATES
)
assert {
"from": "crates:graphforge-storage",
"requires": "crates:graphforge-filesystem",
Expand Down
4 changes: 3 additions & 1 deletion scripts/ci/test-release-registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
VERSION = "0.5.1"
SHA = "a" * 40
NOW = "2030-01-01T12:02:00+00:00"
# One PyPI node, every npm package, and every crates.io crate (#1373).
NODE_COUNT = 1 + len(NPM_PACKAGES) + len(CRATES)


def artifact(path: str, group: str, surface: str, name: str, *, dependencies=None):
Expand Down Expand Up @@ -206,7 +208,7 @@ def main() -> None:
result = plan(manifest, all_verified)
assert result["actions"] == []
assert result["download_groups"] == []
assert result["summary"]["verified"] == 26
assert result["summary"]["verified"] == NODE_COUNT

for node_id in (
"pypi:graphforge",
Expand Down
16 changes: 10 additions & 6 deletions scripts/ci/test-release-rehearsal.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ def fake_python(manifest, _artifacts, _root):
)
assert report["status"] == "passed"
assert report["registry_writes"] == 0
assert report["checks"]["candidate_completeness"]["nodes"] == 26
assert (
report["checks"]["candidate_completeness"]["nodes"] == registry_fixture.NODE_COUNT
)
node_check = report["checks"]["node_cli_skills_clean_consumer"]
assert node_check["loaded_version"] == candidate_fixture.VERSION
host_native = rehearsal._compatible_native_npm_name()
Expand All @@ -122,7 +124,9 @@ def fake_python(manifest, _artifacts, _root):
"@curatelabs/graphforge-agent-skills",
host_native,
]
assert len(report["checks"]["rust_packages"]["packages"]) == 17
assert len(report["checks"]["rust_packages"]["packages"]) == len(
candidate_contract.CRATES
)
assert not any(word in json.dumps(report).lower() for word in rehearsal.FORBIDDEN_TEXT)

with tempfile.TemporaryDirectory() as temporary:
Expand Down Expand Up @@ -168,14 +172,14 @@ def test_sequential_reconciliation() -> None:
availability = _availability()
absent = _all(manifest, {"status": 404})
transitions = _sequential_happy_path(manifest)
assert len(transitions) == 26
assert len(transitions) == registry_fixture.NODE_COUNT
report = rehearsal.simulate_sequential(
manifest, absent, availability, transitions, simulated_at=NOW
)
assert report["complete"] is True
assert report["summary"]["nodes"] == 26
assert report["summary"]["verified"] == 26
assert len(report["events"]) == 26
assert report["summary"]["nodes"] == registry_fixture.NODE_COUNT
assert report["summary"]["verified"] == registry_fixture.NODE_COUNT
assert len(report["events"]) == registry_fixture.NODE_COUNT
assert all(event["sequence"] == index for index, event in enumerate(report["events"], 1))

all_verified = registry_fixture.observation_set(manifest)
Expand Down
3 changes: 3 additions & 0 deletions scripts/license_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,16 @@
"graphforge-core",
"graphforge-value",
"graphforge-cypher",
"graphforge-discovery",
"graphforge-exec",
"graphforge-filesystem",
"graphforge-io",
"graphforge-ir",
"graphforge-knowledge",
"graphforge-observability",
"graphforge-ontology",
"graphforge-plan",
"graphforge-portable-oci",
"graphforge-provenance",
"graphforge-rel",
"graphforge-search",
Expand Down
13 changes: 9 additions & 4 deletions scripts/verify_package_licenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,26 @@
ROOT = Path(__file__).resolve().parents[1]

# Crates intended for crates.io (bindings ship via PyPI/npm).
# Must equal `scripts/ci/crate-publish-plan.py list`; enforced by
# scripts/ci/test-crate-publish-plan.py.
CARGO_PUBLISH_CRATES = (
"graphforge-core",
"graphforge-value",
"graphforge-discovery",
"graphforge-filesystem",
"graphforge-io",
"graphforge-observability",
"graphforge-ast",
"graphforge-knowledge",
"graphforge-ontology",
"graphforge-portable-oci",
"graphforge-provenance",
"graphforge-value",
"graphforge-cypher",
"graphforge-ontology",
"graphforge-ir",
"graphforge-plan",
"graphforge-storage",
"graphforge-io",
"graphforge-rel",
"graphforge-search",
"graphforge-cypher",
"graphforge-exec",
"graphforge-api",
"graphforge-cli",
Expand Down