mikser-io 9.43.0 → 9.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -552,6 +552,9 @@ exactly the one that needs telling.
552
552
 
553
553
  `siblingDestinations(folder, relativePath)` reports files differing only by
554
554
  extension, which may render to the same destination.
555
+ `locateEntityFile(id)` resolves a catalog id to its `{ collection, relativePath }`,
556
+ or `{ error }` — taken from the entity rather than by splitting the id, since the
557
+ prefix is configurable and the extension may have been stripped.
555
558
 
556
559
  ## Search
557
560
 
@@ -615,6 +618,299 @@ it, in any text format, with no per-language grammar involved. It returns the
615
618
  line rather than a verdict about it, so where the heuristic is wrong the
616
619
  evidence is in the result.
617
620
 
621
+ ## Content
622
+
623
+ Reading an entity's source, and deciding what "source" even means for it.
624
+
625
+ ### `readEntityContent(entity, { reload } = {})`
626
+
627
+ Returns one of `{ content }`, `{ contentError }`, `{ contentSkipped }` — an
628
+ object to `Object.assign` onto the entity, or use directly. Dispatches by URI
629
+ scheme: plain paths and `file://` read from disk, `http(s)://` goes through the
630
+ built-in provider, anything else dynamic-imports `mikser-io-provider-<scheme>`.
631
+
632
+ `entity.content` already being a string short-circuits the whole dispatch, which
633
+ spares re-fetching a remote document a source plugin eagerly pulled in. Pass
634
+ `reload: true` when you want the bytes **as they are now** — between builds the
635
+ catalog copy and the file on disk part ways, and a whole-file rewrite built from
636
+ the catalog copy silently discards whatever changed underneath. An entity with
637
+ no `uri` keeps what it has rather than erroring.
638
+
639
+ ### `looksTextual(buffer)` / `isTextEntity(entity)`
640
+
641
+ `looksTextual` answers "is this text?" from the BYTES: no NUL and a clean UTF-8
642
+ decode. This is what decides whether content comes back, and it is why a
643
+ `.liquid`, `.njk`, `.toml` or a format nobody has written yet is readable
644
+ without being added to a list first.
645
+
646
+ `isTextEntity` is a cheap extension guess with no I/O. It is a **hint** — the
647
+ extension list behind it is hand-maintained and therefore wrong about anything
648
+ not yet added. Never gate a read on it.
649
+
650
+ ### `mimeForEntity(entity)`
651
+
652
+ Content type for the entity's `destination`, from the IANA registry via
653
+ `mime-types`. Null when the entity has no destination or the extension is
654
+ unregistered.
655
+
656
+ ### `checksumOf(content)` / `checksum(uri)`
657
+
658
+ `checksumOf` hashes a string; `checksum` hashes a file, sampling head and tail
659
+ for large ones rather than reading the whole thing.
660
+
661
+ ## Collections and sources
662
+
663
+ ### `useCollection(runtime, name)`
664
+
665
+ The folder behind a collection, and guarded writes into it.
666
+
667
+ ```js
668
+ const documents = useCollection(runtime, 'documents')
669
+ documents.folder // absolute path
670
+ await documents.write('blog/post.md', text)
671
+ await documents.remove('blog/post.md')
672
+ documents.resolveWithin('blog/post.md') // absolute path, or throws
673
+ ```
674
+
675
+ `write`, `remove` and `resolveWithin` refuse a path that resolves outside the
676
+ collection folder. This matters whenever the path comes from a request body or
677
+ a form: joining a folder with `../../x` lands outside it. The path is resolved
678
+ and then contained rather than rejected on a literal `..`, so `blog/../post.md`
679
+ still works.
680
+
681
+ ### `useSource(core, options)`
682
+
683
+ Codifies the folder-of-files pattern: scan a folder, emit entities, watch for
684
+ changes, sweep deletions.
685
+
686
+ | Option | Meaning |
687
+ | --- | --- |
688
+ | `collection`, `type`, `folder` | Required. |
689
+ | `extensions` | Default `['*']`. |
690
+ | `ignore` | Glob patterns to skip. |
691
+ | `phase` | `'loaded'` (default) or another lifecycle phase. |
692
+ | `content` | Load file content into the entity at sync time. |
693
+ | `load` | `async (entity) => meta` — your parse step. |
694
+ | `idPrefix` | Defaults to `/<collection>`. |
695
+ | `stripExtensionFromId` | Default false (documents style). |
696
+ | `progress` | Progress label. |
697
+
698
+ ### `sweepDeleted(collection, scanned, onDelete, ownerPrefix)`
699
+
700
+ Removes catalog entities whose files are gone. **`ownerPrefix` is mandatory and
701
+ load-bearing.** Collections are multi-emitter: the file source scans a folder,
702
+ but a CSV plugin fans rows into the same collection, and a remote sync emits
703
+ there with a `gdrive://` uri. The sweep only considers entities whose `uri` is
704
+ rooted under the prefix — without it, every cycle's file sweep wipes every
705
+ other emitter's entities.
706
+
707
+ ### `useRenderer(runtime, { defaultTimeout } = {})`
708
+
709
+ Returns `{ render }` — the batching renderer the engine dispatches through,
710
+ with a per-task timeout (default 30s). A plugin that needs to render something
711
+ outside the normal cycle goes through this rather than importing a renderer
712
+ package directly.
713
+
714
+ ## Query context
715
+
716
+ `queryContext` is the `AsyncLocalStorage` that lets a catalog query made during
717
+ a render record itself as a dependency, so an aggregate page invalidates when a
718
+ new matching entity lands.
719
+
720
+ It only works if the whole tree shares ONE module instance of `mikser-io`. In
721
+ the side-by-side dev layout that means the npm workspace at the parent folder
722
+ is not ergonomics but correctness: without it, npm installs a second copy into
723
+ a sibling's own `node_modules`, a plugin's `queryContext` is then a different
724
+ AsyncLocalStorage than the engine's, queries record no edges, and index pages,
725
+ sitemaps and feeds silently stop rebuilding. Production consumers resolve both
726
+ from their own tree, so the problem is local to the dev workspace.
727
+
728
+ ## Auth
729
+
730
+ Building a token-gated or loopback-only route.
731
+
732
+ | Export | Does |
733
+ | --- | --- |
734
+ | `resolveAuth(config)` | build a verifier from endpoint config |
735
+ | `requireAuth(verifier, options)` | Express middleware |
736
+ | `authorize(req, verifier, { allowRemote, trustLoopback })` | the check itself |
737
+ | `bearer({ token, name, subject, capabilities, scope })` | a static-token verifier |
738
+ | `loopbackOnly({ message })` | middleware refusing non-loopback callers |
739
+ | `hasCapability(principal, capability)` | test a resolved principal |
740
+
741
+ A principal may carry a `scope` — a sift filter that narrows what it can see.
742
+ Anything reading content on a principal's behalf must apply it; see the warning
743
+ under [Search](#search) for why an unscoped read behind a scoped endpoint is
744
+ the failure mode to watch for.
745
+
746
+ ## References
747
+
748
+ The `$`-keyed reference graph (ADR-0007), reachable at `runtime.refs` or via
749
+ `useRefsIndex()`.
750
+
751
+ | Method | Answers |
752
+ | --- | --- |
753
+ | `inboundFor(target)` / `outboundFor(source)` | static `$`-ref edges |
754
+ | `dynamicInboundFor` / `dynamicOutboundFor` | render-time edges (layout, partial, query, lookup) |
755
+ | `inverseClosureOf(seeds)` | everything reachable backwards — what invalidation walks |
756
+ | `resolveRefIds(ref)` | which entities a ref string resolves to |
757
+ | `rename({ from, to })` | rewrite refs across the catalog, as one cascade |
758
+ | `allRefs()` / `size()` | inventory |
759
+
760
+ ### `refFilter(ref)` / `matchesRef(entity, ref)` / `lookupKeys(entity)`
761
+
762
+ One relation in three directions — as a catalog query, as a predicate, and in
763
+ reverse. **They must be changed together.** A key present in one and missing
764
+ from the others is silent: `meta.url` once lived only in `refFilter`, which
765
+ made every `$`-ref to a served path non-invalidating without any error
766
+ anywhere.
767
+
768
+ ### `extractRefs(meta)` / `isRefKey(key)` / `expandEntity(entity, paths, options)` / `projectMeta(meta)`
769
+
770
+ Find the `$`-keys in a meta tree, test one key, inline referenced entities
771
+ along dotted paths, and drop `$`-keys for output.
772
+
773
+ ## Provenance
774
+
775
+ Where a value was **written** — source file, field path, line, column.
776
+
777
+ ```js
778
+ const positions = await useProvenance().positionsFor(entity)
779
+ // { 'items[2].label': { line, col }, … }
780
+ ```
781
+
782
+ | Method | Answers |
783
+ | --- | --- |
784
+ | `positionsFor(entity)` | every leaf of the entity's meta |
785
+ | `locate(entity, fieldPath)` | one position, or null |
786
+ | `forget(id)` | drop a cached entry |
787
+ | `size()` | how many entries are cached |
788
+
789
+ Field paths are free — they come from walking meta, already in memory. Line and
790
+ column need one parse of the raw source, done **on demand** and cached against
791
+ the entity's checksum, so a build pays nothing.
792
+
793
+ `registerProvenanceFormat(name, { test, positions })` adds a format rather than
794
+ special-casing one. A format whose parser reports no ranges registers a
795
+ `probeFormat(name, { test, parse })` instead, which recovers positions in one
796
+ pass without the parser's help.
797
+
798
+ ## Manifest and outputs
799
+
800
+ `runtime.manifest` holds render snapshots. Full treatment lives in
801
+ [diagnostics.md](diagnostics.md) — indexed by the question each surface
802
+ answers — but the ones an application reaches for:
803
+
804
+ | Method | Answers |
805
+ | --- | --- |
806
+ | `affectedBy(entity)` | which destinations would re-render if this changed |
807
+ | `collisions()` | destinations more than one entity writes to |
808
+ | `snapshotsFor(id)` / `snapshotsAt(destination)` | what rendered, and from what |
809
+ | `skipDecision(entity, …)` | the engine's own skip rule, with the reason |
810
+
811
+ `sourcesOf(destination)` is the reverse lookup: what produced this built file,
812
+ each tagged with how it got there. `sourcesBehind(snapshot)` does the same from
813
+ a snapshot you already hold. `resolveOutputPath(destination)` maps a
814
+ destination to a path on disk, and `writeOutput(file, bytes)` writes one.
815
+
816
+ ## Tools
817
+
818
+ The tool registry — named, described, invokable capabilities. Two agent
819
+ workflows exist and are equally real: one speaking MCP over HTTP, one running
820
+ the CLI and reading its output. A tool registered here reaches both.
821
+
822
+ ```js
823
+ registerTool('audit', {
824
+ description: 'What this answers, in prose an agent will actually read.',
825
+ inputSchema: { path: { type: 'string', required: true } },
826
+ }, async ({ path }) => ok({ … }))
827
+ ```
828
+
829
+ `invokeTool(name, args)` runs one — it accepts the bare name or the `mikser_`
830
+ prefixed form. `toolNames()`, `toolSchema(name)` and `toolSchemas()` enumerate.
831
+ `toolResultText(result)` pulls the text back out of a tool result, and
832
+ `toolResultFailed(result)` says whether it failed.
833
+
834
+ The registry is deliberately zod-free: schemas use a neutral
835
+ `{ type, required?, description? }` vocabulary, because it must not depend on
836
+ one transport's schema library. `mikser-io-mcp` converts to zod at bind time.
837
+
838
+ ## Routes
839
+
840
+ An Express router stack has the paths but not the intent. Plugins declare each
841
+ mount as they make it, so a facade generator, a healthcheck list or a
842
+ diagnostics view can read one inventory.
843
+
844
+ ```js
845
+ registerRoute({
846
+ path: '/api',
847
+ plugin: 'api',
848
+ reachability: 'public', // 'public' | 'token' | 'loopback'
849
+ streaming: false, // true for SSE/WS, which a facade must not buffer
850
+ })
851
+ ```
852
+
853
+ `registerRoute` also folds in the origin/URL building and the standard boot
854
+ log. `listRoutes()` returns the inventory; `reachabilityOf` and `routeLocation`
855
+ answer about one route. `isLoopback(ip)` is the check behind
856
+ `reachability: 'loopback'` — note that the server's trust-proxy default is
857
+ `'loopback'`, not Express's `false`, which is what keeps that gate correct
858
+ behind a same-host reverse proxy.
859
+
860
+ ## Cycles and the build report
861
+
862
+ `nextCycleId()` reserves the id of the cycle a pending change will be picked up
863
+ by; `whenCycleCompletes(id)` resolves once it finishes, with its report.
864
+ Together they turn "write and guess" into one call that says what the edit
865
+ invalidated. `currentCycle()` and `buildReport()` read the cycle in progress and
866
+ the last completed report.
867
+
868
+ ## Logging
869
+
870
+ `addLogTransport({ target, options, level })` adds a pino transport from a
871
+ plugin factory or any later hook. Called before the logger is built, it queues;
872
+ after, it live-rebuilds the multistream. This is what lets Better Stack,
873
+ Datadog, Loki, Axiom or Sentry ship as ordinary sibling plugins with no engine
874
+ change per vendor.
875
+
876
+ Prefer this over `runtime.config.logging.transports` from plugin code — the
877
+ declarative form is for user config.
878
+
879
+ ## Junk
880
+
881
+ `registerJunk({ ignore, match })` teaches the engine to skip editor and OS
882
+ debris; `isJunkPath(filePath)` asks.
883
+
884
+ ## What is not here
885
+
886
+ Plugin factories — `yaml()`, `json()`, `frontMatter()`, `assets()`,
887
+ `resources()`, `shares()`, `observer()`, `mapper()`, `commands()`,
888
+ `renderHbs()` — are configured rather than called, and live in
889
+ [configuration.md](configuration.md).
890
+
891
+ `mikser-io` exports more than this page covers, deliberately. Five kinds of
892
+ export are engine plumbing:
893
+
894
+ - **Factories the engine calls once** — `createManifest`, `createRefs`,
895
+ `createProvenance`, `createSqliteDatabase`, `createTrack`, `createIndex`,
896
+ `createSubscribers`.
897
+ - **Schema constants** the test suites build against rather than copying —
898
+ `SNAPSHOTS_SCHEMA`, `REFS_SCHEMA`, `FAILURES_SCHEMA`, `PROVENANCE_SCHEMA`.
899
+ - **Report and cycle internals** — `reportRendered`, `reportSkipped`,
900
+ `reportError`, `emitReport`, `resetReport`, `finishCycle`, `inputHashOf`.
901
+ - **Render-time tracking** — `recordReads`, `untrack`, `trackedInfo`,
902
+ `serializeTrack`, `mergeTrack`, `observeConsumed`. These implement
903
+ dependency recording; a plugin observes its RESULTS through
904
+ `runtime.manifest` and `runtime.refs` instead.
905
+ - **Template helper plumbing** — `assetUrlHelper`, `resourceUrlHelper`,
906
+ `hrefUrlHelpers`, `fileHelpers`, `renderPreset`, wired into renderers rather
907
+ than called.
908
+
909
+ They are exported because the engine's own modules and its test suite need them
910
+ across file boundaries, not as an invitation. If you find yourself reaching for
911
+ one from a plugin, that is worth raising — it usually means a capability is
912
+ missing from the surface above.
913
+
618
914
  ## Database
619
915
 
620
916
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "9.43.0",
3
+ "version": "9.43.1",
4
4
  "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
5
  "main": "index.js",
6
6
  "exports": {