diff --git a/assets/android-chrome-512x512.png b/assets/android-chrome-512x512.png new file mode 100644 index 00000000..ed01eb0b Binary files /dev/null and b/assets/android-chrome-512x512.png differ diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 00000000..55799672 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/deno.json b/deno.json index c7206020..1b1c33d9 100644 --- a/deno.json +++ b/deno.json @@ -4,7 +4,8 @@ "fmt:check": "deno fmt --check deno.json scripts tests", "lint": "deno lint scripts tests", "check": "deno check scripts/**/*.ts tests/**/*.ts", - "test": "deno test --allow-read --allow-env=READABLE_STREAM tests", + "test": "deno test --allow-read --allow-write --allow-env=READABLE_STREAM tests", + "release:set-version": "deno run --allow-read --allow-write scripts/release_set_version.ts", "release:validate": "deno run --allow-read --allow-run=git --allow-env=READABLE_STREAM scripts/release_validate.ts", "ci": "deno task fmt:check && deno task lint && deno task check && deno task test && deno task release:validate" }, diff --git a/.gitignore b/notes/.gitignore similarity index 100% rename from .gitignore rename to notes/.gitignore diff --git a/notes/ont.decision-log.md b/notes/ont.decision-log.md index 253f962c..54fab70c 100644 --- a/notes/ont.decision-log.md +++ b/notes/ont.decision-log.md @@ -10,6 +10,24 @@ created: 1773896763313 Superseded decisions are intentionally retained for traceability. When a decision is reversed or replaced, mark it explicitly rather than deleting it. +### 2026-05-24: Separate resolution targets from resolution observations and clarify reference sources + +- Status: Active +- Decision: Keep `ArtifactResolutionTarget` as the requested-coordinate / resolution-policy relator, and add `ArtifactResolutionObservation` for observed resolution evidence. Add `ReferenceSource`, `ImportSource`, and `IntegrationSource` as `ArtifactResolutionTarget` subclasses alongside existing `ExtractionSource` and `ResourcePageSource`. Replace direct `ReferenceLink` target vocabulary with `hasReferenceSource`; remove `referenceTarget`, `referenceTargetState`, `referenceUriLiteral`, and extraction-scoped observed-source properties from the live model. +- References: [[ont.summary.core]], [[ont.reference-links]], [[ont.task.2026.2026-05-24_1256-artifact-resolution-observations]], [[wa.task.2026.2026-05-22_1128-referencelink-clarification]] +- Why: + - requested coordinates and observations have different lifecycles: coordinates are source-binding policy, while observations are appendable evidence records + - extraction-specific observed evidence would become increasingly awkward as import, integrate, reference, and page-source resolution all need the same evidence pattern + - `referenceTarget` was misleading because the target of the reference relation is already named by `referenceLinkFor`; the RDF data being read is a source + - `ReferenceLink` should remain the semantic curated-reference relator, while `ReferenceSource` carries byte-resolution coordinates + - import and integrate are distinct application concerns and should not be hidden behind bare `ArtifactResolutionTarget` bindings +- Notes: + - `ReferenceLink` remains RDF-reference-data-only in this slice; do not rename it to `RdfReferenceLink` + - use `hasReferenceSource` from `ReferenceLink` to exactly one `ReferenceSource` in the first slice + - use `hasResolutionObservation` from an `ArtifactResolutionTarget` subclass to one or more `ArtifactResolutionObservation` records when an operation intentionally records evidence + - keep `expectsContentDigest` on `ArtifactResolutionTarget`, but record observed byte identity as `observedContentDigest` on observations + - ordinary runtime reads and page generation should not mutate RDF merely by resolving source bytes + ### 2026-05-16: Separate sparse artifact file links from manifestation file links - Status: Active diff --git a/notes/ont.dev.release-runbook.md b/notes/ont.dev.release-runbook.md index dbca4d7c..d9557112 100644 --- a/notes/ont.dev.release-runbook.md +++ b/notes/ont.dev.release-runbook.md @@ -56,6 +56,14 @@ git diff --stat git diff ``` +After choosing the release version and issue date, update deterministic source metadata: + +```sh +deno task release:set-version -- --version 0.1.0 --issued 2026-05-17 +``` + +This command only updates release metadata in the active Turtle files. It does not create release notes, create tags, commit, push, generate Pages output, or infer release intent from source changes. + Check version metadata: ```sh diff --git a/notes/ont.reference-links.md b/notes/ont.reference-links.md index aa13fe33..a49f05c3 100644 --- a/notes/ont.reference-links.md +++ b/notes/ont.reference-links.md @@ -10,18 +10,34 @@ created: 1775175668572 This note captures the current serialization and generation rules for `ReferenceCatalog` and `ReferenceLink`. -Use it as the detailed companion to [ont.summary.core.md](./ont.summary.core.md). The summary note should stay compact; the path, fragment-IRI, and dereferenceability rules belong here. +Use it as the detailed companion to [[ont.summary.core]]. The summary note should stay compact; the path, fragment-IRI, and dereferenceability rules belong here. ## Core Model - `ReferenceCatalog` is a narrow support artifact for managed `ReferenceLink` relators. - A `ReferenceCatalog` is owned by a `Knop` or `SemanticMesh`, but its contents are about the referent or mesh subject rather than about the support object as such. - `referenceLinkFor` points to the actual subject resource, not to a `Knop`. -- `referenceTarget` points to a broad resource identity. -- `referenceTargetState` may be used when a link should pin to a specific `HistoricalState`. +- `hasReferenceSource` points to the `ReferenceSource` that identifies the RDF reference data used by the link. +- `ReferenceSource` is an `ArtifactResolutionTarget` subclass. It uses generic target-resolution vocabulary such as `hasTargetArtifact`, `hasRequestedTargetState`, `targetAccessUrl`, and repository locator properties. +- A first-slice `ReferenceSource` that uses `hasTargetArtifact` should target an `RdfDocument`. Non-RDF content such as Markdown page prose does not belong in `ReferenceLink`. `verifiedAt` and `verifiedBy` remain latest-verification convenience fields on `ReferenceLink`. They do not yet imply a separate verification object model. +If source verification is intentionally recorded, the `ReferenceSource` should link to an `ArtifactResolutionObservation` with `hasResolutionObservation`. Ordinary page generation or source reads should not append observation records merely because bytes were resolved. + +Example: + +```turtle + a sflo:ReferenceLink ; + sflo:referenceLinkFor ; + sflo:hasReferenceRole sflo:referenceRole_canonical ; + sflo:hasReferenceSource . + + a sflo:ReferenceSource ; + sflo:hasTargetArtifact ; + sflo:hasRequestedTargetState . +``` + ## Serialization Direction For a Knop-owned catalog, the support artifact lives under the Knop: @@ -45,6 +61,7 @@ This keeps `ReferenceCatalog` aligned with other support artifacts such as `_met For example: - `` +- `` for the companion `ReferenceSource` Use the same mesh-root `@base` everywhere in the mesh or sub-mesh, and write the full mesh-root-relative fragment IRI explicitly. Do not use bare `<#reference001>` in historical snapshots, because that would re-root the identity to the snapshot document instead of preserving the stable catalog-fragment IRI. @@ -82,5 +99,7 @@ If historical link-name collisions are detected while scanning current plus prio ## Scope Boundaries - `ReferenceCatalog` is for `ReferenceLink` relators only. +- `ReferenceLink` is for curated RDF reference data; do not use it for page prose, Markdown imports, image assets, or generic source provenance. +- `ReferenceSource` helper nodes may live in the same catalog because they are immediate source bindings for the links. - Broader descriptive RDF about the referent should normally live in a payload artifact or dataset. - This note does not broaden `ReferenceCatalog` to cover `owl:sameAs` or arbitrary referent description. diff --git a/notes/ont.release-notes.v0.1.1.md b/notes/ont.release-notes.v0.2.0.md similarity index 57% rename from notes/ont.release-notes.v0.1.1.md rename to notes/ont.release-notes.v0.2.0.md index faba490f..aedc6085 100644 --- a/notes/ont.release-notes.v0.1.1.md +++ b/notes/ont.release-notes.v0.2.0.md @@ -1,6 +1,6 @@ --- id: d7pl15zys1da0kvfyfpni7k -title: 'SFLO Ontology Release Notes v0.1.1' +title: 'SFLO Ontology Release Notes v0.2.0' desc: '' updated: 1779178697607 created: 1779178697607 @@ -8,7 +8,7 @@ created: 1779178697607 ## Summary -`v0.1.1` is the next Semantic Flow ontology release after the initial `v0.1.0` tag. It focuses on publication and source-resolution vocabulary needed by Weave's branch-published and sidecar mesh workflows: explicit publication profiles, clearer artifact resolution modes, repository-backed source bindings, and SHACL severities that distinguish hard violations from warnings and informational modeling guidance. +`v0.2.0` is the next planned Semantic Flow ontology release after the initial `v0.1.0` tag. It folds the unpublished `v0.1.1` branch work into a minor-version bump because the branch now contains breaking vocabulary and SHACL changes. The release focuses on publication and source-resolution vocabulary needed by Weave's branch-published and sidecar mesh workflows: explicit publication profiles, repository-backed source bindings, floating repository working sources, clearer artifact resolution modes, reference-source records, observation evidence records, and ResourcePage presentation configuration. ## Release Surfaces @@ -22,18 +22,28 @@ The tagged source contains: The Pages publication exposes the core ontology, core SHACL, and config ontology under the `/sflo/` project Pages mesh: -- `https://semantic-flow.github.io/sflo/ontology/releases/v0.1.1/ttl/semantic-flow-core-ontology.ttl` -- `https://semantic-flow.github.io/sflo/ontology/shacl/releases/v0.1.1/ttl/semantic-flow-core-shacl.ttl` -- `https://semantic-flow.github.io/sflo/config/releases/v0.1.1/ttl/semantic-flow-config-ontology.ttl` +- `https://semantic-flow.github.io/sflo/ontology/releases/v0.2.0/ttl/semantic-flow-core-ontology.ttl` +- `https://semantic-flow.github.io/sflo/ontology/shacl/releases/v0.2.0/ttl/semantic-flow-core-shacl.ttl` +- `https://semantic-flow.github.io/sflo/config/releases/v0.2.0/ttl/semantic-flow-config-ontology.ttl` The job and provenance ontology source files remain tagged source vocabulary unless or until their non-`/sflo/` publication topology is settled. +## Breaking Changes + +- The unpublished `v0.1.1` release plan is superseded by `v0.2.0`; no `v0.1.1` tag should be cut from this branch. +- `ReferenceLink` no longer uses `referenceTarget`, `referenceTargetState`, or `referenceUriLiteral`. A `ReferenceLink` now points to exactly one `ReferenceSource` with `hasReferenceSource`, and that `ReferenceSource` uses the shared `ArtifactResolutionTarget` vocabulary to identify RDF reference-data bytes. +- Extraction-scoped observed-source properties are removed from the live model: `hasObservedSourceState`, `hasObservedSourceManifestation`, `hasObservedSourceLocatedFile`, `observedSourceLocalRelativePath`, and `observedSourceDigest`. Observed resolution evidence now belongs in `ArtifactResolutionObservation` records linked from an artifact-resolution target with `hasResolutionObservation`. +- The old `artifactResolutionMode_pinned` and `artifactResolutionMode_current` values remain retired. Active resolution mode vocabulary is now `artifactResolutionMode_working` and `artifactResolutionMode_latestState`, with exact requested state, located-file, manifestation, commit, or digest coordinates treated as exact by default. +- ResourcePage presentation configuration now has explicit config and SHACL structure for templates, stylesheets, generated panel selection, panel ordering, inclusion policy, targeting, and data requirements. Older ad hoc assumptions about generated panels are not the modeled contract for this release. +- Repository source modeling now distinguishes exact or ref-backed `RepositorySourceLocator` records from mutable working-source `RepositorySourceFloatingLocator` records. Floating locators deliberately omit branch/ref, commit, digest, and local checkout path evidence. + ## Highlights -- Core artifact resolution vocabulary now distinguishes `artifactResolutionMode_working` from `artifactResolutionMode_latestState`. +- Core artifact resolution vocabulary distinguishes mutable working-byte resolution from latest settled-state resolution. - Exact requested target coordinates, such as a requested `HistoricalState`, `LocatedFile`, manifestation/distribution, commit, or digest, imply exact identity by default. -- `artifactResolutionMode_pinned` and `artifactResolutionMode_current` are removed from the active model; they were too broad for the source-binding and release-publication cases now being modeled. - A requested `ArtifactHistory` without a requested state means "latest state in that history" by default. +- `ReferenceSource`, `ImportSource`, and `IntegrationSource` join `ExtractionSource` and `ResourcePageSource` as shared `ArtifactResolutionTarget` subclasses. +- `ArtifactResolutionObservation` records observed state, manifestation, located file, local path, digest, timestamp, and observer evidence only when an operation intentionally records resolution evidence. - Config vocabulary adds `PublicationProfile`, `publicationProfile_none`, `publicationProfile_githubPages`, and `hasPublicationProfile` so a mesh can persist its resolved static-publication host profile. - GitHub Pages profile semantics are intentionally narrow: the profile covers host controls such as `.nojekyll`; Weave and the ontology do not treat `CNAME` as managed publication state. - `hasNextHistorySegmentHint` and `hasNextStateSegmentHint` are now scoped to the relevant digital-artifact/history progression objects instead of generic config resources. @@ -50,7 +60,7 @@ The job and provenance ontology source files remain tagged source vocabulary unl ## Version Metadata -The actively released Turtle files align their release metadata on `0.1.1` and plural `releases/v0.1.1` paths: +The actively released Turtle files align their release metadata on `0.2.0` and plural `releases/v0.2.0` paths: - `owl:versionInfo` - `dcterms:hasVersion` @@ -62,10 +72,10 @@ The actively released Turtle files align their release metadata on `0.1.1` and p The release process handles the `owl:versionIRI` chicken-and-egg explicitly. `owl:versionIRI` points to immutable raw bytes for the final tag, while the tag itself is created only after the source metadata commit exists. The safe sequence is: -1. Explicitly choose the release version, such as `0.1.1`. -2. Mechanically update source metadata to deterministic `v0.1.1` tag URLs and `releases/v0.1.1` publication URLs. +1. Explicitly choose the release version, such as `0.2.0`. +2. Mechanically update source metadata to deterministic `v0.2.0` tag URLs and `releases/v0.2.0` publication URLs. 3. Validate the Turtle and commit the source release. -4. Create and push the `v0.1.1` tag at that exact commit. +4. Create and push the `v0.2.0` tag at that exact commit. 5. Regenerate the Pages publication from a detached source worktree checked out at the tag commit. 6. Verify that the raw tag URLs and Pages release URLs fetch the expected bytes. @@ -76,7 +86,9 @@ Automation should help with steps 2, 3, 5, and 6, but it should not silently inv The source release validation follows [[ont.dev.release-runbook]]: ```sh -rg 'versionInfo|hasVersion|versionIRI|contentUrl|downloadURL|releases/v0\.1\.1' semantic-flow-*.ttl +rg 'versionInfo|hasVersion|versionIRI|contentUrl|downloadURL|releases/v0\.2\.0' semantic-flow-*.ttl +deno task release:validate -- --version 0.2.0 +deno task ci riot --validate semantic-flow-core-ontology.ttl semantic-flow-core-shacl.ttl semantic-flow-config-ontology.ttl semantic-flow-job-ontology.ttl semantic-flow-prov-ontology.ttl git diff --check ``` diff --git a/notes/ont.summary.core.md b/notes/ont.summary.core.md index 2027a994..e46cf020 100644 --- a/notes/ont.summary.core.md +++ b/notes/ont.summary.core.md @@ -40,7 +40,7 @@ Use `designatorPath` when you mean the modeled mesh-relative naming value on a ` Use `designator` only as informal prose shorthand for that value. Use `Semantic Flow identifier` when you mean the full IRI formed from a mesh base plus a `Knop`'s `designatorPath`. -For detailed `ReferenceCatalog` / `ReferenceLink` serialization and dereferenceability rules, see [ont.reference-links.md](./ont.reference-links.md). +For detailed `ReferenceCatalog` / `ReferenceLink` serialization and dereferenceability rules, see [[ont.reference-links]]. ### Canonical URLs On Static Hosts @@ -141,20 +141,25 @@ Substantive RDF about a referent should normally live in a payload artifact or d - `ResourcePageDefinition` is a separate artifact-level control resource for customized identifier-page composition; it is not the same thing as the generated HTML `ResourcePage` - `KnopAssetBundle` is a bounded helper structure for local `_knop/_assets` modeling and does not by itself imply governed artifacts or recursive inventory capture - `KnopSourceRegistry` is a Knop-owned support artifact for source bindings, conventionally materialized under `_knop/_sources`; it records materialization sources and extraction provenance rather than operational mesh configuration or descriptive payload facts -- `ArtifactResolutionTarget` is the generic policy-bearing relator for application concerns that need to resolve bytes from an artifact, a direct mesh-local path string, a direct access URL, a specific `LocatedFile`, or another explicit packaged target together with optional history/state/mode/fallback inputs +- `ArtifactResolutionTarget` is the generic policy-bearing relator for application concerns that need to resolve bytes from an artifact, a direct mesh-local path string, a direct access URL, a specific `LocatedFile`, or another explicit packaged target together with optional history/state/mode/fallback inputs; it carries requested coordinates and expectations, not observed evidence directly +- `ArtifactResolutionObservation` is the relator for observed resolution evidence such as observed state, manifestation, located file, local path, content digest, timestamp, and observer; targets link to observations with `hasResolutionObservation` when an operation intentionally records evidence - `ExtractionSource` specializes `ArtifactResolutionTarget` for the source RDF document bytes from which a Knop-managed resource was extracted or first grounded; Knops link to it with `hasExtractionSource`, usually as a source-registry fragment such as `D/_knop/_sources#extraction-source` +- `ReferenceSource` specializes `ArtifactResolutionTarget` for RDF reference data used by a `ReferenceLink`; it normally lives in the `ReferenceCatalog` beside the owning link +- `ImportSource` specializes `ArtifactResolutionTarget` for source bytes actively acquired and copied into a governed local working surface by import +- `IntegrationSource` specializes `ArtifactResolutionTarget` for existing source bytes registered where they already live by integrate - `ResourcePageRegion` and `ResourcePageSource` describe page-content composition in core; `ResourcePageSource` specializes `ArtifactResolutionTarget` and uses the generic target/history/state/mode/fallback properties directly while template/chrome policy remains a separate config concern -- `ReferenceLink` may target either a broad resource identity through `referenceTarget` or a version-pinned state through `referenceTargetState`. +- `ReferenceLink` is a curated RDF reference-data relator about the resource named by `referenceLinkFor`; it points to exactly one `ReferenceSource` with `hasReferenceSource`, and that source uses generic target coordinates such as `hasTargetArtifact` and `hasRequestedTargetState` - `designatorPath` is the mesh-relative path-like naming value carried by a `Knop`; it is not a generic path property for every `SemanticFlowResource`. - a `Semantic Flow identifier` is the public IRI formed from `meshBase + Knop.designatorPath`; support resources in the mesh may still have ordinary IRIs without thereby being Semantic Flow identifiers. - `preferredPayloadFileSlug` is the mutable filename preference. Source registries are the current home for two related but distinct records: -- repository-backed materialization bindings, where an `ArtifactResolutionTarget` records the target artifact plus repository URL, ref, resolved commit, repository-relative path, and byte digest for the source that was copied into the mesh -- extraction provenance, where an `ExtractionSource` records the source artifact coordinates used to ground an extracted Knop, the resolution mode, the observed state/manifestation/located file when those are available, an observed digest, and at most a local relative path fallback when no durable located file is modeled +- integration bindings, where an `IntegrationSource` records existing source bytes where they already live, such as repository URL, ref, resolved commit, repository-relative path, local relative path, or byte digest expectations +- import bindings, where an `ImportSource` records source bytes actively acquired and copied into a governed working surface +- extraction provenance, where an `ExtractionSource` records the source artifact coordinates used to ground an extracted Knop and links to `ArtifactResolutionObservation` records for observed state, manifestation, located file, local path, digest, timestamp, or observer evidence when intentionally recorded -These records are not curated references. Use `ReferenceCatalog` / `ReferenceLink` when the mesh wants to say that one resource intentionally references another. Use `_knop/_sources` when the mesh needs to explain where carried bytes or extracted-term evidence came from. Inventory-rooted extraction-source detail blocks are historical only; current serialization keeps the details in `_knop/_sources/sources.ttl` and lets the Knop inventory point to that registry and to the primary extraction source. +These records are not curated references. Use `ReferenceCatalog` / `ReferenceLink` / `ReferenceSource` when the mesh wants to say that one resource intentionally references RDF data about it. Use `_knop/_sources` when the mesh needs to explain where carried bytes or extracted-term evidence came from. Inventory-rooted extraction-source detail blocks are historical only; current serialization keeps the details in `_knop/_sources/sources.ttl` and lets the Knop inventory point to that registry and to the primary extraction source. ## Things To Not Reintroduce diff --git a/notes/ont.task.2026.2026-03-24-release-runbook.md b/notes/ont.task.2026.2026-03-24-release-runbook.md index d465d7ae..d0dedfc8 100644 --- a/notes/ont.task.2026.2026-03-24-release-runbook.md +++ b/notes/ont.task.2026.2026-03-24-release-runbook.md @@ -39,6 +39,7 @@ Because the first action re-renders existing mesh state instead of appending pay - `weave prepare gh-pages` has been removed rather than deprecated; detached publication roots should use composed mesh operations. - The first sflo-owned Deno guardrail harness now runs RDF parseability, duplicate-triple, config namespace, retired-term, and selected SHACL source-binding checks, and [[ont.dev.release-runbook]] now includes `deno task ci` as a release preflight command. - SFLO now has `deno task release:validate`, which checks active source Turtle release metadata, namespace policy, release notes, version URLs, and optional tag-at-HEAD consistency with `--require-tag`. +- SFLO now has `deno task release:set-version`, which rewrites deterministic release metadata in the active Turtle files for an explicit version and issue date without creating tags, commits, release notes, or Pages output. ### Action: Re-generate Resource Pages @@ -133,6 +134,7 @@ The GitHub Actions work should therefore be staged: - For documentation-only work, run `git diff --check`. - For the current Deno harness slice, run `deno task ci` in sflo. +- To prepare source Turtle metadata for a chosen release, run `deno task release:set-version -- --version --issued `, then validate it. - For source release validation only, run `deno task release:validate -- --version `; after the tag exists locally, add `--require-tag`. - [x] Before closing this task, add release-validation coverage for active Turtle files and release metadata. - Before closing this task, add ResourcePage-action coverage that proves the action runs validation/generation without appending payload states. @@ -158,6 +160,7 @@ The GitHub Actions work should therefore be staged: - [ ] Finish or unblock [[wa.task.2026.2026-05-18_0627-remove-prepare]] and [[wa.task.2026.2026-05-17_2206-prepare-symmetry]] enough that branch-published meshes use the same conceptual operations as sidecar meshes. - [x] Update [[ont.dev.release-runbook]] to remove `prepare gh-pages`, document validation/generation for existing publication meshes, and call out the detached-source integration path. - [x] Add an SFLO release-validation script or command covering source Turtle, release metadata, release notes, namespace policy, and tag/version consistency. +- [x] Add an SFLO release metadata setter for deterministic Turtle version, release IRI, tag URL, payload URL, and issued-date updates. - [ ] Add a GitHub Actions workflow for "Re-generate Resource Pages" with a `run_weave_validate` input. - [ ] Add a GitHub Actions workflow for "Release" with a `generate_after_validation` input. - [ ] Add a publication dry-run/reporting step once the publication command path is stable. diff --git a/notes/ont.todo.md b/notes/ont.todo.md new file mode 100644 index 00000000..880a953e --- /dev/null +++ b/notes/ont.todo.md @@ -0,0 +1,11 @@ +--- +id: qqdv0wztins3cdxgbgafbfk +title: Todo +desc: '' +updated: 1779334663984 +created: 1779334603396 +--- + +## Feature Ideas + +- canonicalManifestation (e.g., the one that matches the working file) could be helpful. Also in the case where the manifestation that is included in ResourcePages is different than the working file diff --git a/notes/principles.md b/notes/principles.md index 25a349a5..8900062d 100644 --- a/notes/principles.md +++ b/notes/principles.md @@ -2,7 +2,7 @@ id: p9f3gmbx90yp2trxcvrz8pe title: Principles desc: '' -updated: 1775547393408 +updated: 1779677678383 created: 1773878299267 --- @@ -21,4 +21,8 @@ Transposability is the ability to move a mesh to a different and have ## Psuedo-immutability -Ideally, once minted, states don't change. Excpetions for privacy/security concerns are understandable. \ No newline at end of file +Ideally, once minted, states don't change. Excpetions for privacy/security concerns are understandable. + +## Avoid local path leakage into published meshes + +Maybe it's occasionally avoidable when working files are outside the repo, but ArtifactResolutionTargets work better as URLs or repo targets, since local path relations aren't consistent between devices. \ No newline at end of file diff --git a/notes/task.schema.yml b/notes/task.schema.yml deleted file mode 100644 index 9ceabc26..00000000 --- a/notes/task.schema.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: 1 -schemas: - - id: ont - parent: root - # Match note names like `ont.task.2026.some-task`, then apply the task - # template to the task notes created beneath the year bucket. - children: - - pattern: "task" - children: - - pattern: "[0-2][0-9][0-9][0-9]" - children: - - pattern: "*" - template: template.task diff --git a/scripts/release_metadata.ts b/scripts/release_metadata.ts new file mode 100644 index 00000000..984d3f53 --- /dev/null +++ b/scripts/release_metadata.ts @@ -0,0 +1,88 @@ +export const ACTIVE_RELEASE_FILES = [ + { + file: "semantic-flow-core-ontology.ttl", + ontologyIri: "https://semantic-flow.github.io/sflo/ontology", + namespaceUri: "https://semantic-flow.github.io/sflo/ontology/", + prefix: "sflo", + pagesPublished: true, + }, + { + file: "semantic-flow-core-shacl.ttl", + ontologyIri: "https://semantic-flow.github.io/sflo/ontology/shacl", + namespaceUri: "https://semantic-flow.github.io/sflo/ontology/shacl/", + prefix: "sflo-shacl", + pagesPublished: true, + }, + { + file: "semantic-flow-config-ontology.ttl", + ontologyIri: "https://semantic-flow.github.io/sflo/config", + namespaceUri: "https://semantic-flow.github.io/sflo/config/", + prefix: "sfcfg", + pagesPublished: true, + }, + { + file: "semantic-flow-job-ontology.ttl", + ontologyIri: "https://semantic-flow.github.io/ontology/job", + namespaceUri: "https://semantic-flow.github.io/ontology/job/", + prefix: "sfjob", + pagesPublished: false, + }, + { + file: "semantic-flow-prov-ontology.ttl", + ontologyIri: "https://semantic-flow.github.io/ontology/prov", + namespaceUri: "https://semantic-flow.github.io/ontology/prov/", + prefix: "sfprov", + pagesPublished: false, + }, +] as const; + +export type ReleaseFileDescriptor = typeof ACTIVE_RELEASE_FILES[number]; + +export function releaseIris( + descriptor: ReleaseFileDescriptor, + version: string, +): ReleaseIris { + const releaseIri = `${descriptor.ontologyIri}/releases/v${version}`; + const manifestationIri = `${releaseIri}/ttl`; + const releasePayloadIri = `${manifestationIri}/${descriptor.file}`; + const versionIri = + `https://raw.githubusercontent.com/semantic-flow/sflo/refs/tags/v${version}/${descriptor.file}`; + + return { + manifestationIri, + releaseIri, + releasePayloadIri, + versionIri, + }; +} + +export function isSemver(value: string): boolean { + return /^\d+\.\d+\.\d+$/.test(value); +} + +export function isDate(value: string): boolean { + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); + if (!match) { + return false; + } + + const [, yearText, monthText, dayText] = match; + const year = Number(yearText); + const month = Number(monthText); + const day = Number(dayText); + if (month < 1 || month > 12 || day < 1 || day > 31) { + return false; + } + + const date = new Date(year, month - 1, day); + return date.getFullYear() === year && + date.getMonth() + 1 === month && + date.getDate() === day; +} + +export interface ReleaseIris { + manifestationIri: string; + releaseIri: string; + releasePayloadIri: string; + versionIri: string; +} diff --git a/scripts/release_set_version.ts b/scripts/release_set_version.ts new file mode 100644 index 00000000..e3a3f71a --- /dev/null +++ b/scripts/release_set_version.ts @@ -0,0 +1,264 @@ +import { fromFileUrl, join } from "@std/path"; +import { + ACTIVE_RELEASE_FILES, + isDate, + isSemver, + type ReleaseFileDescriptor, + releaseIris, +} from "./release_metadata.ts"; + +const REPO_ROOT = fromFileUrl(new URL("../", import.meta.url)); +const VERSION_PATTERN = String.raw`\d+\.\d+\.\d+`; +const DATE_PATTERN = String.raw`\d{4}-\d{2}-\d{2}`; + +export interface ReleaseSetVersionOptions { + dryRun?: boolean; + issued: string; + root?: string; + version: string; +} + +export interface ReleaseSetVersionResult { + changedFiles: string[]; + unchangedFiles: string[]; +} + +export async function setReleaseVersion( + options: ReleaseSetVersionOptions, +): Promise { + validateOptions(options); + + const root = options.root ?? REPO_ROOT; + const updates: FileUpdate[] = []; + const errors: string[] = []; + + for (const descriptor of ACTIVE_RELEASE_FILES) { + const path = join(root, descriptor.file); + try { + const previous = await Deno.readTextFile(path); + const next = updateReleaseMetadata( + previous, + descriptor, + options.version, + options.issued, + ); + updates.push({ + changed: previous !== next, + next, + path, + relativePath: descriptor.file, + }); + } catch (error) { + errors.push(`${descriptor.file}: ${errorMessage(error)}`); + } + } + + if (errors.length > 0) { + throw new Error(`Could not set release version:\n- ${errors.join("\n- ")}`); + } + + if (!options.dryRun) { + for (const update of updates) { + if (update.changed) { + await Deno.writeTextFile(update.path, update.next); + } + } + } + + return { + changedFiles: updates + .filter((update) => update.changed) + .map((update) => update.relativePath), + unchangedFiles: updates + .filter((update) => !update.changed) + .map((update) => update.relativePath), + }; +} + +export function updateReleaseMetadata( + contents: string, + descriptor: ReleaseFileDescriptor, + version: string, + issued: string, +): string { + const { releaseIri, versionIri } = releaseIris(descriptor, version); + let next = contents; + + next = replaceRequired( + next, + new RegExp( + `${escapeRegExp(descriptor.ontologyIri)}/releases/v${VERSION_PATTERN}`, + "g", + ), + releaseIri, + "release resource IRIs", + ); + next = replaceRequired( + next, + new RegExp( + `https://raw\\.githubusercontent\\.com/semantic-flow/sflo/refs/tags/v${VERSION_PATTERN}/${ + escapeRegExp(descriptor.file) + }`, + "g", + ), + versionIri, + "raw tag version IRI", + ); + next = replaceRequired( + next, + new RegExp(`owl:versionInfo "${VERSION_PATTERN}"`, "g"), + `owl:versionInfo "${version}"`, + "owl:versionInfo literals", + ); + next = replaceRequired( + next, + new RegExp(`dcterms:issued "${DATE_PATTERN}"\\^\\^xsd:date`, "g"), + `dcterms:issued "${issued}"^^xsd:date`, + "release issued date", + ); + + return next; +} + +export function parseReleaseSetVersionArgs( + args: readonly string[], +): ParsedReleaseSetVersionArgs { + let dryRun = false; + let issued: string | undefined; + let root: string | undefined; + let version: string | undefined; + + for (let index = 0; index < args.length; index++) { + const arg = args[index]; + + if (arg === "--") { + continue; + } + + if (arg === "--dry-run") { + dryRun = true; + continue; + } + + if (arg === "--issued") { + issued = requiredValue(args, ++index, "--issued"); + continue; + } + + if (arg.startsWith("--issued=")) { + issued = arg.slice("--issued=".length); + continue; + } + + if (arg === "--root") { + root = requiredValue(args, ++index, "--root"); + continue; + } + + if (arg.startsWith("--root=")) { + root = arg.slice("--root=".length); + continue; + } + + if (arg === "--version") { + version = requiredValue(args, ++index, "--version"); + continue; + } + + if (arg.startsWith("--version=")) { + version = arg.slice("--version=".length); + continue; + } + + throw new Error(`Unsupported argument: ${arg}`); + } + + if (!version) { + throw new Error("--version is required"); + } + if (!issued) { + throw new Error("--issued is required"); + } + + return { dryRun, issued, root, version }; +} + +function validateOptions(options: ReleaseSetVersionOptions): void { + if (!isSemver(options.version)) { + throw new Error(`--version must be SemVer-shaped, got ${options.version}`); + } + if (!isDate(options.issued)) { + throw new Error(`--issued must be an ISO date, got ${options.issued}`); + } +} + +function replaceRequired( + contents: string, + pattern: RegExp, + replacement: string, + label: string, +): string { + const matches = contents.match(pattern); + if (!matches || matches.length === 0) { + throw new Error(`could not find ${label}`); + } + + return contents.replaceAll(pattern, replacement); +} + +function requiredValue( + args: readonly string[], + index: number, + flag: string, +): string { + const value = args[index]; + if (!value) { + throw new Error(`${flag} requires a value`); + } + return value; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +interface FileUpdate { + changed: boolean; + next: string; + path: string; + relativePath: string; +} + +interface ParsedReleaseSetVersionArgs { + dryRun: boolean; + issued: string; + root?: string; + version: string; +} + +if (import.meta.main) { + try { + const options = parseReleaseSetVersionArgs(Deno.args); + const result = await setReleaseVersion(options); + const verb = options.dryRun ? "would update" : "updated"; + + console.log( + `SFLO release metadata ${verb} for v${options.version} (${options.issued}).`, + ); + + if (result.changedFiles.length > 0) { + console.log(`Changed files:\n- ${result.changedFiles.join("\n- ")}`); + } else { + console.log("No file changes needed."); + } + } catch (error) { + console.error( + `SFLO release metadata update failed: ${errorMessage(error)}`, + ); + Deno.exit(1); + } +} diff --git a/scripts/release_validate.ts b/scripts/release_validate.ts index bcb8b2a8..0acd4ea7 100644 --- a/scripts/release_validate.ts +++ b/scripts/release_validate.ts @@ -1,5 +1,11 @@ import { fromFileUrl, join } from "@std/path"; import { Parser, type Quad, type Term } from "n3"; +import { + ACTIVE_RELEASE_FILES, + isSemver, + type ReleaseFileDescriptor, + releaseIris, +} from "./release_metadata.ts"; const REPO_ROOT = fromFileUrl(new URL("../", import.meta.url)); @@ -44,44 +50,6 @@ const SFLO = { "https://semantic-flow.github.io/sflo/ontology/hasManifestation", } as const; -const ACTIVE_RELEASE_FILES = [ - { - file: "semantic-flow-core-ontology.ttl", - ontologyIri: "https://semantic-flow.github.io/sflo/ontology", - namespaceUri: "https://semantic-flow.github.io/sflo/ontology/", - prefix: "sflo", - pagesPublished: true, - }, - { - file: "semantic-flow-core-shacl.ttl", - ontologyIri: "https://semantic-flow.github.io/sflo/ontology/shacl", - namespaceUri: "https://semantic-flow.github.io/sflo/ontology/shacl/", - prefix: "sflo-shacl", - pagesPublished: true, - }, - { - file: "semantic-flow-config-ontology.ttl", - ontologyIri: "https://semantic-flow.github.io/sflo/config", - namespaceUri: "https://semantic-flow.github.io/sflo/config/", - prefix: "sfcfg", - pagesPublished: true, - }, - { - file: "semantic-flow-job-ontology.ttl", - ontologyIri: "https://semantic-flow.github.io/ontology/job", - namespaceUri: "https://semantic-flow.github.io/ontology/job/", - prefix: "sfjob", - pagesPublished: false, - }, - { - file: "semantic-flow-prov-ontology.ttl", - ontologyIri: "https://semantic-flow.github.io/ontology/prov", - namespaceUri: "https://semantic-flow.github.io/ontology/prov/", - prefix: "sfprov", - pagesPublished: false, - }, -] as const; - export interface ReleaseValidationOptions { expectedVersion?: string; requireTag?: boolean; @@ -102,7 +70,7 @@ type GitRunner = ( interface ParsedReleaseFile { contents: string; - descriptor: typeof ACTIVE_RELEASE_FILES[number]; + descriptor: ReleaseFileDescriptor; quads: readonly Quad[]; } @@ -113,6 +81,14 @@ export async function validateRelease( const errors: string[] = []; const warnings: string[] = []; const parsedFiles: ParsedReleaseFile[] = []; + const expectedVersion = options.expectedVersion; + + if (expectedVersion && !isSemver(expectedVersion)) { + return { + errors: [`--version must be SemVer-shaped, got ${expectedVersion}`], + warnings, + }; + } for (const descriptor of ACTIVE_RELEASE_FILES) { try { @@ -131,7 +107,7 @@ export async function validateRelease( const versions = new Set(); for (const parsedFile of parsedFiles) { - const version = validateReleaseFile(parsedFile, errors); + const version = validateReleaseFile(parsedFile, errors, expectedVersion); if (version) { versions.add(version); } @@ -146,18 +122,13 @@ export async function validateRelease( ); } - if (options.expectedVersion) { - if (!isSemver(options.expectedVersion)) { - errors.push( - `--version must be SemVer-shaped, got ${options.expectedVersion}`, - ); - } - if (version && version !== options.expectedVersion) { + if (expectedVersion) { + if (version && version !== expectedVersion) { errors.push( - `release metadata declares ${version}, but --version requested ${options.expectedVersion}`, + `release metadata declares ${version}, but --version requested ${expectedVersion}`, ); } - version = options.expectedVersion; + version = expectedVersion; } if (version) { @@ -178,6 +149,10 @@ export function parseReleaseValidateArgs(args: readonly string[]): { for (let index = 0; index < args.length; index++) { const arg = args[index]; + if (arg === "--") { + continue; + } + if (arg === "--require-tag") { requireTag = true; continue; @@ -234,10 +209,11 @@ async function validateReleaseNotes( continue; } - const contentUrl = - `${descriptor.ontologyIri}/releases/v${version}/ttl/${descriptor.file}`; - if (!contents.includes(contentUrl)) { - errors.push(`${notePath}: should list published payload ${contentUrl}`); + const { releasePayloadIri } = releaseIris(descriptor, version); + if (!contents.includes(releasePayloadIri)) { + errors.push( + `${notePath}: should list published payload ${releasePayloadIri}`, + ); } } } @@ -280,6 +256,7 @@ async function validateTagPolicy( function validateReleaseFile( parsedFile: ParsedReleaseFile, errors: string[], + expectedVersion?: string, ): string | undefined { const { contents, descriptor, quads } = parsedFile; const { file, namespaceUri, ontologyIri, prefix } = descriptor; @@ -293,7 +270,9 @@ function validateReleaseFile( subject, OWL.versionInfo, errors, - { expectedLiteral: true }, + expectedVersion + ? { expectedLiteralValue: expectedVersion } + : { expectedLiteral: true }, ); const version = versionInfo?.value; @@ -302,11 +281,15 @@ function validateReleaseFile( return version; } - const releaseIri = `${ontologyIri}/releases/v${version}`; - const manifestationIri = `${releaseIri}/ttl`; - const releasePayloadIri = `${manifestationIri}/${file}`; - const versionIri = - `https://raw.githubusercontent.com/semantic-flow/sflo/refs/tags/v${version}/${file}`; + const validationVersion = expectedVersion && isSemver(expectedVersion) + ? expectedVersion + : version; + const { + manifestationIri, + releaseIri, + releasePayloadIri, + versionIri, + } = releaseIris(descriptor, validationVersion); requireSingleObject(quads, file, subject, DCTERMS.hasVersion, errors, { expectedNamedNode: releaseIri, @@ -344,7 +327,7 @@ function validateReleaseFile( expectedNamedNode: ontologyIri, }); requireSingleObject(quads, file, releaseIri, OWL.versionInfo, errors, { - expectedLiteralValue: version, + expectedLiteralValue: validationVersion, }); requireSingleObject(quads, file, releaseIri, SCHEMA.contentUrl, errors, { expectedNamedNode: releasePayloadIri, @@ -502,10 +485,6 @@ function quadTerms(quad: Quad): readonly Term[] { return [quad.subject, quad.predicate, quad.object, quad.graph]; } -function isSemver(value: string): boolean { - return /^\d+\.\d+\.\d+$/.test(value); -} - function compactIri(value: string): string { return value .replace("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:") diff --git a/semantic-flow-config-ontology.ttl b/semantic-flow-config-ontology.ttl index 42493009..2fbffeb1 100644 --- a/semantic-flow-config-ontology.ttl +++ b/semantic-flow-config-ontology.ttl @@ -12,23 +12,23 @@ dcterms:title "Semantic Flow Config Ontology" ; dcterms:description "Vocabulary for Semantic Flow configuration resources, authored config attachment, resolver policy, derived resolved-config records, operational runtime-resolution policy, and resource-page presentation configuration. Content composition terms such as page regions, page sources, and direct target/current-byte locators remain in the core ontology." ; dcterms:creator ; - dcterms:hasVersion ; + dcterms:hasVersion ; dcterms:modified "2026-05-18"^^xsd:date ; - owl:versionInfo "0.1.0" ; - owl:versionIRI ; + owl:versionInfo "0.2.0" ; + owl:versionIRI ; vann:preferredNamespacePrefix "sfcfg" ; vann:preferredNamespaceUri "https://semantic-flow.github.io/sflo/config/" . - + a owl:Ontology, sflo:HistoricalState ; dcterms:isVersionOf ; - dcterms:issued "2026-05-17"^^xsd:date ; - owl:versionInfo "0.1.0" ; - schema:contentUrl ; - sflo:hasManifestation . + dcterms:issued "2026-05-24"^^xsd:date ; + owl:versionInfo "0.2.0" ; + schema:contentUrl ; + sflo:hasManifestation . - - dcat:downloadURL . + + dcat:downloadURL . ################################################################# ################################################################# @@ -894,6 +894,125 @@ rdfs:range ; rdfs:label "has ResourcePage stylesheet" . + a owl:ObjectProperty ; + rdfs:subPropertyOf ; + rdfs:domain ; + rdfs:range ; + rdfs:label "has default ResourcePage presentation config" ; + rdfs:comment "Associates an application, mesh, Knop, or reusable Config with the default ResourcePage presentation profile to use when no nearer presentation config applies." . + + a rdfs:Class ; + rdfs:label "ResourcePage panel" ; + rdfs:comment "A reusable structured ResourcePage component, such as children, references, history, raw source, or Semantic Flow metadata. Panel identities select code-backed structured components; arbitrary external panel renderers are out of scope for the first presentation-config model." . + + a rdfs:Class ; + rdfs:label "ResourcePage panel selection" ; + rdfs:comment "A named presentation-profile component that selects a ResourcePagePanel together with ordering, inclusion policy, targeting, and data-availability constraints." . + + a rdfs:Class ; + rdfs:label "ResourcePage panel inclusion policy" . + + a ; + rdfs:label "auto" ; + rdfs:comment "Include the panel when it is selected, targeted to the page, and the runtime has the required panel data." . + + a rdfs:Class ; + rdfs:label "ResourcePage kind" ; + rdfs:comment "A controlled vocabulary for targeting presentation rules to runtime ResourcePage document kinds." . + + a ; + rdfs:label "identifier ResourcePage" . + + a ; + rdfs:label "Knop ResourcePage" . + + a ; + rdfs:label "simple ResourcePage" . + + a ; + rdfs:label "ReferenceCatalog ResourcePage" . + + a rdfs:Class ; + rdfs:label "ResourcePage panel data requirement" ; + rdfs:comment "A controlled vocabulary describing the structured runtime data a selected code-backed panel needs before it can render." . + + a ; + rdfs:label "children" . + + a ; + rdfs:label "RDF properties" . + + a ; + rdfs:label "blank nodes" . + + a ; + rdfs:label "references" . + + a ; + rdfs:label "current reference links" . + + a ; + rdfs:label "Knop artifacts" . + + a ; + rdfs:label "fact sections" . + + a ; + rdfs:label "raw source" . + + a ; + rdfs:label "history" . + + a ; + rdfs:label "Semantic Flow metadata opt-in" . + + a owl:ObjectProperty ; + rdfs:domain ; + rdfs:range ; + rdfs:label "has ResourcePage panel selection" . + + a owl:ObjectProperty ; + rdfs:domain sflo:ResourcePageDefinition ; + rdfs:range ; + rdfs:label "has generated ResourcePage panel selection" ; + rdfs:comment "Allows an authored ResourcePageDefinition to opt into selected generated/code-backed ResourcePage panels in addition to authored page regions. Runtime should render authored regions before these generated panels unless a later explicit ordering model says otherwise." . + + a owl:ObjectProperty ; + rdfs:domain ; + rdfs:range ; + rdfs:label "has ResourcePage panel" . + + a owl:DatatypeProperty ; + rdfs:domain ; + rdfs:range xsd:nonNegativeInteger ; + rdfs:label "panel order" ; + rdfs:comment "A deterministic sort key for selected ResourcePage panels. Ties are resolved by config precedence and then selection IRI." . + + a owl:ObjectProperty ; + rdfs:domain ; + rdfs:range ; + rdfs:label "has panel inclusion policy" . + + a owl:ObjectProperty ; + rdfs:domain ; + rdfs:range ; + rdfs:label "has panel target page kind" . + + a owl:ObjectProperty ; + rdfs:domain ; + rdfs:range rdfs:Class ; + rdfs:label "has panel target class" . + + a owl:ObjectProperty ; + rdfs:domain ; + rdfs:range ; + rdfs:label "has panel target artifact role" . + + a owl:ObjectProperty ; + rdfs:domain ; + rdfs:range ; + rdfs:label "has panel data requirement" . + a owl:DatatypeProperty ; rdfs:domain ; rdfs:range xsd:dateTimeStamp ; diff --git a/semantic-flow-core-ontology.ttl b/semantic-flow-core-ontology.ttl index a89f2d7a..641479c0 100644 --- a/semantic-flow-core-ontology.ttl +++ b/semantic-flow-core-ontology.ttl @@ -14,23 +14,23 @@ dcterms:title "Semantic Flow Core Ontology" ; dcterms:description "The Semantic Flow Core Ontology defines the classes and properties needed by Semantic Meshes." ; dcterms:creator ; - dcterms:hasVersion ; + dcterms:hasVersion ; dcterms:modified "2026-05-19"^^xsd:date ; - owl:versionIRI ; - owl:versionInfo "0.1.0" ; + owl:versionIRI ; + owl:versionInfo "0.2.0" ; vann:preferredNamespacePrefix "sflo" ; vann:preferredNamespaceUri "https://semantic-flow.github.io/sflo/ontology/" . - + a owl:Ontology, ; dcterms:isVersionOf ; - dcterms:issued "2026-05-17"^^xsd:date ; - owl:versionInfo "0.1.0" ; - schema:contentUrl ; - . + dcterms:issued "2026-05-24"^^xsd:date ; + owl:versionInfo "0.2.0" ; + schema:contentUrl ; + . - - dcat:downloadURL . + + dcat:downloadURL . ################################################################# ################################################################# @@ -258,7 +258,12 @@ a rdfs:Class ; rdfs:label "Reference link" ; - rdfs:comment "A relator describing a reference-data link from a resource to a specific reference data source, including role metadata." . + rdfs:comment "A relator describing a curated RDF reference-data link about a resource, including role metadata and a source binding for the RDF data that supports or describes the resource." . + + a rdfs:Class ; + rdfs:subClassOf ; + rdfs:label "Reference source" ; + rdfs:comment "An artifact-resolution target that identifies RDF reference-data bytes used by a ReferenceLink. ReferenceSource records are normally carried in the ReferenceCatalog beside their owning ReferenceLink." . a rdfs:Class ; rdfs:label "Reference role" ; @@ -278,7 +283,7 @@ rdfs:range ; owl:inverseOf ; rdfs:label "has reference link"@en ; - rdfs:comment "Associates a resource with a ReferenceLink that provides external or supporting reference information about it." . + rdfs:comment "Associates a resource with a ReferenceLink that provides curated RDF reference data about it." . a owl:ObjectProperty ; rdfs:domain ; @@ -293,24 +298,11 @@ rdfs:label "has reference role" ; rdfs:comment "Assigns one or more ReferenceRoles to this ReferenceLink, indicating how the reference data should be used." . - a owl:ObjectProperty ; - rdfs:domain ; - rdfs:range rdfs:Resource ; - rdfs:label "reference target" ; - rdfs:comment "If present, the reference target is a resource used as a reference-data identifier." . - - a owl:ObjectProperty ; - rdfs:subPropertyOf ; - rdfs:domain ; - rdfs:range ; - rdfs:label "reference target state" ; - rdfs:comment "If present, the reference target is specifically a HistoricalState rather than a broader artifact/resource identity." . - - a owl:DatatypeProperty ; + a owl:ObjectProperty ; rdfs:domain ; - rdfs:range xsd:anyURI ; - rdfs:label "reference URI literal" ; - rdfs:comment "If present, a retrievable location for reference data (without asserting denotation). May be used alongside referenceTarget or alone." . + rdfs:range ; + rdfs:label "has reference source" ; + rdfs:comment "Relates a ReferenceLink to the ReferenceSource that identifies the RDF reference-data bytes for the link." . a owl:DatatypeProperty ; rdfs:domain ; @@ -500,15 +492,33 @@ rdfs:label "Artifact resolution target" ; rdfs:comment "A policy-bearing relator that identifies bytes for an application concern by naming a target artifact, a mesh-local file path string, a specific LocatedFile, or another explicit packaged/distributed target together with optional requested state, mode, and fallback policy. At least one concrete target should normally be provided, but this ontology does not enforce that cardinality directly." . + a rdfs:Class ; + rdfs:label "Artifact resolution observation" ; + rdfs:comment "A relator recording evidence observed during a specific artifact-resolution event, such as the resolved state, manifestation, located file, local path, digest, timestamp, and observer. Observations are appendable evidence records and should not be written merely because a runtime reads target bytes." . + a rdfs:Class ; rdfs:label "Repository source locator" ; rdfs:comment "A durable locator for bytes or a tree path in a version-control repository. It records repository identity, ref or commit coordinates, and a repository-relative path without implying a local checkout path or granting runtime access. Content digests may be expressed with hasContentDigest on this locator or expectsContentDigest on the ArtifactResolutionTarget that uses it." . + a rdfs:Class ; + rdfs:label "Repository source floating locator" ; + rdfs:comment "A durable locator for mutable working bytes in a version-control repository checkout. It records repository identity and a path from the repository root, but deliberately omits branch, ref, commit, digest, and local checkout path evidence. Runtime operational policy maps the repository identity to a local checkout and reads the currently checked-out bytes." . + a rdfs:Class ; rdfs:subClassOf ; rdfs:label "Extraction source" ; rdfs:comment "An artifact-resolution target that identifies the RDF document bytes from which a Knop-managed resource was extracted or first grounded. ExtractionSource records are normally carried in the KnopSourceRegistry under '_knop/_sources' and linked from the Knop with hasExtractionSource." . + a rdfs:Class ; + rdfs:subClassOf ; + rdfs:label "Import source" ; + rdfs:comment "An artifact-resolution target that identifies source bytes actively acquired and copied into a governed local working surface by an import operation. ImportSource records are normally carried in the KnopSourceRegistry under '_knop/_sources' and linked from that registry with hasSourceBinding." . + + a rdfs:Class ; + rdfs:subClassOf ; + rdfs:label "Integration source" ; + rdfs:comment "An artifact-resolution target that identifies existing source bytes registered where they already live by an integrate operation. IntegrationSource records are normally carried in the KnopSourceRegistry under '_knop/_sources' and linked from that registry with hasSourceBinding." . + a owl:ObjectProperty ; rdfs:domain ; rdfs:range ; @@ -579,11 +589,15 @@ rdfs:label "has target repository source" ; rdfs:comment "Relates an ArtifactResolutionTarget to a version-control repository locator for the bytes or tree path it should resolve. Runtime policy decides whether and how that locator may be fetched or mapped to a local checkout." . + a owl:ObjectProperty ; + rdfs:range ; + rdfs:label "has repository source floating locator" ; + rdfs:comment "Relates an artifact or ArtifactResolutionTarget to a floating repository source locator for mutable working bytes. This is intended for branch-published meshes whose source bytes live in a separately materialized repository checkout. The local checkout path is resolved operationally and is not persisted in the mesh." . + a owl:DatatypeProperty ; - rdfs:domain ; rdfs:range xsd:anyURI ; rdfs:label "source repository URL" ; - rdfs:comment "Canonical or preferred repository URL for a RepositorySourceLocator. This names repository provenance and does not by itself authorize network access." . + rdfs:comment "Canonical or preferred repository URL for a repository source locator. This names repository provenance and does not by itself authorize network access." . a owl:DatatypeProperty ; rdfs:domain ; @@ -603,6 +617,12 @@ rdfs:label "source repository path" ; rdfs:comment "Path to the source bytes or tree within the repository ref, written relative to the repository root. Values should not be absolute host paths; use '.' for the repository root when the whole tree is the source." . + a owl:DatatypeProperty ; + rdfs:domain ; + rdfs:range xsd:string ; + rdfs:label "source repository path from root" ; + rdfs:comment "Path to mutable working source bytes written from the repository root of a RepositorySourceFloatingLocator. Values should identify a repository-contained file path and should not be absolute host paths." . + a owl:ObjectProperty ; rdfs:domain ; rdfs:range ; @@ -675,41 +695,53 @@ rdfs:label "has artifact resolution fallback policy" ; rdfs:comment "Assigns an ArtifactResolutionFallbackPolicy to an ArtifactResolutionTarget." . - a owl:ObjectProperty ; - rdfs:domain ; + a owl:ObjectProperty ; + rdfs:domain ; + rdfs:range ; + rdfs:label "has resolution observation" ; + rdfs:comment "Relates an ArtifactResolutionTarget to an observed-resolution evidence record produced when an operation intentionally records what it resolved." . + + a owl:ObjectProperty ; + rdfs:domain ; rdfs:range ; - rdfs:label "has observed source state" ; - rdfs:comment "Relates an ExtractionSource to the concrete HistoricalState observed while resolving the source bytes. This records observed evidence and may be more specific than the requested target coordinates." . + rdfs:label "has observed target state" ; + rdfs:comment "Relates an ArtifactResolutionObservation to the concrete HistoricalState observed while resolving target bytes. This records evidence and may be more specific than the requested target coordinates." . - a owl:ObjectProperty ; - rdfs:domain ; + a owl:ObjectProperty ; + rdfs:domain ; rdfs:range ; - rdfs:label "has observed source manifestation" ; - rdfs:comment "Relates an ExtractionSource to the concrete ArtifactManifestation observed while resolving the source bytes." . + rdfs:label "has observed target manifestation" ; + rdfs:comment "Relates an ArtifactResolutionObservation to the concrete ArtifactManifestation observed while resolving target bytes." . - a owl:ObjectProperty ; - rdfs:domain ; + a owl:ObjectProperty ; + rdfs:domain ; rdfs:range ; - rdfs:label "has observed source located file" ; - rdfs:comment "Relates an ExtractionSource to the concrete LocatedFile resource observed while resolving the source bytes. Use observedSourceLocalRelativePath for local checkout paths that are not modeled as mesh LocatedFile resources." . + rdfs:label "has observed target located file" ; + rdfs:comment "Relates an ArtifactResolutionObservation to the concrete LocatedFile resource observed while resolving target bytes. Use observedTargetLocalRelativePath for local checkout paths that are not modeled as mesh LocatedFile resources." . - a owl:DatatypeProperty ; - rdfs:domain ; + a owl:DatatypeProperty ; + rdfs:domain ; rdfs:range xsd:string ; - rdfs:label "observed source local relative path" ; - rdfs:comment "A local relative path for source bytes observed while resolving an ExtractionSource when those bytes are not modeled as a mesh LocatedFile resource. Values should remain relative to the active local workspace or mesh root policy; absolute host paths should not be persisted here." . + rdfs:label "observed target local relative path" ; + rdfs:comment "A local relative path for bytes observed while resolving an ArtifactResolutionTarget when those bytes are not modeled as a mesh LocatedFile resource. Values should remain relative to the active local workspace or mesh root policy; absolute host paths should not be persisted here." . - a owl:DatatypeProperty ; - rdfs:domain ; + a owl:DatatypeProperty ; + rdfs:domain ; rdfs:range xsd:string ; - rdfs:label "observed source digest" ; - rdfs:comment "Digest of the concrete source bytes observed while resolving an ExtractionSource. Literal values SHOULD include the algorithm, for example 'sha256:'." . + rdfs:label "observed content digest" ; + rdfs:comment "Digest of concrete bytes observed while resolving an ArtifactResolutionTarget. Literal values SHOULD include the algorithm, for example 'sha256:'. Multiple values may be used for multiple digest algorithms." . a owl:DatatypeProperty ; - rdfs:domain ; + rdfs:domain ; rdfs:range xsd:dateTime ; rdfs:label "observed at" ; - rdfs:comment "Timestamp for when source or target bytes were observed while resolving an ArtifactResolutionTarget. Prefer durable state, manifestation, located-file, and digest evidence when available; use a timestamp as supplemental evidence or fallback when byte identity cannot be made durable." . + rdfs:comment "Timestamp for when target bytes were observed during an artifact-resolution event. Prefer durable state, manifestation, located-file, and digest evidence when available; use a timestamp as supplemental evidence or fallback when byte identity cannot be made durable." . + + a owl:ObjectProperty ; + rdfs:domain ; + rdfs:range dcterms:Agent ; + rdfs:label "observed by" ; + rdfs:comment "Agent responsible for recording an ArtifactResolutionObservation." . a owl:DatatypeProperty ; rdfs:domain ; diff --git a/semantic-flow-core-shacl.ttl b/semantic-flow-core-shacl.ttl index 127b62be..7d1cb90e 100644 --- a/semantic-flow-core-shacl.ttl +++ b/semantic-flow-core-shacl.ttl @@ -5,6 +5,7 @@ @prefix rdf: . @prefix rdfs: . @prefix schema: . +@prefix sfcfg: . @prefix sflo: . @prefix sh: . @prefix vann: . @@ -15,41 +16,36 @@ rdf:type owl:Ontology; dcterms:description "SHACL shapes for validating Semantic Flow data structures and operational invariants."; dcterms:creator ; - dcterms:hasVersion ; + dcterms:hasVersion ; dcterms:modified "2026-05-19"^^xsd:date; vann:preferredNamespacePrefix "sflo-shacl"; vann:preferredNamespaceUri "https://semantic-flow.github.io/sflo/ontology/shacl/"; - owl:versionIRI ; - owl:versionInfo "0.1.0" . + owl:versionIRI ; + owl:versionInfo "0.2.0" . - + rdf:type owl:Ontology, sflo:HistoricalState; dcterms:isVersionOf ; - dcterms:issued "2026-05-17"^^xsd:date; - owl:versionInfo "0.1.0"; - schema:contentUrl ; - sflo:hasManifestation . + dcterms:issued "2026-05-24"^^xsd:date; + owl:versionInfo "0.2.0"; + schema:contentUrl ; + sflo:hasManifestation . - - dcat:downloadURL . + + dcat:downloadURL . rdf:type sh:NodeShape; - rdfs:comment "A ReferenceLink must identify the thing it is about."; + rdfs:comment "A ReferenceLink must identify the thing it is about and the RDF reference source it uses."; rdfs:label "ReferenceLink basics"; sh:property [ - sh:class sflo:HistoricalState; + sh:class sflo:ReferenceSource; sh:maxCount 1; - sh:message "If present, sflo:referenceTargetState should be a single IRI typed sflo:HistoricalState."; + sh:message "A ReferenceLink MUST have exactly one sflo:hasReferenceSource value typed sflo:ReferenceSource."; + sh:minCount 1; sh:nodeKind sh:IRI; - sh:path sflo:referenceTargetState; - sh:severity sh:Info - ]; - sh:property [ - sh:datatype xsd:anyURI; - sh:message "If present, sflo:referenceUriLiteral must be xsd:anyURI."; - sh:path sflo:referenceUriLiteral; - sh:severity sh:Info + sh:path sflo:hasReferenceSource; + sh:severity sh:Violation ]; sh:property [ sh:message "A ReferenceLink SHOULD have at least one sflo:hasReferenceRole."; @@ -67,6 +63,20 @@ ]; sh:targetClass sflo:ReferenceLink . + + rdf:type sh:NodeShape; + rdfs:comment "A ReferenceSource identifies RDF data used by a ReferenceLink."; + rdfs:label "ReferenceSource basics"; + sh:property [ + sh:class sflo:RdfDocument; + sh:maxCount 1; + sh:message "If present, a ReferenceSource sflo:hasTargetArtifact value MUST be an IRI typed sflo:RdfDocument."; + sh:nodeKind sh:IRI; + sh:path sflo:hasTargetArtifact; + sh:severity sh:Violation + ]; + sh:targetClass sflo:ReferenceSource . + rdf:type sh:NodeShape; rdfs:comment "Reference links must be about the referent or mesh subject, not about the Knop support object itself."; @@ -242,6 +252,79 @@ ]; sh:targetClass sflo:RepositorySourceLocator . + + rdf:type sh:NodeShape; + rdfs:comment "A floating repository source locator records durable repository identity and a path from the repository root for mutable working bytes. It deliberately omits ref, commit, digest, and local checkout path evidence."; + rdfs:label "RepositorySourceFloatingLocator"; + sh:property [ + sh:maxCount 1; + sh:message "A RepositorySourceFloatingLocator MUST declare exactly one sflo:sourceRepositoryUrl literal."; + sh:minCount 1; + sh:nodeKind sh:Literal; + sh:path sflo:sourceRepositoryUrl; + sh:severity sh:Violation + ]; + sh:property [ + sh:datatype xsd:string; + sh:maxCount 1; + sh:message "A RepositorySourceFloatingLocator MUST declare exactly one sflo:sourceRepositoryPathFromRoot string literal."; + sh:minCount 1; + sh:path sflo:sourceRepositoryPathFromRoot; + sh:severity sh:Violation + ]; + sh:property [ + sh:maxCount 0; + sh:message "A RepositorySourceFloatingLocator MUST NOT declare sflo:sourceRepositoryRef; floating working sources use the currently checked-out ref from operational resolution."; + sh:path sflo:sourceRepositoryRef; + sh:severity sh:Violation + ]; + sh:property [ + sh:maxCount 0; + sh:message "A RepositorySourceFloatingLocator MUST NOT declare sflo:sourceRepositoryCommit; commit-pinned source resolution should use RepositorySourceLocator."; + sh:path sflo:sourceRepositoryCommit; + sh:severity sh:Violation + ]; + sh:property [ + sh:maxCount 0; + sh:message "A RepositorySourceFloatingLocator MUST NOT declare sflo:hasContentDigest; digest-pinned source resolution should use exact repository source evidence."; + sh:path sflo:hasContentDigest; + sh:severity sh:Violation + ]; + sh:targetClass sflo:RepositorySourceFloatingLocator . + + + rdf:type sh:NodeShape; + rdfs:comment "A branch-based mesh floating source binding resolves mutable working bytes from a repository checkout selected by operational policy. It should not persist host-local source paths."; + rdfs:label "Floating repository source binding"; + sh:property [ + sh:class sflo:RepositorySourceFloatingLocator; + sh:maxCount 1; + sh:message "A floating repository source binding MUST identify exactly one sflo:hasRepositorySourceFloatingLocator value typed sflo:RepositorySourceFloatingLocator."; + sh:minCount 1; + sh:nodeKind sh:BlankNodeOrIRI; + sh:path sflo:hasRepositorySourceFloatingLocator; + sh:severity sh:Violation + ]; + sh:property [ + sh:maxCount 0; + sh:message "A floating repository source binding MUST NOT declare sflo:targetLocalRelativePath."; + sh:path sflo:targetLocalRelativePath; + sh:severity sh:Violation + ]; + sh:property [ + sh:maxCount 0; + sh:message "A floating repository source binding MUST NOT declare sflo:workingLocalRelativePath."; + sh:path sflo:workingLocalRelativePath; + sh:severity sh:Violation + ]; + sh:property [ + sh:maxCount 0; + sh:message "A floating repository source binding MUST NOT declare sflo:hasWorkingLocatedFile."; + sh:path sflo:hasWorkingLocatedFile; + sh:severity sh:Violation + ]; + sh:targetSubjectsOf sflo:hasRepositorySourceFloatingLocator . + rdf:type sh:NodeShape; rdfs:comment "previousHistoricalState is optional but, if present, must point to exactly one HistoricalState (the immediately preceding published state)."; @@ -400,12 +483,89 @@ ]; sh:sparql [ rdf:type sh:SPARQLConstraint; - sh:message "sflo:expectsContentDigest SHOULD be used on resources typed sflo:ArtifactResolutionTarget or a subclass such as sflo:ExtractionSource."; + sh:message "sflo:expectsContentDigest SHOULD be used on resources typed sflo:ArtifactResolutionTarget or a subclass such as sflo:ExtractionSource, sflo:ReferenceSource, sflo:ImportSource, or sflo:IntegrationSource."; sh:select "PREFIX rdfs: \nPREFIX sflo: \nSELECT $this WHERE {\n $this sflo:expectsContentDigest ?digest .\n FILTER NOT EXISTS { $this a/rdfs:subClassOf* sflo:ArtifactResolutionTarget }\n}"; sh:severity sh:Warning ]; sh:targetSubjectsOf sflo:expectsContentDigest . + + rdf:type sh:NodeShape; + rdfs:comment "ArtifactResolutionTargets may link to explicit observation records when an operation intentionally records resolved evidence."; + rdfs:label "ArtifactResolutionTarget observations"; + sh:property [ + sh:class sflo:ArtifactResolutionObservation; + sh:message "sflo:hasResolutionObservation values MUST be IRIs typed sflo:ArtifactResolutionObservation."; + sh:nodeKind sh:IRI; + sh:path sflo:hasResolutionObservation; + sh:severity sh:Violation + ]; + sh:targetSubjectsOf sflo:hasResolutionObservation . + + + rdf:type sh:NodeShape; + rdfs:comment "An ArtifactResolutionObservation records evidence observed during one artifact-resolution event."; + rdfs:label "ArtifactResolutionObservation"; + sh:property [ + sh:class sflo:HistoricalState; + sh:maxCount 1; + sh:message "If present, sflo:hasObservedTargetState MUST be a single IRI typed sflo:HistoricalState."; + sh:nodeKind sh:IRI; + sh:path sflo:hasObservedTargetState; + sh:severity sh:Violation + ]; + sh:property [ + sh:class sflo:ArtifactManifestation; + sh:maxCount 1; + sh:message "If present, sflo:hasObservedTargetManifestation MUST be a single IRI typed sflo:ArtifactManifestation."; + sh:nodeKind sh:IRI; + sh:path sflo:hasObservedTargetManifestation; + sh:severity sh:Violation + ]; + sh:property [ + sh:class sflo:LocatedFile; + sh:maxCount 1; + sh:message "If present, sflo:hasObservedTargetLocatedFile MUST be a single IRI typed sflo:LocatedFile."; + sh:nodeKind sh:IRI; + sh:path sflo:hasObservedTargetLocatedFile; + sh:severity sh:Violation + ]; + sh:property [ + sh:datatype xsd:string; + sh:maxCount 1; + sh:message "If present, sflo:observedTargetLocalRelativePath MUST be a single local relative path literal."; + sh:path sflo:observedTargetLocalRelativePath; + sh:severity sh:Violation + ]; + sh:property [ + sh:datatype xsd:string; + sh:message "sflo:observedContentDigest values MUST be xsd:string literals."; + sh:path sflo:observedContentDigest; + sh:severity sh:Violation + ]; + sh:property [ + sh:message "sflo:observedContentDigest values SHOULD include an algorithm prefix followed by ':', for example sha256:."; + sh:path sflo:observedContentDigest; + sh:pattern "^[A-Za-z][A-Za-z0-9+._-]*:.+$"; + sh:severity sh:Warning + ]; + sh:property [ + sh:datatype xsd:dateTime; + sh:maxCount 1; + sh:message "If present, sflo:observedAt MUST be a single xsd:dateTime literal."; + sh:path sflo:observedAt; + sh:severity sh:Violation + ]; + sh:property [ + sh:class dcterms:Agent; + sh:maxCount 1; + sh:message "If present, sflo:observedBy MUST be a single IRI or blank node typed dcterms:Agent."; + sh:nodeKind sh:BlankNodeOrIRI; + sh:path sflo:observedBy; + sh:severity sh:Warning + ]; + sh:targetClass sflo:ArtifactResolutionObservation . + rdf:type sh:NodeShape; rdfs:comment "ArtifactResolutionMode usage checks distinguish mutable working-byte resolution from latest settled state resolution."; @@ -581,44 +741,6 @@ sh:path sflo:hasTargetArtifact; sh:severity sh:Violation ]; - sh:property [ - sh:class sflo:HistoricalState; - sh:maxCount 1; - sh:message "If present, sflo:hasObservedSourceState should be a single IRI typed sflo:HistoricalState."; - sh:nodeKind sh:IRI; - sh:path sflo:hasObservedSourceState; - sh:severity sh:Warning - ]; - sh:property [ - sh:class sflo:ArtifactManifestation; - sh:maxCount 1; - sh:message "If present, sflo:hasObservedSourceManifestation should be a single IRI typed sflo:ArtifactManifestation."; - sh:nodeKind sh:IRI; - sh:path sflo:hasObservedSourceManifestation; - sh:severity sh:Warning - ]; - sh:property [ - sh:class sflo:LocatedFile; - sh:maxCount 1; - sh:message "If present, sflo:hasObservedSourceLocatedFile should be a single IRI typed sflo:LocatedFile."; - sh:nodeKind sh:IRI; - sh:path sflo:hasObservedSourceLocatedFile; - sh:severity sh:Warning - ]; - sh:property [ - sh:datatype xsd:string; - sh:maxCount 1; - sh:message "If present, sflo:observedSourceLocalRelativePath should be a single local relative path literal."; - sh:path sflo:observedSourceLocalRelativePath; - sh:severity sh:Warning - ]; - sh:property [ - sh:datatype xsd:string; - sh:maxCount 1; - sh:message "If present, sflo:observedSourceDigest should be a single digest literal."; - sh:path sflo:observedSourceDigest; - sh:severity sh:Warning - ]; sh:targetClass sflo:ExtractionSource . @@ -736,3 +858,131 @@ sh:severity sh:Warning ]; sh:targetClass sflo:DigitalArtifact . + + + rdf:type sh:NodeShape; + rdfs:comment "A ResourcePagePresentationConfig names the built-in or governed presentation assets and panel selections that a renderer can resolve."; + rdfs:label "ResourcePagePresentationConfig structure"; + sh:property [ + sh:class sfcfg:OuterResourcePageTemplate; + sh:maxCount 1; + sh:message "A ResourcePagePresentationConfig MUST have exactly one sfcfg:hasOuterResourcePageTemplate value typed sfcfg:OuterResourcePageTemplate."; + sh:minCount 1; + sh:nodeKind sh:IRI; + sh:path sfcfg:hasOuterResourcePageTemplate; + sh:severity sh:Violation + ]; + sh:property [ + sh:class sfcfg:InnerResourcePageTemplate; + sh:maxCount 1; + sh:message "A ResourcePagePresentationConfig MUST have exactly one sfcfg:hasInnerResourcePageTemplate value typed sfcfg:InnerResourcePageTemplate."; + sh:minCount 1; + sh:nodeKind sh:IRI; + sh:path sfcfg:hasInnerResourcePageTemplate; + sh:severity sh:Violation + ]; + sh:property [ + sh:class sfcfg:ResourcePageStylesheet; + sh:message "A ResourcePagePresentationConfig SHOULD have at least one sfcfg:hasResourcePageStylesheet value typed sfcfg:ResourcePageStylesheet."; + sh:minCount 1; + sh:nodeKind sh:IRI; + sh:path sfcfg:hasResourcePageStylesheet; + sh:severity sh:Warning + ]; + sh:property [ + sh:class sfcfg:ResourcePagePanelSelection; + sh:message "A ResourcePagePresentationConfig SHOULD have at least one sfcfg:hasResourcePagePanelSelection value typed sfcfg:ResourcePagePanelSelection."; + sh:minCount 1; + sh:nodeKind sh:IRI; + sh:path sfcfg:hasResourcePagePanelSelection; + sh:severity sh:Warning + ]; + sh:targetClass sfcfg:ResourcePagePresentationConfig . + + + rdf:type sh:NodeShape; + rdfs:comment "A ResourcePagePanelSelection selects one known panel and constrains when that code-backed panel applies."; + rdfs:label "ResourcePagePanelSelection structure"; + sh:property [ + sh:class sfcfg:ResourcePagePanel; + sh:maxCount 1; + sh:message "A ResourcePagePanelSelection MUST have exactly one sfcfg:hasResourcePagePanel value typed sfcfg:ResourcePagePanel."; + sh:minCount 1; + sh:nodeKind sh:IRI; + sh:path sfcfg:hasResourcePagePanel; + sh:severity sh:Violation + ]; + sh:property [ + sh:datatype xsd:nonNegativeInteger; + sh:maxCount 1; + sh:message "A ResourcePagePanelSelection MUST have exactly one non-negative sfcfg:panelOrder value."; + sh:minCount 1; + sh:path sfcfg:panelOrder; + sh:severity sh:Violation + ]; + sh:property [ + sh:class sfcfg:ResourcePagePanelInclusionPolicy; + sh:maxCount 1; + sh:message "A ResourcePagePanelSelection MUST have exactly one sfcfg:hasPanelInclusionPolicy value typed sfcfg:ResourcePagePanelInclusionPolicy."; + sh:minCount 1; + sh:nodeKind sh:IRI; + sh:path sfcfg:hasPanelInclusionPolicy; + sh:severity sh:Violation + ]; + sh:property [ + sh:class sfcfg:ResourcePageKind; + sh:message "If present, sfcfg:hasPanelTargetPageKind values must be IRIs typed sfcfg:ResourcePageKind."; + sh:nodeKind sh:IRI; + sh:path sfcfg:hasPanelTargetPageKind; + sh:severity sh:Violation + ]; + sh:property [ + sh:class rdfs:Class; + sh:message "If present, sfcfg:hasPanelTargetClass values must be IRIs typed rdfs:Class."; + sh:nodeKind sh:IRI; + sh:path sfcfg:hasPanelTargetClass; + sh:severity sh:Violation + ]; + sh:property [ + sh:class sfcfg:ArtifactRole; + sh:message "If present, sfcfg:hasPanelTargetArtifactRole values must be IRIs typed sfcfg:ArtifactRole."; + sh:nodeKind sh:IRI; + sh:path sfcfg:hasPanelTargetArtifactRole; + sh:severity sh:Violation + ]; + sh:property [ + sh:class sfcfg:ResourcePagePanelDataRequirement; + sh:message "A ResourcePagePanelSelection MUST have at least one sfcfg:hasPanelDataRequirement value typed sfcfg:ResourcePagePanelDataRequirement."; + sh:minCount 1; + sh:nodeKind sh:IRI; + sh:path sfcfg:hasPanelDataRequirement; + sh:severity sh:Violation + ]; + sh:targetClass sfcfg:ResourcePagePanelSelection . + + + rdf:type sh:NodeShape; + rdfs:comment "A ResourcePageDefinition may opt into adjacent presentation config and selected generated ResourcePage panels."; + rdfs:label "ResourcePageDefinition presentation config"; + sh:property [ + sh:class sfcfg:ResourcePagePresentationConfig; + sh:maxCount 1; + sh:message "If present, sfcfg:hasResourcePagePresentationConfig must be a single IRI typed sfcfg:ResourcePagePresentationConfig."; + sh:nodeKind sh:IRI; + sh:path sfcfg:hasResourcePagePresentationConfig; + sh:severity sh:Violation + ]; + sh:property [ + sh:class sfcfg:ResourcePagePanelSelection; + sh:message "If present, sfcfg:hasGeneratedResourcePagePanelSelection values must be IRIs typed sfcfg:ResourcePagePanelSelection."; + sh:nodeKind sh:IRI; + sh:path sfcfg:hasGeneratedResourcePagePanelSelection; + sh:severity sh:Violation + ]; + sh:sparql [ + rdf:type sh:SPARQLConstraint; + sh:message "A ResourcePageDefinition with sfcfg:hasGeneratedResourcePagePanelSelection MUST also declare sfcfg:hasResourcePagePresentationConfig."; + sh:select "PREFIX sfcfg: \nSELECT $this WHERE {\n $this sfcfg:hasGeneratedResourcePagePanelSelection ?selection .\n FILTER NOT EXISTS { $this sfcfg:hasResourcePagePresentationConfig ?config }\n}"; + sh:severity sh:Violation + ]; + sh:targetClass sflo:ResourcePageDefinition . diff --git a/semantic-flow-job-ontology.ttl b/semantic-flow-job-ontology.ttl index eb9d66ea..c38c3531 100644 --- a/semantic-flow-job-ontology.ttl +++ b/semantic-flow-job-ontology.ttl @@ -15,23 +15,23 @@ dcterms:title "Semantic Flow Job Ontology" ; dcterms:description "The Semantic Flow Job ontology defines public Semantic Flow API vocabulary for jobs, targets, progress, results, errors, and job events." ; dcterms:creator ; - dcterms:hasVersion ; + dcterms:hasVersion ; dcterms:modified "2026-05-06"^^xsd:date ; - owl:versionInfo "0.1.0" ; - owl:versionIRI ; + owl:versionInfo "0.2.0" ; + owl:versionIRI ; vann:preferredNamespacePrefix "sfjob" ; vann:preferredNamespaceUri "https://semantic-flow.github.io/ontology/job/" . - + a owl:Ontology, sflo:HistoricalState ; dcterms:isVersionOf ; - dcterms:issued "2026-05-06"^^xsd:date ; - owl:versionInfo "0.1.0" ; - schema:contentUrl ; - sflo:hasManifestation . + dcterms:issued "2026-05-24"^^xsd:date ; + owl:versionInfo "0.2.0" ; + schema:contentUrl ; + sflo:hasManifestation . - - dcat:downloadURL . + + dcat:downloadURL . ################################################################# ################################################################# diff --git a/semantic-flow-prov-ontology.ttl b/semantic-flow-prov-ontology.ttl index 4d7262c0..77901fd5 100644 --- a/semantic-flow-prov-ontology.ttl +++ b/semantic-flow-prov-ontology.ttl @@ -14,23 +14,23 @@ dcterms:title "Semantic Flow Provenance Ontology" ; dcterms:description "The Semantic Flow Meta ontology currently defines optional provenance vocabulary for Semantic Flow resources and jobs, including provenance contexts, delegation chains, and agent-role assignments. It intentionally stays narrow and does not try to define general metadata beyond provenance." ; dcterms:creator ; - dcterms:hasVersion ; + dcterms:hasVersion ; dcterms:modified "2026-05-06"^^xsd:date ; - owl:versionInfo "0.1.0" ; - owl:versionIRI ; + owl:versionInfo "0.2.0" ; + owl:versionIRI ; vann:preferredNamespacePrefix "sfprov" ; vann:preferredNamespaceUri "https://semantic-flow.github.io/ontology/prov/" . - + a owl:Ontology, sflo:HistoricalState ; dcterms:isVersionOf ; - dcterms:issued "2026-05-06"^^xsd:date ; - owl:versionInfo "0.1.0" ; - schema:contentUrl ; - sflo:hasManifestation . + dcterms:issued "2026-05-24"^^xsd:date ; + owl:versionInfo "0.2.0" ; + schema:contentUrl ; + sflo:hasManifestation . - - dcat:downloadURL . + + dcat:downloadURL . ################################################################# ################################################################# diff --git a/tests/helpers/rdf.ts b/tests/helpers/rdf.ts index 83426d80..752dcc10 100644 --- a/tests/helpers/rdf.ts +++ b/tests/helpers/rdf.ts @@ -11,7 +11,13 @@ export const SH = { NodeShape: "http://www.w3.org/ns/shacl#NodeShape", Info: "http://www.w3.org/ns/shacl#Info", Warning: "http://www.w3.org/ns/shacl#Warning", + Violation: "http://www.w3.org/ns/shacl#Violation", + datatype: "http://www.w3.org/ns/shacl#datatype", + maxCount: "http://www.w3.org/ns/shacl#maxCount", message: "http://www.w3.org/ns/shacl#message", + minCount: "http://www.w3.org/ns/shacl#minCount", + path: "http://www.w3.org/ns/shacl#path", + property: "http://www.w3.org/ns/shacl#property", severity: "http://www.w3.org/ns/shacl#severity", sparql: "http://www.w3.org/ns/shacl#sparql", targetClass: "http://www.w3.org/ns/shacl#targetClass", diff --git a/tests/rdf_guardrails_test.ts b/tests/rdf_guardrails_test.ts index 91c5c3c2..25b26173 100644 --- a/tests/rdf_guardrails_test.ts +++ b/tests/rdf_guardrails_test.ts @@ -8,6 +8,11 @@ import { termKey, } from "./helpers/rdf.ts"; +const RDFS = { + Class: "http://www.w3.org/2000/01/rdf-schema#Class", + subClassOf: "http://www.w3.org/2000/01/rdf-schema#subClassOf", +} as const; + const ACTIVE_RDF_FILES = [ "semantic-flow-core-ontology.ttl", "semantic-flow-core-shacl.ttl", @@ -122,3 +127,83 @@ Deno.test("old config names and boolean policy switches stay retired", async () } } }); + +Deno.test("old ReferenceLink and observed-source vocabulary stay retired", async () => { + const retiredFragments = [ + "sflo:referenceTarget", + "", + `${SFLO_NAMESPACE}referenceTarget`, + "sflo:referenceTargetState", + "", + `${SFLO_NAMESPACE}referenceTargetState`, + "sflo:referenceUriLiteral", + "", + `${SFLO_NAMESPACE}referenceUriLiteral`, + "sflo:hasObservedSourceState", + "", + `${SFLO_NAMESPACE}hasObservedSourceState`, + "sflo:hasObservedSourceManifestation", + "", + `${SFLO_NAMESPACE}hasObservedSourceManifestation`, + "sflo:hasObservedSourceLocatedFile", + "", + `${SFLO_NAMESPACE}hasObservedSourceLocatedFile`, + "sflo:observedSourceLocalRelativePath", + "", + `${SFLO_NAMESPACE}observedSourceLocalRelativePath`, + "sflo:observedSourceDigest", + "", + `${SFLO_NAMESPACE}observedSourceDigest`, + ] as const; + + for ( + const relativePath of [ + "semantic-flow-core-ontology.ttl", + "semantic-flow-core-shacl.ttl", + ] as const + ) { + const contents = await readRepoFile(relativePath); + for (const retiredFragment of retiredFragments) { + assertFalse( + contents.includes(retiredFragment), + `${relativePath} still contains retired vocabulary ${retiredFragment}`, + ); + } + } +}); + +Deno.test("core ontology declares shared artifact-resolution source and observation classes", async () => { + const quads = await parseRepoTurtle("semantic-flow-core-ontology.ttl"); + + const artifactResolutionTarget = `${SFLO_NAMESPACE}ArtifactResolutionTarget`; + + for ( + const sourceClass of [ + "ExtractionSource", + "ReferenceSource", + "ResourcePageSource", + "ImportSource", + "IntegrationSource", + ] + ) { + assert( + quads.some((quad) => + quad.subject.value === `${SFLO_NAMESPACE}${sourceClass}` && + quad.predicate.value === RDFS.subClassOf && + quad.object.value === artifactResolutionTarget + ), + `${sourceClass} should subclass ArtifactResolutionTarget`, + ); + } + + assert( + quads.some((quad) => + quad.subject.value === + `${SFLO_NAMESPACE}ArtifactResolutionObservation` && + quad.predicate.value === + "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" && + quad.object.value === RDFS.Class + ), + "ArtifactResolutionObservation should be declared as an rdfs:Class", + ); +}); diff --git a/tests/release_set_version_test.ts b/tests/release_set_version_test.ts new file mode 100644 index 00000000..cc0f2a55 --- /dev/null +++ b/tests/release_set_version_test.ts @@ -0,0 +1,148 @@ +import { assertEquals } from "@std/assert"; +import { dirname, fromFileUrl, join } from "@std/path"; +import { + parseReleaseSetVersionArgs, + setReleaseVersion, +} from "../scripts/release_set_version.ts"; +import { + ACTIVE_RELEASE_FILES, + isDate, + releaseIris, +} from "../scripts/release_metadata.ts"; +import { validateRelease } from "../scripts/release_validate.ts"; + +const REPO_ROOT = fromFileUrl(new URL("../", import.meta.url)); +const RELEASE_FIXTURE_FILES = [ + "semantic-flow-core-ontology.ttl", + "semantic-flow-core-shacl.ttl", + "semantic-flow-config-ontology.ttl", + "semantic-flow-job-ontology.ttl", + "semantic-flow-prov-ontology.ttl", +] as const; +const ISSUE_DATE = "2026-05-20"; + +Deno.test("release set-version rewrites active Turtle metadata for validation", async () => { + const targetVersion = await nextReleaseVersion(); + const root = await copyReleaseFixture(targetVersion); + + const result = await setReleaseVersion({ + issued: ISSUE_DATE, + root, + version: targetVersion, + }); + + assertEquals(result.changedFiles, [ + "semantic-flow-core-ontology.ttl", + "semantic-flow-core-shacl.ttl", + "semantic-flow-config-ontology.ttl", + "semantic-flow-job-ontology.ttl", + "semantic-flow-prov-ontology.ttl", + ]); + + const validation = await validateRelease({ + expectedVersion: targetVersion, + root, + }); + assertEquals(validation.errors, []); +}); + +Deno.test("release set-version dry-run reports changes without writing files", async () => { + const targetVersion = await nextReleaseVersion(); + const root = await copyReleaseFixture(targetVersion); + + const result = await setReleaseVersion({ + dryRun: true, + issued: ISSUE_DATE, + root, + version: targetVersion, + }); + + assertEquals(result.changedFiles.length, 5); + + const validation = await validateRelease({ + expectedVersion: targetVersion, + root, + }); + assertEquals( + validation.errors.some((error) => + error.includes(`expected owl:versionInfo literal "${targetVersion}"`) + ), + true, + ); +}); + +Deno.test("release set-version argument parser supports task separators", () => { + assertEquals( + parseReleaseSetVersionArgs([ + "--", + "--version", + "0.1.1", + "--issued", + "2026-05-20", + "--dry-run", + ]), + { + dryRun: true, + issued: "2026-05-20", + root: undefined, + version: "0.1.1", + }, + ); +}); + +Deno.test("release date validation rejects impossible calendar dates", () => { + assertEquals(isDate("2026-02-28"), true); + assertEquals(isDate("2024-02-29"), true); + assertEquals(isDate("2026-02-29"), false); + assertEquals(isDate("2026-02-31"), false); + assertEquals(isDate("2026-13-01"), false); + assertEquals(isDate("2026-00-01"), false); +}); + +async function copyReleaseFixture(targetVersion: string): Promise { + const root = await Deno.makeTempDir({ prefix: "sflo-release-" }); + + for (const relativePath of RELEASE_FIXTURE_FILES) { + const source = join(REPO_ROOT, relativePath); + const destination = join(root, relativePath); + await Deno.mkdir(dirname(destination), { recursive: true }); + await Deno.copyFile(source, destination); + } + + const notePath = join(root, `notes/ont.release-notes.v${targetVersion}.md`); + await Deno.mkdir(dirname(notePath), { recursive: true }); + await Deno.writeTextFile(notePath, releaseNoteFixture(targetVersion)); + + return root; +} + +async function nextReleaseVersion(): Promise { + const result = await validateRelease({ root: REPO_ROOT }); + assertEquals(result.errors, []); + const version = result.version; + if (!version) { + throw new Error("current release version could not be determined"); + } + + const [major, minor, patch] = version.split(".").map(Number); + return `${major}.${minor}.${patch + 1}`; +} + +function releaseNoteFixture(version: string): string { + const files = ACTIVE_RELEASE_FILES.map((descriptor) => + `- ${descriptor.file}` + ); + const publishedPayloads = ACTIVE_RELEASE_FILES + .filter((descriptor) => descriptor.pagesPublished) + .map((descriptor) => + `- ${releaseIris(descriptor, version).releasePayloadIri}` + ); + + return [ + `# Semantic Flow ontology v${version}`, + "", + ...files, + ...publishedPayloads, + "", + ].join("\n"); +} diff --git a/tests/release_validation_test.ts b/tests/release_validation_test.ts index ee0bfbb0..c6b09bef 100644 --- a/tests/release_validation_test.ts +++ b/tests/release_validation_test.ts @@ -10,24 +10,51 @@ const REPO_ROOT = fromFileUrl(new URL("../", import.meta.url)); Deno.test("release validation accepts the current source release metadata", async () => { const result = await validateRelease({ root: REPO_ROOT }); - assertEquals(result.version, "0.1.0"); + assert(result.version); + assert(/^\d+\.\d+\.\d+$/.test(result.version)); assertEquals(result.errors, []); }); Deno.test("release validation can pin the expected release version", async () => { + const currentVersion = await currentReleaseVersion(); + const expectedVersion = nextPatchVersion(currentVersion); const result = await validateRelease({ - expectedVersion: "9.9.9", + expectedVersion, root: REPO_ROOT, }); assert( result.errors.some((error) => - error.includes("release metadata declares 0.1.0") + error.includes( + `release metadata declares ${currentVersion}, but --version requested ${expectedVersion}`, + ) + ), + ); + assert( + result.errors.some((error) => + error.includes( + `semantic-flow-core-ontology.ttl: expected owl:versionInfo literal "${expectedVersion}"`, + ) + ), + ); + assert( + result.errors.some((error) => + error.includes( + `semantic-flow-core-ontology.ttl: expected dcterms:hasVersion to be `, + ) + ), + ); + assert( + result.errors.some((error) => + error.includes( + `semantic-flow-config-ontology.ttl: expected exactly one schema:contentUrl on https://semantic-flow.github.io/sflo/config/releases/v${expectedVersion}`, + ) ), ); }); Deno.test("release validation can require the release tag to point at HEAD", async () => { + const version = await currentReleaseVersion(); const result = await validateRelease({ root: REPO_ROOT, requireTag: true, @@ -35,7 +62,7 @@ Deno.test("release validation can require the release tag to point at HEAD", asy if (args.join(" ") === "rev-parse HEAD") { return "abc123"; } - if (args.join(" ") === "rev-list -n 1 v0.1.0") { + if (args.join(" ") === `rev-list -n 1 v${version}`) { return "def456"; } throw new Error(`unexpected git args: ${args.join(" ")}`); @@ -43,13 +70,24 @@ Deno.test("release validation can require the release tag to point at HEAD", asy }); assertEquals(result.errors, [ - "required release tag v0.1.0 does not point at HEAD", + `required release tag v${version} does not point at HEAD`, + ]); +}); + +Deno.test("release validation rejects invalid expected version without cascading errors", async () => { + const result = await validateRelease({ + expectedVersion: "not-a-version", + root: REPO_ROOT, + }); + + assertEquals(result.errors, [ + "--version must be SemVer-shaped, got not-a-version", ]); }); Deno.test("release validation argument parser supports version and tag flags", () => { assertEquals( - parseReleaseValidateArgs(["--version", "0.1.1", "--require-tag"]), + parseReleaseValidateArgs(["--", "--version", "0.1.1", "--require-tag"]), { expectedVersion: "0.1.1", requireTag: true, @@ -60,3 +98,15 @@ Deno.test("release validation argument parser supports version and tag flags", ( requireTag: false, }); }); + +async function currentReleaseVersion(): Promise { + const result = await validateRelease({ root: REPO_ROOT }); + assertEquals(result.errors, []); + assert(result.version); + return result.version; +} + +function nextPatchVersion(version: string): string { + const [major, minor, patch] = version.split(".").map(Number); + return `${major}.${minor}.${patch + 1}`; +} diff --git a/tests/shacl_guardrails_test.ts b/tests/shacl_guardrails_test.ts index 9d4f984d..8d884ac0 100644 --- a/tests/shacl_guardrails_test.ts +++ b/tests/shacl_guardrails_test.ts @@ -15,11 +15,23 @@ import { const CORE_SHACL_FILE = "semantic-flow-core-shacl.ttl"; const SHAPES = { + ArtifactResolutionObservation: + `${SFLO_SHACL_NAMESPACE}ArtifactResolutionObservationShape`, + ArtifactResolutionObservationLink: + `${SFLO_SHACL_NAMESPACE}ArtifactResolutionObservationLinkShape`, KnopSourceBinding: `${SFLO_SHACL_NAMESPACE}KnopSourceBindingShape`, LocalWorkingSourceBinding: `${SFLO_SHACL_NAMESPACE}LocalWorkingSourceBindingShape`, + ReferenceLink: `${SFLO_SHACL_NAMESPACE}ReferenceLinkShape`, + ReferenceSource: `${SFLO_SHACL_NAMESPACE}ReferenceSourceShape`, ArtifactResolutionModeUsage: `${SFLO_SHACL_NAMESPACE}ArtifactResolutionModeUsageShape`, + ResourcePagePresentationConfig: + `${SFLO_SHACL_NAMESPACE}ResourcePagePresentationConfigShape`, + ResourcePagePanelSelection: + `${SFLO_SHACL_NAMESPACE}ResourcePagePanelSelectionShape`, + ResourcePageDefinitionPresentationConfig: + `${SFLO_SHACL_NAMESPACE}ResourcePageDefinitionPresentationConfigShape`, } as const; const TERMS = { @@ -27,24 +39,56 @@ const TERMS = { LocalPathAccessRule: `${SFCFG_NAMESPACE}LocalPathAccessRule`, PublicationProfile: `${SFCFG_NAMESPACE}PublicationProfile`, RemoteAccessRule: `${SFCFG_NAMESPACE}RemoteAccessRule`, + ResourcePageDefinition: `${SFLO_NAMESPACE}ResourcePageDefinition`, ResourcePagePresentationConfig: `${SFCFG_NAMESPACE}ResourcePagePresentationConfig`, + ResourcePagePanelSelection: `${SFCFG_NAMESPACE}ResourcePagePanelSelection`, artifactResolutionModeLatestState: `${SFLO_NAMESPACE}artifactResolutionMode_latestState`, artifactResolutionModeWorking: `${SFLO_NAMESPACE}artifactResolutionMode_working`, + hasInnerResourcePageTemplate: + `${SFCFG_NAMESPACE}hasInnerResourcePageTemplate`, + hasGeneratedResourcePagePanelSelection: + `${SFCFG_NAMESPACE}hasGeneratedResourcePagePanelSelection`, + hasPanelDataRequirement: `${SFCFG_NAMESPACE}hasPanelDataRequirement`, + hasPanelInclusionPolicy: `${SFCFG_NAMESPACE}hasPanelInclusionPolicy`, + hasReferenceSource: `${SFLO_NAMESPACE}hasReferenceSource`, + hasResolutionObservation: `${SFLO_NAMESPACE}hasResolutionObservation`, + hasResourcePagePresentationConfig: + `${SFCFG_NAMESPACE}hasResourcePagePresentationConfig`, + hasResourcePagePanel: `${SFCFG_NAMESPACE}hasResourcePagePanel`, + hasResourcePagePanelSelection: + `${SFCFG_NAMESPACE}hasResourcePagePanelSelection`, + hasResourcePageStylesheet: `${SFCFG_NAMESPACE}hasResourcePageStylesheet`, + hasOuterResourcePageTemplate: + `${SFCFG_NAMESPACE}hasOuterResourcePageTemplate`, hasArtifactResolutionMode: `${SFLO_NAMESPACE}hasArtifactResolutionMode`, hasRequestedTargetState: `${SFLO_NAMESPACE}hasRequestedTargetState`, + hasTargetArtifact: `${SFLO_NAMESPACE}hasTargetArtifact`, hasTargetRepositorySource: `${SFLO_NAMESPACE}hasTargetRepositorySource`, + observedContentDigest: `${SFLO_NAMESPACE}observedContentDigest`, + observedAt: `${SFLO_NAMESPACE}observedAt`, + ReferenceSource: `${SFLO_NAMESPACE}ReferenceSource`, + ArtifactResolutionObservation: + `${SFLO_NAMESPACE}ArtifactResolutionObservation`, + panelOrder: `${SFCFG_NAMESPACE}panelOrder`, targetLocalRelativePath: `${SFLO_NAMESPACE}targetLocalRelativePath`, } as const; Deno.test("core SHACL declares key source-binding and resolution-mode shapes", async () => { const quads = await parseRepoTurtle(CORE_SHACL_FILE); + assertNodeShape(quads, SHAPES.ReferenceLink); + assertNodeShape(quads, SHAPES.ReferenceSource); + assertNodeShape(quads, SHAPES.ArtifactResolutionObservation); + assertNodeShape(quads, SHAPES.ArtifactResolutionObservationLink); assertNodeShape(quads, SHAPES.KnopSourceBinding); assertNodeShape(quads, SHAPES.LocalWorkingSourceBinding); assertNodeShape(quads, SHAPES.ArtifactResolutionModeUsage); + assertNodeShape(quads, SHAPES.ResourcePagePresentationConfig); + assertNodeShape(quads, SHAPES.ResourcePagePanelSelection); + assertNodeShape(quads, SHAPES.ResourcePageDefinitionPresentationConfig); assert( hasTriple( @@ -75,6 +119,200 @@ Deno.test("core SHACL declares key source-binding and resolution-mode shapes", a ); }); +Deno.test("ReferenceLink SHACL requires exactly one ReferenceSource", async () => { + const quads = await parseRepoTurtle(CORE_SHACL_FILE); + + assert( + hasTriple( + quads, + SHAPES.ReferenceLink, + SH.targetClass, + `${SFLO_NAMESPACE}ReferenceLink`, + ), + "ReferenceLink shape should target ReferenceLink", + ); + assertRequiredProperty( + quads, + SHAPES.ReferenceLink, + TERMS.hasReferenceSource, + ); + assertPropertyMaxCount( + quads, + SHAPES.ReferenceLink, + TERMS.hasReferenceSource, + 1, + ); + assertOptionalProperty( + quads, + SHAPES.ReferenceSource, + TERMS.hasTargetArtifact, + ); + assertPropertyMaxCount( + quads, + SHAPES.ReferenceSource, + TERMS.hasTargetArtifact, + 1, + ); +}); + +Deno.test("ArtifactResolutionObservation SHACL declares observation evidence constraints", async () => { + const quads = await parseRepoTurtle(CORE_SHACL_FILE); + + assert( + hasTriple( + quads, + SHAPES.ArtifactResolutionObservation, + SH.targetClass, + TERMS.ArtifactResolutionObservation, + ), + "ArtifactResolutionObservation shape should target ArtifactResolutionObservation", + ); + assertOptionalProperty( + quads, + SHAPES.ArtifactResolutionObservation, + TERMS.observedContentDigest, + ); + assertOptionalProperty( + quads, + SHAPES.ArtifactResolutionObservation, + TERMS.observedAt, + ); + assertPropertyMaxCount( + quads, + SHAPES.ArtifactResolutionObservation, + TERMS.observedAt, + 1, + ); + assert( + hasTriple( + quads, + SHAPES.ArtifactResolutionObservationLink, + SH.targetSubjectsOf, + TERMS.hasResolutionObservation, + ), + "ArtifactResolutionObservationLink shape should target hasResolutionObservation subjects", + ); +}); + +Deno.test("ResourcePage presentation SHACL declares required config and panel selection constraints", async () => { + const quads = await parseRepoTurtle(CORE_SHACL_FILE); + + assert( + hasTriple( + quads, + SHAPES.ResourcePagePresentationConfig, + SH.targetClass, + TERMS.ResourcePagePresentationConfig, + ), + "ResourcePagePresentationConfig shape should target ResourcePagePresentationConfig", + ); + assertRequiredProperty( + quads, + SHAPES.ResourcePagePresentationConfig, + TERMS.hasOuterResourcePageTemplate, + ); + assertPropertyMaxCount( + quads, + SHAPES.ResourcePagePresentationConfig, + TERMS.hasOuterResourcePageTemplate, + 1, + ); + assertRequiredProperty( + quads, + SHAPES.ResourcePagePresentationConfig, + TERMS.hasInnerResourcePageTemplate, + ); + assertPropertyMaxCount( + quads, + SHAPES.ResourcePagePresentationConfig, + TERMS.hasInnerResourcePageTemplate, + 1, + ); + assertRequiredProperty( + quads, + SHAPES.ResourcePagePresentationConfig, + TERMS.hasResourcePageStylesheet, + ); + assertRequiredProperty( + quads, + SHAPES.ResourcePagePresentationConfig, + TERMS.hasResourcePagePanelSelection, + ); + + assert( + hasTriple( + quads, + SHAPES.ResourcePagePanelSelection, + SH.targetClass, + TERMS.ResourcePagePanelSelection, + ), + "ResourcePagePanelSelection shape should target ResourcePagePanelSelection", + ); + assertRequiredProperty( + quads, + SHAPES.ResourcePagePanelSelection, + TERMS.hasResourcePagePanel, + ); + assertPropertyMaxCount( + quads, + SHAPES.ResourcePagePanelSelection, + TERMS.hasResourcePagePanel, + 1, + ); + assertRequiredProperty( + quads, + SHAPES.ResourcePagePanelSelection, + TERMS.panelOrder, + ); + assertPropertyMaxCount( + quads, + SHAPES.ResourcePagePanelSelection, + TERMS.panelOrder, + 1, + ); + assertRequiredProperty( + quads, + SHAPES.ResourcePagePanelSelection, + TERMS.hasPanelInclusionPolicy, + ); + assertPropertyMaxCount( + quads, + SHAPES.ResourcePagePanelSelection, + TERMS.hasPanelInclusionPolicy, + 1, + ); + assertRequiredProperty( + quads, + SHAPES.ResourcePagePanelSelection, + TERMS.hasPanelDataRequirement, + ); + assert( + hasTriple( + quads, + SHAPES.ResourcePageDefinitionPresentationConfig, + SH.targetClass, + TERMS.ResourcePageDefinition, + ), + "ResourcePageDefinition presentation shape should target ResourcePageDefinition", + ); + assertOptionalProperty( + quads, + SHAPES.ResourcePageDefinitionPresentationConfig, + TERMS.hasResourcePagePresentationConfig, + ); + assertPropertyMaxCount( + quads, + SHAPES.ResourcePageDefinitionPresentationConfig, + TERMS.hasResourcePagePresentationConfig, + 1, + ); + assertOptionalProperty( + quads, + SHAPES.ResourcePageDefinitionPresentationConfig, + TERMS.hasGeneratedResourcePagePanelSelection, + ); +}); + Deno.test("config ontology carries publication, local access, and resource-page config vocabulary", async () => { const quads = await parseRepoTurtle("semantic-flow-config-ontology.ttl"); @@ -149,6 +387,64 @@ function assertNodeShape(quads: readonly Quad[], subject: string): void { ); } +function assertRequiredProperty( + quads: readonly Quad[], + shape: string, + path: string, +): void { + const propertyShape = findPropertyShape(quads, shape, path); + assert( + propertyShape, + `${shape} should declare a property shape for ${path}`, + ); + assert( + objectsFor(quads, propertyShape.value, SH.minCount).some((term) => + term.value === "1" + ), + `${shape} ${path} property shape should require at least one value`, + ); +} + +function assertOptionalProperty( + quads: readonly Quad[], + shape: string, + path: string, +): void { + assert( + findPropertyShape(quads, shape, path), + `${shape} should declare a property shape for ${path}`, + ); +} + +function assertPropertyMaxCount( + quads: readonly Quad[], + shape: string, + path: string, + maxCount: number, +): void { + const propertyShape = findPropertyShape(quads, shape, path); + assert( + propertyShape, + `${shape} should declare a property shape for ${path}`, + ); + assert( + hasTriple(quads, propertyShape.value, SH.maxCount, String(maxCount)), + `${shape} ${path} property shape should allow at most ${maxCount} value`, + ); +} + +function findPropertyShape( + quads: readonly Quad[], + shape: string, + path: string, +): Term | undefined { + return objectsFor(quads, shape, SH.property).find((candidate) => + objectsFor(quads, candidate.value, SH.path).some((term) => + term.value === path + ) + ); +} + function validateArtifactResolutionModeUsage( dataQuads: readonly Quad[], shapeQuads: readonly Quad[],