diff --git a/content_explorer/quality.py b/content_explorer/quality.py index 5b1ac90..76401f0 100644 --- a/content_explorer/quality.py +++ b/content_explorer/quality.py @@ -140,14 +140,14 @@ def quality_report( entity_id = str(entity.get("id") or "") records += 1 refs = list(entity.get("sourceRefs") or []) - if provenance_ok(refs): + missing_refs = [ref for ref in refs if ref not in known_source_ids] + for ref in missing_refs: + unresolved.append({"id": entity_id, "field": "sourceRefs", "target": str(ref)}) + failures.append(_failure("unresolved-reference", entity_id, str(ref))) + if provenance_ok(refs) and not missing_refs: complete += 1 else: incomplete.append(entity_id) - for ref in refs: - if ref not in known_source_ids: - unresolved.append({"id": entity_id, "field": "sourceRefs", "target": str(ref)}) - failures.append(_failure("unresolved-reference", entity_id, str(ref))) if entity.get("state") == "stale": stale_ids.append(entity_id) failures.append(_failure("stale", entity_id, "state")) diff --git a/tests/test_quality.py b/tests/test_quality.py index bde3b28..7c28620 100644 --- a/tests/test_quality.py +++ b/tests/test_quality.py @@ -12,6 +12,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) +from content_explorer.contract import graph_errors # noqa: E402 from content_explorer.ingest import project_records, publish_directory # noqa: E402 from content_explorer.quality import FAILURE_CLASSES, quality_report, report_dataspaces # noqa: E402 @@ -129,6 +130,29 @@ class ExampleReports(unittest.TestCase): self.assertEqual(allowed_report["failureClasses"]["rights"], 0) self.assertEqual(allowed_report["failureClasses"]["missing-license"], 0) + def test_entity_source_ref_is_unresolved_beside_a_licensed_source(self) -> None: + snapshot, manifest = load_example("systems-thinking") + entity = next( + item for item in snapshot["entities"] + if item["id"] == "urn:ce:systems-thinking:concept:feedback" + ) + licensed = entity["sourceRefs"][0] + self.assertIn(licensed, {source["id"] for source in snapshot["sources"]}) + entity["sourceRefs"] = list(entity["sourceRefs"]) + ["src:does-not-exist"] + report = quality_report(snapshot, manifest, today="2026-10-19") + self.assertTrue(any( + item["id"] == entity["id"] + and item["field"] == "sourceRefs" + and item["target"] == "src:does-not-exist" + for item in report["unresolvedReferences"] + )) + self.assertGreater(report["failureClasses"]["unresolved-reference"], 0) + self.assertIn(entity["id"], report["provenance"]["incomplete"]) + self.assertTrue(any( + error == f"{entity['id']}: missing source src:does-not-exist" + for error in graph_errors(snapshot, manifest) + )) + def test_stale_state_is_counted(self) -> None: snapshot, manifest = load_example("software-architecture") entity_id = snapshot["entities"][0]["id"] diff --git a/workplans/evidence/CONTENT-WP-0005-ImplementationNotes.md b/workplans/evidence/CONTENT-WP-0005-ImplementationNotes.md index 173abc1..197efc1 100644 --- a/workplans/evidence/CONTENT-WP-0005-ImplementationNotes.md +++ b/workplans/evidence/CONTENT-WP-0005-ImplementationNotes.md @@ -25,6 +25,8 @@ Headless Chromium on `npm run dev` at `http://127.0.0.1:3400` walked the same jo `scripts/quality_report.py` prints that JSON for `examples/dataspaces` and exits non-zero when any class count is above zero or provenance is incomplete. On 2026-10-19 both examples are complete and every class count is 0. Their sources are CC0-1.0. A test that sets one source license to MIT sees `MIT` in `sources.licenses`. +An entity source ref is checked even when another source on that entity is licensed. Adding `src:does-not-exist` beside the licensed source on `urn:ce:systems-thinking:concept:feedback` records `unresolved-reference`, lists the entity under incomplete provenance, and matches `graph_errors`. The same extra ref on a relation was already reported. + ## T04 — deviations and notices `research/UXParityMatrix.md` section “Disposition after CONTENT-WP-0005” lists each upstream pattern as reused, generalized, omitted, or deferred. Faceted table and deep-linked view state are reused. Landing, kind directory, detail, backlinks, sources, glossary, graph, and collections are generalized. Timelines, comparisons, medical widgets, and the OnCo mark are omitted. A static API and Ask/MCP are deferred. diff --git a/workplans/evidence/CONTENT-WP-0005-Review.md b/workplans/evidence/CONTENT-WP-0005-Review.md index e0bf682..4167229 100644 --- a/workplans/evidence/CONTENT-WP-0005-Review.md +++ b/workplans/evidence/CONTENT-WP-0005-Review.md @@ -4,9 +4,9 @@ Date: 2026-10-19 T01–T08 are done. The public suite is `.forgejo/workflows/synthetic.yaml`, which runs `sh scripts/ci-synthetic.sh` and does not fetch OnCo data. -Local CI result: `/tmp/grok-goal-60054ed6e434/implementer/ci-local.txt` runs the workflow commands (`pip install -r requirements.txt`, `npm ci` in `ui/`, then `sh scripts/ci-synthetic.sh`) and exits 0. The same suite was run twice before that and both exited 0 (`tests-1.txt`, `tests-2.txt`): 26 unittest tests, 34 Vitest tests. Fixture validation and the third-party notice check are in `fixtures.txt`. +Local CI result: `/tmp/grok-goal-60054ed6e434/implementer/ci-local.txt` runs `sh scripts/ci-synthetic.sh` and exits 0. The same suite was run twice (`tests-1.txt`, `tests-2.txt`) and both exit 0: 27 unittest tests and 34 Vitest tests. Fixture validation and the third-party notice check are in `fixtures.txt`. `tests/test_quality.py` adds `src:does-not-exist` beside the licensed source on `urn:ce:systems-thinking:concept:feedback` and requires an `unresolved-reference` plus incomplete provenance. -Remote CI result: Forgejo check `synthetic / suite` on `9ab74290eee20b10223d06946d934967eaf7b9d5` is success (`Successful in 1m19s`). Recorded in `/tmp/grok-goal-60054ed6e434/implementer/ci-remote.txt`. The runner image already has Node, so the workflow installs the pinned Python packages with pip and does not download a separate toolchain. +Remote CI result: Forgejo check `synthetic / suite` is success. The conclusion for the commit that contains this source-ref check is in `/tmp/grok-goal-60054ed6e434/implementer/ci-remote.txt`. The runner image already has Node, so the workflow installs the pinned Python packages with pip and does not download a separate toolchain. Both example dataspaces pass browse, search, record open, neighbour follow, source and license, and graph focus. The keyboard pass in `a11y.txt` reached search, a result, the record, a relation link, and return at 1280×800 and 390×844, with page errors 0. Screenshots `wide.png` and `narrow.png` show the software-architecture concept index. They are not blank. Their pixels differ, and OnCo pixel equality was not measured (`screenshot-compare.txt`).