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
19 changes: 12 additions & 7 deletions .github/workflows/spec-hash.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Validate the common deterministic spec-hash implementation and fixture profile.
# Validate the common deterministic spec-hash and structural GeoJSON digest profiles.
name: spec-hash

"on":
Expand All @@ -12,8 +12,9 @@ name: spec-hash
- "contracts/common/spec_hash.md"
- "schemas/contracts/v1/common/spec_hash.schema.json"
- "fixtures/contracts/v1/common/spec_hash/**"
- "tests/validators/test_validate_spec_hash.py"
- "tests/validators/test_validate_spec_hash*.py"
- "data/receipts/generated/genrec-spec-hash-validator-*.json"
- "data/receipts/generated/genrec-geojson-feature-digests-*.json"
push:
branches: [main]
paths:
Expand All @@ -25,8 +26,9 @@ name: spec-hash
- "contracts/common/spec_hash.md"
- "schemas/contracts/v1/common/spec_hash.schema.json"
- "fixtures/contracts/v1/common/spec_hash/**"
- "tests/validators/test_validate_spec_hash.py"
- "tests/validators/test_validate_spec_hash*.py"
- "data/receipts/generated/genrec-spec-hash-validator-*.json"
- "data/receipts/generated/genrec-geojson-feature-digests-*.json"
workflow_dispatch:

permissions:
Expand Down Expand Up @@ -75,7 +77,7 @@ jobs:
set -euo pipefail
python -m unittest discover \
--start-directory tests/validators \
--pattern 'test_validate_spec_hash.py' \
--pattern 'test_validate_spec_hash*.py' \
--verbose
python tools/validators/validate_spec_hash.py --fixtures

Expand All @@ -85,14 +87,17 @@ jobs:
python tools/validators/validate_generated_receipt.py \
data/receipts/generated/genrec-spec-hash-validator-20260806.json \
--repo-root .
python tools/validators/validate_generated_receipt.py \
data/receipts/generated/genrec-geojson-feature-digests-20260806.json \
--repo-root .

