Skip to content

v2.8.0

Choose a tag to compare

@github-actions github-actions released this 24 Jun 10:47
· 427 commits to main since this release
9f86667

Minor Changes

  • #91 46b519e Thanks @flyon! - Development-mode source resolution and an instance-count diagnostic.

    • Conditional development exports. package.json now declares a development export condition resolving to ./src/*.ts (and ./src/index.ts for the root). Vite's browser-side resolver picks the TypeScript source in dev mode, enabling HMR-on-source for @_linked/core from a consuming app. Production resolution (importlib/esm, requirelib/cjs, types) is unchanged.
    • LinkedStorage.getLoadedInstanceCount(): number — new public static method reporting how many @_linked/core instances are registered on the global tree (a diagnostic for the Vite-SSR-vs-Node-resolver dual-load split).
  • #91 46b519e Thanks @flyon! - Shape, package, and framework-vocabulary IRIs now use the canonical linked.cm namespace, with a configurable per-package publish root.

    New scheme (arch-aligned):

    • Shape IRIs: https://linked.cm/shape/{packageSlug}/{ShapeName} (PascalCase shape name; previously https://data.lincd.org/module/{sanitized}/shape/{lowercased}).
    • Package IRIs: https://linked.cm/pkg/{packageSlug} (previously …/module/{name}).
    • Framework vocabulary: https://linked.cm/ont/linked-core/ (prefix linked_core; previously https://purl.org/on/lincd/, prefix lincd). The Module term is renamed to Package.

    New / changed public API:

    • linkedPackage(name, { baseUri? }) — packages declare where they publish. baseUri defaults to https://linked.cm/ (first-party); CN injects a workspace-scoped root ({workspaceSlug}.id.create.now) for private packages. The IRI slug is the package basename with the npm scope dropped (@_linked/corecore, @linked.cm/blogblog); there is no separate slug param. Slugs must be globally unique within the publish root — the registry enforces this and reserves the first-party (@_linked) names.
    • New exports LINKED_DATA_ROOT, getPackageUri(), setPackagePublishConfig(), packageNameToSlug() (replaces the removed LINCD_DATA_ROOT).
    • The framework ontology export is now coreOntology (was lincd).

    Breaking: generated shape/package/term IRIs change. Consumers that hardcoded data.lincd.org IRIs, imported LINCD_DATA_ROOT, or used the lincd ontology export must update. Stored data keyed on the old IRIs needs migration.

  • #97 7c2fea9 Thanks @flyon! - Serialize code-defined SHACL shapes into the store and keep them in sync.

    New exports

    • syncShapes(): Promise<Array<() => Promise<void>>> — materializes every code-registered
      (non-framework) NodeShape into the store as SHACL data. Returns built-but-unexecuted thunks so the
      caller controls execution/batching; each thunk runs delete → recreate for one shape (cascade-cleaning
      its old property shapes / list / path subtrees), plus orphan-delete thunks for shapes removed from code.
      import { syncShapes } from "@_linked/core";
      await Promise.all((await syncShapes()).map((run) => run()));
    • rdfList(items, {base?}) — builds an ordered rdf:List (nested List node-data) for use in any
      create/update, so ordered collections (and sh:in) round-trip instead of becoming unordered sets:
      Playlist.create({ tracks: rdfList([t1, t2, t3]) });
    • serializePathToNodeData(pathExpr, baseIri) — translates a PathExpr to sh:path node-data
      (predicate IRI / rdf:List sequence / PathNode for inverse·alternative·cardinality).
    • PathNode shape (linked:PathNode) — operator node for complex property paths.

    New composition flags (delete/update cascade)

    • @objectProperty({ …, contains: true }) marks a property as owning its value(s); @linkedShape({ dependent: true }) marks a shape whose instances may be cascade-deleted when reached through a
      contains edge. Deleting or replacing a contains property now removes the whole owned subtree
      (e.g. an rdf:List spine or a sh:path operator tree), while shared predicate/value IRIs and
      rdf:nil are preserved.
    • @linkedShape({ closed: true, ignoredProperties: [...] }) now persist as sh:closed /
      sh:ignoredProperties.

    New SHACL/ontology terms: sh:equals, sh:disjoint, sh:hasValue, sh:order, sh:group,
    sh:closed, sh:ignoredProperties; linked_core:contains, linked_core:dependent, linked_core:PathNode.

    Potentially breaking: the List shape was rewritten to a pure rdf:List cell shape — its former
    in-memory helpers (fromItems, getContents, addItem(s), isEmpty, items) were removed. Use
    rdfList() to build lists. List had no RDF-backed consumers, so most users are unaffected.

    See docs/reports/016-shacl-rdf-serialization.md for the full design, cascade mechanics, and test coverage.

  • #91 46b519e Thanks @flyon! - SPARQL generation: structured property paths on named properties, and inner pagination for nested selects.

    Structured sh:path on named properties now resolve correctly. A query that references a named property whose SHACL sh:path is structured (a sequence [a, b], an inverse ^p, or an alternative a|b) previously collapsed to a shadow IRI and matched nothing. It now emits the correct SPARQL property-path predicate. Simple single-predicate properties are unaffected (output unchanged).

    Nested selects can now bound a related collection with .limit() / .offset() / .orderBy() — when the outer query targets a single subject:

    // Up to 2 friends, ordered, for one person
    Person.select((p) =>
      p.friends
        .select((f) => f.name)
        .orderBy((f) => f.name)
        .limit(2)
    ).for({ id });

    This emits a real SPARQL sub-SELECT … ORDER BY … LIMIT … OFFSET … that bounds the collection per parent. orderBy accepts a proxy callback (f => f.name) or a property-name string and defaults to ascending.

    Notes:

    • Per-group pagination across multiple parents is not supported and now throws a clear error instead of silently applying a global limit. The same applies to .limit() on a deeper (grandchild) collection, and to .limit() called directly on a traversal without .select(...).
    • Queries with no inner pagination are emitted exactly as before.

Patch Changes

  • #91 46b519e Thanks @flyon! - initTree() is now idempotent: if global.lincd already exists (which
    happens structurally under Vite SSR — Vite resolves @_linked/core/utils/Package
    from src/, and any /* @vite-ignore */ dynamic import via Node's resolver
    gets it from lib/esm/), the function attaches to the existing registry
    instead of throwing or warning.

    Previous behavior used a _lincdMultiWarned one-shot flag that logged a
    warning on the second initialization. This was framed as "interim" but
    was actually the correct semantic for the Vite-SSR-vs-Node-resolver split.
    The new code expresses the same behavior as the explicit design rather
    than as a workaround.

    No API change. Existing consumers see the same lincd global tree they
    saw before. Apps that previously saw "Multiple versions of Linked are
    loaded — accepted during HMR/Vite interim" in their dev log will no
    longer see that line.

    Context: see create-now plan-011 report (docs/reports/009-legacy-lincd-eradication.md).

  • #96 3114383 Thanks @flyon! - Lower multi-valued projected traversals into OPTIONAL (left-join) subtrees.

    Report 014 fixed projection-only singular object traversals (maxCount <= 1)
    to use nested OPTIONAL so a parent with a missing nested object is preserved.
    That gate is now lifted: multi-valued projected traversals (e.g. a
    ShapeSet like knows/friends/pets, with no maxCount) are lowered the
    same way.

    A query such as Person.select(p => [p.givenName, p.knows.select(k => [k.givenName])])
    now returns every person — those with no knows get knows: [] — instead of
    inner-joining the parent away. The result grouper already collects multiple
    child bindings into an array, so no mapping changes were needed.

    Filtered (.where(...)) and otherwise-required traversals keep their existing
    semantics, and paginated nested selects (inner LIMIT/OFFSET) are still
    emitted as sub-SELECTs.