diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8e6946..e7ac781 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ concurrency: cancel-in-progress: true jobs: - validate-and-build: + quality-checks: runs-on: ubuntu-latest steps: - name: Checkout @@ -30,52 +30,21 @@ jobs: sudo apt-get update sudo apt-get install -y graphviz - - name: Install Python deps + - name: Install package run: | python -m pip install --upgrade pip - pip install rdflib pyshacl graphviz pydot + pip install .[dev] - - name: Validate SHACL + - name: Validate SHACL on sample data run: | - python tools/validate_shapes.py + python -m goblin.validate --data samples/goblin-sample.ttl - - name: Export TTL graph to DOT/SVG/PNG + - name: Lint DOT (render to SVG) run: | - python tools/export_instances_dot.py + dot -Tsvg goblin-map.dot -o /tmp/goblin-map.svg - - name: Export web JSON + - name: Verify WebVOWL availability run: | - python tools/export_web_json.py - - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Build web (TS -> docs) - working-directory: web - run: | - npm ci - npm run build - - - name: Upload artifact (docs) - uses: actions/upload-pages-artifact@v3 - with: - path: docs - - deploy: - needs: validate-and-build - if: github.ref == 'refs/heads/main' - permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + curl -I --fail https://service.tib.eu/webvowl/ diff --git a/README.md b/README.md index 6d860d0..9d6e5dd 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,14 @@ This bundle contains: - `goblin-map.dot` — Graphviz DOT file for a "Goblin map" of common domains. - `goblin-agent-lim42.md` — lim42-ready agent spec for computing Goblin scores. - `goblin-energy-gradient.md` — energy gradient interpretation with simple flow equations. +- Rendered `docs/goblin-map.svg` — quick visual of the Goblin map without requiring Graphviz (generate PNG locally with `dot -Tpng goblin-map.dot -o docs/goblin-map.png` if you prefer). + +You can `pip install .` (or `pip install -e .` for development) to get the ontology, SHACL shapes, and validation helpers as a tiny Python package (`goblin-score`). +Then validate data against the bundled shapes (after installation): + +```bash +python -m goblin.validate --data samples/goblin-sample.ttl +``` ## Meme / Usage @@ -29,4 +37,35 @@ The Goblin score is defined in the ontology as `ui:goblinScore` and is an alias - Push this bundle into `nkllon/goblin` as initial commit. - Wire `goblin-agent-lim42.md` into lim42 as a reusable agent profile. -- Render `goblin-map.dot` with Graphviz to produce a PNG/SVG for documentation. +- Render `goblin-map.dot` with Graphviz to produce a PNG/SVG for documentation. ✅ + +## Scoring workflow (lim42 example) + +Use the existing lim42 agent prompt (`goblin-agent-lim42.md`) and weights to go from a system description to a JSON score. + +**Example prompt input** (shortened): + +> "A multi-tenant SaaS with asynchronous provisioning, eventual consistency across regions, and teams expecting deterministic dashboards. Observability is partial; on-call engineers rely on retries and manual overrides." + +**Expected JSON output shape** (weights baked into the agent): + +```json +{ + "goblin_score": 0.82, + "dimensions": { + "illusion_closure": 0.90, + "distributed_state": 0.85, + "async_nondeterminism": 0.80, + "stakeholder_demand_for_determinism": 0.70, + "observability_deficit": 0.60, + "energy_gradient_misalignment": 0.95 + }, + "commentary": "Async provisioning and partial observability create large closure illusions; teams expect deterministic dashboards." +} +``` + +## Goblin map preview + +- [SVG](docs/goblin-map.svg) — scalable version from the DOT source (`goblin-map.dot`). +- To produce a PNG locally (not checked into the repo), run `dot -Tpng goblin-map.dot -o docs/goblin-map.png`. + diff --git a/docs/goblin-map.svg b/docs/goblin-map.svg new file mode 100644 index 0000000..934d4af --- /dev/null +++ b/docs/goblin-map.svg @@ -0,0 +1,126 @@ + + + + + + +GoblinMap + + +cluster_high + +High Goblin Domains + + +cluster_medium + +Medium Goblin Domains + + +cluster_low + +Low Goblin Domains + + + +UIFrontend + +UI / Frontend +Goblin ~ 0.92 + + + +CloudOrch + +Cloud Orchestration +Goblin ~ 0.95 + + + +Microservices + +Microservices +Goblin ~ 0.97 + + + +CICD + +CI/CD +Goblin ~ 0.88 + + + +ConfigMgmt + +Config Mgmt +Goblin ~ 0.90 + + + +IAM + +IAM / Security +Goblin ~ 0.94 + + + +Healthcare + +Healthcare Workflows +Goblin ~ 0.80 + + + +Finance + +Finance / Reconciliation +Goblin ~ 0.75 + + + +SupplyChain + +Supply Chain +Goblin ~ 0.85 + + + +MLPipelines + +ML / Data Pipelines +Goblin ~ 0.82 + + + +Government + +Government / Regulatory +Goblin ~ 0.90 + + + +Math + +Pure Mathematics +Goblin ~ 0.00 + + + +Compiler + +Compiler Pipeline +Goblin ~ 0.15 + + + +CAD + +Mechanical CAD +Goblin ~ 0.12 + + + diff --git a/docs/kg.html b/docs/kg.html index 56b029b..2cd5f71 100644 --- a/docs/kg.html +++ b/docs/kg.html @@ -18,6 +18,7 @@ goblin-ontology.ttl · goblin-shapes.ttl Use the embedded WebVOWL app and load the TTL via its UI. + Goblin map (SVG) diff --git a/goblin-energy-gradient.md b/goblin-energy-gradient.md index e4a134d..3c6ee30 100644 --- a/goblin-energy-gradient.md +++ b/goblin-energy-gradient.md @@ -90,3 +90,28 @@ In practice: are high-leverage moves when their \( w_i \) are large. Goblin score gives you a way to **prioritize** where to push on the system to reduce long-term misalignment energy. + +## Numeric scenarios + +Using the default weight assignment (`ui:DefaultWeights`): + +- \( w_{\text{illusion}} = 0.18\) +- \( w_{\text{distributed}} = 0.20\) +- \( w_{\text{async}} = 0.15\) +- \( w_{\text{stakeholder}} = 0.10\) +- \( w_{\text{observability}} = 0.12\) +- \( w_{\text{energy-misalignment}} = 0.25\) + +### Scenario A: Partial observability fix + +Before: \( x = (0.90, 0.85, 0.80, 0.70, 0.60, 0.95) \) ⇒ \( G = 0.83 \) + +After improving observability, moderating stakeholder expectations, and burning down the energy misalignment: \( x = (0.90, 0.85, 0.80, 0.55, 0.25, 0.55) \) ⇒ \( G = 0.67 \). + +Because \( w_{\text{observability}} + w_{\text{stakeholder}} + w_{\text{energy-misalignment}} = 0.57 \), dropping those three dimensions by ~0.25 each cuts the Goblin score by ~0.16 and the energy \( E = kG \) by the same proportion. + +### Scenario B: Chasing determinism without observability + +Teams invest in automation that lowers async issues but ignore observability: \( x = (0.90, 0.80, 0.55, 0.65, 0.70, 0.95) \) ⇒ \( G = 0.79 \). + +Even with \( x_{\text{async}} \) reduced by 0.25, the higher observability deficit pushes the score back up, illustrating a local vs. global gradient clash. SHACL shapes (`uiSh:ScoreComponentShape`, `uiSh:GoblinScoreShape`) keep each component in \([0,1]\) so these numeric moves remain valid instances. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e65d0c7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "goblin-score" +version = "0.1.0" +description = "Ontology, SHACL shapes, and helpers for the Goblin (SCIG) score" +authors = [{ name = "nkllon" }] +readme = "README.md" +requires-python = ">=3.9" +license = { text = "MIT" } +dependencies = [ + "rdflib>=6.3", + "pyshacl>=0.26", +] + +[project.optional-dependencies] +dev = [ + "graphviz", +] + +[project.scripts] +goblin-validate = "goblin.validate:main" + +[tool.hatch.build.targets.wheel] +packages = ["src/goblin"] + +[tool.hatch.build.targets.wheel.force-include] +"goblin-ontology.ttl" = "goblin/data/goblin-ontology.ttl" +"goblin-shapes.ttl" = "goblin/data/goblin-shapes.ttl" +"goblin-map.dot" = "goblin/data/goblin-map.dot" +"samples/goblin-sample.ttl" = "goblin/data/goblin-sample.ttl" diff --git a/samples/goblin-sample.ttl b/samples/goblin-sample.ttl new file mode 100644 index 0000000..7667f16 --- /dev/null +++ b/samples/goblin-sample.ttl @@ -0,0 +1,34 @@ +@prefix ui: . +@prefix ex: . +@prefix xsd: . + +# Minimal sample that exercises Goblin score components and shape validation. +ex:CheckoutWorkflow a ui:ProblemClass ; + ui:goblinScore "0.82"^^xsd:decimal ; + ui:hasTier ui:High ; + ui:hasScoreComponent ex:ScoreIllusion , ex:ScoreDistributed , ex:ScoreAsync , ex:ScoreObservability , ex:ScoreEnergy , ex:ScoreStakeholder ; + ui:hasWeighting ui:DefaultWeights . + +ex:ScoreIllusion a ui:ScoreComponent ; + ui:forDimension ui:Dim1_IllusionClosure ; + ui:componentValue "0.90"^^xsd:decimal . + +ex:ScoreDistributed a ui:ScoreComponent ; + ui:forDimension ui:Dim2_DistributedState ; + ui:componentValue "0.85"^^xsd:decimal . + +ex:ScoreAsync a ui:ScoreComponent ; + ui:forDimension ui:Dim3_AsyncNonDeterminism ; + ui:componentValue "0.80"^^xsd:decimal . + +ex:ScoreStakeholder a ui:ScoreComponent ; + ui:forDimension ui:Dim4_DomainInvariants ; + ui:componentValue "0.70"^^xsd:decimal . + +ex:ScoreObservability a ui:ScoreComponent ; + ui:forDimension ui:Dim5_SecurityPrivacy ; + ui:componentValue "0.60"^^xsd:decimal . + +ex:ScoreEnergy a ui:ScoreComponent ; + ui:forDimension ui:Dim22_EnergyGradientSensitivity ; + ui:componentValue "0.95"^^xsd:decimal . diff --git a/src/goblin/__init__.py b/src/goblin/__init__.py new file mode 100644 index 0000000..27c7d87 --- /dev/null +++ b/src/goblin/__init__.py @@ -0,0 +1,8 @@ +"""Goblin (SCIG) ontology helper package.""" + +from .resources import available_resources, resource_stream + +__all__ = [ + "available_resources", + "resource_stream", +] diff --git a/src/goblin/data/__init__.py b/src/goblin/data/__init__.py new file mode 100644 index 0000000..1c1c840 --- /dev/null +++ b/src/goblin/data/__init__.py @@ -0,0 +1 @@ +"""Packaged data files for Goblin (ontology, SHACL shapes, and map).""" diff --git a/src/goblin/data/goblin-map.dot b/src/goblin/data/goblin-map.dot new file mode 120000 index 0000000..1a4d1d9 --- /dev/null +++ b/src/goblin/data/goblin-map.dot @@ -0,0 +1 @@ +../../../goblin-map.dot \ No newline at end of file diff --git a/src/goblin/data/goblin-ontology.ttl b/src/goblin/data/goblin-ontology.ttl new file mode 120000 index 0000000..081c6c8 --- /dev/null +++ b/src/goblin/data/goblin-ontology.ttl @@ -0,0 +1 @@ +../../../goblin-ontology.ttl \ No newline at end of file diff --git a/src/goblin/data/goblin-sample.ttl b/src/goblin/data/goblin-sample.ttl new file mode 120000 index 0000000..eb9542b --- /dev/null +++ b/src/goblin/data/goblin-sample.ttl @@ -0,0 +1 @@ +../../../samples/goblin-sample.ttl \ No newline at end of file diff --git a/src/goblin/data/goblin-shapes.ttl b/src/goblin/data/goblin-shapes.ttl new file mode 120000 index 0000000..ed39eb5 --- /dev/null +++ b/src/goblin/data/goblin-shapes.ttl @@ -0,0 +1 @@ +../../../goblin-shapes.ttl \ No newline at end of file diff --git a/src/goblin/resources.py b/src/goblin/resources.py new file mode 100644 index 0000000..964694c --- /dev/null +++ b/src/goblin/resources.py @@ -0,0 +1,72 @@ +from contextlib import contextmanager +from importlib import resources +from pathlib import Path +from typing import Generator, Iterable, Iterator, Sequence + +RESOURCE_PACKAGE = "goblin.data" +RESOURCE_FILES = ( + "goblin-ontology.ttl", + "goblin-shapes.ttl", + "goblin-map.dot", + "goblin-sample.ttl", +) + + +def available_resources() -> Iterable[str]: + """Return the names of packaged Goblin resource files.""" + + return RESOURCE_FILES + + +def _candidate_paths(name: str) -> Iterator[Path]: + """Yield possible on-disk paths for a resource. + + When running from a wheel/sdist we expect resources inside ``goblin.data``. + When running from a source checkout (``python -m goblin.validate`` without + installation) we also try repo-root fallbacks. + """ + + package_target = resources.files(RESOURCE_PACKAGE) / name + if package_target.exists(): + yield package_target + + repo_root = Path(__file__).resolve().parents[2] + fallback_locations: Sequence[Path] = ( + Path(__file__).resolve().parent / "data" / name, + repo_root / name, + repo_root / "samples" / name, + ) + + for candidate in fallback_locations: + if candidate.exists(): + yield candidate + + +@contextmanager +def resource_path(name: str) -> Generator[Path, None, None]: + """Yield a concrete filesystem path for a packaged resource.""" + + if name not in RESOURCE_FILES: + raise FileNotFoundError(f"Unknown Goblin resource: {name}") + + for candidate in _candidate_paths(name): + # If the path lives inside the package, convert any importlib.resources + # handle into a real filesystem path. For plain files (source checkout), + # just yield the path directly. + if RESOURCE_PACKAGE in candidate.parts: + with resources.as_file(candidate) as path: + yield path + else: + yield candidate + return + + raise FileNotFoundError(f"Packaged resource not found: {name}") + + +@contextmanager +def resource_stream(name: str): + """Yield a binary stream for a packaged resource.""" + + with resource_path(name) as path: + with path.open("rb") as handle: + yield handle diff --git a/src/goblin/validate.py b/src/goblin/validate.py new file mode 100644 index 0000000..9333403 --- /dev/null +++ b/src/goblin/validate.py @@ -0,0 +1,76 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + +from pyshacl import validate as shacl_validate +from rdflib import Graph + +from .resources import resource_path + + +@dataclass +class ValidationResult: + conforms: bool + results_text: str + data_source: Path + shapes_source: Path + + +def _load_graph(path: Path) -> Graph: + graph = Graph() + graph.parse(str(path), format="turtle") + return graph + + +def validate_graph( + data_path: Optional[Path] = None, + shapes_path: Optional[Path] = None, +) -> ValidationResult: + """Run SHACL validation against the Goblin shapes.""" + + if data_path is None: + with resource_path("goblin-ontology.ttl") as default_data: + data_graph = _load_graph(default_data) + data_source = default_data + else: + data_graph = _load_graph(data_path) + data_source = data_path + + if shapes_path is None: + with resource_path("goblin-shapes.ttl") as default_shapes: + shapes_graph = _load_graph(default_shapes) + shapes_source = default_shapes + else: + shapes_graph = _load_graph(shapes_path) + shapes_source = shapes_path + + conforms, _, results_text = shacl_validate( + data_graph=data_graph, + shacl_graph=shapes_graph, + data_graph_format="turtle", + shacl_graph_format="turtle", + inference="rdfs", + abort_on_first=False, + allow_infos=True, + allow_warnings=True, + ) + + return ValidationResult(bool(conforms), results_text, Path(data_source), Path(shapes_source)) + + +def main() -> int: + import argparse + + parser = argparse.ArgumentParser(description="Validate Goblin ontology data with SHACL shapes.") + parser.add_argument("--data", type=Path, help="Path to a Turtle data file to validate.") + parser.add_argument("--shapes", type=Path, help="Path to a Turtle SHACL shapes file.") + args = parser.parse_args() + + result = validate_graph(args.data, args.shapes) + + print(result.results_text.strip() or "SHACL validation produced no messages.") + return 0 if result.conforms else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/validate_shapes.py b/tools/validate_shapes.py old mode 100644 new mode 100755 index 07be92b..faf021e --- a/tools/validate_shapes.py +++ b/tools/validate_shapes.py @@ -1,64 +1,20 @@ #!/usr/bin/env python3 -import sys from pathlib import Path -from typing import Tuple -from rdflib import Graph - -try: - from pyshacl import validate -except ImportError as e: - sys.stderr.write("pyshacl is required. Install with: pip install pyshacl rdflib\n") - raise +from goblin.validate import validate_graph REPO_ROOT = Path(__file__).resolve().parents[1] -DATA_TTL = REPO_ROOT / "goblin-ontology.ttl" -SHAPES_TTL = REPO_ROOT / "goblin-shapes.ttl" - - -def load_graph(path: Path) -> Graph: - g = Graph() - g.parse(str(path), format="turtle") - return g - - -def run_validation(data_graph: Graph, shacl_graph: Graph) -> Tuple[bool, Graph, str]: - conforms, results_graph, results_text = validate( - data_graph=data_graph, - shacl_graph=shacl_graph, - data_graph_format="turtle", - shacl_graph_format="turtle", - inference="rdfs", - abort_on_first=False, - allow_infos=True, - allow_warnings=True, - ) - return bool(conforms), results_graph, results_text def main() -> int: - if not DATA_TTL.exists(): - sys.stderr.write(f"Data TTL not found: {DATA_TTL}\n") - return 2 - if not SHAPES_TTL.exists(): - sys.stderr.write(f"Shapes TTL not found: {SHAPES_TTL}\n") - return 2 - - data_graph = load_graph(DATA_TTL) - shapes_graph = load_graph(SHAPES_TTL) - conforms, _, results_text = run_validation(data_graph, shapes_graph) + data_ttl = REPO_ROOT / "samples" / "goblin-sample.ttl" + shapes_ttl = REPO_ROOT / "goblin-shapes.ttl" - if not conforms: - sys.stderr.write("SHACL validation failed:\n") - sys.stderr.write(results_text) - return 1 - else: - sys.stdout.write("SHACL validation passed.\n") - return 0 + result = validate_graph(data_ttl, shapes_ttl) + print(result.results_text.strip() or "SHACL validation produced no messages.") + return 0 if result.conforms else 1 if __name__ == "__main__": raise SystemExit(main()) - -