- name: Record trust boundary
if: always()
run: |
{
echo "### Common spec-hash validation"
echo "### Common spec-hash and GeoJSON digest validation"
echo
echo "- Result: ${{ job.status }}."
echo "- Scope: bounded RFC 8785 JCS + SHA-256 computation, current sha256:<hex> shape, fixture polarity, and authoring-receipt integrity."
echo "- Boundary: a green result creates no source, evidence, policy, review, promotion, release, publication, or public-use authority."
echo "- Scope: bounded RFC 8785 JCS + SHA-256 computation, current sha256:<hex> shape, structural GeoJSON geometry/record digests, fixture polarity, and authoring-receipt integrity."
echo "- Boundary: GeoJSON digest equality is structural under a declared CRS/precision profile; it is not topological equivalence and creates no source, evidence, policy, review, promotion, release, publication, or public-use authority."
} >> "$GITHUB_STEP_SUMMARY"
122 changes: 122 additions & 0 deletions data/receipts/generated/genrec-geojson-feature-digests-20260806.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
{
"receipt_id": "genrec-geojson-feature-digests-20260806",
"contract_version": "3.0.0",
"artifact_paths": [
".github/workflows/spec-hash.yml",
"packages/hashing/src/hashing/__init__.py",
"packages/hashing/src/hashing/cli.py",
"packages/hashing/src/hashing/geojson.py",
"tests/validators/test_validate_spec_hash_geojson.py",
"tools/spec_hash/GEOJSON_FEATURE_DIGESTS.md"
],
"artifact_hashes": {
".github/workflows/spec-hash.yml": "sha256:2f8163719dfd706f546144c152a51f508a50d69f5e143ec370543ac8d39f32c5",
"packages/hashing/src/hashing/__init__.py": "sha256:288e6783711382dd1a3953f79613b4294eec9907e3716f378b1d10e15f83fc5d",
"packages/hashing/src/hashing/cli.py": "sha256:155cfac4ff345967cb2cfe9fe6890ef066d04ea6f8f4969efc279b99366ae568",
"packages/hashing/src/hashing/geojson.py": "sha256:491ed9f7eb84202299e5c174b1c2bd598de4d3883df6095849e5f7e780471eb6",
"tests/validators/test_validate_spec_hash_geojson.py": "sha256:681f02b7e38cb71200a6ef7d09613de898491a2a602d0c4ac784bb475b8f844a",
"tools/spec_hash/GEOJSON_FEATURE_DIGESTS.md": "sha256:25f4efe988dc613c7545274f5a1b042ae6e9cbd866c609ccd9927a792600d5b0"
},
"model_identity": {
"provider": "OpenAI",
"model": "GPT-5.6 Pro",
"version": "2026-08-06"
},
"prompt_or_contract": "sha256:22251a0f1604b769d1d7ae0c33ff3c75e34dea0d8083bf104260a80b60c32cbb",
"parameters": {
"seed": null,
"temperature": null,
"top_p": null,
"max_tokens": null,
"tools_enabled": [
"GitHub connector",
"uploaded-file search",
"local deterministic validation"
]
},
"inputs": {
"attached_docs": [
"New Ideas.pdf"
],
"evidence_refs": [
"attachment:New Ideas.pdf#verifiable-digests-attestations-stac-pmtiles-cog",
"repo:packages/hashing/src/hashing/core.py@a6bbaa2a7986858bd72629cf3a77181b9e72a761",
"repo:tools/spec_hash/README.md@a6bbaa2a7986858bd72629cf3a77181b9e72a761",
"repo:docs/adr/ADR-0029-adopt-directory-governance-standard-v2.md@a6bbaa2a7986858bd72629cf3a77181b9e72a761"
]
},
"truth_labels": {
".github/workflows/spec-hash.yml": "CONFIRMED",
"packages/hashing/src/hashing/__init__.py": "CONFIRMED",
"packages/hashing/src/hashing/cli.py": "CONFIRMED",
"packages/hashing/src/hashing/geojson.py": "CONFIRMED",
"tests/validators/test_validate_spec_hash_geojson.py": "CONFIRMED",
"tools/spec_hash/GEOJSON_FEATURE_DIGESTS.md": "CONFIRMED"
},
"validation_gates": [
{
"gate": "local-focused-tests",
"outcome": "PASS",
"reason": "Three deterministic no-network GeoJSON digest tests passed, including a golden vector, hash-domain separation, explicit profile inputs, fail-closed invalid input, and bounded CLI behavior."
},
{
"gate": "local-python-compile",
"outcome": "PASS",
"reason": "Changed Python modules and the new focused test compiled successfully."
},
{
"gate": "local-rfc8785-dependency-parity",
"outcome": "SKIPPED",
"reason": "The execution image did not expose the repository-pinned rfc8785 distribution; focused local tests used a test-only compatible canonicalization stand-in that is not committed. Exact dependency parity is delegated to hosted CI."
},
{
"gate": "artifact-byte-binding",
"outcome": "PASS",
"reason": "Every declared authored artifact is bound to its prepared SHA-256 bytes without receipt self-reference."
},
{
"gate": "hosted-exact-head-ci",
"outcome": "SKIPPED",
"reason": "Hosted validation requires the pushed exact pull-request head."
},
{
"gate": "human-review",
"outcome": "SKIPPED",
"reason": "Human review remains pending on the draft pull request."
}
],
"policy_decisions": [
"docs/adr/ADR-0029-adopt-directory-governance-standard-v2.md"
],
"citations": [
{
"id": "attachment:new-ideas-geojson-digests",
"validated": true,
"evidence_ref": "attachment:New Ideas.pdf#verifiable-digests-attestations-stac-pmtiles-cog"
},
{
"id": "repository:hashing-core",
"validated": true,
"evidence_ref": "repo:packages/hashing/src/hashing/core.py@a6bbaa2a7986858bd72629cf3a77181b9e72a761"
},
{
"id": "repository:directory-governance-adr",
"validated": true,
"evidence_ref": "repo:docs/adr/ADR-0029-adopt-directory-governance-standard-v2.md@a6bbaa2a7986858bd72629cf3a77181b9e72a761"
}
],
"human_review": {
"reviewer_ids": [],
"state": "pending",
"timestamp": null
},
"override_record": null,
"created_at": "2026-08-07T04:03:38Z",
"emitter": "OpenAI GPT-5.6 Pro via ChatGPT",
"links": {
"pr_number": 2099,
"adr_link": "docs/adr/ADR-0029-adopt-directory-governance-standard-v2.md",
"drift_register_entry": null
},
"notes": "Additive structural GeoJSON Feature digest profile using the repository RFC 8785 JCS plus SHA-256 implementation. The profile binds declared CRS and precision, emits separate geometry and record digests, and never claims topology or spatial equivalence. No source, evidence, policy, review, promotion, release, publication, or public-use authority is created."
}
16 changes: 16 additions & 0 deletions packages/hashing/src/hashing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,35 @@
load_json_file,
verify_spec_hash,
)
from .geojson import (
DEFAULT_COORDINATE_PRECISION,
GEOJSON_DIGEST_PROFILE,
GeoJSONDigestError,
GeoJSONFeatureDigests,
compute_geojson_feature_digests,
compute_geojson_geometry_hash,
normalize_geojson_geometry,
)

__all__ = [
"CANONICALIZATION_PROFILE",
"DEFAULT_COORDINATE_PRECISION",
"GEOJSON_DIGEST_PROFILE",
"HASH_ALGORITHM",
"SPEC_HASH_PREFIX",
"CanonicalizationFailure",
"GeoJSONDigestError",
"GeoJSONFeatureDigests",
"JsonInputError",
"SpecHashError",
"SpecHashFormatError",
"VerificationResult",
"canonicalize_json",
"compute_geojson_feature_digests",
"compute_geojson_geometry_hash",
"compute_spec_hash",
"is_valid_spec_hash",
"load_json_file",
"normalize_geojson_geometry",
"verify_spec_hash",
]
93 changes: 89 additions & 4 deletions packages/hashing/src/hashing/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Command-line interface for bounded KFM spec-hash computation and comparison."""
"""Command-line interface for bounded KFM deterministic content digests."""

from __future__ import annotations

Expand All @@ -19,6 +19,12 @@
load_json_file,
verify_spec_hash,
)
from .geojson import (
DEFAULT_COORDINATE_PRECISION,
GEOJSON_DIGEST_PROFILE,
GeoJSONDigestError,
compute_geojson_feature_digests,
)

SCOPE = "common.spec_hash"
NON_EFFECTS = [
Expand All @@ -34,13 +40,13 @@ def _emit(payload: dict[str, object]) -> None:
print(json.dumps(payload, sort_keys=True, separators=(",", ":")))


def _base(status: str) -> dict[str, object]:
def _base(status: str, *, scope: str = SCOPE) -> dict[str, object]:
return {
"authority": "NONE",
"canonicalization": CANONICALIZATION_PROFILE,
"hash_algorithm": HASH_ALGORITHM,
"non_effects": NON_EFFECTS,
"scope": SCOPE,
"scope": scope,
"status": status,
}

Expand Down Expand Up @@ -100,9 +106,51 @@ def _verify(subject_path: Path, hash_path: Path) -> int:
return 2


def _geojson_feature(
path: Path,
*,
crs: str,
precision: int,
excluded_property_keys: Sequence[str],
include_feature_id: bool,
) -> int:
scope = "geojson.feature_digests"
try:
feature = load_json_file(path)
digests = compute_geojson_feature_digests(
feature,
crs=crs,
coordinate_precision=precision,
excluded_property_keys=excluded_property_keys,
include_feature_id=include_feature_id,
)
payload = _base("GEOJSON_FEATURE_DIGESTS_CREATED", scope=scope)
payload.update(digests.as_dict())
payload["input"] = str(path)
_emit(payload)
return 0
except JsonInputError:
payload = _base("JSON_INPUT_INVALID", scope=scope)
except GeoJSONDigestError:
payload = _base("GEOJSON_DIGEST_INPUT_INVALID", scope=scope)
except CanonicalizationFailure:
payload = _base("CANONICALIZATION_ERROR", scope=scope)
payload.update(
{
"input": str(path),
"normalization_profile": GEOJSON_DIGEST_PROFILE,
}
)
_emit(payload)
return 2


def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Compute or verify deterministic RFC 8785 JCS + SHA-256 spec hashes."
description=(
"Compute or verify deterministic RFC 8785 JCS + SHA-256 content "
"digests."
)
)
subparsers = parser.add_subparsers(dest="command", required=True)

Expand All @@ -113,11 +161,48 @@ def main(argv: Sequence[str] | None = None) -> int:
verify_parser.add_argument("input", type=Path)
verify_parser.add_argument("hash_record", type=Path)

geojson_parser = subparsers.add_parser(
"geojson-feature",
help="compute separate structural geometry and record digests",
)
geojson_parser.add_argument("input", type=Path)
geojson_parser.add_argument(
"--crs",
required=True,
help="declared CRS bound into both digests, for example EPSG:4326",
)
geojson_parser.add_argument(
"--precision",
type=int,
default=DEFAULT_COORDINATE_PRECISION,
help="coordinate decimal places retained before RFC 8785 hashing",
)
geojson_parser.add_argument(
"--exclude-property",
action="append",
default=[],
dest="excluded_property_keys",
help="top-level property key to exclude; repeat for multiple keys",
)
geojson_parser.add_argument(
"--include-feature-id",
action="store_true",
help="bind the optional top-level GeoJSON Feature id into record_sha256",
)

args = parser.parse_args(argv)
if args.command == "compute":
return _compute(args.input)
if args.command == "verify":
return _verify(args.input, args.hash_record)
if args.command == "geojson-feature":
return _geojson_feature(
args.input,
crs=args.crs,
precision=args.precision,
excluded_property_keys=args.excluded_property_keys,
include_feature_id=args.include_feature_id,
)
parser.error("unsupported command")
return 2

Expand Down
Loading
Loading