Compare commits

...
Author SHA1 Message Date
Quixos Subtree Publisher 27af7af6c5 Publish quixos-protocol from 473886a58fdacf15f2c873b77da96faf3be7fa65 2026-10-31 14:55:33 +00:00
Quixos Development Agent 01f26965-84fa-4a79-8069-725682c13065 7545f7392d Preserve authorized cold-load browser refusals and private aggregate metadata usage (#641) 2026-10-31 14:50:06 +00:00
Quixos Development Agent 01f26965-84fa-4a79-8069-725682c13065 94ddc8bedf Expose bounded post-build Source Check failures to authors (#641) 2026-10-31 14:48:20 +00:00
Quixos Subtree Publisher c56948ebd0 Publish quixos-protocol from e8df5e3396060619b33ab3cf9458231ef220ccc7 2026-10-31 14:19:06 +00:00
Quixos Development Agent 01f26965-84fa-4a79-8069-725682c13065 6f5b5265b6 Exercise alias pin discovery and refresh in always-run Nix fixtures (#713) 2026-10-31 14:15:38 +00:00
Quixos Development Agent 01f26965-84fa-4a79-8069-725682c13065 31d155fd88 Keep managed source paths canonical across outer workbench aliases (#713) 2026-10-31 14:09:51 +00:00
Quixos Subtree Publisher a025525f47 Publish quixos-protocol from 806b0430d952b5a9aab5b2a63496c272aced8e50 2026-10-29 14:36:45 +00:00
Timothy J. Aveni e13fde36fb Resolve managed resource aliases within the canonical workbench (#588) 2026-10-29 07:36:45 -07:00
Quixos Subtree Publisher d829d584af Publish quixos-protocol from ca5b05584b89e90cb259b4596b1bc01d3d692136 2026-10-25 21:28:36 +00:00
Quixos Development Agent 5ffdc2ce-2936-4c70-8208-3a9da53f4aa9 ee84025322 Use explicit navigation projections for React presentation readiness (#339) 2026-10-24 06:39:33 +00:00
Timothy J. Aveni bc9a7ef2cf Integrate Kubernetes compute, scoped Agent placement and observable releases (#195) 2026-10-18 19:52:45 -07:00
Quixos Subtree Publisher badad181de Publish quixos-protocol from 6b91014ff1d6c9c54d0782f586935e694f9ee72f 2026-10-13 12:37:58 +00:00
Timothy J. Aveni 85163b6e67 Reduce repeated compiler startup in monorepo QX formatting 2026-10-13 05:37:58 -07:00
Quixos Subtree Publisher e8373a4cd1 Publish quixos-protocol from e4393946ba63cfef75580f6aa6ba840035e7d43c 2026-10-13 10:19:49 +00:00
Timothy J. Aveni e92b08025c Avoid rehashing validated Graph execution metadata during Candidate loading 2026-10-13 02:58:29 -07:00
Timothy J. Aveni 62a09605cf Reuse verified immutable plans and streamline cooperative hashing
Continue final hosted development reliability qualification.
2026-10-12 20:16:56 -07:00
Timothy J. Aveni 47b316180d Keep checked execution groups and public catalog preparation cooperative 2026-10-12 19:21:34 -07:00
Timothy J. Aveni a4ba6169a6 Reuse checked reflection inputs before acquiring controller ownership 2026-10-12 18:30:07 -07:00
Timothy J. Aveni a48fccc328 Keep cold Candidate verification cooperative with controller renewals 2026-10-12 18:24:38 -07:00
Timothy J. Aveni 755c008a88 Repair concurrent Graph recovery, Source Job residency and developer source provisioning 2026-10-12 15:39:34 -07:00
Timothy J. Aveni bbf9056dfa Unify scoped development with ordinary Workspace authoring commands 2026-10-12 13:09:47 -07:00
Timothy J. Aveni ecf97173b4 Retain private platform source while resolving its context-free Nix flake reference 2026-10-11 16:41:18 -07:00
Timothy J. Aveni 0f41b12ca0 Supply exact private platform source to sealed checks without private evaluator egress 2026-10-11 16:07:52 -07:00
Timothy J. Aveni fa698dd306 Protect active development Graphs from rejected launcher cleanup 2026-10-11 03:42:32 -07:00
Quixos Subtree Publisher b50a7f080c Publish quixos-protocol from 8014b4ba717313e1b494c59a8e2781df58bf9a85 2026-10-11 05:28:35 +00:00
Timothy J. Aveni 2139f138ca Write Agent bootstrap plans to files instead of bounded command output 2026-10-10 22:28:35 -07:00
Timothy J. Aveni 8fe374fe25 Run canonical application source in independent development Graphs
Initialize camino-dev from an exact canonical source pin with native Git/jj history, compiler-bound Workspace identity, and retained instructions/toolchain. Share the strict pin decoder with source acquisition; retain monorepo fixture behavior separately.

Qualified the maintained real Git/jj/compiler regression, all 57 Reader repositories, packaged launcher startup/session, retained-Graph restart and Chromium Text/settings opening without page errors. Lint and documentation checks pass. Hosted source lifecycle qualification and maintained-copy removal remain pending.
2026-10-10 19:37:35 -07:00
Timothy J. Aveni ad2a851b94 Consolidate Reader source ownership and pinned application instances
Add exact recursive source checkout, independent Git/jj developer workbenches and source-only backup/recovery commands, with Workspace instance binding outside source. Qualify two empty Reader Graphs, packaged source commands, Agent VM compilation, exact Gitea read grants and reconciled Reader settings/annotation stability. Publish checked Reader Package/root revisions to canonical review branches and retention tags, retain all 57 repositories independently in private versioned storage, verify downloaded-backup recovery and advance the example pin.

Add bounded Central pin acquisition and selection-specific read-only identities, qualified against all 57 canonical repositories without write authority. Replace regex fork dependency discovery with compiler-parsed lock imports and rewrite proposals; preserve source identities and unrelated files, passing instance binding through packed and ordinary host checks.

Hosted run-pin versus authoring preparation and maintained duplicate removal remain pending. Live source selections, Template defaults and default source branches are unchanged.
2026-10-10 18:38:57 -07:00
Quixos Subtree Publisher 721b730263 Publish quixos-protocol from a61bdda658be4878403b8d60aecb71fe5a614ac0 2026-10-10 10:49:05 +00:00
Timothy J. Aveni c2c01a9878 Implement progressive query admission, session boundaries and event-driven Studio observations 2026-10-10 03:49:05 -07:00
Quixos Subtree Publisher 9fae801b7f Publish quixos-protocol from 286ada5419c4fc2184b387cea4715fae1a74bde5 2026-10-10 00:10:14 +00:00
Timothy J. Aveni 2ae7f00acb Reuse checked bootstrap catalogs and coordinate execution deadlines 2026-10-09 17:10:14 -07:00
Timothy J. Aveni 041a9e26ad Document package architecture and organize current guidance and project backlog 2026-10-09 10:33:39 -07:00
Quixos Subtree Publisher 7f3fdfe64e Publish quixos-protocol from d1ab9c912133511cfd3de65550965e0e8806678f 2026-10-08 19:32:06 +00:00
Timothy J. Aveni 0a7bdf7657 Qualify restricted SDK syntax independently of build architecture 2026-10-08 12:32:06 -07:00
Quixos Subtree Publisher 137dcaa101 Publish quixos-protocol from 2ef07e2524ed167421be56b81a5f498a2759ca5d 2026-10-08 19:30:33 +00:00
Timothy J. Aveni a86de3fe5b Qualify restricted SDK syntax independently of build architecture 2026-10-08 12:30:33 -07:00
Timothy J. Aveni 165510341d Repair packed agent startup and bound activation metadata retention 2026-10-08 11:19:19 -07:00
Quixos Subtree Publisher b406738249 Publish quixos-protocol from 11d2ad06170609a09118cbabd4245b5828a148d1 2026-10-08 18:19:19 +00:00
Timothy J. Aveni ffd728b061 Keep pin-upgraded workbench paths relative for relocation 2026-10-08 07:59:49 -07:00
Quixos Subtree Publisher e78b7adf28 Publish quixos-protocol from c4319441699b7b8c5af7e70dade6e4ad8cccddea 2026-10-08 14:59:49 +00:00
Timothy J. Aveni a935273913 Use execution workspace review semantics during bottom-up pin upgrades 2026-10-08 07:43:32 -07:00
Quixos Subtree Publisher e162dc3013 Publish quixos-protocol from e4075003e669be0e307ec8c0585cccb1bd08276a 2026-10-08 14:43:32 +00:00
Quixos Subtree Publisher e65ddad798 Publish quixos-protocol from 932fe7714761fb8b5db4bd6401357cdff762a428 2026-10-08 05:08:28 +00:00
Timothy J. Aveni 31a48e746d Admit checked restricted artifacts without individual code approvals 2026-10-07 21:43:44 -07:00
Quixos Subtree Publisher f9a8d64564 Publish quixos-protocol from abc91ab1cbbd61ffd7f0a49dbc5768598ac8745c 2026-10-08 00:03:01 +00:00
Timothy J. Aveni 61534d3fb5 Keep checked browser runtime versions independent of authority upgrades 2026-10-07 17:03:01 -07:00
Timothy J. Aveni 391f02f78a Pool host-approved restricted code with scoped and fair graph execution 2026-10-07 16:47:19 -07:00
Timothy J. Aveni c8c56d8ea7 Bound checked candidate metadata and restricted invocation resources 2026-10-07 15:51:22 -07:00
Quixos Subtree Publisher 0443992dd4 Publish quixos-protocol from 452f3c48aadceaf08e56ae9c226e98a7f9df4900 2026-10-07 13:30:33 +00:00
Timothy J. Aveni 9248332da6 Allow explicitly exported read-only edge dependency ports 2026-10-07 06:30:33 -07:00
Quixos Subtree Publisher b633a91a41 Publish quixos-protocol from dd7527da2714f3b0941ed908357fd82e98f45bc4 2026-10-07 12:07:09 +00:00
Timothy J. Aveni 192a22c936 Handle optional source-binding configuration in structural package upgrades 2026-10-07 05:07:09 -07:00
Quixos Subtree Publisher 3f0ef70ecd Publish quixos-protocol from 6e51f796de7af0da8c053f770b89d243f74c5d61 2026-10-07 05:33:13 +00:00
Timothy J. Aveni 5c618e0e41 Build checked workspace source in a sealed tenant-scoped VM 2026-10-06 21:16:21 -07:00
Timothy J. Aveni be8a19b5c5 Report conservative execution placement from checked dependency closures 2026-10-06 16:28:33 -07:00
Timothy J. Aveni 9d8bc5e2ab Scope Camino storage and database authority to each workspace 2026-10-06 16:22:59 -07:00
Quixos Subtree Publisher 5b44c8154d Publish quixos-protocol from 37a50c17063a41d23037b39785f7054992d7c708 2026-10-06 04:38:24 +00:00
Timothy J. Aveni 61edac68a8 Expose checked graph transactions, Agent CLI and bounded evaluation 2026-10-01 20:23:50 -07:00
Timothy J. Aveni ef9a4e067a Expose workspace data to the agent and curate its authoring guides
Check named public GraphQL documents against the serving installation using the
existing compiler, linker and authorized snapshot interpreter. Keep listObjects
as a convenience over workspace-root queries, with no separate filter language.
Bound compilation/results, cache by content and serving plan, and fence cutovers.

Share graph discovery and reads between qx-workspace graph and the gardener's
workspace_graph tool. Local authoring shells discover the live endpoint through
the private control socket; configured public mounts also support direct clients.
Fix pending paginated inspections to wait for their confirmed window, and retain
semantic integration assertions instead of locking a server ticket's shape.

Replace the long prompt with a 384-word entry point and five focused guides.
Build immutable bundles with profiles, deep links, source hashes and word/link
checks. Keep the docs-maintenance process and feedback outside agent guidance.
Running agents use current guides while authored source and checker pins remain.

Validation: nix fmt; nix fmt -- --check; nix run .#lint; existing CLI checks;
protocol/client/coordinator build tests; complete Studio integration; 13 agent
unit tests. A real GPT-6.1 Sol medium/priority turn summarized two synthetic saved
comments and a document draft in 27.7s without source changes or cutover. Live
reads also succeeded after an ordinary check/activation in the retained fixture.
2026-10-01 13:15:24 -07:00
Timothy J. Aveni bd42306059 Fix Studio query contracts for authored workspaces
Keep query definition identity scoped to interfaces present in the generated
query schema. Exclude retention provenance from both interface revisions and
generic applications while preserving nominal identities, closed arguments,
operation contracts, documents, budgets and fragment dependencies. Advance the
query semantics version so prior compiler outputs cannot share the new identity.

Add a generic compiler regression covering unrelated imports, retained generic
sources and invalidation when an observed getter changes. This fixes Studio's
CLIENT_QUERY_MISMATCH when template contracts become editable local repositories.

Validation: protocol tests (296 total, 293 passed, 3 existing skips); nix fmt,
monorepo lint, final formatting check and diff check; camino-dev/qx-workspace
builds. All 17 Studio query descriptors match a rebuilt copy of the user's
workspace. Headless fresh-workspace canvas/library/Reader/reload check passes.

Repair the existing local qx-reader workbench through its ordinary check and
activation path, updating only its pinned protocol/checker CLI. Retain the
original toolchain GC root and record checker provenance. Source commit, runtime
and template pins, and database are preserved. Verify the selected candidate is
ready at epoch 2 and its canvas loads through the corrected Studio bundle without
creating application objects. Stop the independent smoke-test services.
2026-10-01 09:49:40 -07:00
Timothy J. Aveni 7c1830bcd5 Instrument and improve incremental workspace checks
Profile source capture, Nix evaluation/realization, resource/query checking,
and implementation builds without putting timings into immutable outputs.
Keep modular repositories; bound independent capture work, snapshot jj once
per observation, and retain final concurrent-edit and Git completeness checks.

Compose exact resource/closure/frontend/toolchain checks as memoized Nix
derivations. Preserve whole-workspace validation and the full compiler path
when no checked dependency is supplied. Share canonical Nixpkgs by content,
index workspace build contexts once, and narrow lock/driver/compiler inputs
with conservative complete-source fallback. Batch React witness checks only
when distinct source closures cannot share ambient declarations; otherwise
run the original independent TypeScript checks.

Seed local authoring toolchains with exact canonical fetch identities when
they match the selected inputs; retain store-path inputs for overrides.
Add generic correctness checks rather than permanent Reader timing/content
fixtures. Verify actual Reader edits and rejection/recovery in a private
workbench, preserving the original user's workbench and runtime.

Query checking currently caches at resource/closure boundaries, not individual
query semantic dependency boundaries. Template helper pins and reproducible
fixture snapshots follow in a separate publication change.
2026-10-01 04:03:19 -07:00
Timothy J. Aveni e8d19d2bb7 Build Camino packages from explicit contract inputs
Project source-free capability contracts and keep exact source provenance in package assembly. Reuse compiled groups across unrelated graph and dependency implementation edits. Keep React public inputs separate from workspace query lowering and retain explicit type-companion dependencies.

Allow independent execution-package checks, scope resource compilation to declared dependencies, and memoize source closure traversal. Document generic-specialization limits and measured Reader rebuild behavior.

Verified protocol, artifact, package, React, and managed activation checks; retained-Git independent/workspace artifact equality and incompatible type dependency rejection; nix fmt -- --check and lint. One-off Reader measurements recompiled 1 of 13 groups for a child edit and 0 for a workspace QX default edit.
2026-09-30 22:55:07 -07:00
Timothy J. Aveni b95cd149c6 Correct Reader producer completion and prepare only the active source
Route durable completion to the initiating PresentationMount, guarded against stale navigation and unrelated fan-out. Publish Gutenberg passages directly, preserve unfinished manual drafts, and open completed documents in the main reader pane. Prepare the selected provider and explicitly opened inspector only. Consolidate Studio factory contracts so optional execution placement is determined by implementations. Support typed stateful presentation props and local-module role selection in the React build layer. Update integration, browser, portal, and authoring documentation.
2026-09-28 21:54:18 -07:00
Timothy J. Aveni e9c15fe40f Make local graph execution optional across composed packages 2026-09-28 21:05:42 -07:00
Timothy J. Aveni 1e72713ae2 Implement durable typed journals, subscriptions, and Reader document connections
Add checked native journal/subscription bindings, managed-reference retention,
bounded history and backpressure, atomic delivery receipts, and fenced hosting.
Preserve source publication prediction and replica reconciliation; subscription
attachment, controls, and delivery remain authoritative in this initial version.

Connect independent manual/Gutenberg document producers to Reader bookshelves
through typed adapters. Add connection inspection, shared provider selection,
authoring documentation, and restart/rejection/fan-out integration coverage.

Verified compiler suite (287 passed, two existing skips), Nix runtime and
PostgreSQL qualification, five example assemblies, headless Reader integration,
Chromium Reader integration, resource pins, formatting, and lint.
No pinned workspace runtime was deployed.
2026-09-28 19:35:51 -07:00
Timothy J. Aveni 0ace71eae6 Generalize live field bindings and use them for Studio canvas gestures 2026-09-28 11:12:11 -07:00
Timothy J. Aveni 696023b4ba Implement main-thread Camino sessions and synchronous CRDT editing 2026-09-27 23:57:29 -07:00
Timothy J. Aveni ad72185a6c Implement independent presentation readiness boundaries 2026-09-27 20:30:07 -07:00
Timothy J. Aveni b499298299 Complete prepared presentation build and runtime integration 2026-09-27 18:51:05 -07:00
Timothy J. Aveni fdb7cd3f6f Add checked query composition foundations for prepared presentations
Move React navigation query generation behind an explicit build frontend. Add checked result-dependent query bindings and pure composition evaluation, including one-snapshot server execution. Preserve the existing rendering path while the React manifest, worker preparation, and replica admission integration remain pending; see docs/PREPARED_PRESENTATIONS_PROGRESS.md.
2026-09-27 16:28:33 -07:00
Timothy J. Aveni ad64ba532f Scope presentation associations to PresentationMount and retain navigation queries
Generate checked navigation query pairs from closed React contracts before
package checking. Retain per-type subscriptions under the logical
PresentationMount and resolve cold component metadata independently of code.

Keep scoped occurrence membership and idempotent local preparation in the
presentation library; copy directly through state contracts without reading
other PresentationMounts' source associations. Preserve normal transaction
creation, pending references, portal synchronization and detach.

Exercise automatic object refs in Reader queries, imported fragments and
structured/aggregate results. Add heterogeneous owner-index, live dependency,
cold metadata, browser ownership and latency coverage. Document measured
improvements, supported query limits and qualification evidence.
2026-09-26 12:32:47 -07:00
Timothy J. Aveni 3728612e8c Implement implicit object references and capability query foundations 2026-09-26 11:44:32 -07:00
Timothy J. Aveni 58a75da83f Lower checked shared edges into the portable graph model 2026-09-25 12:34:13 -07:00
Timothy J. Aveni c482a99aca Package ordinary PresentationMount contracts and typed React hosting 2026-09-25 12:26:34 -07:00
Timothy J. Aveni e7778b3518 Qualify typed presentation factories through public state interfaces 2026-09-25 12:11:01 -07:00
Timothy J. Aveni 76d5cff6fb Support complete public relationship observations for optimistic presentation hosting 2026-09-25 12:09:10 -07:00
Timothy J. Aveni 1bc3c69de3 Support checked keyed relationships across portable operations and queries 2026-09-25 12:02:05 -07:00
Timothy J. Aveni e0e8563224 Expose closed module types and define stable presentation occurrences 2026-09-25 11:37:27 -07:00
Timothy J. Aveni a83239892c Prove PresentationMount with authored generic graph operations 2026-09-25 11:30:14 -07:00
Timothy J. Aveni 73148d76e1 Add checked private storage to reusable conformance rules
Scope attachments and projections by stable rule-application identity; keep lexical names private to each application. Lower ordinary initialization mappings and concrete captured creation targets through existing checking. Preserve stateless forwarding, conflict detection, and the materialization fence. Add tests for multiple same-atom specializations, name isolation, generic edges, factory seeds, and storage evolution. Full protocol Nix build and tests pass.
2026-09-25 11:04:22 -07:00
Timothy J. Aveni 902739b284 Add shared Quixos React controls and delayed component fallback 2026-09-24 12:11:15 -07:00
Timothy J. Aveni abd5d57fb9 Implement rule-based Studio creation, usable canvas and Reader, and consolidated package authoring 2026-09-23 00:53:01 -07:00
Timothy J. Aveni 8455599c3c Implement reusable interface conformance rules 2026-09-23 00:17:13 -07:00
Timothy J. Aveni 20e9f4b056 Restore the workspace canvas and atomic Studio creation workflow 2026-09-22 22:17:00 -07:00
Timothy J. Aveni 595eda834f Restore generic Studio object navigation and discovery 2026-09-22 19:05:13 -07:00
Timothy J. Aveni 1b296daf42 Separate graph execution identity from installation provenance 2026-09-22 18:27:59 -07:00
Timothy J. Aveni c5e79ae3c9 Add bounded public member inspection through shared query scopes 2026-09-22 18:11:54 -07:00
Timothy J. Aveni bcc915d597 Expose bounded public reflection and presentation discovery 2026-09-22 17:22:48 -07:00
Timothy J. Aveni ca83dbd1a7 Add bounded live query cursor continuation 2026-09-22 17:20:00 -07:00
Timothy J. Aveni b8571d054b Build reusable React component hosting and typed fragment composition 2026-09-22 16:46:33 -07:00
Timothy J. Aveni a8869e4545 Add exported query fragments and scoped coverage observations 2026-09-22 16:18:57 -07:00
Timothy J. Aveni eba85ba158 Standardize Camino query references and retire live field sidecars 2026-09-22 15:41:27 -07:00
Timothy J. Aveni 152f58a3c6 Make Camino examples and scaffolds complete Yarn and Nix packages 2026-09-22 15:31:42 -07:00
Timothy J. Aveni 82a4542746 Build composable Camino packages with explicit type and source dependencies 2026-09-22 14:42:54 -07:00
Timothy J. Aveni 7173751f37 Implement typed module contracts and conformance bindings 2026-09-22 13:51:32 -07:00
Timothy J. Aveni 60b44a6637 Load checked installations independently of the running compiler (WI-16)
Keep compiler-dependent checks in Nix builds; load stored plans and programs with original provenance and runtime artifact validation. Allow an identical executable world to receive a new activation epoch after a toolchain rebuild.

Compiler-update/altered-program regression, coordinator and full sandboxed integration checks pass; lint and formatting pass. Default platform/source wiring remains the next task.
2026-09-21 22:01:47 -07:00
Timothy J. Aveni e5544dde21 Prove cross-package application composition through public capabilities (WI-15) 2026-09-21 19:07:32 -07:00
Timothy J. Aveni d824634859 Host checked adapter-object controllers through bounded registries (WI-13) 2026-09-21 18:00:27 -07:00
Timothy J. Aveni 1e08ccd937 Build and activate checked component packages through normal source authoring (WI-12) 2026-09-21 17:38:40 -07:00
Timothy J. Aveni e1ad15c620 Implement Studio root and placement capabilities on the replica graph (WI-10) 2026-09-21 16:37:00 -07:00
Timothy J. Aveni 2cf53280e4 Build checked component artifacts and shared React session SDK (WI-09) 2026-09-21 16:01:48 -07:00
Timothy J. Aveni 24073d7f2a Add runtime-owned replica Worker bridge and structured editing (WI-08) 2026-09-21 15:22:44 -07:00
Timothy J. Aveni a80982a780 Add interface-oriented client sessions and headless capability access (WI-07) 2026-09-21 14:45:46 -07:00
Timothy J. Aveni d159d9b281 Host persistent Camino services and authenticated workspace bootstrap (WI-06) 2026-09-21 14:02:54 -07:00
Timothy J. Aveni 09fd6b7190 Commit managed activation selections and reconcile forward (WI-05) 2026-09-21 13:30:33 -07:00
Timothy J. Aveni 89414107f9 Add independently owned reconnectable execution units (WI-03) 2026-09-21 12:44:36 -07:00
Timothy J. Aveni 884b12f759 Implement checked workspace integration contracts (WI-01) 2026-09-21 11:53:38 -07:00
Timothy J. Aveni 3c0378358f Run checked execution candidates in isolated development workspaces 2026-09-21 02:11:09 -07:00
Timothy J. Aveni 70344ca06a Review execution semantic-major changes without storage migration planning 2026-09-21 01:38:15 -07:00
Timothy J. Aveni 01a3ec5ae2 Scaffold explicit query placement and authored GraphQL documents 2026-09-21 01:30:57 -07:00
Timothy J. Aveni 20ef63007c Assemble generic execution installations from checked artifacts 2026-09-21 01:16:51 -07:00
Timothy J. Aveni f69c81f607 Check execution workspace candidates with pinned artifact builders 2026-09-21 01:09:31 -07:00
Timothy J. Aveni 7652b4c0b9 Add local-first execution authoring support 2026-09-21 00:57:50 -07:00
Timothy J. Aveni b4ac9700c6 Expose confirmed server projections through the typed replica client 2026-09-21 00:27:39 -07:00
Timothy J. Aveni 08e322e59f Reject unsupported query operators during execution planning 2026-09-20 23:58:07 -07:00
Timothy J. Aveni 47a3fe9eae Preserve nested CRDT recovery and serialize compound editor submissions 2026-09-20 23:26:13 -07:00
Timothy J. Aveni 2056c1e352 Support captured CRDT changes in compound transaction contracts 2026-09-20 23:07:09 -07:00
Timothy J. Aveni 33f674321b Qualify collection compare-and-swap and explicit ensure mutations 2026-09-20 22:57:27 -07:00
Timothy J. Aveni 115976cacd Expose authoritative lifecycle deletion through capability bindings 2026-09-20 22:48:09 -07:00
Timothy J. Aveni d80464d285 Qualify optimistic tree reparenting with authoritative cycle rejection 2026-09-20 22:25:11 -07:00
Timothy J. Aveni c0669d2ed8 Support explicitly reference-selected read-only interface dependencies 2026-09-20 22:15:15 -07:00
Timothy J. Aveni 266d2d98d0 Compile and dispatch authoritative acceptance hooks with target-specific package roots 2026-09-20 22:05:59 -07:00
Timothy J. Aveni 1e69b0e662 Qualify recurring occurrence completion with portable graph transactions 2026-09-20 21:48:07 -07:00
Timothy J. Aveni d638a9ed28 Run asynchronous object controllers from accepted graph jobs 2026-09-20 21:31:56 -07:00
Timothy J. Aveni 1dadc642d1 Build checked asynchronous service registries with Nix 2026-09-20 21:06:45 -07:00
Timothy J. Aveni c4ae58831d Generate typed asynchronous service capability bindings 2026-09-20 20:35:16 -07:00
Timothy J. Aveni 21502ed023 Qualify read-only forwarding and scoped portable discovery 2026-09-20 19:55:29 -07:00
Timothy J. Aveni 619b3d61c7 Select checked implementations explicitly by target and ABI 2026-09-20 19:51:15 -07:00
Timothy J. Aveni d701464312 Support checked closed generic portable bindings 2026-09-20 19:47:10 -07:00
Timothy J. Aveni 74a16d3646 Build the packaged local-first reader and cross-package task application 2026-09-20 05:40:01 -07:00
Timothy J. Aveni 824caddbf2 Author and apply bounded structured CRDT edits through the mutation log 2026-09-20 04:41:35 -07:00
Timothy J. Aveni c75888833b Evaluate coherent local queries over explicit replica scopes 2026-09-20 04:04:35 -07:00
Timothy J. Aveni 3f1976815f Compile and execute bounded GraphQL replica admission 2026-09-20 03:04:44 -07:00
Timothy J. Aveni d9bd5b2800 Acquire installed portable operations with explicit authority and input coverage 2026-09-20 02:19:00 -07:00
Timothy J. Aveni 8fd800e69a Execute checked capability bindings through scoped package runners 2026-09-20 01:49:25 -07:00
Timothy J. Aveni ab41e67dc5 Build checked portable package artifacts and shared runtime assets 2026-09-20 00:42:19 -07:00
Timothy J. Aveni 60b1bdb39f Generate portable capability contexts and checked handler registries 2026-09-20 00:25:53 -07:00
Timothy J. Aveni 0aaa80a4ba Confine checked artifact loading and browser asset lookup 2026-09-19 23:38:06 -07:00
Timothy J. Aveni ac36edf62d Compile closed execution plans and checked artifact manifests 2026-09-19 23:27:00 -07:00
Timothy J. Aveni 15c132a38b Compile complete local-first reader and scope fixtures 2026-09-19 22:42:29 -07:00
Timothy J. Aveni 4c0c404a39 Add explicit CRDT edit contracts and structured query values 2026-09-19 22:33:20 -07:00
Timothy J. Aveni 0555c929b1 Add factory creation ports and owner initialization contracts 2026-09-19 22:20:55 -07:00
Timothy J. Aveni 03ac7f8557 Add local-first execution contracts to the capability language 2026-09-19 22:09:40 -07:00
Timothy J. Aveni 757531bb63 Add activated workspace reflection and Studio definition watches
Expose bounded snapshot-bound definition reads and latest-only watches from the serving checked bundle. Add checked reflection dependency ports, generated SDK bindings and selector-scoped invocation authority.

Move Studio factory, contract and query discovery to public reflection inventories. Share an active-definition watch across canvas and standalone routes and invalidate/reload on epoch changes instead of polling GetWorkspace.

Document the API and deferred scope; cover compiler/codegen, SDK injection, transport authorization, activation publication, pagination and reconnect behavior. Keep all work local; no deployment or publication.
2026-09-19 10:18:47 -07:00
Timothy J. Aveni 1aa55f7a35 Add composable interface operation forwarding
Support bind member.operation to interface Target<Args>.member.operation.
Check exact signatures and scopes, same-receiver conformances, cycles and
watch pairing. Resolve chains for native queries, editing and dispatch while
retaining the terminal conformance owner and injected package dependencies.
Track forwarded providers in dependency and semantic-major review machinery.

Cover native get/set/watch chains, generic targets, invalid bindings, package
forwarding, runtime owner authority and watch cancellation. Document the
language feature and keep Studio search adoption explicitly pending.
No push or deployment.
2026-09-19 10:18:47 -07:00
Timothy J. Aveni 2594403503 Add native literal substring query predicates
Implement checked containsInsensitive string predicates in GraphQL, protobuf,
native SQL and bounded residual evaluation. ASCII folding is deterministic;
SQL pattern characters remain literal. Retain deferred full-text index design.

Studio migration awaits a decision on checked native getter forwarding across
conformances; do not duplicate titles or bypass private state ownership.
Compiler and local full-stack substring-query acceptance pass.
2026-09-19 10:18:47 -07:00
Timothy J. Aveni 5634d3ee78 Add checked workspace-interface query roots
Implement scoped interface extents across QX, generated bindings, SDK,
Orch, Camino and browser queries. Preserve native projection, aggregation,
pagination and watch invalidation without synthetic persisted objects.
Validate exact query grants and scope freshness on execution and replay.
Add local PostgreSQL, transport, browser and code-generation coverage.

C1 implementation slice only: full-text indexes and Studio search adoption
(C2/C3) remain tracked in the implementation handoff. No publication or deployment.
2026-09-19 10:18:47 -07:00
Timothy J. Aveni 0810775c07 Give query fields explicit read ownership and detachment
Separate shared optimistic values from query-owned and independent read lifetimes. Add opaque generated field handles, explicit readonly-safe detachment, owner status, query refresh routing, and atomic hydration validation. Preserve replay fencing and native/CRDT writes without automatic leaf fallback.

Regenerate first-party bindings, update authoring guidance, and test browser replay, StrictMode, transport recovery and independent lifetimes. Local only: interface search/indexing and activated reflection remain planned; remaining browser acceptance is recorded in the A-C-B handoff.
2026-09-19 10:18:47 -07:00
Quixos Subtree Publisher b0d31ba51c Publish quixos-protocol from c87835bff85794bc232dbaaddc1eb3b593c2aab8 2026-09-18 09:19:49 +00:00
Timothy J. Aveni 2dffdbcd9f Implement shared live fields and checked query hydration
Add native register acknowledgments and idempotent mutation replay, shared optimistic field controllers, overlapping custom setters, non-suspending hooks and explicit Suspense. Carry checked @live provenance through batched queries and hydrate shared browser fields with coverage leases. Update tracker/scaffolds/guides and verify compiler, PostgreSQL, browser and immutable workspace paths.
2026-09-18 02:19:49 -07:00
Quixos Subtree Publisher c9265e7903 Publish quixos-protocol from 4c88728f144add3457b4273d4b9a04ee53c53be3 2026-09-18 06:40:14 +00:00
Timothy J. Aveni a05f58f7fe Build query-backed task tracker default template
Add shared colored tags, native tag/status filters, cursor-paged task and
completion views, estimated-hour aggregates and repeat-after-completion actions.
Keep task views independently renderable and isolate component styling.

Cover immutable fresh-template/additive checks, real PostgreSQL/orch package
actions and query watches, and wide/narrow browser layouts. Accept documented
hyphenated query budget names in the capability lexer.

Template publication/default selection pending installation AWS SSO renewal;
no live workspace data or deployment configuration changed.
2026-09-17 23:40:14 -07:00
Quixos Subtree Publisher 93c8cae1e6 Publish quixos-protocol from d6b0ff39db819c339a08e2203eff401c86fa9b73 2026-09-18 02:52:55 +00:00
Timothy J. Aveni 5a611bbc9b Enforce aggregation contract budgets and expose residual timing 2026-09-17 19:52:55 -07:00
Quixos Subtree Publisher aadc35581d Publish quixos-protocol from 1c2dbc6ca2c3917292f158cbbaef531836eaacca 2026-09-18 02:43:46 +00:00
Timothy J. Aveni e61b0a36ac Implement checked aggregation plans, native SQL, and bounded RPC capture 2026-09-17 19:43:46 -07:00
Quixos Subtree Publisher ae0a5064a4 Publish quixos-protocol from 53b9a56e8cb120c34d6b5c85b52b18080dd1d9a3 2026-09-17 23:49:44 +00:00
Timothy J. Aveni cd13120937 Implement query execution, scoped RPC enrichment, live collections, and scaffold integration 2026-09-17 16:49:44 -07:00
Timothy J. Aveni 61a410f98f Add queryable capability contracts and checked GraphQL artifacts 2026-09-17 14:55:10 -07:00
Timothy J. Aveni e3501772c5 Add exact runtime conformance lookup and agent-visible language limits
Add authorized exact-contract lookup, revision-fenced views, generated typed package/browser descriptors, and lifecycle-safe React discovery. Preserve closed generic contracts without competing-conformance policies or new authority grants. Document supported workarounds and privacy boundaries for agent feedback. Publish matching protocol/SDK exports and update scaffold/starter pins; leave the deployed template unchanged.
2026-09-17 12:04:06 -07:00
Quixos Subtree Publisher 7863ce3e12 Publish quixos-protocol from db6d7990a4ffec273258ac313b7d2aeac57b261a 2026-09-17 18:54:46 +00:00
Timothy J. Aveni ea51ca89e7 Add exact runtime conformance lookup and agent-visible language limits 2026-09-17 11:54:46 -07:00
Quixos Subtree Publisher c3fe7c86b8 Publish quixos-protocol from a45fd9a85b541ce6b449b28e8b47954bf04de96b 2026-09-17 18:49:38 +00:00
Timothy J. Aveni 74f00fe28f Add exact runtime conformance lookup and agent-visible language limits 2026-09-17 11:49:38 -07:00
Quixos Subtree Publisher eaf6203b73 Publish quixos-protocol from 0355d845d7ad4b797343b681e9b5e1a77c63c720 2026-09-17 18:47:28 +00:00
Timothy J. Aveni 17f6941f64 Add exact runtime conformance lookup and agent-visible language limits 2026-09-17 11:47:28 -07:00
Timothy J. Aveni 59735c5e38 Add checked React field bindings and Web Studio factories
Replace opaque props with checked generic presentation contracts and lazy typed
interface references. Generate readonly/writable field APIs and component checks.

Preserve CRDT editing through explicit resolved getter/setter contracts, binding-
fenced delta RPCs, native watches and replica-aware field adapters. Custom setters
retain semantic writes; storage snapshots never grant write authority. Cover
concurrent edits, lost acknowledgements, readonly contracts and authorization.

Add receiver-free static factory dispatch, state-field binding shorthand, and
conformance-based creation. Migrate TODO, editable scaffolds and authoring guides.
Verify language/codegen, SDK, RPC, browser lifecycle, local scaffolds, production
browser bundling and CRDT persistence with temporary PostgreSQL.
2026-09-16 16:49:09 -07:00
Quixos Subtree Publisher 499b65b274 Publish quixos-protocol from 1de28b236de076d239752b77553f833dfbdb5b43 2026-09-16 23:49:09 +00:00
Timothy J. Aveni 52803dda05 Implement capability generics, checked package specializations and CRUD scaffolding
Add kinded parameters, capability bounds, Self, aliases and closed application identities. Check generic implementations universally and build candidate-specific codecs and descriptors from immutable schemas. Preserve lexical aliases and exact dispatch identities in package and host bindings.

Add an imperative CRUD+index domain scaffold with explicit soft-deletion semantics, source/codegen regression coverage, installed CLI tests and an authoring guide. Existing Web Studio opaque props and class-level create-menu migration are separate from the implemented language core.
2026-09-16 10:42:12 -07:00
Timothy J. Aveni 9358b5ed0e Revert generated-file renaming; retain formatter exclusions 2026-09-15 20:13:10 -07:00
Timothy J. Aveni e557a057d8 Mark first-party generated files with .gen; preserve third-party conventions 2026-09-15 17:57:46 -07:00
Timothy J. Aveni 1afc9feb94 Name optional component error notifications extraErrorAction 2026-09-15 17:31:17 -07:00
Timothy J. Aveni 8f28ab1b93 Add lightweight correctness linting and resolve formatting conflicts 2026-09-15 15:41:24 -07:00
Timothy J. Aveni 00fc2b9ff1 Format authored monorepo code with pinned language formatters 2026-09-15 15:41:24 -07:00
Timothy J. Aveni a174faea5c Add imperative feature bundles, intrinsic component sizing, and authoring timing logs
Generate state-backed atoms, interfaces, relationships and React/CSS packages
in a resumable command that prints the full source diff. Keep scaffold output
ordinary editable code. Shorten installed authoring guides while preserving
contracts and clarify historical design context.

Test generated bundles through immutable Nix verification and cover browser
isolation, nested sizing, interruption recovery, and command help.
2026-09-15 14:20:02 -07:00
Timothy J. Aveni 53708ac06f Provide typed React live-field authoring, strict CLI arguments, and timed build stages 2026-09-14 23:47:05 -07:00
Timothy J. Aveni a84c985256 Fix standalone candidate binding options and test real React props contracts 2026-09-14 22:11:36 -07:00
Timothy J. Aveni 6c2f030243 Simplify workspace authoring and isolate managed component styles
Use imperative scaffolds without a registry/refresh lifecycle; preserve authored runtime wiring and generate checked descriptors. Consolidate binding options, embed checked TSX/CSS source, queue mutable convergence while allowing parallel immutable checks, and preload real CLI observations. Add Shadow DOM boundaries with scoped overlays and composed-event handling. Update template/toolchain pins and document VM-free authoring, browser, and stateful acceptance; no deployment or default-template selection.
2026-09-14 20:55:32 -07:00
Quixos Subtree Publisher 0d5220a6ce Publish quixos-protocol from 8484d99fcf78a49341a17597bc62211e5f1807dd 2026-09-15 03:49:38 +00:00
Timothy J. Aveni e753ffe601 Simplify workspace authoring and isolate managed component styles 2026-09-14 20:49:38 -07:00
Quixos Subtree Publisher 332b84f258 Publish quixos-protocol from fc13e9b8411c2210b12db5e558e2b4fa7785505e 2026-09-15 03:42:37 +00:00
Timothy J. Aveni 4982cb7e9f Simplify workspace authoring and isolate managed component styles 2026-09-14 20:42:37 -07:00
Timothy J. Aveni 14b0ef25c3 Repair workspace authoring: typed RPC inputs, automatic preservation, and template releases
Use checked input contracts and protobuf JSON for CLI roundtrips; generate Web Studio platform inputs; reject invalid constructors before allocation and filter Createable eligibility.

Preserve compatible storage without no-op migrations, report activation readiness, and validate migration coverage before maintenance. Follow authored package declarations during scaffold refresh and queue source capture.

Add a real local TODO check/activate/create/place/edit acceptance, publish updated protocol and SDK dependencies, and make verified template default selection explicit.
2026-09-14 16:04:59 -07:00
Quixos Subtree Publisher c16271510a Publish quixos-protocol from 0023a184c839088480702512751ebb05bf170162 2026-09-14 22:54:30 +00:00
Timothy J. Aveni 8aac091f6c Make workspace authoring transitions and typed invocation coherent 2026-09-14 15:54:30 -07:00
Timothy J. Aveni 01ca965c7f Make workspace authoring converge through immutable Nix candidates
Coordinate registered resource edits bottom-up into retained exact remote sources.
Use one Nix-owned source graph for provisional checking, template publication,
explicit baseline upgrades and host activation; retain independent runtime pins.

Add scoped contract inspection, historical recovery, derived worklists, crash-safe
locks, named dependency adoption and plain-QX structural editing. Repair TODO
ownership and template instantiation, and document the supported agent workflow.

Validated with protocol and command suites, real jj/Nix convergence and cache
checks, TS/React installed-command acceptance, and fresh TODO first-edit acceptance.
No live deployment or public publication performed. Props projection generation
and a one-command rich feature generator remain explicitly outside this delivery.
2026-09-14 12:25:47 -07:00
Timothy J. Aveni fae4e48f72 Unify workspace authoring, verification and scaffolding workflows
Use exact jj snapshots and one candidate-bound Nix builder for incremental checks, template validation and activation. Keep provenance internal and separate recovery checkpoint failures from local command success.

Provision workspace-scoped managed package/interface repositories with recoverable Central effects. Add TypeScript/React presets, function and dependency commands, and scaffold enrollment for all TODO packages. Install authoring guides and controlled Codex sandbox rules.

Invalidate module resolutions across cutover, including in-flight races, and content-address host platform entries. Strengthen domain-model and verification instructions.

Validated real jj/Nix authoring, React/Slate dependency installation, bottom-up local Git publication, packaged CLI tests, PostgreSQL recovery/auth tests, Web Studio tests and host configuration. Public protocol/helpers and the validated 19-resource TODO template are published. Retained the approved exact private baseline and updated the installation's default template pin to 68d54f0d52be433ebf60bdc1faf7646c57f90307. Master and live deployments remain unchanged. See docs/WORKSPACE_AUTHORING_PROGRESS.md.
2026-09-13 23:06:38 -07:00
Quixos Subtree Publisher 6ecf565549 Publish quixos-protocol from 7fd5e15105cef21d2b1c3da860c53c5033f66256 2026-09-14 05:46:08 +00:00
Timothy J. Aveni 73cdeadc2c Unify workspace authoring, verification and scaffolding workflows 2026-09-13 22:46:08 -07:00
Timothy J. Aveni 16b28f1bc4 Migrate TODO implementations to generated candidate bindings 2026-09-13 20:28:50 -07:00
Quixos Subtree Publisher dcef58d6c5 Publish quixos-protocol from 22bb3d02264980d74de65c34bbbcb81764dc65c0 2026-09-14 03:28:50 +00:00
Quixos Subtree Publisher f7fcc7d08b Publish quixos-protocol from e25eee6ce4f13702b2454a9354bc79a29eb2e4a1 2026-09-14 03:01:53 +00:00
Timothy J. Aveni 295181244f Migrate TODO implementations to generated candidate bindings 2026-09-13 20:01:53 -07:00
Timothy J. Aveni 42af87f8bd Fix supervised credentials and declare typed RPC records for canvas placement 2026-09-13 16:38:55 -07:00
Quixos Subtree Publisher e9fb42dce4 Publish quixos-protocol from 61cb9aa5a580c2846d426464228c7d1082b7021a 2026-09-13 23:38:55 +00:00
Quixos Subtree Publisher 9867bf4552 Publish quixos-protocol from 0e6bb95d903004916bd93670497ab310a076a28b 2026-09-13 19:34:15 +00:00
Timothy J. Aveni 483bc68a94 Implement workspace evolution, migrations, and runtime continuity
Enable evolution by default for source-backed workspaces. Add stable
conformance ownership, semantic-major review, candidate typechecking,
and durable fenced cutover with explicit migrations and forward recovery.

Independently supervise package runtimes so unchanged resource owners keep
their processes and connections across cutover. Add scoped invocation
authority, resource sessions, and typed callback rebinding.

Wire opaque object references through generated bindings and RPCs. Add
canonical relationship sets, keyed maps, and ordered lists with scoped
transactional mutations, revision checks, and inverse consistency. Support
planned cascade deletion, protection, tombstones, and lifecycle foundations.

Add journaled structural edits, package/function/migration scaffolding,
managed repository creation, and resumable bottom-up dependency pin
publication. Document lifetime boundaries, revision pinning, prototype
compatibility policy, commands, and deferred work.

Validate with 210 tests, user-systemd process/connection continuity,
generated-package TypeScript checks, and Nix host/protocol checks.
TTL handoff, physical reclamation, general multi-step migrations, and
root-systemd migration isolation acceptance remain deferred.
2026-09-10 18:29:25 -07:00
Timothy J. Aveni 1e25f391e7 Browse immutable workspace source and jj history in Central 2026-09-10 00:51:26 -07:00
Timothy J. Aveni 4e17693d82 Generate typed QX bindings and add source editing tools 2026-09-08 15:42:20 -07:00
Timothy J. Aveni ce793cc54f Build workspace agent, capability graph, and versioned cutovers 2026-09-06 11:12:04 -07:00
Quixos Subtree Publisher fd6fee133d Publish quixos-protocol from b384206b9c01a9ac50030a583d028b71291bc8c5 2026-09-06 07:07:06 +00:00
Timothy J. Aveni f5ced64533 Build workspace agent, capability graph, and versioned cutovers 2026-09-06 00:07:06 -07:00
Quixos Subtree Publisher eabf721e94 Publish quixos-protocol from 33266c4ba9e0dd7b6370715522b281807e8b038b 2026-09-06 00:49:07 +00:00
Timothy J. Aveni 0c46850973 Add the workspace coding agent, launch gate, and composable locks
- run a singleton Codex App Server adapter in each dev workspace
- gate Web Studio with a reloadable fragment-token to HttpOnly-session exchange
- add ChatGPT device login and streamed chat to Web Studio
- isolate authoring access in a declarative NixOS service account
- split Web Studio canvas, object, panel, and model concerns
- check generated Quixos protocol clients for drift
- resolve same-repository quixos.lock fragments deterministically
- preserve the collaborative canvas gesture fixes
2026-09-05 17:49:07 -07:00
Timothy J. Aveni c4d42a0ac5 Use the client runtime for canvas state 2026-09-05 12:11:30 -07:00
Quixos Subtree Publisher fd6160ec67 Publish quixos-protocol from 3ea7dcc6a38ba0c5ddc099b966d48c5aeae732b1 2026-09-05 04:04:43 +00:00
Timothy J. Aveni fd039f095b Introduce persisted Web Studio component capabilities 2026-09-04 21:04:43 -07:00
Timothy J. Aveni f4987093f6 Introduce repository-backed capability workspaces
- define and validate the capability and resource-lock languages
- provision workspace source repositories through Central Gitea
- build package runtimes from pinned standalone sources
- replace legacy schema compilation with workspace persistence plans
- add stable optimistic registers and Automerge CRDT documents
- modernize TypeScript/Nix package builds and runtime activation readiness
2026-09-04 19:08:10 -07:00
332 changed files with 52465 additions and 3771 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
{
"version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git",
"sourceCommit": "4af89fa7c703d9fecf36b4e97b76658a503dd5aa",
"sourceRepo": "https://gitea.internal.quixos.org/quixos/quixos.git",
"sourceCommit": "473886a58fdacf15f2c873b77da96faf3be7fa65",
"sourcePath": "quixos-protocol",
"exportName": "quixos-protocol",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
"mirrorRemote": "https://repos.quixos.org/quixos-toolchain/quixos-protocol.git"
}
+89 -15
View File
@@ -1,8 +1,8 @@
# Quixos protocol and capability compiler
This package owns the shared protobuf APIs for Camino, `quixos-orch`, package
runtimes, package descriptors, and generic runtime values. It also owns the v1
capability authoring language and semantic compiler.
Shared protobuf APIs for Camino, orch, package runtimes/descriptors and values,
plus the capability language/compiler. Commands below are for backend development;
workspace authors use `qx-workspace check` for all verification.
## Capability language
@@ -13,34 +13,107 @@ on parse-tree shapes or declaration order.
```sh
nix develop
quixos-capability-compile ../quixos-instance/workspaces/todo.capabilities.qx
quixos-workspace-compile \
--root ../quixos-instance/workspaces/todo \
--checkout-root /tmp/quixos-resolved-resources
```
The compiler validates exact identities, interface operation coverage,
`workspace.qx` contains workspace-local atoms, attachments, conformances, and
constructor bindings. Each imported interface has an `interface.qx` in its own
repository; each imported package similarly has a `package.qx`. Source
locations never appear in those declarations. `quixos.lock` supplies their
exact Git repositories and commits, and the workspace compiler resolves that
dependency graph recursively.
Inside an interface or package manifest, `import interface Named;` is a true
source dependency: the repository lock must pin it, and tooling may use the
full contract for generated types. `external atom` and `external interface`
declare nominal identities which the final importing workspace must provide.
The latter is appropriate for relationship targets and other opaque
references; it deliberately does not grant the contract needed for an
interface invocation port. This distinction permits mutually referential
interface identities without creating a cycle in the Git Merkle graph.
The compiler validates exact identities, recursive lock/source agreement,
external requirement satisfaction, interface operation coverage,
attachment ownership, native state/edge providers, package receiver and
dependency-port requirements, constructors, and the exact runtime closure.
Generics, interface composition, declarative forwarding, automatic
relationship materialization, and first-class bundles are intentionally absent
from v1.
Local workspace fragments, source-editing APIs, and generated package contracts
are documented in [QX bindings and tooling](../docs/QX_BINDINGS_AND_TOOLING.md).
`quixos-resource-compile --schema-out` exports the portable generator input;
`quixos-codegen-ts` is the first backend. `quixos-qx` provides parse, lint,
format, and nominal-atom scaffold commands.
## Repository locks
Every workspace, interface, and package repository carries a `quixos.lock`.
It pins the exact Quixos toolchain revision plus exact Git sources for imported
interfaces and packages. The v1 Git source is deliberately only a repository
URL and full commit ID; the publisher-owned reachability tag and Nix fetch
details are derived from those values.
A resource lock is not a miniature workspace: it contains only the exact
Quixos commit that resource was authored against plus its dependency list.
It also pins exact Git sources for interfaces and packages named by that
resource's own manifest. Resource Git sources are deliberately only a
repository URL and full commit ID; the publisher-owned reachability tag and
Nix fetch details are derived from those values.
A workspace root additionally records its Quixos selection policy and ref.
Its `commit` is the exact compatibility baseline shared by the resource graph,
not necessarily the runtime candidate. `pinned` requires the ref, baseline,
and candidate to be one commit. `track-development` lets Central resolve the
named development ref to a newer candidate while preserving the authored
baseline. `track-release` is reserved for compatibility-filtered release-line
advancement and is not implemented yet.
```sh
quixos-lock-check quixos.lock
qx resource publish
quixos-resource-compile \
--root . --kind package \
--repository https://repos.quixos.org/example/package-example.git \
--commit 1111111111111111111111111111111111111111 \
--checkout-root /tmp/quixos-resource-dependencies
qx-workspace resource publish
```
The second command runs from a jj checkout. It validates the lock, snapshots
the current working-copy commit, and pushes the immutable tag
The resource compiler validates the local manifest against the recursively
resolved locks without turning the resource into a workspace. The final
command runs from a jj checkout. It performs that same recursive compilation,
snapshots the current working-copy commit, and pushes the immutable tag
`refs/tags/quixos-reachability/<commit>` without advancing an authoring
bookmark.
A workspace root may split its resource pins into same-repository fragments:
```text
quixos-lock version 1 {
quixos source {
repository "https://repos.quixos.org/quixos/quixos.git";
policy track-development;
ref "dev/alice/main";
commit "1111111111111111111111111111111111111111";
}
import "locks/web-studio.lock";
}
```
```text
quixos-lock fragment version 1 {
package CanvasRuntime source {
repository "https://repos.quixos.org/org-quixos-web-studio/package-canvas-runtime.git";
commit "2222222222222222222222222222222222222222";
}
}
```
Import paths are normalized, repository-root-relative paths. Absolute paths,
URLs, traversal, empty segments, directories, and symbolic links are rejected.
Fragments may import other fragments; cycles fail, shared fragments are loaded
once, and duplicate resource bindings fail across the flattened closure.
`quixos-lock-check` resolves the complete closure and reports its `sourceFiles`
alongside the flattened resources. The root Git commit content-addresses every
fragment, so fragments do not become independent repositories or identities.
## Package descriptors
Executable package metadata remains protobuf text format:
@@ -59,6 +132,7 @@ their exact dependency ports.
nix develop -c yarn test
```
The suite covers parsing/diagnostics, source-order independence, ordinary call
operations, semantic validation, exact closure/tree-shaking, private and shared
attachments, package port injection, and constructors.
The suite covers parsing/diagnostics, recursive repository assembly, external
requirement validation, source-order independence, ordinary call operations,
semantic validation, exact closure/tree-shaking, private and shared
attachments, related-object interface-port injection, and constructors.
+159 -61
View File
@@ -6,75 +6,172 @@
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
outputs =
{
self,
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
nodejs = pkgs.nodejs_24;
quixos-protocol = (pkgs.callPackage ./yarn-project.nix { inherit nodejs; }) {
src = pkgs.lib.cleanSource ./.;
overrideAttrs = old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
pkgs.esbuild
pkgs.protobuf
];
buildPhase = ''
runHook preBuild
export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:$PWD/proto''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}"
patchShebangs node_modules/.bin node_modules/@bufbuild/protoc-gen-es/bin
yarn build
esbuild dist/src/descriptor-check.js \
--bundle --platform=node --target=node24 --format=esm \
--outfile=quixos-descriptor-check.mjs
esbuild dist/src/capability-language/cli.js \
--bundle --platform=node --target=node24 --format=esm \
--outfile=quixos-capability-compile.mjs
esbuild dist/src/resource-lock/cli.js \
--bundle --platform=node --target=node24 --format=esm \
--outfile=quixos-lock-check.mjs
runHook postBuild
'';
doCheck = true;
checkPhase = ''
runHook preCheck
yarn test
runHook postCheck
'';
installPhase = ''
runHook preInstall
mkdir -p "$out"
cp --reflink=auto --recursive grammar "$out/grammar"
cp --reflink=auto --recursive proto "$out/proto"
cp --reflink=auto --recursive dist "$out/dist"
cp package.json "$out/package.json"
mkdir -p "$out/bin"
cat > "$out/bin/quixos-descriptor-check" <<EOF
#!/bin/sh
export PATH="${pkgs.protobuf}/bin:\$PATH"
export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:$out/proto\''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}"
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-descriptor-check.mjs" "\$@"
EOF
chmod +x "$out/bin/quixos-descriptor-check"
cat > "$out/bin/quixos-capability-compile" <<EOF
#!/bin/sh
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-capability-compile.mjs" "\$@"
EOF
chmod +x "$out/bin/quixos-capability-compile"
cat > "$out/bin/quixos-lock-check" <<EOF
#!/bin/sh
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-lock-check.mjs" "\$@"
EOF
chmod +x "$out/bin/quixos-lock-check"
mkdir -p "$out/libexec/quixos-protocol"
install -m644 quixos-descriptor-check.mjs "$out/libexec/quixos-protocol/quixos-descriptor-check.mjs"
install -m644 quixos-capability-compile.mjs "$out/libexec/quixos-protocol/quixos-capability-compile.mjs"
install -m644 quixos-lock-check.mjs "$out/libexec/quixos-protocol/quixos-lock-check.mjs"
runHook postInstall
'';
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
pkgs.diffutils
pkgs.esbuild
pkgs.protobuf
pkgs.git
pkgs.jujutsu
pkgs.gnutar
pkgs.util-linux
pkgs.nix
];
buildPhase = ''
runHook preBuild
mkdir -p "$TMPDIR/generated-before"
cp --recursive src/gen "$TMPDIR/generated-before/proto"
cp --recursive src/capability-language/generated "$TMPDIR/generated-before/capability"
cp --recursive src/resource-lock/generated "$TMPDIR/generated-before/lock"
export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:$PWD/proto''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}"
patchShebangs node_modules/.bin node_modules/@bufbuild/protoc-gen-es/bin
yarn build
esbuild dist/src/execution-world/index.js --bundle --platform=node --target=node24 --format=esm --outfile=execution-world.mjs
esbuild dist/src/react-build/package-queries.js --bundle --platform=node --target=node24 --format=esm --outfile=react-query-generator.mjs
esbuild dist/src/bindings/client.js --bundle --platform=node --target=node24 --format=esm --outfile=client-codegen.mjs
diff --recursive --unified "$TMPDIR/generated-before/proto" src/gen
diff --recursive --unified "$TMPDIR/generated-before/capability" src/capability-language/generated
diff --recursive --unified "$TMPDIR/generated-before/lock" src/resource-lock/generated
esbuild dist/src/descriptor-check.js \
--bundle --platform=node --target=node24 --format=esm \
--outfile=quixos-descriptor-check.mjs
esbuild dist/src/capability-language/cli.js \
--bundle --platform=node --target=node24 --format=esm \
--outfile=quixos-capability-compile.mjs
esbuild dist/src/capability-language/workspace-cli.js \
--bundle --platform=node --target=node24 --format=esm \
--outfile=quixos-workspace-compile.mjs
esbuild dist/src/capability-language/resource-cli.js \
--bundle --platform=node --target=node24 --format=esm \
--outfile=quixos-resource-compile.mjs
esbuild dist/src/resource-lock/cli.js \
--bundle --platform=node --target=node24 --format=esm \
--outfile=quixos-lock-check.mjs
esbuild dist/src/bindings/cli.js --bundle --platform=node --target=node24 --format=esm --outfile=quixos-codegen-ts.mjs
esbuild dist/src/bindings/portable-client-cli.js --bundle --platform=node --target=node24 --format=esm --outfile=quixos-codegen-client.mjs
esbuild dist/src/bindings/react-platform.js --bundle --platform=node --target=node24 --format=esm --outfile=react-platform.mjs
esbuild dist/src/capability-language/tool-cli.js --bundle --platform=node --target=node24 --format=esm --outfile=quixos-qx.mjs
esbuild dist/src/capability-language/source-check-driver.js --bundle --platform=node --target=node24 --format=esm --outfile=source-check-driver.mjs
runHook postBuild
'';
doCheck = true;
checkPhase = ''
runHook preCheck
yarn test
node dist/test/emit-portable-reader.js
node dist/test/emit-managed.js
runHook postCheck
'';
installPhase = ''
runHook preInstall
mkdir -p "$out"
install -Dm644 nix/checked-package.nix "$out/share/checked-package.nix"
install -Dm644 nix/checked-artifacts.nix "$out/share/checked-artifacts.nix"
install -Dm644 nix/package-context.nix "$out/share/package-context.nix"
install -Dm644 nix/lock-source.nix "$out/share/lock-source.nix"
install -Dm644 nix/package-context.mjs "$out/share/package-context.mjs"
install -Dm644 nix/check-package.mjs "$out/share/check-package.mjs"
install -Dm644 nix/check-execution.mjs "$out/share/check-execution.mjs"
substitute nix/checked-candidate.nix "$out/share/checked-candidate.nix" \
--replace-fail '@nixpkgs@' '${pkgs.path}' \
--replace-fail '@nixpkgsNarHash@' '${nixpkgs.narHash}'
cp --reflink=auto --recursive grammar "$out/grammar"
cp --reflink=auto --recursive proto "$out/proto"
cp --reflink=auto --recursive dist "$out/dist"
cp package.json "$out/package.json"
mkdir -p "$out/share/quixos-protocol/qualification"
cp -r dist/portable-reader dist/managed "$out/share/quixos-protocol/qualification/"
mkdir -p "$out/bin"
for tool in quixos-codegen-ts quixos-codegen-client quixos-qx; do
printf '#!/bin/sh\nexec ${pkgs.nodejs_24}/bin/node "%s/libexec/quixos-protocol/%s.mjs" "$@"\n' "$out" "$tool" > "$out/bin/$tool"
chmod +x "$out/bin/$tool"
done
cat > "$out/bin/quixos-descriptor-check" <<EOF
#!/bin/sh
export PATH="${pkgs.protobuf}/bin:\$PATH"
export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:$out/proto\''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}"
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-descriptor-check.mjs" "\$@"
EOF
chmod +x "$out/bin/quixos-descriptor-check"
cat > "$out/bin/quixos-capability-compile" <<EOF
#!/bin/sh
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-capability-compile.mjs" "\$@"
EOF
chmod +x "$out/bin/quixos-capability-compile"
cat > "$out/bin/quixos-workspace-compile" <<EOF
#!/bin/sh
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-workspace-compile.mjs" "\$@"
EOF
chmod +x "$out/bin/quixos-workspace-compile"
cat > "$out/bin/quixos-resource-compile" <<EOF
#!/bin/sh
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-resource-compile.mjs" "\$@"
EOF
chmod +x "$out/bin/quixos-resource-compile"
cat > "$out/bin/quixos-lock-check" <<EOF
#!/bin/sh
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-lock-check.mjs" "\$@"
EOF
chmod +x "$out/bin/quixos-lock-check"
mkdir -p "$out/libexec/quixos-protocol"
install -m644 execution-world.mjs "$out/libexec/quixos-protocol/execution-world.mjs"
install -m644 react-query-generator.mjs "$out/libexec/quixos-protocol/react-query-generator.mjs"
install -m644 client-codegen.mjs "$out/libexec/quixos-protocol/client-codegen.mjs"
install -m644 react-platform.mjs "$out/libexec/quixos-protocol/react-platform.mjs"
install -m644 quixos-codegen-ts.mjs quixos-qx.mjs "$out/libexec/quixos-protocol/"
install -m644 source-check-driver.mjs "$out/libexec/quixos-protocol/"
install -m644 quixos-codegen-client.mjs "$out/libexec/quixos-protocol/"
install -m644 quixos-descriptor-check.mjs "$out/libexec/quixos-protocol/quixos-descriptor-check.mjs"
install -m644 quixos-capability-compile.mjs "$out/libexec/quixos-protocol/quixos-capability-compile.mjs"
install -m644 quixos-workspace-compile.mjs "$out/libexec/quixos-protocol/quixos-workspace-compile.mjs"
install -m644 quixos-resource-compile.mjs "$out/libexec/quixos-protocol/quixos-resource-compile.mjs"
install -m644 quixos-lock-check.mjs "$out/libexec/quixos-protocol/quixos-lock-check.mjs"
runHook postInstall
'';
};
};
in {
in
{
packages.default = quixos-protocol;
packages.inspector = (pkgs.callPackage ./yarn-project.nix { inherit nodejs; }) {
src = pkgs.lib.fileset.toSource {
root = ./.;
fileset = pkgs.lib.fileset.unions [
./package.json
./yarn.lock
./.yarnrc.yml
./.yarn/plugins
./src
];
};
overrideAttrs = old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ pkgs.esbuild ];
doCheck = false;
buildPhase = ''
esbuild src/capability-language/inspect-cli.ts --bundle --platform=node --target=node24 --format=esm --outfile=inspect.mjs
'';
installPhase = ''
mkdir -p "$out/bin" "$out/lib"
cp inspect.mjs "$out/lib/inspect.mjs"
printf '#!${pkgs.runtimeShell}\nexec ${nodejs}/bin/node --max-old-space-size=128 %s/lib/inspect.mjs\n' "$out" > "$out/bin/quixos-qx-inspect"
chmod +x "$out/bin/quixos-qx-inspect"
'';
};
};
checks.default = quixos-protocol;
devShells.default = pkgs.mkShell {
packages = [
@@ -94,5 +191,6 @@ EOF
export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:$PWD/proto''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}"
'';
};
});
}
);
}
+340 -45
View File
@@ -2,6 +2,17 @@ grammar QuixosCapability;
document
: workspaceDecl EOF
| interfaceResourceDecl EOF
| packageResourceDecl EOF
| fragmentDecl EOF
;
fragmentDecl
: FRAGMENT LBRACE workspaceItem* RBRACE
;
sourceImportDecl
: IMPORT stringLiteral SEMI
;
workspaceDecl
@@ -10,71 +21,132 @@ workspaceDecl
;
workspaceItem
: atomDecl
| interfaceDecl
| packageDecl
: sourceImportDecl
| atomDecl
| resourceImportDecl
| sharedAttachmentDecl
| conformanceDecl
| constructorBindingDecl
| presentationDecl
| typeAliasDecl
;
resourceImportDecl
: IMPORT INTERFACE identifier SEMI
| IMPORT PACKAGE identifier SEMI
;
externalAtomDecl
: EXTERNAL ATOM identifier ID stringLiteral SEMI
;
externalInterfaceDecl
: EXTERNAL INTERFACE identifier REVISION stringLiteral SEMI
;
symbolDecl
: SYMBOL identifier ID stringLiteral SEMI
;
resourcePreamble
: resourceImportDecl
| externalAtomDecl
| externalInterfaceDecl
| symbolDecl
| typeAliasDecl
;
atomDecl
: ATOM identifier ID stringLiteral (DOC stringLiteral)? SEMI
;
interfaceDecl
: INTERFACE identifier ID stringLiteral REVISION stringLiteral sourceBlock
interfaceResourceDecl
: resourcePreamble* INTERFACE identifier typeParameters? ID stringLiteral REVISION stringLiteral
(REQUIRES interfaceType (COMMA interfaceType)*)?
LBRACE interfaceMember* RBRACE
;
sourceBlock
: SOURCE LBRACE
REPOSITORY stringLiteral SEMI
COMMIT stringLiteral SEMI
RBRACE
typeParameters
: LT typeParameter (COMMA typeParameter)* GT
;
typeParameter
: VALUE identifier (COLON STORABLE)?
| OBJECT identifier (IMPLEMENTS interfaceType (AMP interfaceType)*)?
;
interfaceType
: identifier typeArguments?
;
typeArguments
: LT typeArgument (COMMA typeArgument)* GT
;
typeArgument
: ATOM identifier
| INTERFACE interfaceType
| OBJECT identifier
| valueType
;
typeAliasDecl
: TYPE identifier typeParameters? EQUAL valueType SEMI
;
interfaceMember
: valueMember
| relationshipMember
| operationMember
| moduleMember
;
moduleMember
: MODULE identifier ID stringLiteral CONTRACT stringLiteral (SUBJECT targetConstraint)? SEMI
;
operationMember
: OPERATION identifier ID stringLiteral COLON valueType ARROW valueType
LBRACE CALL ID stringLiteral SEMI RBRACE
: STATIC? OPERATION identifier ID stringLiteral COLON valueType ARROW valueType
LBRACE CALL ID stringLiteral executionClause? SEMI RBRACE
;
valueMember
: VALUE identifier ID stringLiteral COLON valueType
: queryReadContract? (VALUE | FIELD) identifier ID stringLiteral COLON valueType
LBRACE valueMemberOperation* RBRACE
;
valueMemberOperation
: GET ID stringLiteral SEMI
| SET ID stringLiteral SEMI
: GET ID stringLiteral executionClause? SEMI
| SET ID stringLiteral executionClause? SEMI
| EDIT ID stringLiteral executionClause? SEMI
| WATCH START ID stringLiteral STOP ID stringLiteral SEMI
;
relationshipMember
: RELATION identifier ID stringLiteral COLON cardinality targetConstraint ORDERED?
: queryReadContract? RELATION identifier ID stringLiteral COLON cardinality targetConstraint ORDERED? (KEYED stringLiteral)?
LBRACE relationshipOperation* RBRACE
;
queryReadContract
: QUERYABLE (RPC | PORTABLE)?
;
relationshipOperation
: RESOLVE ID stringLiteral SEMI
| CONNECT ID stringLiteral SEMI
| DISCONNECT ID stringLiteral SEMI
: RESOLVE ID stringLiteral executionClause? SEMI
| CONNECT ID stringLiteral executionClause? SEMI
| DISCONNECT ID stringLiteral executionClause? SEMI
| WATCH START ID stringLiteral STOP ID stringLiteral SEMI
;
targetConstraint
: ATOM identifier
| INTERFACE identifier
| INTERFACE identifier typeArguments?
| OBJECT identifier
;
packageDecl
: PACKAGE identifier ID stringLiteral REVISION stringLiteral sourceBlock
packageResourceDecl
: resourcePreamble* PACKAGE identifier ID stringLiteral REVISION stringLiteral
(SEMANTIC_MAJOR INTEGER)?
LBRACE packageExport* RBRACE
;
@@ -82,16 +154,69 @@ packageExport
: packageOperationExport
| packageFunctionExport
| packageConstructorExport
| packageQuery
| packageQuerySpecialization
| moduleExport
;
moduleExport
: MODULE identifier ID stringLiteral IMPLEMENTS interfaceType DOT identifier SEMI
;
presentationDecl
: PRESENTATION identifier ID stringLiteral FOR interfaceType USING identifier DOT identifier SEMI
;
packageQuery
: QUERY FRAGMENT? identifier typeParameters? ID stringLiteral ROOT WORKSPACE? interfaceType
DOCUMENT stringLiteral
(OPERATION stringLiteral LBRACE queryClause* RBRACE
| FRAGMENT stringLiteral (OPERATION stringLiteral)? (LBRACE queryClause* RBRACE | SEMI))
;
packageQuerySpecialization
: QUERY identifier ID stringLiteral SPECIALIZE identifier typeArguments SEMI
;
queryClause
: REPLICA SEMI
| LOCAL FROM identifier SEMI
| FRAGMENTS stringLiteral SEMI
| FRAGMENTS identifier DOT identifier (WITH LBRACE fragmentVariable* RBRACE)? SEMI
| VIEW OBJECT? identifier AS interfaceType SEMI
| MAX identifier INTEGER SEMI
| ALLOW interfaceType DOT identifier identifier stringLiteral SEMI
| WATCH SEMI
| POLL INTEGER stringLiteral SEMI
;
fragmentVariable : identifier TO identifier SEMI;
packageOperationExport
: OPERATION identifier ID stringLiteral COLON valueType ARROW valueType
MODE operationMode eventClause? RECEIVER receiverRequirement dependencyBlock? SEMI
: OPERATION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
MODE operationMode executionClause? eventClause? RECEIVER receiverRequirement dependencyBlock? preparationClause? acceptanceClause? SEMI
;
packageFunctionExport
: FUNCTION identifier ID stringLiteral COLON valueType ARROW valueType
dependencyBlock? SEMI
: FUNCTION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
executionClause? dependencyBlock? preparationClause? acceptanceClause? SEMI
;
acceptanceClause
: ACCEPT USING identifier
;
preparationClause
: PREPARES CREATION identifier USING stringLiteral
;
executionClause
: EXECUTION REMOTE
| EXECUTION (SNAPSHOT | TRANSACTION) (TARGETS LBRACK identifierList RBRACK)?
;
selectedOperations
: USING LBRACK memberOperationRef (COMMA memberOperationRef)* RBRACK
;
packageConstructorExport
@@ -114,7 +239,8 @@ operationMode
receiverRequirement
: ANY
| ATOM identifier
| INTERFACES LBRACK identifierList? RBRACK
| OBJECT identifier
| INTERFACES LBRACK (interfaceType (COMMA interfaceType)*)? RBRACK
;
identifierList
@@ -126,10 +252,13 @@ dependencyBlock
;
dependencyPort
: STATE identifier ID stringLiteral COLON valueType primitiveList SEMI
| EDGE identifier ID stringLiteral COLON cardinality targetConstraint primitiveList SEMI
| INTERFACE identifier ID stringLiteral COLON identifier SEMI
| CONSTRUCTOR identifier ID stringLiteral COLON identifier SEMI
: REFLECTION identifier ID stringLiteral LBRACK identifierList RBRACK SEMI
| STATE identifier ID stringLiteral COLON valueType primitiveList SEMI
| EDGE identifier ID stringLiteral COLON cardinality targetConstraint (KEYED stringLiteral)? primitiveList SEMI
| INTERFACE identifier ID stringLiteral COLON identifier typeArguments? selectedOperations? (FROM INPUT)? SEMI
| CREATION identifier ID stringLiteral COLON identifier INPUT valueType SEMI
| CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI
| QUERY identifier ID stringLiteral COLON identifier DOT identifier SEMI
;
primitiveList
@@ -139,6 +268,7 @@ primitiveList
primitive
: READ
| WRITE
| EDIT
| RESOLVE
| CONNECT
| DISCONNECT
@@ -161,8 +291,10 @@ stateDecl
;
storagePolicy
: OPTIMISTIC_REGISTER
: REGISTER
| CRDT LPAREN valueType RPAREN
| JOURNAL jsonObject
| SUBSCRIPTION LPAREN valueType RPAREN
;
edgeDecl
@@ -170,20 +302,55 @@ edgeDecl
;
edgeEndpoint
: targetConstraint PROJECTION identifier ID stringLiteral cardinality ORDERED? SEMI
: targetConstraint PROJECTION identifier ID stringLiteral cardinality ORDERED?
(ON_DELETE stringLiteral)? RETAIN_OTHER? (KEYED stringLiteral)? PUBLIC_TRAVERSAL? SEMI
;
conformanceDecl
: CONFORM identifier AS identifier LBRACE conformanceItem* RBRACE
: CONFORM typeParameters? identifier conformanceSourceArguments? AS identifier typeArguments? (ID stringLiteral)? (SEMANTIC_MAJOR INTEGER)?
LBRACE conformanceItem* RBRACE
;
conformanceSourceArguments
: typeArguments
;
conformanceItem
: PRIVATE attachmentDecl
| operationBindingDecl
| stateFieldBindingDecl
| initializationDecl
| relationshipMaterializationDecl
| moduleBindingDecl
;
moduleBindingDecl
: BIND identifier TO identifier DOT identifier SEMI
;
initializationDecl
: INITIALIZE LBRACE initializationField* RBRACE SEMI
;
initializationField
: identifier TO STATE identifier SEMI
| identifier TO EDGE identifier DOT identifier SEMI
;
creationOwnerMapping
: interfaceType FROM INPUT (DOT identifier)* SEMI
;
stateFieldBindingDecl
: BIND identifier TO STATE identifier SEMI
;
relationshipMaterializationDecl
: MATERIALIZE identifier IF ABSENT USING CONSTRUCTOR identifier VIA EDGE identifier DOT identifier SEMI
;
operationBindingDecl
: BIND memberOperationRef TO operationProvider SEMI
: BIND memberOperationRef TO operationProvider (QUERY_REASON stringLiteral)? SEMI
;
memberOperationRef
@@ -195,6 +362,7 @@ operationName
| CALL
| GET
| SET
| EDIT
| RESOLVE
| CONNECT
| DISCONNECT
@@ -205,14 +373,23 @@ operationName
;
operationProvider
: STATE identifier DOT statePrimitive
: LIFECYCLE DOT identifier
| JOURNAL identifier DOT identifier
| SUBSCRIPTION identifier DOT identifier
| STATE identifier DOT statePrimitive
| EDGE identifier DOT identifier DOT edgePrimitive
| PACKAGE identifier DOT identifier dependencyBindingBlock?
| INTERFACE identifier typeArguments? DOT memberOperationRef
| PACKAGE identifier DOT identifier typeArguments? dependencyBindingBlock? acceptanceBinding?
;
acceptanceBinding
: ACCEPT dependencyBindingBlock
;
statePrimitive
: READ
| WRITE
| EDIT
| WATCH_START
| WATCH_STOP
;
@@ -230,10 +407,13 @@ dependencyBindingBlock
;
dependencyBinding
: identifier TO STATE identifier SEMI
| identifier TO EDGE identifier DOT identifier SEMI
| identifier TO INTERFACE identifier SEMI
: identifier TO WORKSPACE REFLECTION LBRACK identifierList RBRACK SEMI
| identifier TO STATE identifier (VIA EDGE identifier DOT identifier)? SEMI
| identifier TO EDGE identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
| identifier TO INTERFACE identifier typeArguments? ((VIA EDGE identifier DOT identifier) | (FROM INPUT))? SEMI
| identifier TO CREATION identifier WITH LBRACE creationOwnerMapping* RBRACE SEMI
| identifier TO CONSTRUCTOR identifier SEMI
| identifier TO WORKSPACE? QUERY identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
;
constructorBindingDecl
@@ -246,9 +426,21 @@ valueType
| WATCH_HANDLE
| MESSAGE stringLiteral
| ATOM_REF LT identifier GT
| INTERFACE_REF LT identifier GT
| INTERFACE_REF LT identifier typeArguments? GT
| REF LT identifier GT
| OPTIONAL LT valueType GT
| LIST LT valueType GT
| RECORD LBRACE recordField* RBRACE
| VARIANT ID stringLiteral LBRACE variantCase+ RBRACE
| identifier typeArguments?
;
recordField
: identifier COLON valueType SEMI
;
variantCase
: identifier ID stringLiteral COLON valueType SEMI
;
scalarType
@@ -294,27 +486,113 @@ jsonArray
identifier
: IDENTIFIER
| START
| STOP
| EDIT
| CREATION
| INITIALIZE
| EXECUTION
| SNAPSHOT
| TRANSACTION
| REMOTE
| TARGETS
| PORTABLE
| REPLICA
| LOCAL
| FROM
| REGISTER
| INTERFACES
| REFLECTION
| SOURCE
| QUERY
| SPECIALIZE
| ROOT
| DOCUMENT
| FRAGMENTS
| FRAGMENT
| VIEW
| MAX
| ALLOW
| POLL
| RPC
| QUERYABLE
| RESULT_BYTES
| RPC_CALLS
| DEADLINE_MS
| PRESENTATION
| SUBJECT
| MESSAGE
| JOURNAL
| SUBSCRIPTION
| CONNECT
| FOR
;
stringLiteral
: STRING_LITERAL
;
EDIT: 'edit';
MODULE: 'module';
CONTRACT: 'contract';
PRESENTATION: 'presentation';
SUBJECT: 'subject';
FOR: 'for';
CREATION: 'creation';
VARIANT: 'variant';
PREPARES: 'prepares';
INITIALIZE: 'initialize';
WORKSPACE: 'workspace';
EXECUTION: 'execution';
SNAPSHOT: 'snapshot';
TRANSACTION: 'transaction';
REMOTE: 'remote';
TARGETS: 'targets';
PORTABLE: 'portable';
REPLICA: 'replica';
LOCAL: 'local';
FROM: 'from';
RESULT_BYTES: 'result-bytes';
RPC_CALLS: 'rpc-calls';
DEADLINE_MS: 'deadline-ms';
QUERY: 'query';
REFLECTION: 'reflection';
SPECIALIZE: 'specialize';
ROOT: 'root';
DOCUMENT: 'document';
FRAGMENTS: 'fragments';
VIEW: 'view';
MAX: 'max';
ALLOW: 'allow';
POLL: 'poll';
QUERYABLE: 'queryable';
RPC: 'rpc';
QUERY_REASON: 'query-reason';
TYPE: 'type';
OBJECT: 'object';
STORABLE: 'storable';
IMPLEMENTS: 'implements';
REF: 'ref';
FRAGMENT: 'fragment';
IMPORT: 'import';
EXTERNAL: 'external';
SYMBOL: 'symbol';
ATOM: 'atom';
INTERFACE: 'interface';
INTERFACES: 'interfaces';
PACKAGE: 'package';
VALUE: 'value';
FIELD: 'field';
RELATION: 'relation';
OPERATION: 'operation';
FUNCTION: 'function';
CONSTRUCTOR: 'constructor';
CONSTRUCTS: 'constructs';
INPUT: 'input';
CONFORM: 'conform';
AS: 'as';
BIND: 'bind';
STATIC: 'static';
TO: 'to';
PRIVATE: 'private';
SHARED: 'shared';
@@ -322,6 +600,12 @@ STATE: 'state';
EDGE: 'edge';
PROJECTION: 'projection';
WITH: 'with';
USING: 'using';
ACCEPT: 'accept';
VIA: 'via';
MATERIALIZE: 'materialize';
IF: 'if';
ABSENT: 'absent';
ON: 'on';
POLICY: 'policy';
DEFAULT: 'default';
@@ -329,6 +613,14 @@ SOURCE: 'source';
REPOSITORY: 'repository';
COMMIT: 'commit';
REVISION: 'revision';
SEMANTIC_MAJOR: 'semantic-major';
ON_DELETE: 'on-delete';
LIFECYCLE: 'lifecycle';
JOURNAL: 'journal';
SUBSCRIPTION: 'subscription';
RETAIN_OTHER: 'retain-other';
KEYED: 'keyed';
PUBLIC_TRAVERSAL: 'public-traversal';
ID: 'id';
DOC: 'doc';
MODE: 'mode';
@@ -351,7 +643,7 @@ WATCH_START: 'watch-start';
WATCH_STOP: 'watch-stop';
SUBSCRIBE: 'subscribe';
UNSUBSCRIBE: 'unsubscribe';
OPTIMISTIC_REGISTER: 'optimistic-register';
REGISTER: 'register';
CRDT: 'crdt';
OPTIONAL_ONE: 'optional-one';
EXACTLY_ONE: 'exactly-one';
@@ -365,6 +657,7 @@ ATOM_REF: 'atom-ref';
INTERFACE_REF: 'interface-ref';
OPTIONAL: 'optional';
LIST: 'list';
RECORD: 'record';
BOOL: 'bool';
BYTES: 'bytes';
DOUBLE: 'double';
@@ -390,6 +683,8 @@ LPAREN: '(';
RPAREN: ')';
LT: '<';
GT: '>';
AMP: '&';
EQUAL: '=';
INTEGER: '-'? [0-9]+;
JSON_NUMBER: '-'? ('0' | [1-9] [0-9]*) ('.' [0-9]+)? ([eE] [+-]? [0-9]+)?;
@@ -398,6 +693,6 @@ STRING_LITERAL: '"' (ESC | ~["\\\r\n])* '"';
fragment ESC: '\\' (["\\/bfnrt] | 'u' HEX HEX HEX HEX);
fragment HEX: [0-9a-fA-F];
LINE_COMMENT: '//' ~[\r\n]* -> skip;
BLOCK_COMMENT: '/*' .*? '*/' -> skip;
WS: [ \t\r\n]+ -> skip;
LINE_COMMENT: '//' ~[\r\n]* -> channel(HIDDEN);
BLOCK_COMMENT: '/*' .*? '*/' -> channel(HIDDEN);
WS: [ \t\r\n]+ -> channel(HIDDEN);
+30 -2
View File
@@ -1,20 +1,26 @@
grammar QuixosLock;
document
: QUIXOS_LOCK VERSION INTEGER LBRACE quixosEntry resourceEntry* RBRACE EOF
: QUIXOS_LOCK FRAGMENT? VERSION INTEGER LBRACE
(quixosEntry | importEntry | resourceEntry)* RBRACE EOF
;
quixosEntry
: QUIXOS SOURCE sourceBlock
: QUIXOS SOURCE quixosSourceBlock
;
resourceEntry
: resourceKind identifier SOURCE sourceBlock
;
importEntry
: IMPORT stringLiteral SEMI
;
resourceKind
: INTERFACE
| PACKAGE
| BUILD
;
sourceBlock
@@ -24,6 +30,20 @@ sourceBlock
RBRACE
;
quixosSourceBlock
: LBRACE
REPOSITORY stringLiteral SEMI
(POLICY sourcePolicy SEMI REF stringLiteral SEMI)?
COMMIT stringLiteral SEMI
RBRACE
;
sourcePolicy
: PINNED
| TRACK_RELEASE
| TRACK_DEVELOPMENT
;
identifier
: IDENTIFIER
;
@@ -33,13 +53,21 @@ stringLiteral
;
QUIXOS_LOCK: 'quixos-lock';
FRAGMENT: 'fragment';
VERSION: 'version';
QUIXOS: 'quixos';
SOURCE: 'source';
INTERFACE: 'interface';
PACKAGE: 'package';
BUILD: 'build';
REPOSITORY: 'repository';
COMMIT: 'commit';
POLICY: 'policy';
REF: 'ref';
PINNED: 'pinned';
TRACK_RELEASE: 'track-release';
TRACK_DEVELOPMENT: 'track-development';
IMPORT: 'import';
LBRACE: '{';
RBRACE: '}';
+47
View File
@@ -0,0 +1,47 @@
// Validate compiler-owned plans and immutable artifact receipts before publishing check outputs.
import { readFile, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { pathToFileURL } from "node:url";
import { join } from "node:path";
const [compilerPath, planPath, checksPath, output] = process.argv.slice(2);
const compiler = await import(pathToFileURL(compilerPath));
const plan = JSON.parse(await readFile(planPath, "utf8"));
const checks = JSON.parse(await readFile(checksPath, "utf8"));
compiler.compileWorkspaceEntry(plan, JSON.parse(await readFile(join(output, "entry.json"), "utf8")));
const roots = Object.create(null);
for (const check of checks) {
if (Object.hasOwn(roots, check.packageRevisionId)) throw Error("Duplicate checked package revision");
roots[check.packageRevisionId] = check.artifactPath;
}
const generator = createHash("sha256")
.update(await readFile(compilerPath))
.digest("hex");
// These compiler-dependent checks belong to the Nix build, not runtime recovery.
const candidate = JSON.parse(await readFile(join(output, "candidate.json"), "utf8"));
const { executionContracts: _legacyContracts, ...workspace } = candidate;
if (compiler.hexDigest(compiler.compileExecutionPlan(workspace)) !== compiler.hexDigest(plan))
throw Error("CANDIDATE_PLAN_MISMATCH: build plan differs from source compilation");
const checked = await compiler.loadCheckedArtifactWorld(plan, roots, generator);
const installation = await compiler.loadExecutionInstallation(plan, roots);
const kernel = compiler.compileKernelProgram(plan),
queries = compiler.compileQueryProgram(plan);
const graph = compiler.graphExecutionRepresentation(plan, installation, kernel, queries);
for (const [file, value] of [
["graph-execution.json", graph],
["execution-world.json", checked.world],
["installation.json", installation],
["kernel.json", kernel],
["queries.json", queries],
[
"execution-artifacts.json",
{
codec: "camino-checked-execution-v2",
generator,
roots,
manifests: checked.manifests,
execution: compiler.hexDigest(graph),
programs: { kernel: compiler.hexDigest(kernel), queries: compiler.hexDigest(queries) },
},
],
])
await writeFile(join(output, file), JSON.stringify(value) + "\n");
+41
View File
@@ -0,0 +1,41 @@
// An independent package check witnesses its contract and bytes, without
// inventing a workspace or granting installation/activation authority.
import { readFile, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { pathToFileURL } from "node:url";
import { join } from "node:path";
const [compilerPath, schemaPath, checksPath, output] = process.argv.slice(2);
const compiler = await import(pathToFileURL(compilerPath));
const schema = JSON.parse(await readFile(schemaPath, "utf8"));
const checks = JSON.parse(await readFile(checksPath, "utf8"));
const generator = createHash("sha256")
.update(await readFile(compilerPath))
.digest("hex");
const evidence = [];
for (const check of checks) {
const pkg = schema.packages.find((pkg) => pkg.revisionId === check.packageRevisionId);
if (!pkg) throw Error("Unknown checked package");
const contract = compiler.packageBuildContract(schema, pkg.revisionId);
const sourceDigest = compiler.hexDigest({ revision: pkg.revisionId, source: pkg.source });
const manifest = compiler.parsePackageArtifacts(
JSON.parse(await readFile(join(check.artifactPath, "share/quixos/package-artifacts.json"), "utf8")),
);
compiler.checkPackageCoverage(
{ ...contract.definition.package, bindingDigest: contract.digest, sourceDigest },
manifest,
);
const receipt = JSON.parse(await readFile(join(check.artifactPath, "share/quixos/build-receipt.json"), "utf8"));
const expected = compiler.expectedPackageReceipt(contract, manifest, sourceDigest, generator);
if (compiler.hexDigest(receipt) !== compiler.hexDigest(expected.receipt))
throw Error("Invalid package build receipt");
const read = await compiler.artifactFileReader({ [pkg.revisionId]: check.artifactPath });
await compiler.checkArtifactFiles(manifest, read);
for (const [file, bytes] of expected.sources)
if (!Buffer.from(bytes).equals(await read(pkg.revisionId, file, bytes.length)))
throw Error("Modified generated binding " + file);
evidence.push({ ...check, contract: contract.digest, sourceDigest, receipt });
}
await writeFile(
join(output, "package-checks.json"),
JSON.stringify({ codec: "camino-package-checks-v1", generator, packages: evidence }),
);
+49
View File
@@ -0,0 +1,49 @@
# Candidate artifact evaluation. This is separate from the old server activation path;
# the new coordinator consumes its manifest/receipt, not the legacy server ABI.
{
source,
plan,
generator,
core,
sharedRuntime,
typescript,
packageRevisionId,
system ? builtins.currentSystem,
}:
let
packageSource = builtins.path {
path = /. + source;
name = "quixos-package-source";
filter =
path: _:
!(builtins.elem (baseNameOf path) [
".git"
".jj"
]);
};
package = builtins.getFlake (
"path:" + builtins.unsafeDiscardStringContext (toString packageSource)
);
pkgs = import package.inputs.nixpkgs { inherit system; };
buildContext = import ./package-context.nix {
inherit pkgs packageRevisionId;
protocol = builtins.storePath generator;
plan = builtins.path {
path = /. + plan;
name = "candidate-execution-plan.json";
};
};
checked =
package.quixosPackages.${system}.checkedArtifacts
or (throw "Package ${packageRevisionId} lacks checkedArtifacts for the portable execution world.");
in
checked (
buildContext
// {
inherit packageRevisionId;
generator = builtins.storePath generator;
core = builtins.storePath core;
sharedRuntime = builtins.storePath sharedRuntime;
typescript = builtins.storePath typescript;
}
)
+438
View File
@@ -0,0 +1,438 @@
# All source trees and recursive locks are fetched by Nix at exact retained
# revisions. No authoring-directory overlay or externally assembled schema.
{
repository,
commit,
kind,
generator,
contractOnly ? false,
workspaceId ? null,
platformSourceJSON ? null,
system ? builtins.currentSystem,
}:
let
pkgs = import (/. + "@nixpkgs@") {
inherit system;
config = { };
overlays = [ ];
};
protocol = builtins.storePath generator;
sourceKey = source: "${source.kind}:${source.repository}@${source.commit}";
fetch =
source:
builtins.fetchGit {
url = source.repository;
rev = source.commit;
ref = "refs/tags/quixos-reachability/${source.commit}";
shallow = true;
};
# Memoize by exact source identity. A diamond-shaped dependency graph must
# not recursively evaluate every path through the same imported subtree.
loadSource =
source:
let
directory = fetch source;
lockSource = import ./lock-source.nix { source = directory; };
lockFile = pkgs.runCommand "qx-source-lock.json" { } ''
${protocol}/bin/quixos-lock-check ${lockSource}/quixos.lock > "$out"
'';
lock = builtins.fromJSON (builtins.readFile lockFile);
in
source // { inherit directory lock; };
sourceEntry = source: {
key = sourceKey source;
value = loadSource source;
};
sourceGraph = builtins.genericClosure {
startSet = [ (sourceEntry { inherit kind repository commit; }) ];
operator =
entry:
map (
resource: sourceEntry (resource.source // { inherit (resource) kind; })
) entry.value.lock.resources;
};
nodeTable = builtins.listToAttrs (
map (entry: {
name = entry.key;
value = entry.value // {
children = map (
resource:
nodeTable.${sourceKey (resource.source // { inherit (resource) kind; })}
// {
inherit (resource) binding;
}
) entry.value.lock.resources;
};
}) sourceGraph
);
root = nodeTable.${sourceKey { inherit kind repository commit; }};
configuration =
if builtins.pathExists (root.directory + "/quixos.check.json") then
builtins.fromJSON (builtins.readFile (root.directory + "/quixos.check.json"))
else
{ };
execution = (configuration.backend or "typescript") == "execution";
platformSource = if platformSourceJSON == null then null else builtins.fromJSON platformSourceJSON;
runtime =
if platformSource == null then
builtins.getFlake (
"git+${root.lock.quixos.repository}?rev=${root.lock.quixos.commit}&ref=refs/tags/quixos-reachability/${root.lock.quixos.commit}"
)
else
assert platformSource.repository == root.lock.quixos.repository;
assert platformSource.commit == root.lock.quixos.commit;
# getFlake accepts a context-free flake reference. storePath first checks
# presence; the resulting flake retains its immutable source as an input.
# Dropping string context here does not permit an authored mutable path.
builtins.getFlake (
builtins.unsafeDiscardStringContext "path:${builtins.storePath platformSource.directory}"
);
runtimePackages = runtime.packages.${system};
resourceClosure =
includeBuild: node:
map (entry: entry.value) (
builtins.genericClosure {
startSet = pkgs.lib.optional (includeBuild || node.kind != "build") {
key = sourceKey node;
value = node;
};
operator =
entry:
map (child: {
key = sourceKey child;
value = child;
}) (builtins.filter (child: includeBuild || child.kind != "build") entry.value.children);
}
);
capabilityFlatten = resourceClosure false;
# Frontends are an explicit build choice. The capability assembler itself
# neither discovers React contracts nor loads executable package code.
queryFrontends =
node:
pkgs.lib.unique (
pkgs.lib.concatMap (
node:
let
config = node.directory + "/quixos.check.json";
in
if builtins.pathExists config then
(builtins.fromJSON (builtins.readFile config)).queryFrontends or [ ]
else
[ ]
) (capabilityFlatten node)
);
queryGeneratorFlags =
node:
if queryFrontends node == [ ] then
""
else if queryFrontends node == [ "react" ] then
"--query-generator ${protocol}/libexec/quixos-protocol/react-query-generator.mjs"
else
throw "Unsupported query frontend selection: ${builtins.toJSON (queryFrontends node)}";
snapshots =
node:
pkgs.writeText "qx-nix-source-graph.json" (
builtins.toJSON {
resources = map (node: {
inherit (node)
kind
repository
commit
directory
;
}) (builtins.filter (entry: entry.kind != "workspace") (resourceClosure true node));
}
);
# Each resource is checked once per exact source/closure/frontend/toolchain.
# Composition imports these compiler outputs; authoring receipts are never
# build inputs. Missing entries in ordinary CLI use take the full check path.
compiledNodes = builtins.mapAttrs (_: node: compileNode node) nodeTable;
compile = node: compiledNodes.${sourceKey node};
checkedResources =
node:
pkgs.writeText "qx-checked-resources.json" (
builtins.toJSON (
map
(dependency: {
inherit (dependency) kind repository commit;
artifact = compile dependency;
})
(
builtins.filter (
dependency:
dependency.kind != "workspace"
&& dependency.kind != "build"
&& sourceKey dependency != sourceKey node
) (resourceClosure true node)
)
)
);
compileNode =
node:
pkgs.runCommand "qx-${node.kind}-contract" { QUIXOS_TRACE_PROFILE = "1"; } ''
mkdir -p "$out"
${
if node.kind == "workspace" then
''
${protocol}/bin/quixos-workspace-compile --root ${node.directory} \
${queryGeneratorFlags root} \
--checked-resources ${checkedResources node} \
--source-root-commit ${pkgs.lib.escapeShellArg node.commit} \
${
pkgs.lib.optionalString (
workspaceId != null
) "--workspace-id ${pkgs.lib.escapeShellArg workspaceId}"
} \
--checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots node} \
--graph-out "$out/graph.json" --schemas-out "$out/package-schemas.json" \
${
pkgs.lib.optionalString (execution && kind == "workspace")
''--execution-plan-out "$out/execution-plan.json" --build-contexts-out "$out/build-contexts.json"''
} > "$out/candidate.json"
''
else
''
${protocol}/bin/quixos-resource-compile --root ${node.directory} \
${queryGeneratorFlags root} \
--checked-resources ${checkedResources node} \
--kind ${node.kind} --repository ${pkgs.lib.escapeShellArg node.repository} \
--commit ${pkgs.lib.escapeShellArg node.commit} \
--checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots node} --snapshot-only true \
--graph-out "$out/graph.json" --schema-out "$out/bindings.json" > "$out/candidate.json"
''
}
'';
contract = compile root;
# Parse shared workspace outputs once. Repeating fromJSON in every package
# makes composition quadratic in package count and in the serialized world.
workspacePackages = builtins.listToAttrs (
map
(pkg: {
name = sourceKey (pkg.source // { kind = "package"; });
value = pkg;
})
(builtins.fromJSON (builtins.readFile "${contract}/execution-plan.json"))
.definition.bindingSchema.packages
);
workspaceContexts = builtins.fromJSON (builtins.readFile "${contract}/build-contexts.json");
buildDependencies =
node:
builtins.listToAttrs (
map (dependency: {
name = dependency.binding;
value = {
src = dependency.directory;
source = { inherit (dependency) repository commit; };
flake = builtins.getFlake "git+${dependency.repository}?rev=${dependency.commit}&ref=refs/tags/quixos-reachability/${dependency.commit}";
};
}) (builtins.filter (dependency: dependency.kind == "build") node.children)
);
reactContracts =
node: moduleInterfaces:
let
interfaces = builtins.attrValues (
builtins.listToAttrs (
map (dependency: {
name = sourceKey dependency;
value = dependency;
}) (builtins.filter (dependency: dependency.kind == "interface") (capabilityFlatten node))
)
);
in
pkgs.lib.concatMap (
dependency:
if !builtins.pathExists (dependency.directory + "/flake.nix") then
[ ]
else
let
source = builtins.getFlake "git+${dependency.repository}?rev=${dependency.commit}&ref=refs/tags/quixos-reachability/${dependency.commit}";
build = source.quixosInterfaces.${system}.reactTypes or null;
in
if build == null then
[ ]
else if
!(builtins.any (
entry:
let
resource = builtins.fromJSON (builtins.readFile "${compile dependency}/candidate.json");
in
entry.revisionId == resource.revision.revisionId
|| (entry.definitionId or null) == resource.revision.revisionId
) moduleInterfaces)
then
[ ]
else
[
(build {
interface = "${compile dependency}/candidate.json";
generator = protocol;
typescript = runtimePackages.camino-replica-core.dependencies;
reactSdk = runtimePackages.camino-react;
dependencies = buildDependencies dependency;
})
]
) interfaces;
checkPackage =
node:
let
compiled = compile node;
candidate =
if execution && kind == "workspace" then
{
revision =
workspacePackages.${sourceKey node}
or (throw "Source package missing from workspace compilation: ${node.repository}");
}
else
builtins.fromJSON (builtins.readFile "${compiled}/candidate.json");
buildContext = import ./package-context.nix {
inherit pkgs protocol;
contexts = if kind == "workspace" then "${contract}/build-contexts.json" else null;
contextData =
if execution && kind == "workspace" then
workspaceContexts.${candidate.revision.revisionId}
else
null;
plan = "${compiled}/bindings.json";
packageRevisionId = candidate.revision.revisionId;
};
buildContract = builtins.fromJSON (builtins.readFile buildContext.buildContract);
componentEntries = if execution then buildContract.definition.package.components or [ ] else [ ];
package = builtins.getFlake (
"git+${node.repository}?rev=${node.commit}&ref=refs/tags/quixos-reachability/${node.commit}"
);
checked =
if execution then
package.quixosPackages.${system}.checkedArtifacts
or (throw "Package ${node.repository} lacks checkedArtifacts; execution workspaces cannot import legacy servers.")
else
package.quixosPackages.${system}.checkedServer
or (throw "Package ${node.repository} lacks checkedServer; use the supported package scaffold.");
artifact = checked (
if execution then
buildContext
// {
generator = protocol;
core = runtimePackages.camino-replica-core;
sharedRuntime = runtimePackages.camino-shared-runtime;
typescript = runtimePackages.camino-replica-core.dependencies;
packageRevisionId = candidate.revision.revisionId;
dependencies = buildDependencies node;
buildEnvironment = {
inherit pkgs system;
nixpkgsSource = toString pkgs.path;
narHash = "@nixpkgsNarHash@";
};
}
// pkgs.lib.optionalAttrs (componentEntries != [ ]) {
react = {
sdk = runtimePackages.camino-react;
runtime = runtimePackages.camino-react-runtime;
contracts = reactContracts node buildContract.definition.moduleInterfaces;
};
}
else
{
schema =
if kind == "workspace" then
pkgs.writeText "package-specialization-schema.json" (
builtins.toJSON
(builtins.fromJSON (builtins.readFile "${contract}/package-schemas.json"))
.${candidate.revision.revisionId}
)
else
"${compiled}/bindings.json";
generator = protocol;
packageRevisionId = candidate.revision.revisionId;
}
);
in
{
packageRevisionId = candidate.revision.revisionId;
artifactPath = artifact;
};
checks =
if contractOnly then
[ ]
else
map checkPackage (
builtins.filter
(node: node.kind == "package" && (kind == "workspace" || sourceKey node == sourceKey root))
(
builtins.attrValues (
builtins.listToAttrs (
map (node: {
name = sourceKey node;
value = node;
}) (capabilityFlatten root)
)
)
)
);
manifest = pkgs.writeText "qx-candidate-checks.json" (builtins.toJSON checks);
bindingOptions = pkgs.writeText "qx-typescript-options.json" (
builtins.toJSON (
(builtins.fromJSON (builtins.readFile "${root.directory}/quixos.check.json")).options or { }
)
);
in
assert
workspaceId == null
|| (
kind == "workspace"
&& builtins.isString workspaceId
&& builtins.match "[a-zA-Z0-9][a-zA-Z0-9:._-]{0,255}" workspaceId != null
);
pkgs.runCommand (if contractOnly then "qx-contract" else "qx-checked-candidate") { } ''
mkdir -p "$out"
cp ${
builtins.toFile "qx-candidate-source.json" (builtins.toJSON { inherit repository commit kind; })
} "$out/source.json"
cp ${contract}/candidate.json "$out/candidate.json"
cp ${contract}/graph.json "$out/graph.json"
cp ${manifest} "$out/checks.json"
${pkgs.lib.optionalString (execution && kind == "workspace") ''
${
if builtins.pathExists "${root.directory}/quixos.entry.json" then
''
cp ${root.directory}/quixos.entry.json "$out/entry.json"
''
else
''
echo null > "$out/entry.json"
''
}
cp ${contract}/execution-plan.json "$out/execution-plan.json"
${pkgs.lib.optionalString (!contractOnly) ''
${pkgs.nodejs_24}/bin/node ${./check-execution.mjs} \
${protocol}/libexec/quixos-protocol/execution-world.mjs \
"$out/execution-plan.json" "$out/checks.json" "$out"
# The optional development host is acquired only when explicitly run.
# Embedding its executable here made every source check build Web Studio
# and a full host even when only checked Package outputs were consumed.
cp ${
pkgs.writeText "qx-isolated-runtime.json" (
builtins.toJSON {
codec = "quixos-isolated-runtime/1";
source = {
inherit (root.lock.quixos) repository commit;
};
}
)
} "$out/isolated-runtime.json"
''}
''}
${pkgs.lib.optionalString (
kind != "workspace"
) ''cp ${contract}/bindings.json "$out/bindings.json"''}
${pkgs.lib.optionalString (execution && kind == "package" && !contractOnly) ''
${pkgs.nodejs_24}/bin/node ${./check-package.mjs} \
${protocol}/libexec/quixos-protocol/execution-world.mjs ${contract}/bindings.json \
"$out/checks.json" "$out"
''}
${pkgs.lib.optionalString (kind == "package" && !execution) ''
${protocol}/bin/quixos-codegen-ts ${contract}/bindings.json \
${pkgs.lib.escapeShellArg (builtins.fromJSON (builtins.readFile "${contract}/candidate.json")).revision.revisionId} \
"$out/bindings.ts" ${bindingOptions}
''}
''
+35
View File
@@ -0,0 +1,35 @@
# One derivation path for provisional checking and activation. The source and
# schema come from exact committed inputs resolved by the Quixos compiler.
{
source,
schema,
generator,
packageRevisionId,
system ? builtins.currentSystem,
}:
let
packageSource = builtins.path {
path = /. + source;
name = "quixos-package-source";
filter =
path: _:
let
name = baseNameOf path;
in
name != ".git" && name != ".jj";
};
package = builtins.getFlake (
"path:" + builtins.unsafeDiscardStringContext (toString packageSource)
);
checked =
package.quixosPackages.${system}.checkedServer
or (throw "Package ${packageRevisionId} lacks checkedServer; use the supported package scaffold.");
in
checked {
inherit packageRevisionId;
schema = builtins.path {
path = /. + schema;
name = "candidate-package-bindings.json";
};
generator = builtins.storePath generator;
}
+16
View File
@@ -0,0 +1,16 @@
# Lock parsing only reads this one file when no import token can occur. A
# conservative substring probe also sends comments/URLs containing "import"
# through the complete-source path. Fragment names may have any extension.
{ source }:
let
regular = ((builtins.readDir source)."quixos.lock" or null) == "regular";
text = if regular then builtins.readFile (source + "/quixos.lock") else "import";
in
if builtins.replaceStrings [ "import" ] [ "" ] text != text then
source
else
builtins.path {
path = source;
name = "qx-lock-source";
filter = filename: _: filename == toString source || filename == "${source}/quixos.lock";
}
+42
View File
@@ -0,0 +1,42 @@
{ pkgs, protocol }:
let
base = ''quixos-lock version 1 { quixos source { repository "https://example.test/quixos.git"; commit "1111111111111111111111111111111111111111"; } }'';
fixture =
extra: lock:
pkgs.runCommand "qx-lock-source-fixture" { } ''
mkdir -p "$out/nested"
printf '%s' ${pkgs.lib.escapeShellArg lock} > "$out/quixos.lock"
printf '%s' ${pkgs.lib.escapeShellArg extra} > "$out/implementation.ts"
printf '%s' 'quixos-lock fragment version 1 {}' > "$out/nested/dependencies.txt"
'';
first = fixture "first implementation" base;
edited = fixture "edited implementation" base;
changed = fixture "first implementation" (
builtins.replaceStrings
[ "1111111111111111111111111111111111111111" ]
[ "2222222222222222222222222222222222222222" ]
base
);
fragments = fixture "first implementation" (
builtins.replaceStrings [ "quixos source" ] [ ''import "nested/dependencies.txt"; quixos source'' ]
base
);
conservative = fixture "first implementation" (base + "\n// import in a comment\n");
slice = source: import ./lock-source.nix { inherit source; };
parse =
source:
pkgs.runCommand "qx-source-lock.json" { } ''
${protocol}/bin/quixos-lock-check ${slice source}/quixos.lock > "$out"
'';
in
assert slice first == slice edited;
assert (parse first).drvPath == (parse edited).drvPath;
assert slice first != slice changed;
pkgs.runCommand "qx-lock-source-check" { } ''
${protocol}/bin/quixos-lock-check ${slice first}/quixos.lock > first.json
${protocol}/bin/quixos-lock-check ${slice edited}/quixos.lock > edited.json
cmp first.json edited.json
${protocol}/bin/quixos-lock-check ${slice conservative}/quixos.lock > comment.json
cmp first.json comment.json
${protocol}/bin/quixos-lock-check ${slice fragments}/quixos.lock > "$out"
''
+15
View File
@@ -0,0 +1,15 @@
import fs from "node:fs/promises";
import { pathToFileURL } from "node:url";
const compiler = await import(pathToFileURL(process.argv[2]));
const input = JSON.parse(await fs.readFile(process.argv[3], "utf8"));
const revision = process.argv[4];
const schema = input.definition?.bindingSchema ?? input;
if (input.definition && compiler.hexDigest(input.definition) !== input.digest) throw Error("Invalid execution plan");
const pkg = schema.packages.find((entry) => entry.revisionId === revision);
if (!pkg) throw Error("Unknown build package " + revision);
process.stdout.write(
JSON.stringify({
contract: compiler.packageBuildContract(schema, revision),
sourceDigest: compiler.hexDigest({ revision, source: pkg.source }),
}),
);
+33
View File
@@ -0,0 +1,33 @@
# Workspace composition may reevaluate this projection after any graph edit.
# Only the canonical, source-free value becomes a package compilation input.
{
pkgs,
protocol,
plan ? null,
contexts ? null,
contextData ? null,
packageRevisionId,
}:
let
projected = pkgs.runCommand "camino-package-context.json" { } ''
${pkgs.nodejs_24}/bin/node ${./package-context.mjs} \
${protocol}/libexec/quixos-protocol/execution-world.mjs ${plan} \
${pkgs.lib.escapeShellArg packageRevisionId} > "$out"
'';
context =
if contextData != null then
contextData
else if contexts == null then
builtins.fromJSON (builtins.readFile projected)
else
(builtins.fromJSON (builtins.readFile contexts)).${packageRevisionId};
in
{
# This is typed semantic data (nominal IDs, types and generated source), never
# Nix artifact paths. Remove the projection derivation's string context, not
# any code/toolchain dependency. Those are separate explicit helper arguments.
buildContract = builtins.toFile "camino-package-contract.json" (
builtins.unsafeDiscardStringContext (builtins.toJSON context.contract)
);
sourceDigest = builtins.unsafeDiscardStringContext context.sourceDigest;
}
+12 -3
View File
@@ -6,14 +6,21 @@
"type": "module",
"bin": {
"quixos-capability-compile": "dist/src/capability-language/cli.js",
"quixos-codegen-client": "dist/src/bindings/portable-client-cli.js",
"quixos-codegen-ts": "dist/src/bindings/cli.js",
"quixos-descriptor-check": "dist/src/descriptor-check.js",
"quixos-lock-check": "dist/src/resource-lock/cli.js"
"quixos-lock-check": "dist/src/resource-lock/cli.js",
"quixos-qx": "dist/src/capability-language/tool-cli.js",
"quixos-resource-compile": "dist/src/capability-language/resource-cli.js",
"quixos-workspace-compile": "dist/src/capability-language/workspace-cli.js"
},
"exports": {
"./bindings": "./dist/src/bindings/index.js",
"./capability-model": "./dist/src/capability-model/index.js",
"./capability-language": "./dist/src/capability-language/index.js",
"./resource-lock": "./dist/src/resource-lock/index.js",
"./gen/*": "./dist/src/gen/*"
"./gen/*": "./dist/src/gen/*",
"./execution-world": "./dist/src/execution-world/index.js"
},
"scripts": {
"build": "yarn generate && tsc -p tsconfig.build.json",
@@ -24,9 +31,11 @@
"typecheck": "yarn generate && tsc --noEmit"
},
"dependencies": {
"@babel/parser": "^7.28.0",
"@bufbuild/protobuf": "^2.12.1",
"@bufbuild/protoc-gen-es": "^2.12.1",
"antlr4ng": "^3.0.16"
"antlr4ng": "^3.0.16",
"graphql": "16.11.0"
},
"devDependencies": {
"@types/node": "^24",
+230 -19
View File
@@ -3,8 +3,11 @@ syntax = "proto3";
package camino;
import "camino/schema.proto";
import "quixos/refs.proto";
service CaminoService {
rpc ExecuteQuery(QueryRequest) returns (QueryResponse);
rpc QueryChanges(QueryChangesRequest) returns (QueryChangesResponse);
rpc InstallPersistencePlan(InstallPersistencePlanRequest) returns (InstallPersistencePlanResponse);
rpc GetPersistencePlan(GetPersistencePlanRequest) returns (GetPersistencePlanResponse);
rpc CreateObject(CreateObjectRequest) returns (CreateObjectResponse);
@@ -15,14 +18,22 @@ service CaminoService {
rpc ConnectEdge(ConnectEdgeRequest) returns (ConnectEdgeResponse);
rpc ResolveEdge(ResolveEdgeRequest) returns (ResolveEdgeResponse);
rpc DisconnectEdge(DisconnectEdgeRequest) returns (DisconnectEdgeResponse);
rpc ReadCollection(ResolveEdgeRequest) returns (ReadCollectionResponse);
rpc ReplaceCollection(ReplaceCollectionRequest) returns (ReadCollectionResponse);
rpc ListOps(ListOpsRequest) returns (ListOpsResponse);
rpc WatchObject(WatchObjectRequest) returns (stream WatchObjectEvent);
}
message NullValue {}
message ObjectValue { map<string, Value> fields = 1; }
message ListValue { repeated Value values = 1; }
message RefValue { string object_id = 1; }
message ObjectValue {
map<string, Value> fields = 1;
}
message ListValue {
repeated Value values = 1;
}
message RefValue {
string object_id = 1;
}
message CrdtValue {
string type = 1;
string encoding = 2;
@@ -41,7 +52,9 @@ message StateValueSource {
CrdtValue crdt_snapshot = 6;
}
message ValueSource { StateValueSource state = 1; }
message ValueSource {
StateValueSource state = 1;
}
message Value {
oneof kind {
@@ -57,28 +70,67 @@ message Value {
CrdtValue crdt_value = 10;
}
ValueSource source = 11;
// Trusted query coordinator annotation. Travels with values through residual
// row selection, then is removed in favor of final-path hydration metadata.
QueryFieldOrigin query_origin = 12;
}
message InstallPersistencePlanRequest { PersistencePlan plan = 1; }
message InstallPersistencePlanResponse { PersistencePlan plan = 1; }
message QueryFieldOrigin {
string selection_id = 1;
string atom_id = 2;
string interface_revision_id = 3;
string member_id = 4;
StateValueSource source = 5;
}
message QueryLiveField {
repeated QueryPathPart path = 1;
string selection_id = 2;
string object_id = 3;
quixos.CapabilityRef capability = 4;
string watch_operation_id = 5;
string setter_operation_id = 6;
quixos.FieldEditing editing = 7;
StateValueSource source = 8;
}
message InstallPersistencePlanRequest {
PersistencePlan plan = 1;
}
message InstallPersistencePlanResponse {
PersistencePlan plan = 1;
}
message GetPersistencePlanRequest {}
message GetPersistencePlanResponse { PersistencePlan plan = 1; }
message GetPersistencePlanResponse {
PersistencePlan plan = 1;
}
message CreateObjectRequest {
string atom_id = 1;
map<string, Value> initial_state = 2;
}
message CreateObjectResponse { CaminoObject object = 1; }
message GetObjectRequest { string object_id = 1; }
message GetObjectResponse { CaminoObject object = 1; }
message ListObjectsRequest { string atom_id = 1; }
message ListObjectsResponse { repeated CaminoObject objects = 1; }
message CreateObjectResponse {
CaminoObject object = 1;
}
message GetObjectRequest {
string object_id = 1;
}
message GetObjectResponse {
CaminoObject object = 1;
}
message ListObjectsRequest {
string atom_id = 1;
}
message ListObjectsResponse {
repeated CaminoObject objects = 1;
}
message ReadStateRequest {
string object_id = 1;
string slot_id = 2;
}
message ReadStateResponse { Value value = 1; }
message ReadStateResponse {
Value value = 1;
}
message WriteStateRequest {
string object_id = 1;
string slot_id = 2;
@@ -98,22 +150,54 @@ message ConnectEdgeRequest {
optional int32 ordinal = 5;
optional int32 target_ordinal = 6;
}
message ConnectEdgeResponse { CaminoEdge edge = 1; }
message ConnectEdgeResponse {
CaminoEdge edge = 1;
}
message ResolveEdgeRequest {
string object_id = 1;
string edge_type_id = 2;
string projection_id = 3;
}
message ResolveEdgeResponse { repeated CaminoEdge edges = 1; }
message DisconnectEdgeRequest { string edge_id = 1; }
message DisconnectEdgeResponse { string edge_id = 1; }
message ResolveEdgeResponse {
repeated CaminoEdge edges = 1;
}
message DisconnectEdgeRequest {
string edge_id = 1;
}
message DisconnectEdgeResponse {
string edge_id = 1;
}
message ListOpsRequest { string object_id = 1; }
message ListOpsResponse { repeated CaminoOp ops = 1; }
message CollectionEntry {
// Existing entry identity to preserve; empty allocates a new canonical edge.
string edge_id = 1;
string target_object_id = 2;
Value key = 3;
}
message ReadCollectionResponse {
uint64 revision = 1;
repeated CollectionEntry entries = 2;
}
message ReplaceCollectionRequest {
string object_id = 1;
string edge_type_id = 2;
string projection_id = 3;
uint64 expected_revision = 4;
repeated CollectionEntry entries = 5;
}
message ListOpsRequest {
string object_id = 1;
}
message ListOpsResponse {
repeated CaminoOp ops = 1;
}
message WatchObjectRequest {
string object_id = 1;
string after_op_id = 2;
bool include_snapshot = 3;
// Managed runtimes must watch only their injected attachments.
repeated string attachment_ids = 4;
}
message WatchObjectEvent {
string object_id = 1;
@@ -141,6 +225,8 @@ message CaminoEdge {
optional int32 first_ordinal = 7;
optional int32 second_ordinal = 8;
string created_at = 9;
string first_key_json = 10;
string second_key_json = 11;
}
message CaminoOp {
@@ -152,3 +238,128 @@ message CaminoOp {
string created_at = 6;
string client_mutation_id = 7;
}
message QueryRequest {
string query_id = 1;
oneof root {
string object_id = 2;
quixos.WorkspaceQueryScope workspace_scope = 5;
}
map<string, Value> variables = 3;
// Typed callers require this exact checked definition. Administrative/raw
// callers may omit it to explicitly select the currently installed definition.
string expected_definition_digest = 4;
}
message QueryPathPart {
oneof part {
string field = 1;
uint32 index = 2;
}
}
message QueryPendingField {
repeated QueryPathPart path = 1;
string object_id = 2;
string interface_revision_id = 3;
string member_id = 4;
string operation_id = 5;
string value_type_json = 6;
// Internal residual facts are separate from the authored result projection.
optional uint32 residual_window = 7;
uint32 residual_row = 8;
string residual_field = 9;
optional uint32 relational_capture = 10;
uint32 captured_object = 11;
}
message QueryStats {
uint32 sql_count = 1;
double sql_ms = 2;
uint32 rpc_count = 3;
double rpc_ms = 4;
uint32 result_bytes = 5;
double preparation_ms = 6;
double total_ms = 7;
uint32 relational_stages = 8;
uint32 captured_candidates = 9;
double relational_ms = 10;
double residual_ms = 11;
}
message QueryResponse {
Value value = 1;
string data_version = 2;
string binding_digest = 3;
repeated QueryPendingField pending = 4;
QueryStats stats = 5;
// Redeemable only through the host's private control socket, not an RPC grant.
string preparation_token = 6;
repeated QueryFieldFailure errors = 7;
repeated QueryResidualWindow residual_windows = 8;
// Native reads share one database snapshot; package enrichment does not.
string consistency = 9; // native-snapshot | mixed
repeated QueryRelationalCapture relational_captures = 10;
repeated QueryLiveField live_fields = 11;
}
// Private coordinator input, removed before publishing a result. Memberships
// and native facts share one snapshot; package reads are sampled afterwards.
message QueryCapturedMember {
string object_id = 1;
string entry_id = 2;
Value map_key = 3;
}
message QueryCapturedMembers {
repeated QueryCapturedMember entries = 1;
}
message QueryCapturedObject {
string object_id = 1;
map<string, Value> fields = 2;
map<string, string> field_types = 3;
map<string, QueryCapturedMembers> relationships = 4;
}
message QueryRelationalCapture {
repeated QueryPathPart path = 1;
QueryRelationalPlan plan = 2;
string root_object_id = 3;
repeated QueryCapturedObject objects = 4;
string variables_json = 5;
uint32 row_limit = 6;
uint32 candidate_limit = 7;
optional uint32 residual_window = 8;
repeated string result_path = 9;
}
message QueryResidualRow {
string entry_id = 1;
map<string, Value> fields = 2;
}
message QueryResidualOrder {
string field = 1;
bool descending = 2;
}
message QueryResidualWindow {
repeated QueryPathPart path = 1;
repeated QueryResidualRow rows = 2;
string predicate_json = 3;
repeated QueryResidualOrder order = 4;
uint32 limit = 5;
bool bounded_all = 6;
repeated QuerySelection selection = 7;
map<string, string> field_types = 8;
bool relational = 9;
repeated string matched_entries = 10;
}
message QueryFieldFailure {
repeated QueryPathPart path = 1;
string error = 2;
}
message QueryChangesRequest {
string query_id = 1;
oneof root {
string object_id = 2;
quixos.WorkspaceQueryScope workspace_scope = 4;
}
string data_version = 3;
}
message QueryChangesResponse {
bool changed = 1;
}
+277
View File
@@ -18,6 +18,7 @@ message AtomDefinition {
message AtomConformance {
string atom_id = 1;
string interface_revision_id = 2;
string conformance_id = 3;
}
message StateAttachment {
@@ -27,6 +28,7 @@ message StateAttachment {
string value_type_json = 4;
string storage_policy_json = 5;
string default_value_json = 6;
string owner_conformance_id = 7;
}
message EndpointConstraint {
@@ -42,6 +44,13 @@ message EdgeEndpoint {
EndpointConstraint constraint = 3;
Cardinality cardinality = 4;
bool ordered = 5;
// Empty means restrict. Direction is the endpoint being deleted.
string on_delete = 6;
bool retain_other = 7;
// Empty for sets/lists, otherwise string, boolean, or int64 map keys.
string key_type = 8;
// Explicit read-only dependency injection traversal, not mutation authority.
bool public_traversal = 9;
}
message EdgeAttachment {
@@ -49,6 +58,7 @@ message EdgeAttachment {
string display_name = 2;
EdgeEndpoint first = 3;
EdgeEndpoint second = 4;
string owner_conformance_id = 5;
}
// Camino consumes this persistence-only projection of a checked workspace.
@@ -60,4 +70,271 @@ message PersistencePlan {
repeated AtomConformance conformances = 4;
repeated StateAttachment states = 5;
repeated EdgeAttachment edges = 6;
repeated InstalledQuery queries = 7;
}
// Immutable checked query IR. Installed only with the checked persistence plan;
// query callers select its ID, never send or modify these definitions.
message QueryArgument {
oneof value {
string variable = 1;
string literal_json = 2;
QueryArgumentList list = 3;
QueryArgumentObject object = 4;
}
}
message QueryArgumentList {
repeated QueryArgument values = 1;
}
message QueryArgumentObject {
map<string, QueryArgument> fields = 1;
}
message QueryCondition {
bool include = 1;
QueryArgument value = 2;
}
message QuerySelection {
string name = 1;
string key = 2;
string interface_revision_id = 3;
string member_id = 4;
string target_interface_revision_id = 5;
repeated QueryCondition conditions = 6;
map<string, QueryArgument> arguments = 7;
repeated QuerySelection selection = 8;
QueryRelationalPlan relational = 9;
QueryPredicate predicate = 10;
// Stable checked selection identity; empty for ordinary value projections.
string live_selection_id = 11;
}
// Resolved IDs, not GraphQL names, determine execution. Response selections
// remain separate so aliases and fragments cannot change relational semantics.
message QueryPathStep {
oneof step {
bool source = 1;
bool target = 2;
QueryRelationPath relation = 3;
}
}
message QueryRelationPath {
string interface_revision_id = 1;
string member_id = 2;
string target_interface_revision_id = 3;
bool optional = 4;
}
message QueryFieldOperand {
string interface_revision_id = 1;
string member_id = 2;
repeated string value_path = 3;
}
message QueryExpression {
repeated QueryPathStep path = 1;
oneof leaf {
QueryFieldOperand field = 2;
string ref = 3;
bool entry = 4;
bool map_key = 5;
}
string value_type_json = 6;
}
enum QueryAggregateOperator {
QUERY_AGGREGATE_OPERATOR_UNSPECIFIED = 0;
QUERY_AGGREGATE_OPERATOR_COUNT = 1;
QUERY_AGGREGATE_OPERATOR_COUNT_PRESENT = 2;
QUERY_AGGREGATE_OPERATOR_SUM = 3;
QUERY_AGGREGATE_OPERATOR_AVG = 4;
QUERY_AGGREGATE_OPERATOR_MIN = 5;
QUERY_AGGREGATE_OPERATOR_MAX = 6;
}
message QueryReduction {
QueryAggregateOperator operator = 1;
QueryExpression operand = 2;
string value_type_json = 3;
}
message QueryOperand {
oneof operand {
QueryExpression expression = 1;
QueryReduction reduction = 2;
}
}
enum QueryComparisonOperator {
QUERY_COMPARISON_OPERATOR_UNSPECIFIED = 0;
QUERY_COMPARISON_OPERATOR_EQ = 1;
QUERY_COMPARISON_OPERATOR_IN = 2;
QUERY_COMPARISON_OPERATOR_IS_NULL = 3;
QUERY_COMPARISON_OPERATOR_LT = 4;
QUERY_COMPARISON_OPERATOR_LTE = 5;
QUERY_COMPARISON_OPERATOR_GT = 6;
QUERY_COMPARISON_OPERATOR_GTE = 7;
QUERY_COMPARISON_OPERATOR_CONTAINS_INSENSITIVE = 8;
}
message QueryComparison {
QueryOperand operand = 1;
QueryComparisonOperator operator = 2;
QueryArgument value = 3;
}
message QueryPredicateList {
repeated QueryPredicate children = 1;
}
enum QueryRelationPredicateOperator {
QUERY_RELATION_PREDICATE_OPERATOR_UNSPECIFIED = 0;
QUERY_RELATION_PREDICATE_OPERATOR_SOME = 1;
QUERY_RELATION_PREDICATE_OPERATOR_NONE = 2;
QUERY_RELATION_PREDICATE_OPERATOR_IS = 3;
QUERY_RELATION_PREDICATE_OPERATOR_IS_NULL = 4;
}
message QueryRelationPredicate {
repeated QueryPathStep path = 1;
QueryRelationPath relation = 2;
QueryRelationPredicateOperator operator = 3;
QueryPredicate predicate = 4;
QueryArgument value = 5;
}
message QueryReductionPredicate {
repeated QueryPathStep path = 1;
QueryRelationPath relation = 2;
QueryPredicate where = 3;
QueryPredicate having = 4;
}
message QueryInterfacePredicate {
string interface_revision_id = 1;
QueryPredicate predicate = 2;
}
message QueryPredicate {
oneof predicate {
QueryPredicateList and = 1;
QueryPredicateList or = 2;
QueryPredicate not = 3;
QueryComparison compare = 4;
QueryRelationPredicate relation = 5;
QueryReductionPredicate reduce = 6;
QueryInterfacePredicate as = 7;
}
}
message QueryRow {
oneof row {
QueryObjectRow object = 1;
QueryPairRow pair = 2;
}
}
message QueryObjectRow {
string interface_revision_id = 1;
bool membership = 2;
string key_type = 3;
}
message QueryPairRow {
QueryRow source = 1;
QueryRow target = 2;
}
message QueryKey {
repeated string path = 1;
QueryExpression expression = 2;
}
message QueryDistinct {
repeated QueryKey keys = 1;
}
message QueryExpansion {
repeated QueryPathStep path = 1;
QueryRelationPath relation = 2;
}
message QueryRelationalStage {
uint32 id = 1;
optional uint32 input = 2;
QueryRow row = 3;
oneof operation {
QueryRelationPath source = 4;
QueryPredicate filter = 5;
QueryExpansion expand = 6;
QueryDistinct distinct = 7;
}
}
message QueryReductionProjection {
repeated string path = 1;
QueryReduction reduction = 2;
}
message QueryAggregateOrder {
QueryOperand operand = 1;
bool descending = 2;
}
message QueryRelationalTerminal {
uint32 stage = 1;
repeated string path = 2;
bool groups = 3;
repeated QueryKey keys = 4;
repeated QueryReductionProjection reductions = 5;
QueryPredicate where = 6;
QueryPredicate having = 7;
repeated QueryAggregateOrder order = 8;
QueryArgument first = 9;
QueryArgument all = 10;
QueryArgument after = 11;
repeated QuerySelection selection = 12;
bool residual = 13;
// Internal ordinary-relationship residual: yields ordered membership IDs.
bool rows = 14;
}
message QueryRelationalPlan {
repeated QueryRelationalStage stages = 1;
repeated QueryRelationalTerminal terminals = 2;
}
message QueryReadBinding {
string atom_id = 1;
string interface_revision_id = 2;
string member_id = 3;
string getter_operation_id = 4;
string slot_id = 5;
string edge_type_id = 6;
string projection_id = 7;
bool rpc = 8;
string watch_start_operation_id = 9;
string watch_stop_operation_id = 10;
string field_name = 11;
string value_type_json = 12;
Cardinality cardinality = 13;
string key_type = 14;
}
// Checked schema survives even when no atom currently implements an interface.
message QueryFieldContract {
string interface_revision_id = 1;
string member_id = 2;
string field_name = 3;
string value_type_json = 4;
Cardinality cardinality = 5;
string key_type = 6;
}
message QueryBudgets {
uint32 rows = 1;
uint32 depth = 2;
uint32 result_bytes = 3;
uint32 candidates = 4;
uint32 rpc_calls = 5;
uint32 concurrency = 6;
uint32 deadline_ms = 7;
}
message QueryPlacement {
oneof kind {
bool replica = 1;
string local_source_query_id = 2;
}
}
message InstalledQuery {
string id = 1;
string definition_digest = 2;
string binding_digest = 3;
oneof root {
string object_interface = 4;
string workspace_interface = 14;
}
repeated QuerySelection selection = 5;
repeated QueryReadBinding bindings = 6;
QueryBudgets budgets = 7;
string variables_type_json = 8;
string output_type_json = 9;
map<string, QueryArgument> variable_defaults = 10;
bool watch = 11;
uint32 polling_interval_ms = 12;
bool rpc_predicate_or_order = 13;
repeated string root_atom_ids = 15;
repeated QueryFieldContract fields = 16;
QueryPlacement placement = 17;
}
+204 -7
View File
@@ -3,14 +3,24 @@ syntax = "proto3";
package quixos.orch;
import "camino/api.proto";
import "camino/schema.proto";
import "quixos/package.proto";
import "quixos/refs.proto";
import "quixos/runtime.proto";
service OrchestratorRuntime {
rpc ReadDefinitions(DefinitionRequest) returns (DefinitionPage);
rpc WatchDefinitions(DefinitionRequest) returns (stream DefinitionPage);
rpc ExecuteQuery(camino.QueryRequest) returns (camino.QueryResponse);
rpc WatchQuery(camino.QueryRequest) returns (stream QueryEvent);
rpc TryConform(TryConformRequest) returns (TryConformResponse);
rpc InvokeCapability(InvokeCapabilityRequest) returns (InvokeCapabilityResponse);
rpc EditCapabilityField(EditCapabilityFieldRequest) returns (InvokeCapabilityResponse);
rpc InvokeClassCapability(InvokeClassCapabilityRequest) returns (InvokeCapabilityResponse);
rpc WatchCapability(WatchCapabilityRequest) returns (stream WatchCapabilityEvent);
rpc ConstructObject(ConstructObjectRequest) returns (ConstructObjectResponse);
rpc ResolveOrConstructRelatedObject(ResolveOrConstructRelatedObjectRequest)
returns (ResolveOrConstructRelatedObjectResponse);
rpc GetWorkspace(GetWorkspaceRequest) returns (GetWorkspaceResponse);
rpc ListPackageDescriptors(ListPackageDescriptorsRequest) returns (ListPackageDescriptorsResponse);
rpc ListPackageRuntimes(ListPackageRuntimesRequest) returns (ListPackageRuntimesResponse);
@@ -18,16 +28,133 @@ service OrchestratorRuntime {
rpc CloseActivation(CloseActivationRequest) returns (CloseActivationResponse);
}
// Activated definitions, not runtime metrics or object data. Nested type JSON
// uses the compiler's canonical ValueType/InterfaceRevision public contracts.
message DefinitionSelector {
string kind = 1; // active | atoms | interfaces | conformances | queries | indexes
repeated string ids = 2;
string atom_id = 3;
string interface_revision_id = 4;
string package_revision_id = 5;
}
message DefinitionIdentity {
string workspace_id = 1;
string workspace_revision_id = 2;
string source_root_commit = 3;
string workspace_epoch = 4;
string definition_digest = 5;
}
message DefinitionRequest {
DefinitionSelector selector = 1;
uint32 first = 2; // 1..100; 0 uses 50
string after = 3;
string snapshot = 4; // issued definition_digest; current serving snapshot only
string workspace_id = 5; // required for injected package authority
string workspace_epoch = 6; // required for invocation-bound package calls
}
message ReflectedAtom {
string id = 1;
string display_name = 2;
string constructor_input_type_json = 3;
}
message ReflectedInterface {
string id = 1;
string interface_id = 2;
string display_name = 3;
string contract_json = 4; // canonical interface contract, excluding source location
}
message ReflectedConformance {
string id = 1;
string atom_id = 2;
string interface_revision_id = 3;
string definition_id = 4;
repeated string operation_ids = 5;
repeated ClassCapability class_capabilities = 6;
bool instance = 7;
}
message ReflectedQuery {
string id = 1;
string name = 2;
string package_revision_id = 3;
string definition_digest = 4;
string binding_digest = 5;
string root_kind = 6;
string interface_revision_id = 7;
string variables_type_json = 8;
string output_type_json = 9;
string effects_json = 10;
bool watch = 11;
WorkspaceQueryScope workspace_scope = 12;
}
message ReflectedIndex {
string id = 1;
string contract_json = 2;
}
message DefinitionRecord {
oneof definition {
ReflectedAtom atom = 1;
ReflectedInterface interface = 2;
ReflectedConformance conformance = 3;
ReflectedQuery query = 4;
ReflectedIndex index = 5;
}
}
message DefinitionPage {
DefinitionIdentity identity = 1;
repeated DefinitionRecord records = 2;
string next_cursor = 3;
}
message QueryEvent {
string run_id = 1;
uint64 sequence = 2;
string kind = 3;
camino.QueryResponse snapshot = 4;
repeated camino.QueryPathPart path = 5;
camino.Value value = 6;
string error = 7;
}
// Exact closed interface lookup; no policy selection or competing conformances.
message TryConformRequest {
string object_id = 1;
string interface_revision_id = 2;
}
message TryConformResponse {
// Absent only when this object lacks a known contract. Other failures are errors.
quixos.ConformanceWitness conformance = 1;
}
message ConstructObjectRequest {
string atom_id = 1;
map<string, camino.Value> input = 2;
}
message ConstructObjectResponse { camino.CaminoObject object = 1; }
message ConstructObjectResponse {
camino.CaminoObject object = 1;
}
message ResolveOrConstructRelatedObjectRequest {
string object_id = 1;
string interface_revision_id = 2;
string member_id = 3;
}
message ResolveOrConstructRelatedObjectResponse {
camino.CaminoObject object = 1;
bool constructed = 2;
}
message InvokeCapabilityRequest {
quixos.CapabilityRef capability = 1;
string object_id = 2;
map<string, camino.Value> input = 3;
// Identifies one logical client mutation across the capability and Camino
// layers so a live-value controller can recognize its own confirmation.
string client_mutation_id = 4;
}
message InvokeClassCapabilityRequest {
string conformance_id = 1;
string operation_id = 2;
map<string, camino.Value> input = 3;
}
message InvokeCapabilityResponse {
string invocation_id = 1;
@@ -35,6 +162,21 @@ message InvokeCapabilityResponse {
bool ok = 3;
camino.Value result = 4;
string error = 5;
repeated quixos.runtime.DerivedDependency dependencies = 6;
FieldEditing field_editing = 7;
}
message EditCapabilityFieldRequest {
// The public getter; setter must belong to the same value member.
quixos.CapabilityRef capability = 1;
string object_id = 2;
string setter_operation_id = 3;
string binding_digest = 4;
oneof edit {
camino.CrdtValue update = 5;
camino.Value replacement = 7;
}
string client_mutation_id = 6;
}
message WatchCapabilityRequest {
@@ -50,22 +192,77 @@ message WatchCapabilityEvent {
repeated quixos.runtime.DerivedDependency dependencies = 5;
string error = 6;
bool initial = 7;
FieldEditing field_editing = 8;
}
message GetWorkspaceRequest {}
message GetWorkspaceRequest {
// Revision polling must not download the entire interface graph.
bool include_interface_contracts = 1;
bool include_query_contracts = 2;
}
message GetWorkspaceResponse {
string workspace_id = 1;
string workspace_revision_id = 2;
string source_root_commit = 3;
// Checked constructors whose wire input can be empty. The create panel uses
// class factory conformances instead of this constructor inventory.
repeated string empty_input_constructible_atom_ids = 4;
repeated CapabilityInputContract capability_inputs = 5;
repeated ConstructorInputContract constructor_inputs = 6;
// Exact closed interface contracts used by checked presentation consumers.
string interfaces_json = 7;
repeated ClassCapability class_capabilities = 8;
repeated QueryDescription queries = 9;
uint32 active_query_executions = 10;
}
message QueryDescription {
string id = 1;
string name = 2;
string package_revision_id = 3;
string document = 4;
string schema = 5;
repeated string source_files = 6;
string effects_json = 7;
camino.InstalledQuery plan = 8;
WorkspaceQueryScope workspace_scope = 9;
}
message ClassCapability {
string conformance_id = 1;
string atom_id = 2;
string interface_revision_id = 3;
string definition_id = 4;
string operation_id = 5;
string input_type_json = 6;
string output_type_json = 7;
}
message CapabilityInputContract {
string interface_revision_id = 1;
string operation_id = 2;
string type_json = 3;
}
message ConstructorInputContract {
string atom_id = 1;
string type_json = 2;
}
message ListActivationsRequest {}
message ListPackageDescriptorsRequest {}
message ListPackageDescriptorsResponse { repeated quixos.PackageDescriptor descriptors = 1; }
message ListPackageDescriptorsResponse {
repeated quixos.PackageDescriptor descriptors = 1;
}
message ListPackageRuntimesRequest {}
message ListPackageRuntimesResponse { repeated PackageRuntimeStatus runtimes = 1; }
message ListActivationsResponse { repeated Activation activations = 1; }
message CloseActivationRequest { string activation_id = 1; string reason = 2; }
message CloseActivationResponse { Activation activation = 1; }
message ListPackageRuntimesResponse {
repeated PackageRuntimeStatus runtimes = 1;
}
message ListActivationsResponse {
repeated Activation activations = 1;
}
message CloseActivationRequest {
string activation_id = 1;
string reason = 2;
}
message CloseActivationResponse {
Activation activation = 1;
}
message Activation {
string activation_id = 1;
+51 -1
View File
@@ -5,6 +5,30 @@ package quixos;
message CapabilityRef {
string interface_revision_id = 1;
string operation_id = 2;
// Optional fence for a view acquired through TryConform. Not an authority grant.
ConformanceWitness conformance = 3;
}
// Resolved native editing semantics. A source snapshot alone grants no writer.
message FieldEditing {
string getter_operation_id = 1;
string setter_operation_id = 2;
string document_type = 3;
string binding_digest = 4;
enum Mode {
UNSPECIFIED = 0;
REGISTER = 1;
CRDT = 2;
}
Mode mode = 5;
}
message ConformanceWitness {
string object_id = 1;
string interface_revision_id = 2;
string conformance_id = 3;
string workspace_revision_id = 4;
string workspace_epoch = 5;
}
message PackageExportRef {
@@ -17,9 +41,35 @@ message InjectedDependency {
oneof binding {
string state_slot_id = 2;
EdgeDependency edge = 3;
string receiver_interface_revision_id = 4;
string interface_revision_id = 4;
string constructor_atom_id = 5;
string query_id = 7;
WorkspaceQueryDependency workspace_query = 8;
ReflectionDependency reflection = 9;
}
// Defaults to the invocation receiver. A checked dependency traversal can
// select a related object explicitly before the package runtime starts.
string object_id = 6;
}
message ReflectionDependency {
string workspace_id = 1;
string workspace_epoch = 2;
repeated string selectors = 3;
}
// Scope evidence is not authority by itself. The caller must also hold the
// matching workspace-query grant (or the authenticated workspace user grant).
message WorkspaceQueryScope {
string workspace_id = 1;
string workspace_revision_id = 2;
string workspace_epoch = 3;
string interface_revision_id = 4;
string binding_digest = 5;
}
message WorkspaceQueryDependency {
string query_id = 1;
WorkspaceQueryScope scope = 2;
}
message EdgeDependency {
+30 -1
View File
@@ -9,13 +9,39 @@ service PackageRuntime {
rpc Handshake(HandshakeRequest) returns (HandshakeResponse);
rpc Invoke(InvokeRequest) returns (InvokeResponse);
rpc Watch(WatchRequest) returns (stream WatchEvent);
rpc GetInvocationStatus(InvocationControlRequest) returns (InvocationStatus);
rpc CancelInvocation(InvocationControlRequest) returns (InvocationStatus);
}
message HandshakeRequest { string orch_protocol_version = 1; }
message HandshakeRequest {
string orch_protocol_version = 1;
string nonce = 2;
}
message HandshakeResponse {
string package_revision_id = 1;
string runtime_protocol_version = 2;
repeated string export_ids = 3;
string instance_id = 4;
string authentication_proof = 5;
repeated string capabilities = 6;
}
message InvocationContext {
string workspace_epoch = 1;
string instance_id = 2;
string binding_digest = 3;
string grant = 4;
string session_id = 5;
// Host-selected owner; packages must not invent workspace-local ownership.
string owner_conformance_id = 6;
}
message InvocationControlRequest {
string invocation_id = 1;
}
message InvocationStatus {
string invocation_id = 1;
// unknown, running, cancellation-requested, completed, failed
string state = 2;
}
message InvokeRequest {
@@ -24,11 +50,13 @@ message InvokeRequest {
string object_id = 3;
map<string, camino.Value> input = 4;
repeated quixos.InjectedDependency dependencies = 5;
InvocationContext context = 6;
}
message InvokeResponse {
bool ok = 1;
camino.Value result = 2;
string error = 3;
repeated DerivedDependency dependencies = 4;
}
message WatchRequest {
@@ -37,6 +65,7 @@ message WatchRequest {
string object_id = 3;
map<string, camino.Value> input = 4;
repeated quixos.InjectedDependency dependencies = 5;
InvocationContext context = 6;
}
message DerivedDependency {
+52
View File
@@ -0,0 +1,52 @@
import { parse } from "@babel/parser";
import fs from "node:fs/promises";
import path from "node:path";
/** Enforce the authored bundled-code contract, not a security sandbox. */
export function bundlePolicyErrors(source: string, filename: string): string[] {
const ast = parse(source, { sourceType: "module", plugins: ["typescript", "jsx"] });
const errors: string[] = [];
const visit = (value: unknown) => {
if (Array.isArray(value)) {
value.forEach(visit);
return;
}
if (!value || typeof value !== "object") return;
const n = value as Record<string, any>;
if (typeof n.type !== "string") return;
const fail = (message: string) => errors.push(`${filename}:${n.loc?.start.line ?? 1}: ${message}`);
if (n.type === "MetaProperty" && n.meta.name === "import")
fail("Bundled package code cannot use import.meta; import packaged assets statically");
if (n.type === "Identifier" && ["__dirname", "__filename"].includes(n.name))
fail("Bundled package code cannot depend on module filesystem locations");
if (["CallExpression", "NewExpression"].includes(n.type)) {
if (n.callee?.type === "Identifier" && ["eval", "Function"].includes(n.callee.name))
fail("Dynamic code generation is unsupported in bundled package code");
if (
(n.callee?.type === "Import" || (n.callee?.type === "Identifier" && n.callee.name === "require")) &&
(n.arguments.length !== 1 || n.arguments[0].type !== "StringLiteral")
)
fail("Module imports must have a static string specifier");
}
if (n.type === "ImportExpression" && n.source.type !== "StringLiteral")
fail("Module imports must have a static string specifier");
Object.values(n).forEach(visit);
};
visit(ast);
return errors;
}
export async function checkBundleSources(directory: string): Promise<void> {
const errors: string[] = [];
async function walk(current: string) {
for (const entry of await fs.readdir(current, { withFileTypes: true })) {
if (["gen", "node_modules"].includes(entry.name)) continue;
const file = path.join(current, entry.name);
if (entry.isSymbolicLink()) throw new Error(`Authored source symlinks are unsupported: ${file}`);
if (entry.isDirectory()) await walk(file);
else if (/\.[cm]?[jt]sx?$/.test(entry.name) && !entry.name.endsWith(".d.ts"))
errors.push(...bundlePolicyErrors(await fs.readFile(file, "utf8"), file));
}
}
await walk(directory);
if (errors.length) throw new Error(errors.join("\n"));
}
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env node
import { readFile, writeFile } from "node:fs/promises";
import {
generateTypeScriptBindings,
generatePortableTypeScriptBindings,
generateRemoteTypeScriptBindings,
} from "./index.js";
import path from "node:path";
import { reactPlatformTypes } from "./react-platform.js";
import { generateReactBindings } from "./react.js";
const main = async () => {
const [schema, revision, output, options, ...rest] = process.argv.slice(2);
if (!schema || !revision || !output || rest.length)
throw new Error("usage: quixos-codegen-ts SCHEMA.json PACKAGE_REVISION OUTPUT.ts [OPTIONS.json]");
const config = options ? JSON.parse(await readFile(options, "utf8")) : {};
const contracts = JSON.parse(await readFile(schema, "utf8"));
if ([config.portable, config.remote, config.react].filter(Boolean).length > 1)
throw new Error("Portable, remote and React bindings are separate artifacts");
if (config.remote !== undefined && config.remote !== true) throw new Error("Remote binding mode must be true");
if (config.portable && typeof config.portable.target !== "string")
throw new Error("Portable bindings require an explicit target");
const generated = config.remote
? generateRemoteTypeScriptBindings(
contracts,
revision,
config.runtimeModule ? { runtimeModule: config.runtimeModule } : {},
)
: config.portable
? generatePortableTypeScriptBindings(contracts, revision, {
...config.portable,
...(config.runtimeModule ? { runtimeModule: config.runtimeModule } : {}),
})
: generateTypeScriptBindings(contracts, revision, config);
await writeFile(output, generated);
if (config.react) {
await writeFile(path.join(path.dirname(output), "web-studio-react-runtime.d.ts"), reactPlatformTypes);
await writeFile(
path.join(path.dirname(output), "react-props.gen.ts"),
generateReactBindings(contracts, revision, config.react.propsExports, config.react.components),
);
}
};
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
+109
View File
@@ -0,0 +1,109 @@
import type { InterfaceRevision, ValueType } from "../capability-model/types.js";
/** Host clients have no package receiver, but must use the same checked
* interface signatures and argument framing as generated package ports. */
export const generateClientContracts = (
interfaces: InterfaceRevision[],
messages: Record<string, string>,
qualified = false,
) => {
if (interfaces.some((entry) => entry.template))
throw new Error("Host contracts require closed interface applications, not generic definitions");
const type = (value: ValueType): string => {
switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin":
return value.name === "unit" ? "undefined" : "string";
case "scalar":
return {
bool: "boolean",
string: "string",
bytes: "Uint8Array",
int32: "number",
uint32: "number",
double: "number",
int64: "bigint",
uint64: "bigint",
}[value.name];
case "object-ref":
return qualified
? `CapabilityReference<${JSON.stringify(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`
: `{readonly $quixosRef: string}`;
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional":
return `(${type(value.value)} | null)`;
case "list":
return `Array<${type(value.value)}>`;
case "record":
return `{${Object.entries(value.fields)
.map(([name, field]) => `${JSON.stringify(name)}${field.kind === "optional" ? "?" : ""}: ${type(field)}`)
.join("; ")}}`;
case "message": {
const binding = messages[value.descriptorId];
if (!binding) throw new Error(`Missing host message type ${value.descriptorId}`);
return binding;
}
}
};
const operations = interfaces.flatMap((iface) =>
iface.members.flatMap((member) =>
member.operations
.filter((operation) => operation.mode === "call")
.map((operation) => ({ ...operation, interfaceRevisionId: iface.revisionId })),
),
);
if (qualified) {
const contracts = interfaces.map((iface) => {
const members = iface.members.flatMap((member) =>
member.operations
.filter((op) => op.mode === "call")
.map((op) => ` ${JSON.stringify(op.id)}: {input: ${type(op.inputType)}; output: ${type(op.outputType)}};`),
);
return `${JSON.stringify(iface.revisionId)}: {\n${members.join("\n")}\n}`;
});
if (new Set(interfaces.map((iface) => iface.revisionId)).size !== interfaces.length)
throw new Error("Duplicate closed interface identity");
return (
`// Generated closed capability contracts. Dispatch by interface AND operation.\n` +
`declare const referenceType: unique symbol;\nexport type CapabilityReference<T extends string> = {readonly $quixosRef: string; readonly [referenceType]: T};\n` +
`export type CapabilityContracts = {${contracts.join(";\n")}};\n` +
`export type CapabilityInput<I extends keyof CapabilityContracts, O extends keyof CapabilityContracts[I]> = CapabilityContracts[I][O] extends {input: infer T} ? T : never;\n` +
`export type CapabilityOutput<I extends keyof CapabilityContracts, O extends keyof CapabilityContracts[I]> = CapabilityContracts[I][O] extends {output: infer T} ? T : never;\n` +
`export const capabilityApplications = ${JSON.stringify(Object.fromEntries(interfaces.map((iface) => [iface.revisionId, { definitionId: iface.application?.definitionId ?? iface.revisionId, arguments: iface.application?.arguments ?? [], ...(iface.application?.self ? { self: iface.application.self } : {}) }])))} as const;\n`
);
}
if (new Set(operations.map((entry) => entry.id)).size !== operations.length)
throw new Error(
"Host operation IDs are ambiguous across interfaces; select a closed interface application explicitly",
);
return (
`// Generated from checked QX interfaces. Regenerate with scripts/generate-platform-contracts.mjs.\n` +
`export type PlatformInputs = {\n${operations.map((operation) => ` ${JSON.stringify(operation.id)}: ${type(operation.inputType)};`).join("\n")}\n};\n` +
`export const platformOperations = ${JSON.stringify(
Object.fromEntries(
operations.map((operation) => [
operation.id,
{
interfaceRevisionId: operation.interfaceRevisionId,
input:
operation.inputType.kind === "builtin" && operation.inputType.name === "unit"
? "unit"
: ["record", "message"].includes(operation.inputType.kind)
? "fields"
: "value",
},
]),
),
null,
2,
)} as const;\n`
);
};
/** New consumers use exact closed interface identities; operation IDs alone are not unique. */
export const generateAppliedClientContracts = (
interfaces: InterfaceRevision[],
messages: Record<string, string> = {},
) => generateClientContracts(interfaces, messages, true);
+190
View File
@@ -0,0 +1,190 @@
import type { GenericPackageExport, GenericDependencyPort } from "../capability-model/generic-packages.js";
import type { InterfaceRevision, ValueType } from "../capability-model/types.js";
import type {
ValueTypeExpression,
ObjectTypeExpression,
TypeArgumentExpression,
TypeParameter,
ValueAliasDefinition,
} from "../capability-model/generics.js";
/** Universal source contracts. Only closed exports get executable codecs. */
export const genericImplementationType = (
definition: GenericPackageExport,
interfaces: InterfaceRevision[],
concrete: (type: ValueType) => string,
queryPort: (requirement: Extract<GenericDependencyPort["requirement"], { kind: "query" }>) => string = () => {
throw new Error("Query binding schema required");
},
): string => {
const names = new Map(definition.parameters.map((parameter, index) => [parameter.id, `T${index}`]));
type Scope = { arguments: Map<string, string>; aliases: ValueAliasDefinition[] };
const rootScope = (): Scope => ({ arguments: new Map(), aliases: definition.aliases });
const object = (entries: [string, string][]) =>
`{${entries.map(([key, value]) => `${JSON.stringify(key)}:${value}`).join(";")}}`;
const target = (type: ObjectTypeExpression, scope: Scope): string => {
if (type.kind === "parameter") {
const bound = scope.arguments.get(type.parameterId);
if (bound) return bound;
const name = names.get(type.parameterId);
if (!name) throw new Error(`Unbound object parameter ${type.parameterId}`);
return name;
}
if (type.kind === "atom") return JSON.stringify(`atom:${type.atomId}`);
if (type.kind === "interface") return JSON.stringify(`interface:${type.interfaceRevisionId}`);
if (type.kind === "application")
return `QxApplied<${JSON.stringify(type.application.definitionId)}, [${type.application.arguments.map((arg) => argument(arg, scope)).join(",")}]>`;
throw new Error("Generic package Self must be expressed as an explicit object parameter");
};
const argument = (arg: TypeArgumentExpression, scope: Scope): string =>
arg.kind === "value" ? value(arg.type, scope) : target(arg.target, scope);
const bind = (parameters: TypeParameter[], args: TypeArgumentExpression[], scope: Scope): Scope => {
if (parameters.length !== args.length) throw new Error("Wrong generic arity during code generation");
const result = new Map(scope.arguments);
// Render arguments in their original lexical scope before introducing binders.
const rendered = args.map((arg) => argument(arg, scope));
parameters.forEach((parameter, index) => result.set(parameter.id, rendered[index]));
return { ...scope, arguments: result };
};
const value = (type: ValueTypeExpression, scope: Scope, depth = 0): string => {
if (depth > 128) throw new Error("Generic type expansion exceeds depth limit");
switch (type.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "parameter": {
const bound = scope.arguments.get(type.parameterId);
if (bound) return bound;
const name = names.get(type.parameterId);
if (!name) throw new Error(`Unbound value parameter ${type.parameterId}`);
return name;
}
case "object-ref":
return `QxObjectRef<${target(type.expectation, scope)}>`;
case "record":
return object(Object.entries(type.fields).map(([name, field]) => [name, value(field, scope, depth + 1)]));
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional":
return `(${value(type.value, scope, depth + 1)} | null)`;
case "list":
return `Array<${value(type.value, scope, depth + 1)}>`;
case "alias": {
const alias = scope.aliases.find((entry) => entry.id === type.definitionId);
if (!alias) throw new Error(`Missing alias ${type.definitionId}`);
return value(alias.body, bind(alias.parameters, type.arguments, scope), depth + 1);
}
default:
return concrete(type);
}
};
const params = (type: ValueTypeExpression, scope: Scope) =>
type.kind === "builtin" && type.name === "unit" ? "" : `input:${value(type, scope)}`;
const port = (entry: GenericDependencyPort): string => {
const requirement = entry.requirement,
scope = rootScope();
switch (requirement.kind) {
case "reflection":
return `QxReflectionPort<${requirement.selectors.map((value) => JSON.stringify(value)).join(" | ")}>`;
case "state":
return object([
...requirement.primitives.map((primitive): [string, string] => {
if (primitive === "read") return ["get", `()=>Promise<${value(requirement.valueType, scope)}>`];
if (primitive === "write") return ["set", `(value:${value(requirement.valueType, scope)})=>Promise<void>`];
throw new Error(`Unsupported generic state primitive ${primitive}`);
}),
...(requirement.primitives.includes("read")
? [["live", "()=>Promise<QxLiveValue>"] as [string, string]]
: []),
]);
case "edge": {
const ref = `QxObjectRef<${target(requirement.target, scope)}>`;
const methods = requirement.primitives.map((primitive): [string, string] => {
if (primitive === "resolve") return ["resolve", `()=>Promise<Array<${ref}>>`];
if (primitive === "connect" || primitive === "disconnect")
return [primitive, `(target:${ref})=>Promise<void>`];
throw new Error(`Unsupported generic edge primitive ${primitive}`);
});
if (requirement.primitives.includes("resolve"))
methods.push(["collection", `()=>Promise<RelationshipCollection<${ref}>>`]);
if (
["resolve", "connect", "disconnect"].every((primitive) =>
requirement.primitives.includes(primitive as "resolve"),
)
)
methods.push([
"replace",
`(entries:RelationshipEntry<${ref}>[],expectedRevision:bigint)=>Promise<RelationshipCollection<${ref}>>`,
]);
return object(methods);
}
case "query":
return queryPort(requirement);
case "creation":
throw new Error("Creation ports require the portable transaction context generator (LF-09)");
case "constructor":
return object([
[
"construct",
`(${params(requirement.inputType, scope)})=>Promise<QxObjectRef<${target(requirement.target, scope)}>>`,
],
]);
case "interface": {
const contract = interfaces.find((entry) => entry.revisionId === requirement.application.definitionId);
if (!contract) throw new Error(`Missing generic port contract ${requirement.application.definitionId}`);
const local = {
...bind(contract.template?.parameters ?? [], requirement.application.arguments, scope),
aliases: contract.template?.aliases ?? [],
};
const operations = (contract.template?.members ?? contract.members).flatMap((member) =>
member.operations
.filter(
(op) =>
op.mode === "call" &&
op.scope !== "class" &&
(!requirement.operations || requirement.operations.includes(op.id)),
)
.map((op) => ({ ...op, name: `${member.displayName}.${op.displayName}` })),
);
const methods: [string, string][] = [
["objectId", `QxObjectRef<${target({ kind: "application", application: requirement.application }, scope)}>`],
["live", object(operations.map((op) => [op.name, `(${params(op.inputType, local)})=>Promise<QxLiveValue>`]))],
...operations.map(
(op) =>
[op.name, `(${params(op.inputType, local)})=>Promise<${value(op.outputType, local)}>`] as [
string,
string,
],
),
];
return object([...methods, ["contract", `QxInterfaceContract<${object(methods)}>`]]);
}
}
};
const declarations = definition.parameters
.map((parameter) => `${names.get(parameter.id)}${parameter.kind === "object" ? " extends string" : ""}`)
.join(",");
const receiver = definition.receiverRequirement;
const context = object([
["conform", "QxConformer"],
...(definition.kind === "function"
? []
: [
[
"objectId",
`QxObjectRef<${receiver.kind === "target" ? target(receiver.target, rootScope()) : "string"}>`,
] as [string, string],
]),
["input", value(definition.inputType, rootScope())],
["ports", object(definition.dependencyPorts.map((entry) => [entry.displayName, port(entry)]))],
]);
const contextWithLifecycle =
definition.kind === "function"
? `${context} & {signal?: AbortSignal}`
: `${context} & QxContextLifecycle<${context} & {signal?: AbortSignal}>`;
const result = value(definition.eventType ?? definition.outputType, rootScope());
const handler = `<${declarations}>(context:${contextWithLifecycle})=>${result}|Promise<${result}>`;
const derived = `{kind:"derived";get:${handler}}`;
return definition.eventType
? derived
: `(${handler})${definition.kind === "operation" && definition.mode === "call" ? ` | ${derived}` : ""}`;
};
+448
View File
@@ -0,0 +1,448 @@
import type {
InterfaceRevision,
PackageRevision,
ValueType,
DependencyPort,
WorkspaceRevision,
} from "../capability-model/types.js";
import type { CompiledCapabilityResourceRepository } from "../capability-language/assembly.js";
import { genericImplementationType } from "./generics.js";
import { capabilityId } from "../capability-model/types.js";
/** Source-free package compilation inputs. Nominal references do not import
* implementations or concrete workspace conformances. */
export type BindingContractSchema = {
format: "quixos-bindings";
version: 1;
interfaces: Omit<InterfaceRevision, "source">[];
packages: Pick<PackageRevision, "revisionId" | "exports" | "genericExports">[];
};
/** Portable generator input. New backends consume this instead of the parser or TS runtime. */
export type BindingSchema = {
format: "quixos-bindings";
version: 1;
interfaces: InterfaceRevision[];
interfaceTemplates?: InterfaceRevision[];
packages: PackageRevision[];
};
export const bindingSchema = (
compiled: Pick<CompiledCapabilityResourceRepository, "resources"> &
Partial<Pick<CompiledCapabilityResourceRepository, "capabilityResources">>,
): BindingSchema => ({
format: "quixos-bindings",
version: 1,
interfaceTemplates: (compiled.capabilityResources ?? compiled.resources).flatMap((node) =>
node.resource.kind === "interface" && node.resource.revision.template ? [node.resource.revision] : [],
),
interfaces: [
...new Map(
(compiled.capabilityResources ?? compiled.resources)
.flatMap((node) => [
...(node.resource.kind === "interface" ? [node.resource.revision] : []),
...(node.resource.kind === "build" ? [] : (node.resource.specializations ?? [])),
])
.filter((entry) => !entry.template)
.map((entry) => [entry.revisionId, entry]),
).values(),
],
packages: (compiled.capabilityResources ?? compiled.resources).flatMap((node) =>
node.resource.kind === "package" ? [node.resource.revision] : [],
),
});
export const specializeBindingSchema = (
base: BindingSchema,
workspace: WorkspaceRevision,
revisionId: string,
): BindingSchema => {
const pkg = workspace.packageImports.find((entry) => entry.revisionId === revisionId);
if (!pkg) throw new Error(`Missing candidate package ${revisionId}`);
const needed = new Set<string>();
const visit = (value: unknown): void => {
if (!value || typeof value !== "object") return;
if ("interfaceRevisionId" in value && typeof value.interfaceRevisionId === "string")
needed.add(value.interfaceRevisionId);
Object.values(value).forEach(visit);
};
visit(pkg.exports);
const interfaces = new Map<string, InterfaceRevision>(base.interfaces.map((entry) => [entry.revisionId, entry]));
for (const id of needed) {
const iface = workspace.interfaceImports.find((entry) => entry.revisionId === id);
if (iface) {
interfaces.set(id, iface);
visit(iface.members);
}
}
return {
...base,
interfaces: [...interfaces.values()],
packages: base.packages.map((entry) => {
const candidate = workspace.packageImports.find((value) => value.revisionId === entry.revisionId);
if (!candidate) throw new Error(`Missing dependency package ${entry.revisionId}`);
return candidate;
}),
};
};
export type TypeScriptBindingOptions = {
runtimeModule?: string;
/** Each export must implement MessageBinding<T>, providing both TS type and wire codec. */
messages?: Record<string, { module: string; export: string }>;
};
export function generatePackageDescriptor(schema: BindingSchema, revisionId: string): string {
const pkg = schema.packages.find((entry) => entry.revisionId === revisionId);
if (!pkg) throw new Error(`Unknown package revision ${revisionId}`);
return (
`# Generated from the checked package contract\npackage_id: ${JSON.stringify(pkg.packageId)}\npackage_revision_id: ${JSON.stringify(pkg.revisionId)}\nruntime_protocol_version: "quixos-capabilities-v1"\n` +
pkg.exports
.map(
(entry) =>
`exports: { export_id: ${JSON.stringify(entry.id)} runtime_symbol: ${JSON.stringify(entry.displayName)} }\n`,
)
.join("")
);
}
const q = JSON.stringify;
const object = (entries: [string, string][]) =>
`{ ${entries.map(([key, value]) => `${q(key)}: ${value}`).join("; ")} }`;
const unit = (type: ValueType) => type.kind === "builtin" && type.name === "unit";
export const generateTypeScriptBindings = (
schema: BindingSchema,
packageRevisionId: string,
options: TypeScriptBindingOptions = {},
) => {
if (schema.format !== "quixos-bindings" || schema.version !== 1)
throw new Error("Unsupported binding schema version");
if (schema.interfaces.some((entry) => entry.template))
throw new Error("Package bindings require closed interface applications, not generic definitions");
const pkg = schema.packages.find((entry) => entry.revisionId === packageRevisionId);
if (!pkg) throw new Error(`Unknown package revision ${packageRevisionId}`);
const messages = new Map<string, string>();
const type = (value: ValueType): string => {
switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin":
return value.name === "unit" ? "null" : "QxWatchHandle";
case "scalar":
return {
bool: "boolean",
bytes: "Uint8Array",
string: "string",
int64: "bigint",
uint64: "bigint",
double: "number",
int32: "number",
uint32: "number",
}[value.name];
case "object-ref":
return `QxObjectRef<${q(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`;
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional":
return `(${type(value.value)} | null)`;
case "list":
return `Array<${type(value.value)}>`;
case "record":
return `{ ${Object.entries(value.fields)
.map(([name, field]) => `${q(name)}: ${type(field)}`)
.join("; ")} }`;
case "message": {
if (!options.messages?.[value.descriptorId])
throw new Error(`Missing TypeScript message binding for ${value.descriptorId}`);
if (!messages.has(value.descriptorId)) messages.set(value.descriptorId, `message${messages.size}`);
return `BindingValue<typeof ${messages.get(value.descriptorId)}>`;
}
}
};
const ref = (target: { kind: "atom"; atomId: string } | { kind: "interface"; interfaceRevisionId: string }) =>
`QxObjectRef<${q(target.kind === "atom" ? `atom:${target.atomId}` : `interface:${target.interfaceRevisionId}`)}>`;
const params = (input: ValueType) => (unit(input) ? "" : `input: ${type(input)}`);
const port = (entry: DependencyPort): { type: string; spec: unknown } => {
const requirement = entry.requirement;
switch (requirement.kind) {
case "reflection":
return {
type: `QxReflectionPort<${requirement.selectors.map((value) => q(value)).join(" | ")}>`,
spec: { kind: "reflection", id: entry.id, selectors: requirement.selectors },
};
case "query": {
const query = schema.packages
.find((candidate) => candidate.revisionId === requirement.packageRevisionId)
?.checkedQueries?.find((query) => query.declaration.id === requirement.queryId);
if (!query) throw new Error(`Missing checked query ${requirement.packageRevisionId}:${requirement.queryId}`);
return {
type: object([
["execute", `(variables: ${type(query.variables)}) => Promise<${type(query.output)}>`],
...(query.declaration.watch
? ([
[
"watch",
`(variables: ${type(query.variables)}, signal: AbortSignal) => AsyncIterable<QxQuerySnapshot<${type(query.output)}>>`,
],
] as [string, string][])
: []),
]),
spec: {
kind: "query",
id: entry.id,
definitionDigest: query.definitionDigest,
variables: query.variables,
output: query.output,
watch: query.declaration.watch,
},
};
}
case "state": {
const methods = requirement.primitives.map((primitive): [string, string] => {
if (primitive === "read") return ["get", `() => Promise<${type(requirement.valueType)}>`];
if (primitive === "write") return ["set", `(value: ${type(requirement.valueType)}) => Promise<void>`];
throw new Error(`State primitive ${primitive} is not supported by the TypeScript runtime binding yet`);
});
if (requirement.primitives.includes("read")) methods.push(["live", "() => Promise<QxLiveValue>"]);
return { type: object(methods), spec: { ...requirement, id: entry.id } };
}
case "edge": {
const methods = requirement.primitives.map((primitive): [string, string] => {
if (primitive === "resolve") return [primitive, `() => Promise<Array<${ref(requirement.target)}>>`];
if (primitive === "connect" || primitive === "disconnect")
return [primitive, `(target: ${ref(requirement.target)}) => Promise<void>`];
throw new Error(`Edge primitive ${primitive} is not supported by the TypeScript runtime binding yet`);
});
if (requirement.primitives.includes("resolve"))
methods.push(["collection", `() => Promise<RelationshipCollection<${ref(requirement.target)}>>`]);
if (
["resolve", "connect", "disconnect"].every((primitive) =>
requirement.primitives.includes(primitive as "resolve"),
)
)
methods.push([
"replace",
`(entries: RelationshipEntry<${ref(requirement.target)}>[], expectedRevision: bigint) => Promise<RelationshipCollection<${ref(requirement.target)}>>`,
]);
return { type: object(methods), spec: { kind: "edge", id: entry.id, primitives: requirement.primitives } };
}
case "interface": {
const contract = schema.interfaces.find(
(candidate) => candidate.revisionId === requirement.interfaceRevisionId,
);
if (!contract) throw new Error(`Missing imported interface contract ${requirement.interfaceRevisionId}`);
// Streaming ports need a future streaming ABI; ordinary calls are fully typed today.
const operations = contract.members.flatMap((member) =>
member.operations
.filter(
(operation) =>
operation.mode === "call" &&
operation.scope !== "class" &&
(!requirement.operations || requirement.operations.includes(operation.id)),
)
.map((operation) => ({ ...operation, name: `${member.displayName}.${operation.displayName}` })),
);
const methods: [string, string][] = [
["objectId", ref({ kind: "interface", interfaceRevisionId: requirement.interfaceRevisionId })],
[
"live",
object(
operations.map((operation) => [
operation.name,
`(${params(operation.inputType)}) => Promise<QxLiveValue>`,
]),
),
],
...operations.map((operation): [string, string] => [
operation.name,
`(${params(operation.inputType)}) => Promise<${type(operation.outputType)}>`,
]),
];
return {
type: object([...methods, ["contract", `QxInterfaceContract<${object(methods)}>`]]),
spec: {
kind: "interface",
id: entry.id,
interfaceRevisionId: requirement.interfaceRevisionId,
operations: Object.fromEntries(
operations.map(({ name, id, inputType, outputType }) => [name, { id, inputType, outputType }]),
),
},
};
}
case "creation":
throw new Error("Creation ports require the portable transaction context generator (LF-09)");
case "constructor": {
const input = requirement.inputType;
if (!input)
throw new Error(
`Constructor port ${entry.id} needs an explicit input contract: add 'input TYPE' after ${requirement.atomId} in QX`,
);
return {
type: object([
["construct", `(${params(input)}) => Promise<${ref({ kind: "atom", atomId: requirement.atomId })}>`],
]),
spec: { kind: "constructor", id: entry.id, inputType: input },
};
}
}
};
const exports = [...pkg.exports].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
const names = new Set<string>();
const specs: Record<string, unknown> = {};
const contexts: [string, string][] = [];
const handlers: [string, string][] = [];
const results: [string, string][] = [];
for (const entry of exports) {
if (names.has(entry.displayName)) throw new Error(`Duplicate export name ${entry.displayName}`);
names.add(entry.displayName);
const ports = entry.dependencyPorts.map((dependency) => ({ name: dependency.displayName, ...port(dependency) }));
if (new Set(ports.map((p) => p.name)).size !== ports.length)
throw new Error(`Duplicate dependency name in ${entry.displayName}`);
const receiver =
entry.kind === "constructor"
? ref({ kind: "atom", atomId: entry.constructsAtom })
: entry.kind === "operation" && entry.receiverRequirement.kind === "exact-atom"
? ref({ kind: "atom", atomId: entry.receiverRequirement.atomId })
: entry.kind === "operation" && entry.receiverRequirement.kind === "all-interfaces"
? `QxObjectRef<${entry.receiverRequirement.interfaceRevisionIds.map((id) => q(`interface:${id}`)).join(" | ") || "never"}>`
: "QxObjectRef<string>";
const contextShape = object([
["conform", "QxConformer"],
...(entry.kind === "function" ? [] : [["objectId", receiver] as [string, string]]),
["input", type(entry.inputType)],
["ports", object(ports.map((port) => [port.name, port.type]))],
]);
contexts.push([
entry.displayName,
entry.kind === "function"
? `${contextShape} & {signal?: AbortSignal}`
: `${contextShape} & QxContextLifecycle<${contextShape} & {signal?: AbortSignal}>`,
]);
const event = entry.kind === "operation" ? entry.eventType : undefined;
const contextType = `Contexts[${q(entry.displayName)}]`;
const outputType = type(event ?? entry.outputType);
results.push([entry.displayName, outputType]);
// Watch-start handlers produce events through the runtime's derived stream protocol.
if (!entry.application)
handlers.push([
entry.displayName,
event
? `QxDerived<${contextType}, ${outputType}>`
: `QxHandler<${contextType}, ${outputType}>${entry.kind === "operation" && entry.mode === "call" ? ` | QxDerived<${contextType}, ${outputType}>` : ""}`,
]);
specs[entry.displayName] = {
...(entry.kind === "function" ? { receiver: "none" } : {}),
inputType: entry.inputType,
outputType: entry.outputType,
...(event ? { eventType: event } : {}),
ports: Object.fromEntries(ports.map((port) => [port.name, port.spec])),
};
}
for (const definition of pkg.genericExports ?? []) {
handlers.push([
definition.displayName,
genericImplementationType(
definition,
[...schema.interfaces, ...(schema.interfaceTemplates ?? [])],
type,
(requirement) => port({ id: capabilityId.dependencyPort("query"), displayName: "query", requirement }).type,
),
]);
}
// Unused imported interfaces must not introduce new message-codec obligations.
// A used port still fails above if its own required codec is missing.
const hasCodec = (value: ValueType): boolean => {
if (value.kind === "message") return !!options.messages?.[value.descriptorId];
if (value.kind === "variant") return false;
if (value.kind === "record") return Object.values(value.fields).every(hasCodec);
if (value.kind === "list" || value.kind === "optional") return hasCodec(value.value);
return true;
};
const contracts = schema.interfaces
.filter((iface) =>
iface.members.every((member) =>
member.operations
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
.every((operation) => hasCodec(operation.inputType) && hasCodec(operation.outputType)),
),
)
.map((iface) => {
const generated = port({
id: capabilityId.dependencyPort("contract"),
displayName: iface.displayName,
requirement: { kind: "interface", interfaceRevisionId: iface.revisionId },
});
const spec = generated.spec as { operations: unknown };
return {
iface,
type: generated.type,
code: `defineQxInterfaceContract<${generated.type}>(${q(iface.revisionId)}, ${JSON.stringify(spec.operations)})`,
};
});
const imports = [...messages].map(([id, alias]) => {
const binding = options.messages![id]!;
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(binding.export))
throw new Error(`Invalid message binding export ${binding.export}`);
return `import { ${binding.export} as ${alias} } from ${q(binding.module)};`;
});
const queryVariables = object(
(pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, type(query.variables)]),
);
const queryResults = object(
(pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, type(query.output)]),
);
const queryDescriptors = object(
(pkg.checkedQueries ?? []).map((query) => [
query.declaration.displayName,
`QxQueryDescriptor<QueryVariables[${q(query.declaration.displayName)}], QueryResults[${q(query.declaration.displayName)}], ${q(query.declaration.root)}>`,
]),
);
const signatures = `${object(contexts)} ${object(handlers)} ${object(results)} ${queryVariables} ${queryResults} ${queryDescriptors} ${contracts.map((entry) => entry.type).join(" ")}`;
const typeImports = [
"BindingValue",
"QxObjectRef",
"QxWatchHandle",
"QxHandler",
"QxDerived",
"QxContextLifecycle",
"QxConformer",
"QxInterfaceContract",
"QxLiveValue",
"RelationshipCollection",
"RelationshipEntry",
"QxQuerySnapshot",
"QxReflectionPort",
"QxQueryDescriptor",
].filter((name) => new RegExp(`\\b${name}\\b`).test(signatures));
return (
`// Generated by quixos-codegen-ts. Do not edit. Binding ABI version 1.\n` +
`import { ${contracts.length ? "defineQxInterfaceContract, " : ""}${exports.length ? "bindQxHandler, " : ""}${[...typeImports, "QxHandlerSpec", "QxMessages"].map((name) => `type ${name}`).join(", ")} } from ${q(options.runtimeModule ?? "@quixos/camino-package-runtime")};\n` +
imports.join("\n") +
`\nexport const packageRevisionId = ${q(pkg.revisionId)};\n` +
`declare const appliedType: unique symbol;\ntype QxApplied<Definition extends string, Arguments extends readonly unknown[]> = string & {readonly [appliedType]: (value: [Definition, Arguments]) => [Definition, Arguments]};\n` +
`export type Contexts = ${object(contexts)};\nexport type Results = ${object(results)};\nexport type Implementation = ${object(handlers)};\n` +
`export type QueryVariables = ${queryVariables};\nexport type QueryResults = ${queryResults};\n` +
`export const queries: ${queryDescriptors} = ${JSON.stringify(Object.fromEntries((pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, { id: `${pkg.revisionId}:${query.declaration.id}`, definitionDigest: query.definitionDigest, root: query.declaration.root, variables: query.variables, output: query.output, watch: query.declaration.watch }])), null, 2)} as const;\n` +
`export const contracts = {${contracts.map((entry) => `${q(entry.iface.revisionId)}: ${entry.code}`).join(",\n")}};\n` +
`export const interfaces = {${contracts
.filter((entry) => contracts.filter((other) => other.iface.displayName === entry.iface.displayName).length === 1)
.map((entry) => `${q(entry.iface.displayName)}: contracts[${q(entry.iface.revisionId)}]`)
.join(",\n")}};\n` +
`const messages = { ${[...messages].map(([id, alias]) => `${q(id)}: ${alias}`).join(", ")} } satisfies QxMessages;\n` +
`const specs = ${JSON.stringify(specs, null, 2)} satisfies Record<string, QxHandlerSpec>;\n` +
`export const createRuntime = (implementation: Implementation) => ({\n packageRevisionId,\n exports: {\n` +
exports
.map(
(entry) =>
` ${q(entry.id)}: bindQxHandler(specs[${q(entry.displayName)}], implementation[${q(entry.application ? pkg.genericExports!.find((definition) => definition.id === entry.application!.exportId)!.displayName : entry.displayName)}], messages),`,
)
.join("\n") +
`\n },\n});\n`
);
};
export {
generatePortableTypeScriptBindings,
generateRemoteTypeScriptBindings,
portableValueSchema,
PortableBindingError,
type PortableBindingOptions,
} from "./portable.js";
export * from "./portable-client.js";
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env node
import { readFile, writeFile } from "node:fs/promises";
import { generatePortableClientBindings } from "./portable-client.js";
async function main() {
const [plan, output, target = "target:org.quixos.execution.javascript@1", ...rest] = process.argv.slice(2);
if (!plan || !output || rest.length)
throw Error("usage: quixos-codegen-client EXECUTION_PLAN.json OUTPUT.ts [TARGET]");
await writeFile(output, generatePortableClientBindings(JSON.parse(await readFile(plan, "utf8")), { target }));
}
void main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
+128
View File
@@ -0,0 +1,128 @@
import { compileKernelProgram } from "../execution-world/kernel.js";
import { hexDigest, type ExecutionPlan } from "../execution-world/plan.js";
import { resultInterfaces } from "../execution-world/projections.js";
import { portableValueSchema, tsType, PortableBindingError } from "./portable.js";
import type { QuerySelection } from "../query/types.js";
const conditional = (selections: readonly QuerySelection[]): boolean =>
selections.some((selection) => selection.conditions.length > 0 || conditional(selection.selection));
/** Closed-world app descriptors. Handler bindings and application acquisition share the same public identities. */
export function generatePortableClientBindings(
plan: ExecutionPlan,
options: { target: string; runtimeModule?: string },
): string {
if (hexDigest(plan.definition) !== plan.digest) throw new PortableBindingError("client", "Invalid plan digest");
const executionPlan = (compileKernelProgram(plan) as { plan: string }).plan;
const workspace = plan.definition.workspace,
atoms = new Map<string, string[]>(),
unavailable: { key: string; reason: string }[] = [];
const seen = new Set<string>();
for (const operation of plan.definition.operations) {
if (operation.execution.kind === "remote" || !operation.availableTargets.includes(options.target)) {
unavailable.push({ key: operation.key, reason: "OPERATION_NOT_PORTABLE" });
continue;
}
let input: string, output: string;
try {
input = tsType(operation.input);
output = tsType(operation.output);
} catch (error) {
if (!(error instanceof PortableBindingError)) throw error;
unavailable.push({ key: operation.key, reason: "UNSUPPORTED_OPERATION_CODEC" });
continue;
}
const atom = workspace.atoms.find((a) => a.id === operation.atomId)!;
const iface = workspace.interfaceImports.find((i) => i.revisionId === operation.interfaceRevisionId)!;
const member = iface.members.find((m) => m.operations.some((o) => o.id === operation.operationId))!;
const facet = member.operations.find((o) => o.id === operation.operationId)!;
const name = `${iface.displayName}.${member.displayName}.${facet.displayName}`;
const alias = JSON.stringify([atom.displayName, name]);
if (seen.has(alias)) throw new PortableBindingError(alias, "Ambiguous application descriptor alias");
seen.add(alias);
const roots = resultInterfaces(plan, operation.key);
const descriptor = {
key: operation.key,
plan: executionPlan,
effect: operation.execution.kind,
input: portableValueSchema(operation.input),
output: portableValueSchema(operation.output),
resultInterfaces: roots,
};
const list = atoms.get(atom.displayName) ?? [];
list.push(
`${JSON.stringify(name)}: defineOperationDescriptor<${input}, ${output}, ${roots.map((r) => JSON.stringify(r)).join(" | ") || "never"}, ${JSON.stringify(operation.execution.kind)}>(${JSON.stringify(descriptor)})`,
);
atoms.set(atom.displayName, list);
}
const queries = new Map<string, string[]>();
const views = new Map<string, string[]>();
const serverProjections = new Map<string, string[]>();
const results = new Map<string, string[]>();
for (const query of plan.definition.queries) {
const declaration = query.query.checked.declaration;
const pkg = workspace.packageImports.find((p) => p.revisionId === query.query.packageRevisionId)!;
const alias = JSON.stringify([pkg.displayName, declaration.displayName]);
if (seen.has(`query:${alias}`)) throw new PortableBindingError(alias, "Ambiguous query alias");
seen.add(`query:${alias}`);
const addResult = () => {
if (conditional(query.query.checked.selection)) {
unavailable.push({ key: query.id, reason: "CONDITIONAL_RESULT_DECODER_UNSUPPORTED" });
return;
}
const decoders = results.get(pkg.displayName) ?? [];
decoders.push(
`${JSON.stringify(declaration.displayName)}: (data: Json): ${tsType(query.query.checked.output)} => decodeArgument(${JSON.stringify(portableValueSchema(query.query.checked.output))}, { codec: "camino-value-v1", usage: "argument", value: data }, { workspace: workspaceId(${JSON.stringify(workspace.workspaceId)}), limits: { bytes: ${declaration.budgets.resultBytes + 128}, nodes: 100000, depth: 64 } }) as ${tsType(query.query.checked.output)}`,
);
results.set(pkg.displayName, decoders);
};
if (!declaration.placement) {
const operations = query.operations.map(
(key) => plan.definition.operations.find((operation) => operation.key === key)!,
);
if (
operations.some(
(operation) => operation.execution.kind !== "snapshot" || !operation.execution.targets.includes("server"),
)
) {
unavailable.push({ key: query.id, reason: "QUERY_NOT_SNAPSHOT_PROJECTION" });
continue;
}
const list = serverProjections.get(pkg.displayName) ?? [];
list.push(
JSON.stringify(declaration.displayName) +
": " +
JSON.stringify({
kind: "server-projection",
id: query.id,
plan: executionPlan,
root: declaration.root.interface,
rootKind: declaration.root.kind,
}) +
" as const satisfies ServerProjection<" +
JSON.stringify(declaration.root.interface) +
", " +
JSON.stringify(declaration.root.kind) +
">",
);
serverProjections.set(pkg.displayName, list);
addResult();
continue;
}
if (declaration.placement.kind === "local") {
const list = views.get(pkg.displayName) ?? [];
list.push(
`${JSON.stringify(declaration.displayName)}: ${JSON.stringify({ id: query.id, plan: executionPlan, source: JSON.stringify([query.query.packageRevisionId, declaration.placement.sourceQueryId]) })} as const satisfies LocalQueryDescriptor`,
);
views.set(pkg.displayName, list);
addResult();
continue;
}
const list = queries.get(pkg.displayName) ?? [];
list.push(
`${JSON.stringify(declaration.displayName)}: { id: ${JSON.stringify(query.id)}, plan: ${JSON.stringify(executionPlan)}, root: ${JSON.stringify(declaration.root.interface)}, rootKind: ${JSON.stringify(declaration.root.kind)} } as const satisfies ReplicaProjection<${JSON.stringify(declaration.root.interface)}, ${JSON.stringify(declaration.root.kind)}>`,
);
queries.set(pkg.displayName, list);
}
const groups = (entries: Map<string, string[]>) =>
[...entries].map(([name, values]) => `${JSON.stringify(name)}: {${values.join(",\n")}}`).join(",\n");
return `// Generated application descriptors. Regenerate; do not edit.\nimport { defineOperationDescriptor, decodeArgument, workspaceId, type Json, type PreparedChange, type ManagedReference, type ReplicaProjection, type ServerProjection, type LocalQueryDescriptor } from ${JSON.stringify(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const planDigest = ${JSON.stringify(executionPlan)};\nexport const operations = {${groups(atoms)}} as const;\nexport const queries = {${groups(queries)}} as const;\nexport const views = {${groups(views)}} as const;\nexport const serverProjections = {${groups(serverProjections)}} as const;\nexport const results = {${groups(results)}} as const;\nexport const unavailable = ${JSON.stringify(unavailable)} as const;\n`;
}
+365
View File
@@ -0,0 +1,365 @@
import { portableReflectionType } from "../execution-world/reflection.js";
import type { BindingContractSchema } from "./index.js";
import type {
ValueType,
ObjectExpectation,
PackageExport,
DependencyPort,
ExecutionContract,
} from "../capability-model/types.js";
export class PortableBindingError extends Error {
constructor(
readonly path: string,
message: string,
) {
super(`${path}: ${message}`);
this.name = "PortableBindingError";
}
}
const q = JSON.stringify;
const unit: ValueType = { kind: "builtin", name: "unit" };
const string: ValueType = { kind: "scalar", name: "string" };
const bool: ValueType = { kind: "scalar", name: "bool" };
const isUnit = (t: ValueType) => t.kind === "builtin" && t.name === "unit";
const expectation = (e: ObjectExpectation) => ({
kind: e.kind,
id: e.kind === "atom" ? e.atomId : e.interfaceRevisionId,
});
/** The protocol IR and replica wire schema intentionally share structure, not reference identity. */
export function portableValueSchema(type: ValueType): unknown {
switch (type.kind) {
case "variant":
return {
kind: "variant",
id: type.id,
cases: Object.fromEntries(
Object.entries(type.cases).map(([label, entry]) => [
label,
{ id: entry.id, value: portableValueSchema(entry.value) },
]),
),
};
case "change":
return {
kind: "record",
fields: {
codec: { kind: "scalar", name: "string" },
profile: { kind: "scalar", name: "string" },
generation: { kind: "scalar", name: "string" },
changes: { kind: "list", value: { kind: "scalar", name: "bytes" } },
},
};
case "scalar":
return type;
case "builtin":
if (type.name === "unit") return type;
break;
case "object-ref":
return { kind: type.kind, expectation: expectation(type.expectation) };
case "optional":
case "list":
return { kind: type.kind, value: portableValueSchema(type.value) };
case "record":
return {
kind: type.kind,
fields: Object.fromEntries(
Object.entries(type.fields).map(([key, value]) => [key, portableValueSchema(value)]),
),
};
}
throw new PortableBindingError(
type.kind,
"Unsupported portable value; use a supported record/reference contract (native edits and generic adapters have separate tasks)",
);
}
export function tsType(type: ValueType): string {
portableValueSchema(type);
switch (type.kind) {
case "variant":
return `(${Object.entries(type.cases)
.map(([label, entry]) => `{ readonly kind: ${q(label)}; readonly value: ${tsType(entry.value)} }`)
.join(" | ")})`;
case "change":
return `PreparedChange<${tsType(type.value)}>`;
case "scalar":
return type.name === "bool"
? "boolean"
: type.name === "string"
? "string"
: type.name === "bytes"
? "Uint8Array"
: ["int64", "uint64"].includes(type.name)
? "bigint"
: "number";
case "builtin":
return "null";
case "object-ref": {
const e = expectation(type.expectation);
return `ManagedReference<${q(e.kind)}, ${q(e.id)}>`;
}
case "optional":
return `(${tsType(type.value)} | null)`;
case "list":
return `ReadonlyArray<${tsType(type.value)}>`;
case "record":
return `{ ${Object.entries(type.fields)
.map(([key, value]) => `readonly ${q(key)}: ${tsType(value)}`)
.join("; ")} }`;
default:
throw new PortableBindingError(type.kind, "Unsupported type");
}
}
interface Method {
name: string;
portId: string;
portName: string;
action: { kind: string; primitive?: string; operationId?: string; selector?: string };
input: ValueType;
output: ValueType;
keyed: boolean;
explicitInput?: boolean;
}
function methods(
schema: BindingContractSchema,
entry: PackageExport,
port: DependencyPort,
target: string,
remote = false,
): Method[] {
const req = port.requirement;
const path = `${entry.displayName}.ports.${port.displayName}`;
const method = (
name: string,
action: Method["action"],
input: ValueType,
output: ValueType,
keyed = false,
): Method => ({ name, portId: port.id, portName: port.displayName, action, input, output, keyed });
const writable = () => {
if (remote)
throw new PortableBindingError(path, "Remote writes and creation require a named transaction capability");
if (entry.execution?.kind !== "transaction")
throw new PortableBindingError(path, "Snapshot context cannot expose mutation ports");
};
switch (req.kind) {
case "reflection":
if (remote)
throw new PortableBindingError(
path,
"Use an ordinary portable snapshot capability to read installation metadata",
);
return req.selectors.map((selector) =>
method(selector, { kind: "reflection", selector }, unit, portableReflectionType(selector)),
);
case "creation":
writable();
return [
method(
"create",
{ kind: "creation" },
req.inputType,
{ kind: "object-ref", expectation: { kind: "atom", atomId: req.atomId } },
true,
),
];
case "state":
return req.primitives.map((primitive) => {
if (primitive === "read") return method("get", { kind: "state", primitive }, unit, req.valueType);
if (primitive === "write") {
writable();
return method("set", { kind: "state", primitive }, req.valueType, unit);
}
if (primitive === "edit") {
writable();
return method("edit", { kind: "state", primitive }, { kind: "change", value: req.valueType }, unit);
}
throw new PortableBindingError(path, `State ${primitive} is not supported by this portable adapter yet`);
});
case "edge": {
const ref: ValueType = { kind: "object-ref", expectation: req.target };
const mapKey: ValueType | undefined = req.keyType
? { kind: "scalar", name: req.keyType === "boolean" ? "bool" : req.keyType }
: undefined;
const item: ValueType = { kind: "record", fields: { entry: string, target: ref, ...(mapKey ? { mapKey } : {}) } };
const connectInput: ValueType = mapKey ? { kind: "record", fields: { mapKey, target: ref } } : ref;
return req.primitives.map((primitive) => {
if (primitive === "resolve")
return method("resolve", { kind: "edge", primitive }, unit, { kind: "list", value: item });
writable();
if (primitive === "connect") return method("connect", { kind: "edge", primitive }, connectInput, string, true);
if (primitive === "disconnect") return method("disconnect", { kind: "edge", primitive }, string, bool);
throw new PortableBindingError(path, `Edge ${primitive} is not supported by this portable adapter yet`);
});
}
case "interface": {
const iface = schema.interfaces.find((i) => i.revisionId === req.interfaceRevisionId);
if (!iface) throw new PortableBindingError(path, `Missing interface ${req.interfaceRevisionId}`);
const operations = iface.members.flatMap((member) => member.operations.map((op) => ({ member, op })));
if (req.operations?.some((id) => !operations.some(({ op }) => op.id === id)))
throw new PortableBindingError(path, "Unknown selected interface operation");
return operations
.filter(({ op }) => !req.operations || req.operations.includes(op.id))
.map(({ member, op }) => {
const execution: ExecutionContract = op.execution ?? { kind: "remote" };
if (
(execution.kind === "remote" && !remote) ||
op.mode !== "call" ||
(op.scope === "class" && (remote || req.receiver === "input"))
)
throw new PortableBindingError(
`${path}.${member.displayName}.${op.displayName}`,
`Selected operation is not an object call available in portable target ${target}; select a portable subset or move this handler to remote execution`,
);
if (execution.kind === "transaction" && !remote) writable();
if (req.receiver === "input" && remote)
throw new PortableBindingError(path, "Reference-selected dependencies require portable execution");
return {
...method(
`${member.displayName}.${op.displayName}`,
{ kind: "interface", operationId: op.id },
req.receiver === "input"
? {
kind: "record",
fields: {
receiver: {
kind: "object-ref",
expectation: { kind: "interface", interfaceRevisionId: req.interfaceRevisionId },
},
input: op.inputType,
},
}
: op.inputType,
op.outputType,
execution.kind === "transaction",
),
...(op.scope === "class" ? { explicitInput: true } : {}),
};
});
}
default:
throw new PortableBindingError(path, `Unsupported ${req.kind} port in portable bindings`);
}
}
export interface PortableBindingOptions {
target: string;
runtimeModule?: string;
}
/** Generate one target's handlers. The same source signatures are used in browser/server builds. */
export function generatePortableTypeScriptBindings(
schema: BindingContractSchema,
revision: string,
options: PortableBindingOptions,
): string {
return generateBindings(schema, revision, options, false);
}
export function generateRemoteTypeScriptBindings(
schema: BindingContractSchema,
revision: string,
options: { runtimeModule?: string } = {},
): string {
return generateBindings(schema, revision, { ...options, target: "server" }, true);
}
function generateBindings(
schema: BindingContractSchema,
revision: string,
options: PortableBindingOptions,
remote: boolean,
): string {
const mode = remote ? "Remote" : "Portable";
const pkg = schema.packages.find((p) => p.revisionId === revision);
if (!pkg) throw new PortableBindingError(revision, "Missing package");
const entries = pkg.exports.filter((e) =>
remote
? !e.execution || e.execution.kind === "remote"
: e.execution && e.execution.kind !== "remote" && e.execution.targets.includes(options.target),
);
const names = new Set<string>();
const contexts: string[] = [],
implementations: string[] = [],
adapters: string[] = [],
descriptors: unknown[] = [];
for (const entry of entries) {
const effect = entry.execution?.kind ?? "remote";
// The checked binding schema already contains concrete generic applications.
// Each has its own identity, value codecs and specialized dependency ports.
if (entry.kind === "constructor")
throw new PortableBindingError(entry.id, "Legacy constructors are not supported; declare an ordinary factory");
if (names.has(entry.displayName)) throw new PortableBindingError(entry.id, "Duplicate handler name");
names.add(entry.displayName);
const portNames = new Set<string>();
const all: Method[] = [],
facets: string[] = [];
for (const port of entry.dependencyPorts) {
if (portNames.has(port.displayName))
throw new PortableBindingError(entry.id, `Duplicate port name ${port.displayName}`);
portNames.add(port.displayName);
const selected = methods(schema, entry, port, options.target, remote);
if (new Set(selected.map((m) => m.name)).size !== selected.length)
throw new PortableBindingError(port.id, "Duplicate method name");
all.push(...selected);
facets.push(
`readonly ${q(port.displayName)}: { ${selected.map((m) => `readonly ${q(m.name)}: (${[...(isUnit(m.input) && !m.explicitInput ? [] : [`input: ${tsType(m.input)}`]), ...(m.keyed ? [remote ? "options: ServerMutationOptions" : "options: MutationKey"] : [])].join(", ")}) => Promise<${tsType(m.output)}>`).join("; ")} }`,
);
}
const receiver =
entry.kind === "function"
? null
: entry.receiverRequirement.kind === "exact-atom"
? { kind: "atom", id: entry.receiverRequirement.atomId }
: "object";
const receiverType =
receiver === null
? "null"
: receiver === "object"
? "ExactReference"
: `ManagedReference<"atom", ${q(receiver.id)}>`;
contexts.push(
`readonly ${q(entry.displayName)}: { readonly effect: ${q(effect)}; ${remote ? "readonly signal: AbortSignal;" : ""} readonly input: ${tsType(entry.inputType)}; readonly receiver: ${receiverType}; readonly ports: { ${facets.join("; ")} } }`,
);
const output = tsType(entry.outputType);
implementations.push(
`readonly ${q(entry.displayName)}: (context: Contexts[${q(entry.displayName)}]) => ${output} | Promise<${output}>`,
);
adapters.push(
`${q(entry.id)}: context => installed[${q(entry.displayName)}](context as Contexts[${q(entry.displayName)}])`,
);
descriptors.push({
id: entry.id,
name: entry.displayName,
effect,
receiver,
input: portableValueSchema(entry.inputType),
output: portableValueSchema(entry.outputType),
methods: all.map((m) => ({ ...m, input: portableValueSchema(m.input), output: portableValueSchema(m.output) })),
});
}
const groups = new Map<string, typeof entries>();
for (const template of pkg.genericExports ?? []) {
if (template.execution?.kind !== "remote" && template.execution?.targets.includes(options.target))
groups.set(template.displayName, []);
}
for (const entry of entries) {
if (!entry.application) continue;
const template = pkg.genericExports?.find((item) => item.id === entry.application!.exportId);
if (!template) throw new PortableBindingError(entry.id, "Missing generic export template");
groups.set(template.displayName, [...(groups.get(template.displayName) ?? []), entry]);
}
const generic = [...groups].map(([name, applications]) => {
if (!applications.length) return `${q(name)}: (_handler: unknown) => ({})`;
contexts.push(
`readonly ${q(name)}: ${applications.map((entry) => `Contexts[${q(entry.displayName)}]`).join(" | ")}`,
);
const type = applications
.map(
(entry) =>
`((context: Contexts[${q(entry.displayName)}]) => ${tsType(entry.outputType)} | Promise<${tsType(entry.outputType)}>)`,
)
.join(" & ");
const handlers = applications
.map((entry) => `${q(entry.displayName)}: (context: Contexts[${q(entry.displayName)}]) => handler(context)`)
.join(",");
return `${q(name)}: (handler: ${type}) => ({${handlers}})`;
});
return `// Generated ${remote ? "remote" : "portable"} bindings. Regenerate from the checked capability schema; do not edit.\nimport { define${mode}Registry, type ${mode}Export, type PreparedChange, type ManagedReference, type ExactReference, type ${remote ? "ServerMutationOptions" : "MutationKey"} } from ${q(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const target = ${q(options.target)};\nexport type Contexts = { ${contexts.join(";\n")} };\nexport type ${mode}Implementation = { ${implementations.join(";\n")} };\nexport const declarations = ${JSON.stringify(descriptors, null, 2)} as const satisfies readonly ${mode}Export[];\nexport const generic = { ${generic.join(",\n")} };\nexport function createRegistry(handlers: ${mode}Implementation) { const installed = { ...handlers }; return define${mode}Registry(declarations, { ${adapters.join(",\n")} }); }\n`;
}
+88
View File
@@ -0,0 +1,88 @@
import { createHash } from "node:crypto";
import type { InterfaceRevision, PackageRevision, ValueType } from "../capability-model/types.js";
import { defaultQueryBudgets, type QueryDeclaration } from "../query/types.js";
/** React contract tooling generates ordinary queries before normal checking.
* Neither the query language nor its execution planner knows presentation IDs. */
export function presentationQueries(pkg: PackageRevision, interfaces: readonly InterfaceRevision[]) {
const documents = new Map<string, string>();
const generated: QueryDeclaration[] = [];
const contracts = new Map(interfaces.map((contract) => [contract.revisionId, contract]));
const navigation = new Set<string>();
for (const module of pkg.modules ?? []) {
const contract = contracts.get(module.interfaceRevisionId);
const member = contract?.members.find((member) => member.id === module.memberId);
if (member?.kind !== "module" || member.contract !== "org.quixos.react.component/2") continue;
const argument = contract?.application?.arguments[1];
if (argument?.kind !== "object" || argument.target.kind !== "interface")
throw Error("Stateful React presentations require a closed navigation interface");
navigation.add(argument.target.interfaceRevisionId);
}
const valueSelection = (type: ValueType): string => {
if (type.kind === "optional" || type.kind === "list") return valueSelection(type.value);
return type.kind === "record"
? `{${Object.entries(type.fields)
.map(([name, type]) => `${name} ${valueSelection(type)}`)
.join(" ")}}`
: "";
};
for (const id of [...navigation].sort()) {
const contract = contracts.get(id as InterfaceRevision["revisionId"]);
if (!contract || contract.template) throw Error(`Missing closed presentation navigation ${id}`);
const fields = contract.members.flatMap((member) => {
if ((member.kind !== "value" && member.kind !== "relationship") || !member.queryRead) return [];
if (member.kind === "value") return [`${member.displayName} ${valueSelection(member.valueType)}`];
if (member.target.kind !== "interface")
throw Error(`Navigation relationship ${member.displayName} must declare an interface view`);
return [
member.cardinality === "many" || member.cardinality === "many-unique"
? `${member.displayName}(all:512) {entries {node {_camino {ref}}}}`
: `${member.displayName} {_camino {ref}}`,
];
});
const stem = `PresentationNavigation${createHash("sha256").update(id).digest("hex").slice(0, 12)}`;
// Retain full state for operations and hidden occurrences, independently
// from the identity projection used to prepare a rendering occurrence.
for (const identity of [false, true]) {
const prefix = `presentation:navigation:${id}:${identity ? "identity-" : ""}`;
const inputs = prefix + "inputs";
for (const local of [false, true]) {
const name = `${stem}${identity ? "Identity" : ""}${local ? "View" : "Inputs"}`;
const document = `generated/${name}.graphql`;
documents.set(
document,
`query ${name}($presentationMount: QxRef_PresentationMount!) {
root { ${local ? "availableObjects" : "objects"}(all:512, where:{_camino:{as:{PresentationState:{presentationMount:{eq:$presentationMount}}}}}) {
entries { node { ${identity ? "" : fields.join("\n")}
... on PresentationState {
_presentationOccurrence: occurrence
_presentationSource: source {_camino {ref}}
_presentationParent: parent {_camino {ref}}
_presentationMount: presentationMount {_camino {ref}}
}
} }
} }
}`,
);
generated.push({
id: local ? prefix + "view" : inputs,
displayName: name,
root: { kind: "workspace-interface", interface: contract.revisionId },
document,
operation: name,
fragments: [],
views: [],
allowances: [],
budgets: { ...defaultQueryBudgets, rows: 5000, candidates: 5000, resultBytes: 8388608 },
watch: local,
placement: local ? { kind: "local", sourceQueryId: inputs } : { kind: "replica" },
});
}
}
}
for (const query of generated)
if (pkg.queries?.some((existing) => existing.id === query.id || existing.displayName === query.displayName))
throw Error(`Presentation query conflicts with an authored query: ${query.id}`);
pkg.queries = [...(pkg.queries ?? []), ...generated];
return documents;
}
+78
View File
@@ -0,0 +1,78 @@
/** One authoring contract, distributed by scaffolding and immutable codegen.
* Web Studio checks its concrete exports against this contract during its build. */
export const reactPlatformTypes = `// Generated by the Quixos React platform contract. Do not edit.
declare module "@quixos/web-studio-react-runtime" {
import type * as React from "react";
export function useComponentOverlayContainer(): HTMLElement;
export function useComponentStyleRoot(): ShadowRoot;
/** Aggregate transport counters only; contains no object identities or values. */
export function getFieldTransportMetrics(): Partial<Record<"get" | "watch" | "register" | "crdt" | "setter", Readonly<{started: number; active: number; completed: number; failed: number; totalMs: number}>>>;
export type ObjectRef<AtomId extends string> = string & {
readonly $quixosAtom: AtomId;
};
export type ReadableField<T> = {readonly value?: T; readonly $field: "query" | "independent"};
export type QueryField<T> = ReadableField<T> & {readonly $field: "query"};
export type IndependentField<T> = ReadableField<T> & {readonly $field: "independent"};
/** set(T) uses the resolved capability: native CRDT fields send incremental edits;
* custom/manual setters receive semantic values. Storage provenance grants no authority. */
export type WritableField<T> = ReadableField<T> & {readonly $writeType?: (value: T) => T; readonly writable: true};
export type WritableQueryField<T> = WritableField<T> & QueryField<T>;
export type WritableIndependentField<T> = WritableField<T> & IndependentField<T>;
export function detachField<T>(field: WritableQueryField<T>): WritableIndependentField<T>;
export function detachField<T>(field: QueryField<T>): IndependentField<T>;
export type LiveFieldProp<T> = ReadableField<T>;
export type InterfaceReference<I extends string, Fields> = {readonly $quixosRef: string; readonly interfaceRevisionId: I; readonly fields: Fields};
export type ReactInterfaceContract<View> = {readonly interfaceRevisionId: string; readonly $viewType?: (view: View) => View};
export function defineReactInterfaceContract<View>(interfaceRevisionId: string, interfaces: unknown): ReactInterfaceContract<View>;
export function tryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>, options?: {signal?: AbortSignal}): Promise<View | undefined>;
export type ConformanceResult<View> = {status: "loading"} | {status: "absent"} | {status: "available"; view: View} | {status: "error"; error: Error};
export function useTryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>): ConformanceResult<View>;
export type QueryValueType = {kind: "builtin" | "scalar"; name: string} | {kind: "record"; fields: Record<string, QueryValueType>} | {kind: "optional" | "list"; value: QueryValueType} | {kind: "object-ref"; expectation: {kind: "atom"; atomId: string} | {kind: "interface"; interfaceRevisionId: string}};
export type QueryReference<Contract extends string = string> = {readonly $quixosRef: string; readonly queryContract: Contract};
export type QueryLiveProjection = {path: readonly string[]; selectionId: string; interfaceRevisionId: string; getOperationId: string; setOperationId?: string; watchOperationId?: string; inputKind: "fields" | "value"; valueType: QueryValueType; conditions: {include: boolean; defaultValue?: boolean; value: {kind: "variable"; name: string} | {kind: "literal"; value: unknown}}[]};
export interface QueryDescriptor<Variables, Result, Kind extends "object" | "workspace-interface" = "object" | "workspace-interface"> {readonly id: string; readonly definitionDigest: string; readonly root: {readonly kind: Kind; readonly interface: string}; readonly variables: QueryValueType; readonly output: QueryValueType; readonly watch: boolean; readonly liveFields?: readonly QueryLiveProjection[]; readonly $types?: (variables: Variables, result: Result) => [Variables, Result]}
export type QueryPartial<T> = T extends {readonly $quixosRef: string} | ReadableField<unknown> ? T : T extends readonly (infer Item)[] ? QueryPartial<Item>[] : T extends object ? {[Key in keyof T]?: QueryPartial<T[Key]>} : T;
export type QueryFieldState = {path: readonly (string | number)[]; status: "pending" | "error"; error?: string};
export type QueryState<T> = {refreshing: boolean; fields: QueryFieldState[]; runId?: string; sequence?: bigint; consistency?: string} & ({status: "loading"; data?: undefined; error?: undefined} | {status: "ready"; data: T; error?: undefined} | {status: "partial"; data: QueryPartial<T>; error?: undefined} | {status: "error"; data?: T | QueryPartial<T>; error: Error});
export type WorkspaceQueryScope = {readonly $workspaceQueryScope: true};
export function getWorkspaceQueryScope<Variables, Result>(descriptor: QueryDescriptor<Variables, Result, "workspace-interface">): Promise<WorkspaceQueryScope>;
export function useQuery<Variables, Result>(descriptor: QueryDescriptor<Variables, Result, "object">, options: {root: string | {readonly $quixosRef: string}; variables: Variables}): QueryState<Result> & {refresh(): void};
export function useQuery<Variables, Result>(descriptor: QueryDescriptor<Variables, Result, "workspace-interface">, options: {scope: WorkspaceQueryScope; variables: Variables}): QueryState<Result> & {refresh(): void};
export interface QueryConnection<Row> {entries: {key: string; node: Row; cursor?: string | null}[]; pageInfo: {hasNextPage: boolean; endCursor?: string | null}}
export function CollectionView<Result, Row>(props: {result: QueryState<Result> & {refresh(): void}; connection(data: QueryPartial<Result>): QueryPartial<QueryConnection<Row>> | undefined; renderItem(row: QueryPartial<Row>, entryKey: string): React.ReactNode; onCreate?: () => void; onLoadMore?: (cursor: string | null) => void; onReset?: () => void; loadingMore?: boolean; empty?: React.ReactNode; prefetch?: boolean}): React.ReactElement;
export type ReactComponentHostProps<Action> = {
onAction?: (action: Action) => void;
fallback?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
/** Optional extra action. The host still displays and reports errors by default. */
extraErrorAction?: (error: Error) => void;
};
export type ReactComponentImplementationProps<CaminoProps, RenderProps, Action> = {
camino: CaminoProps;
render: RenderProps;
dispatch: (action: Action) => void;
};
export const createWebStudioComponent: <
ForObject extends ObjectRef<string>, RenderProps extends object, Action,
>(config: { expectedAtomId: string }) => (props: {
forObject: ForObject;
} & RenderProps & ReactComponentHostProps<Action>) => any;
export const invokeCapability: <Result = unknown>(
objectId: string,
interfaceRevisionId: string,
operationId: string,
value?: unknown,
options?: {clientMutationId?: string; signal?: AbortSignal},
) => Promise<Result>;
export const h: typeof React.createElement;
export type FieldReadState = "connecting" | "live" | "snapshot" | "inactive" | "not-selected" | "error" | "obsolete";
export type LiveFieldState<T> = {readState: FieldReadState; refreshing: boolean; stale: boolean; pendingWrites: number; writeError?: Error; refresh(): Promise<void>; discard(): void} & ({status: "loading"; value?: undefined; error?: undefined} | {status: "ready"; value: T; error?: Error} | {status: "error"; value?: undefined; error: Error});
export type WritableLiveFieldState<T> = LiveFieldState<T> & {set(value: T): Promise<void>; retry(): Promise<void>};
export function useLiveField<T>(field: ReadableField<T>, options: {id: string; write: (value: T) => Promise<void>}): WritableLiveFieldState<T>;
export function useLiveField<T>(field: WritableField<T>): WritableLiveFieldState<T>;
export function useLiveField<T>(field: ReadableField<T>): LiveFieldState<T>;
export function useSuspenseLiveField<T>(field: WritableField<T>): WritableLiveFieldState<T> & {status: "ready"; value: T};
export function useSuspenseLiveField<T>(field: ReadableField<T>): LiveFieldState<T> & {status: "ready"; value: T};
}
`;
+167
View File
@@ -0,0 +1,167 @@
import type { BindingSchema } from "./index.js";
import type { ValueType } from "../capability-model/types.js";
/** Browser projection of the SAME checked RPC types, not a second props schema. */
export function generateReactBindings(
schema: BindingSchema,
packageRevisionId: string,
propsExports: readonly string[],
components: readonly { module: string; propsExport: string }[] = [],
) {
const pkg = schema.packages.find((entry) => entry.revisionId === packageRevisionId);
if (!pkg) throw new Error(`Unknown package ${packageRevisionId}`);
const q = JSON.stringify;
const references = new Map<string, string>();
const declarations: string[] = [];
const type = (value: ValueType): string => {
switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin":
if (value.name !== "unit") throw new Error(`Unsupported React props builtin ${value.name}`);
return "null";
case "scalar":
return {
string: "string",
bool: "boolean",
bytes: "Uint8Array",
int64: "bigint",
uint64: "bigint",
int32: "number",
uint32: "number",
double: "number",
}[value.name];
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional":
return `(${type(value.value)} | null)`;
case "list":
return `Array<${type(value.value)}>`;
case "record":
return `{${Object.entries(value.fields)
.map(([name, field]) => `${q(name)}: ${type(field)}`)
.join(";")}}`;
case "message":
throw new Error(`React props require checked values, not opaque message ${value.descriptorId}`);
case "object-ref": {
if (value.expectation.kind === "atom") return `ObjectRef<${q(value.expectation.atomId)}>`;
const id = value.expectation.interfaceRevisionId;
const existing = references.get(id);
if (existing) return existing;
const name = `Interface${references.size}`;
references.set(id, name);
const iface = schema.interfaces.find((entry) => entry.revisionId === id);
if (!iface) throw new Error(`Missing React reference contract ${id}`);
const fields = iface.members.flatMap((member) => {
if (member.kind === "operation") return [];
const get = member.operations.find((op) => op.displayName === (member.kind === "value" ? "get" : "resolve"));
if (!get) return [];
const writable = member.kind === "value" && member.operations.some((op) => op.displayName === "set");
return [`${q(member.displayName)}: ${writable ? "WritableField" : "ReadableField"}<${type(get.outputType)}>`];
});
declarations.push(`export type ${name} = InterfaceReference<${q(id)}, {${fields.join(";")}}> ;`);
return name;
}
}
};
// Only exports explicitly selected as props are projected. Non-props exports
// may legitimately use opaque messages or types unrelated to the browser.
const selected = propsExports.map((name) => {
const entry = pkg.exports.find((candidate) => candidate.displayName === name);
if (!entry) throw new Error(`Unknown React props export ${name}`);
return entry;
});
const results = selected.map(
(entry) =>
`${q(entry.displayName)}: ${type(entry.kind === "operation" ? (entry.eventType ?? entry.outputType) : entry.outputType)}`,
);
const checks = components.map((component, i) => {
if (!propsExports.includes(component.propsExport))
throw new Error(`Component refers to unselected props export ${component.propsExport}`);
if (!component.module.startsWith("."))
throw new Error("React component check must name a local module relative to generated bindings");
return `type Component${i} = CheckedComponent<${q(component.propsExport)}, typeof import(${q(component.module)})["default"]>;`;
});
// Emit optional discovery descriptors only when the browser has all codecs.
// Unrelated opaque-message imports must not break an otherwise checked UI.
const browserValue = (value: ValueType, seen: Set<string>): boolean => {
if (value.kind === "message") return false;
if (value.kind === "builtin") return value.name === "unit";
if (value.kind === "variant") return false;
if (value.kind === "record") return Object.values(value.fields).every((field) => browserValue(field, seen));
if (value.kind === "list" || value.kind === "optional") return browserValue(value.value, seen);
if (value.kind === "object-ref" && value.expectation.kind === "interface") {
const id = value.expectation.interfaceRevisionId;
if (seen.has(id)) return true;
const contract = schema.interfaces.find((entry) => entry.revisionId === id);
return !!contract && browserInterface(contract, new Set([...seen, id]));
}
return true;
};
const browserInterface = (iface: (typeof schema.interfaces)[number], seen: Set<string>): boolean =>
iface.members.every((member) =>
member.operations
.filter((op) => op.mode === "call" && op.scope !== "class")
.every((op) => browserValue(op.inputType, seen) && browserValue(op.outputType, seen)),
);
const contracts = schema.interfaces
.filter((iface) => browserInterface(iface, new Set([iface.revisionId])))
.map((iface) => {
const reference = type({
kind: "object-ref",
expectation: { kind: "interface", interfaceRevisionId: iface.revisionId },
});
const calls = iface.members.flatMap((member) =>
member.operations
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
.map(
(operation) =>
`${q(`${member.displayName}.${operation.displayName}`)}: (${operation.inputType.kind === "builtin" && operation.inputType.name === "unit" ? "" : `input: ${type(operation.inputType)}`}) => Promise<${type(operation.outputType)}>`,
),
);
return { iface, view: `${reference} & {call: {${calls.join(";")}}}` };
});
const queryType = (value: ValueType): string => {
if (value.kind === "object-ref")
return `QueryReference<${q(value.expectation.kind === "atom" ? value.expectation.atomId : value.expectation.interfaceRevisionId)}>`;
if (value.kind === "optional") return `(${queryType(value.value)} | null)`;
if (value.kind === "list") return `Array<${queryType(value.value)}>`;
if (value.kind === "record")
return `{${Object.entries(value.fields)
.map(([name, field]) => `${q(name)}: ${queryType(field)}`)
.join(";")}}`;
return type(value);
};
const queries = pkg.checkedQueries ?? [];
const queryCode =
`export type QueryVariables = {${queries.map((query) => `${q(query.declaration.displayName)}: ${queryType(query.variables)}`).join(";")}};\n` +
`export type QueryResults = {${queries.map((query) => `${q(query.declaration.displayName)}: ${queryType(query.output)}`).join(";")}};\n` +
`export const queries: {${queries.map((query) => `${q(query.declaration.displayName)}: QueryDescriptor<QueryVariables[${q(query.declaration.displayName)}], QueryResults[${q(query.declaration.displayName)}], ${q(query.declaration.root.kind)}>`).join(";")}} = ${JSON.stringify(
Object.fromEntries(
queries.map((query) => [
query.declaration.displayName,
{
id: `${pkg.revisionId}:${query.declaration.id}`,
definitionDigest: query.definitionDigest,
root: query.declaration.root,
variables: query.variables,
output: query.output,
watch: query.declaration.watch,
},
]),
),
)};\n`;
return (
`// Generated from checked QX contracts. Do not edit.\nimport {defineReactInterfaceContract} from "@quixos/web-studio-react-runtime";\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField, QueryField, WritableQueryField, QueryDescriptor, QueryReference} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
queryCode +
`const contractsData = ${JSON.stringify(schema.interfaces)};\n` +
`export const reactContracts = {${contracts.map(({ iface, view }) => `${q(iface.revisionId)}: defineReactInterfaceContract<${view}>(${q(iface.revisionId)}, contractsData)`).join(",\n")}};\n` +
`export const reactInterfaces = {${contracts
.filter(({ iface }) => contracts.filter((other) => other.iface.displayName === iface.displayName).length === 1)
.map(({ iface }) => `${q(iface.displayName)}: reactContracts[${q(iface.revisionId)}]`)
.join(",\n")}};\n` +
(checks.length
? `type CheckedComponent<K extends keyof ReactResults, C extends (props: {camino: ReactResults[K]; render: any; dispatch: (action: any) => void}) => unknown> = C;\n${checks.join("\n")}\n`
: "")
);
}
+632
View File
@@ -0,0 +1,632 @@
import { readFile, realpath } from "node:fs/promises";
import path from "node:path";
import { profile } from "./profile.js";
import { loadQxSources, resolveQxSources } from "./source-loader.js";
import { compileQuery, compileRepositoryQuery } from "../query/compile.js";
import { queryTreeProjections } from "../query/tree.js";
import type { QueryDeclaration } from "../query/types.js";
import { compileQueryComposition, type QueryComposition } from "../query/composition.js";
import { checkQueryTemplate } from "../query/templates.js";
import { readRepositorySource } from "./source-loader.js";
import { parseControllerCatalog } from "../capability-model/controllers.js";
import { linkQueries } from "../query/link.js";
import {
capabilityId,
compileWorkspaceRevision,
validateMigrationCatalog,
contentDigest,
type AtomDefinition,
type CompiledWorkspaceRevision,
type InterfaceRevision,
type PackageRevision,
type SourceRevision,
type WorkspaceRevision,
} from "../capability-model/index.js";
import {
loadQuixosLock,
type GitSource,
type LockedResource,
type QuixosRepositoryLock,
} from "../resource-lock/index.js";
import {
compileCapabilityResourceSource,
compileCapabilitySource,
type CapabilityImportEnvironment,
type CapabilityResource,
type CapabilityResourceImport,
} from "./parser.js";
export type CapabilityRepositorySnapshot = {
directory: string;
};
export type CapabilityRepositoryResolver = (
source: GitSource,
kind: LockedResource["kind"],
) => Promise<CapabilityRepositorySnapshot>;
/** Already checked outputs of this compiler, composed by the Nix build DAG.
* Source capture/check reports never grant this authority. */
export type CheckedResourceResolver = (
kind: LockedResource["kind"],
source: GitSource,
lock: QuixosRepositoryLock,
dependencies: ReadonlyMap<string, ResolvedCapabilityResource>,
) => Promise<CapabilityResource | undefined>;
/** An explicitly selected build frontend supplies ordinary query sources. The
* assembler never chooses a frontend by inspecting an interface/contract ID.
* Generated sources go through the same query and imported-fragment checks. */
export type PackageQueryGenerator = ((input: {
readonly package: Readonly<PackageRevision>;
readonly interfaces: readonly InterfaceRevision[];
readonly readSource: (name: string) => Promise<string>;
}) => Promise<{
readonly queries: readonly QueryDeclaration[];
readonly documents: ReadonlyMap<string, string>;
readonly compositions?: readonly QueryComposition[];
readonly artifacts?: Readonly<Record<string, unknown>>;
}>) & {
/** Optional explicit frontend stage after workspace binding resolution.
* Its output still passes normal workspace/query validation. */
completeWorkspace?: (workspace: WorkspaceRevision) => Promise<void>;
};
export type ResolvedCapabilityResource = {
key: string;
kind: LockedResource["kind"];
source: GitSource;
directory: string;
lock: QuixosRepositoryLock;
resource: CapabilityResource | { kind: "build" };
dependencies: ReadonlyMap<string, ResolvedCapabilityResource>;
};
export type CompiledWorkspaceRepository = {
workspace: WorkspaceRevision;
plan: CompiledWorkspaceRevision;
lock: QuixosRepositoryLock;
resources: ResolvedCapabilityResource[];
capabilityResources: ResolvedCapabilityResource[];
directResources: ReadonlyMap<string, ResolvedCapabilityResource>;
};
export type CompiledCapabilityResourceRepository = {
resource: CapabilityResource | { kind: "build" };
lock: QuixosRepositoryLock;
resources: ResolvedCapabilityResource[];
capabilityResources: ResolvedCapabilityResource[];
directResources: ReadonlyMap<string, ResolvedCapabilityResource>;
};
const sourceKey = (kind: LockedResource["kind"], source: GitSource) =>
`${kind}\0${source.repository}\0${source.commit.toLowerCase()}`;
const bindingKey = (kind: LockedResource["kind"], binding: string) => `${kind}\0${binding}`;
const importsKey = (entry: CapabilityResourceImport | LockedResource) => bindingKey(entry.kind, entry.binding);
const assertImportsMatchLock = (
label: string,
imports: readonly CapabilityResourceImport[],
resources: readonly LockedResource[],
) => {
const authored = [...imports].map(importsKey).sort();
const locked = resources
.filter((entry) => entry.kind !== "build")
.map(importsKey)
.sort();
if (authored.length !== locked.length || authored.some((entry, index) => entry !== locked[index])) {
throw new Error(
`${label} imports do not match quixos.lock:\n` +
`authored: ${authored.join(", ") || "none"}\n` +
`locked: ${locked.join(", ") || "none"}`,
);
}
};
const exactRevisions = <
Revision extends {
revisionId: string;
source: SourceRevision;
},
>(
revisions: readonly Revision[],
) => {
const seen = new Set<string>();
return revisions.filter((revision) => {
const key = `${revision.revisionId}\0${revision.source.repository}\0${revision.source.commit}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
};
const exactAtoms = (atoms: readonly AtomDefinition[]) => {
const seen = new Set<string>();
return atoms.filter((atom) => {
if (seen.has(atom.id)) return false;
seen.add(atom.id);
return true;
});
};
const diagnosticsMessage = (
label: string,
diagnostics: readonly {
fileName: string;
line: number;
column: number;
phase: string;
code: string;
message: string;
path?: string;
}[],
) =>
`${label} did not compile:\n${diagnostics
.map((entry) => {
const location =
entry.line > 0
? `${entry.fileName}:${entry.line}:${entry.column + 1}`
: `${entry.fileName}${entry.path ? `:${entry.path}` : ""}`;
return `${location}: ${entry.phase} ${entry.code}: ${entry.message}`;
})
.join("\n")}`;
const revisionFor = (node: ResolvedCapabilityResource) => {
if (node.resource.kind === "build") throw Error("Build sources have no capability revision");
return node.resource.revision;
};
const resourceClosure = (
roots: readonly ResolvedCapabilityResource[],
capabilityOnly = false,
): ResolvedCapabilityResource[] => {
const result: ResolvedCapabilityResource[] = [];
const seen = new Set<string>();
const visit = (node: ResolvedCapabilityResource) => {
if (seen.has(node.key) || (capabilityOnly && node.kind === "build")) return;
seen.add(node.key);
for (const dependency of node.dependencies.values()) visit(dependency);
result.push(node);
};
for (const root of roots) visit(root);
return result;
};
const environmentFor = (
direct: readonly [LockedResource, ResolvedCapabilityResource][],
closure: readonly ResolvedCapabilityResource[],
): CapabilityImportEnvironment => {
closure = resourceClosure(
direct.map(([, node]) => node),
true,
);
return {
interfaces: new Map(
direct.flatMap(([locked, node]) =>
node.resource.kind === "interface" ? [[locked.binding, node.resource.revision] as const] : [],
),
),
packages: new Map(
direct.flatMap(([locked, node]) =>
node.resource.kind === "package" ? [[locked.binding, node.resource.revision] as const] : [],
),
),
interfaceClosure: exactRevisions(
closure.flatMap((node) => [
...(node.resource.kind === "interface" ? [node.resource.revision] : []),
...(node.resource.kind === "build" ? [] : (node.resource.specializations ?? [])),
]),
),
packageClosure: exactRevisions(
closure.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])),
),
externalAtoms: exactAtoms(
closure.flatMap((node) => (node.resource.kind === "build" ? [] : node.resource.externalAtoms)),
),
};
};
const createResourceGraphResolver = (
quixosCommit: string,
resolveResource: CapabilityRepositoryResolver,
generateQueries?: PackageQueryGenerator,
checkedResource?: CheckedResourceResolver,
) => {
const graphStarted = performance.now();
let checkedResources = 0,
reusedResources = 0,
checkedQueries = 0;
const resolved = new Map<string, Promise<ResolvedCapabilityResource>>();
const active: string[] = [];
const visit = async (
locked: LockedResource,
suppliedSnapshot?: CapabilityRepositorySnapshot,
): Promise<ResolvedCapabilityResource> => {
const key = sourceKey(locked.kind, locked.source);
const cached = resolved.get(key);
if (cached) {
if (active.includes(key)) {
throw new Error(`Resource dependency cycle: ${[...active, key].join(" -> ")}`);
}
return await cached;
}
const pending = (async () => {
active.push(key);
try {
const snapshot = suppliedSnapshot ?? (await resolveResource(locked.source, locked.kind));
const lockResult = await loadQuixosLock(path.join(snapshot.directory, "quixos.lock"));
if (!lockResult.ok) {
throw new Error(diagnosticsMessage(`${locked.kind} ${locked.binding} lock`, lockResult.diagnostics));
}
if (lockResult.lock.quixos.policy) {
throw new Error(
`${locked.kind} ${locked.binding} lock declares workspace Quixos policy ` +
`${lockResult.lock.quixos.policy}; resource locks may only declare their exact authored-against commit`,
);
}
if (lockResult.lock.quixos.commit.toLowerCase() !== quixosCommit.toLowerCase()) {
throw new Error(
`${locked.kind} ${locked.binding} selects Quixos ${lockResult.lock.quixos.commit}, ` +
`but the repository graph selects ${quixosCommit}`,
);
}
const directPairs: Array<[LockedResource, ResolvedCapabilityResource]> = [];
for (const dependency of lockResult.lock.resources) {
directPairs.push([dependency, await visit(dependency)]);
}
if (locked.kind === "build") {
return {
key,
kind: locked.kind,
source: locked.source,
directory: snapshot.directory,
lock: lockResult.lock,
resource: { kind: "build" as const },
dependencies: new Map(
directPairs.map(([dependency, node]) => [bindingKey(dependency.kind, dependency.binding), node]),
),
};
}
const dependencyClosure = resourceClosure(directPairs.map(([, node]) => node));
const dependencies = new Map(
directPairs.map(([dependency, node]) => [bindingKey(dependency.kind, dependency.binding), node]),
);
const checked = await checkedResource?.(locked.kind, locked.source, lockResult.lock, dependencies);
if (checked) {
if (
checked.kind !== locked.kind ||
checked.revision.source.repository !== locked.source.repository ||
checked.revision.source.commit !== locked.source.commit
)
throw new Error("Checked resource identity mismatch");
assertImportsMatchLock("Checked resource", checked.imports, lockResult.lock.resources);
reusedResources++;
return {
key,
kind: locked.kind,
source: locked.source,
directory: snapshot.directory,
lock: lockResult.lock,
resource: structuredClone(checked),
dependencies,
};
}
const checkStarted = performance.now();
const environment = environmentFor(directPairs, dependencyClosure);
const manifestName = locked.kind === "interface" ? "interface.qx" : "package.qx";
const manifestPath = path.join(snapshot.directory, manifestName);
const sourceText = await readFile(manifestPath, "utf8");
const compiled = compileCapabilityResourceSource(sourceText, {
source: locked.source,
fileName: manifestPath,
environment,
});
if (!compiled.ok) {
throw new Error(diagnosticsMessage(`${locked.kind} ${locked.binding}`, compiled.diagnostics));
}
if (compiled.resource.kind !== locked.kind) {
throw new Error(`${manifestPath} declares ${compiled.resource.kind}, not ${locked.kind}`);
}
assertImportsMatchLock(manifestPath, compiled.resource.imports, lockResult.lock.resources);
if (compiled.resource.kind === "package") {
const queryInterfaces = [
...(environment.interfaceClosure ?? []),
...(compiled.resource.specializations ?? []),
];
const generated = await generateQueries?.({
package: structuredClone(compiled.resource.revision),
interfaces: structuredClone(queryInterfaces),
readSource: (name) => readRepositorySource(snapshot.directory, name),
});
if (generated) {
if (generated.artifacts) compiled.resource.revision.buildArtifacts = structuredClone(generated.artifacts);
const queries = [...(compiled.resource.revision.queries ?? [])];
for (const query of generated.queries) {
if (queries.some((entry) => entry.id === query.id || entry.displayName === query.displayName))
throw new Error(`Generated query conflicts with an authored query: ${query.id}`);
if (!generated.documents.has(query.document))
throw new Error(`Generated query has no source: ${query.document}`);
queries.push(structuredClone(query));
}
compiled.resource.revision.queries = queries;
}
for (const template of compiled.resource.revision.queryTemplates ?? [])
await checkQueryTemplate(template, queryInterfaces, (name) =>
readRepositorySource(snapshot.directory, name),
);
if (compiled.resource.revision.queries?.length) {
const pkg = compiled.resource.revision;
const checked = new Map<string, import("../query/types.js").CheckedQuery>();
const compile = async (
id: string,
parents: readonly string[] = [],
): Promise<import("../query/types.js").CheckedQuery> => {
if (parents.includes(id)) throw new Error(`Query fragment cycle: ${[...parents, id].join(" -> ")}`);
const previous = checked.get(id);
if (previous) return previous;
const query = pkg.queries!.find((query) => query.id === id);
if (!query) throw new Error(`Unknown fragment ${pkg.revisionId}/${id}`);
const resolveFragment = async (packageId: string, fragmentId: string) => {
if (packageId === pkg.revisionId) return compile(fragmentId, [...parents, id]);
const imported = environment.packageClosure
?.find((other) => other.revisionId === packageId)
?.checkedQueries?.find((query) => query.declaration.id === fragmentId);
if (!imported) throw new Error(`Unchecked imported fragment ${packageId}/${fragmentId}`);
return imported;
};
const result = generated?.documents.has(query.document)
? await compileQuery(
query,
queryInterfaces,
async (name) =>
generated.documents.get(name) ?? (await readRepositorySource(snapshot.directory, name)),
resolveFragment,
)
: await compileRepositoryQuery(snapshot.directory, query, queryInterfaces, resolveFragment);
checked.set(id, result);
return result;
};
pkg.checkedQueries = [];
for (const query of pkg.queries!) pkg.checkedQueries.push(await compile(query.id));
const projections = pkg.checkedQueries.flatMap(queryTreeProjections);
for (const projection of projections) {
if (pkg.queries!.some((query) => query.id === projection.declaration.id))
throw new Error(`Generated Query Tree projection conflicts with ${projection.declaration.id}`);
pkg.queries!.push(projection.declaration);
pkg.checkedQueries.push(projection);
}
}
if (generated?.compositions?.length) {
const pkg = compiled.resource.revision;
const queries = new Map((pkg.checkedQueries ?? []).map((query) => [query.declaration.id, query]));
for (const dependency of environment.packageClosure ?? [])
for (const query of dependency.checkedQueries ?? [])
queries.set(JSON.stringify([dependency.revisionId, query.declaration.id]), query);
const ids = new Set<string>();
pkg.queryCompositions = generated.compositions.map((composition) => {
if (ids.has(composition.id)) throw new Error(`Duplicate query composition: ${composition.id}`);
ids.add(composition.id);
return compileQueryComposition(composition, queries);
});
}
let controllerText: string | undefined;
try {
controllerText = await readRepositorySource(snapshot.directory, "quixos.controllers.json");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
if (controllerText !== undefined)
compiled.resource.revision.controllers = parseControllerCatalog(
controllerText,
queryInterfaces,
compiled.resource.revision.checkedQueries,
);
let catalogText: string | undefined;
try {
catalogText = await readFile(path.join(snapshot.directory, "quixos.migrations.json"), "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
if (catalogText !== undefined) {
const catalog = validateMigrationCatalog(
JSON.parse(catalogText),
new Set(compiled.resource.revision.exports.map((entry) => entry.id)),
);
const root = await realpath(snapshot.directory);
for (const migration of catalog.migrations) {
const implementation = await realpath(path.join(root, migration.implementation.file));
if (!implementation.startsWith(`${root}${path.sep}`))
throw new Error("Migration implementation escapes its package");
if (contentDigest(await readFile(implementation, "utf8")) !== migration.implementation.digest)
throw new Error(`Migration implementation digest mismatch: ${migration.id}`);
}
compiled.resource.revision.migrationCatalog = catalog;
}
}
checkedResources++;
const queries =
compiled.resource.kind === "package" ? (compiled.resource.revision.checkedQueries?.length ?? 0) : 0;
checkedQueries += queries;
profile("resource-check", checkStarted, {
kind: locked.kind,
resource: compiled.resource.revision.displayName,
queries,
});
return {
key,
kind: locked.kind,
source: locked.source,
directory: snapshot.directory,
lock: lockResult.lock,
resource: compiled.resource,
dependencies: new Map(
directPairs.map(([dependency, node]) => [bindingKey(dependency.kind, dependency.binding), node]),
),
};
} finally {
active.pop();
}
})();
resolved.set(key, pending);
return await pending;
};
return {
visit,
nodes: async () => {
const nodes = await Promise.all(resolved.values());
profile("resource-graph", graphStarted, { checkedResources, reusedResources, checkedQueries });
return nodes;
},
};
};
export const compileCapabilityResourceRepository = async (options: {
rootDirectory: string;
kind: LockedResource["kind"];
source: GitSource;
resolveResource: CapabilityRepositoryResolver;
generateQueries?: PackageQueryGenerator;
checkedResource?: CheckedResourceResolver;
}): Promise<CompiledCapabilityResourceRepository> => {
const lockResult = await loadQuixosLock(path.join(options.rootDirectory, "quixos.lock"));
if (!lockResult.ok) {
throw new Error(diagnosticsMessage("Resource lock", lockResult.diagnostics));
}
const resolver = createResourceGraphResolver(
lockResult.lock.quixos.commit,
options.resolveResource,
options.generateQueries,
options.checkedResource,
);
const root = await resolver.visit(
{
kind: options.kind,
binding: "<root>",
source: options.source,
},
{ directory: options.rootDirectory },
);
return {
resource: root.resource,
lock: root.lock,
resources: await resolver.nodes(),
capabilityResources: resourceClosure([root], true),
directResources: root.dependencies,
};
};
export const compileWorkspaceRepository = async (options: {
rootDirectory: string;
resolveResource: CapabilityRepositoryResolver;
workspaceId?: string;
workspaceRevisionId?: string;
sourceRootCommit?: string;
/** An editor's proposed source snapshot; locks and dependency revisions remain exact. */
readSource?: (name: string) => Promise<string>;
generateQueries?: PackageQueryGenerator;
checkedResource?: CheckedResourceResolver;
}): Promise<CompiledWorkspaceRepository> => {
const rootLockResult = await loadQuixosLock(path.join(options.rootDirectory, "quixos.lock"));
if (!rootLockResult.ok) {
throw new Error(diagnosticsMessage("Workspace lock", rootLockResult.diagnostics));
}
const rootLock = rootLockResult.lock;
const resolver = createResourceGraphResolver(
rootLock.quixos.commit,
options.resolveResource,
options.generateQueries,
options.checkedResource,
);
const directPairs: Array<[LockedResource, ResolvedCapabilityResource]> = [];
for (const locked of rootLock.resources) {
directPairs.push([locked, await resolver.visit(locked)]);
}
const nodes = await resolver.nodes();
const environment = environmentFor(directPairs, nodes);
const workspacePath = path.join(options.rootDirectory, "workspace.qx");
const sources = options.readSource
? await resolveQxSources(options.readSource)
: await loadQxSources(options.rootDirectory);
const compiled = compileCapabilitySource(sources.source, workspacePath, environment);
if (!compiled.ok) {
throw new Error(
diagnosticsMessage(
"Workspace",
compiled.diagnostics.map((diagnostic) =>
diagnostic.line > 0
? { ...diagnostic, ...sources.originalPosition(diagnostic.line, diagnostic.column) }
: diagnostic,
),
),
);
}
assertImportsMatchLock(workspacePath, compiled.imports, rootLock.resources);
const availableInterfaceIds = new Set((environment.interfaceClosure ?? []).map((iface) => iface.revisionId));
const availableAtomIds = new Set(compiled.workspace.atoms.map((atom) => atom.id));
for (const node of resourceClosure(
directPairs.map(([, node]) => node),
true,
)) {
if (node.resource.kind === "build") continue;
for (const requirement of node.resource.externalInterfaces) {
if (!availableInterfaceIds.has(requirement.revisionId)) {
throw new Error(
`${node.kind} ${node.resource.revision.displayName} requires external interface ` +
`${requirement.binding} (${requirement.revisionId}), but the workspace resource graph does not provide it`,
);
}
}
for (const atom of node.resource.externalAtoms) {
if (!availableAtomIds.has(atom.id)) {
throw new Error(
`${node.kind} ${node.resource.revision.displayName} requires external atom ` +
`${atom.displayName} (${atom.id}), but the workspace does not define it`,
);
}
}
}
const workspace: WorkspaceRevision = {
...compiled.workspace,
...(options.workspaceId ? { workspaceId: capabilityId.workspace(options.workspaceId) } : {}),
...(options.workspaceRevisionId
? { id: capabilityId.workspaceRevision(options.workspaceRevisionId) }
: options.sourceRootCommit
? {
id: capabilityId.workspaceRevision(
`workspace-revision:${options.workspaceId ?? compiled.workspace.workspaceId}:${options.sourceRootCommit}`,
),
}
: {}),
...(options.sourceRootCommit ? { sourceRootCommit: options.sourceRootCommit } : {}),
};
await options.generateQueries?.completeWorkspace?.(workspace);
const checked = compileWorkspaceRevision(workspace);
if (!checked.ok) {
throw new Error(
`Instantiated workspace did not compile:\n${checked.issues
.map((entry) => `${entry.path}: ${entry.message}`)
.join("\n")}`,
);
}
const linkedQueries = linkQueries(workspace);
if (linkedQueries.length) {
workspace.linkedQueries = linkedQueries;
checked.plan.source.linkedQueries = structuredClone(linkedQueries);
}
return {
workspace,
plan: checked.plan,
lock: rootLock,
resources: nodes,
capabilityResources: resourceClosure(
directPairs.map(([, node]) => node),
true,
),
directResources: new Map(directPairs.map(([locked, node]) => [bindingKey(locked.kind, locked.binding), node])),
};
};
+197
View File
@@ -0,0 +1,197 @@
import fs from "node:fs/promises";
import { appendFileSync } from "node:fs";
import path from "node:path";
import { createHash, randomUUID } from "node:crypto";
import { authoringContext } from "./authoring-context.js";
import type { convergeAuthoring } from "./authoring-converge.js";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { buildImmutableCandidate, checkerIdentity } from "./checked-build.js";
import {
planEvolution,
reviewSemanticMajors,
type WorkspaceRevision,
type EvolutionReview,
} from "../capability-model/index.js";
export const checkRecordName = (directory: string) => createHash("sha256").update(directory).digest("hex") + ".json";
export async function checkAuthoring(
start: string,
output: string,
options: { baseline?: string; reviews?: string; contractOnly?: boolean } = {},
) {
const started = performance.now();
const timings: Record<string, number> = {};
const context = await authoringContext(start);
const location = await fs.realpath(start);
const directory = location === context.workbench ? "root" : path.relative(context.workbench, location);
const resource = context.resources.find((entry) => entry.directory === directory);
if (!resource) throw new Error("Run check from a registered repository root or the workbench");
await fs.mkdir(output, { mode: 0o700 });
const report: {
directory: string;
checker: string;
candidateOnly: true;
activationEvidence: false;
commit?: string;
artifactPath?: string;
blockers: string[];
phase: string;
output: string;
compilation?: "passed";
runtime?: "execution";
activationReadiness?: "preserve" | "migration-required" | "blocked";
migrationRequired?: string[];
} = {
directory,
checker: checkerIdentity(),
candidateOnly: true,
activationEvidence: false,
blockers: [],
phase: "convergence",
output,
};
const progress = async (running = true) => {
const file = path.join(output, "report.json"),
temp = `${file}.tmp`;
await fs.writeFile(temp, JSON.stringify({ ...report, timings, running }, null, 2));
await fs.rename(temp, file);
};
await progress();
try {
console.error(`[${new Date().toISOString()}] Check: capture source and converge dependencies`);
// Serialize only source capture, not the potentially slow Nix build.
// Repository-scoped agents can check separate immutable candidates in parallel.
const capture = promisify(callback)("quixos-qx", ["converge", context.workbench, directory], {
maxBuffer: 4 * 1024 * 1024,
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_TRACE_CAPTURE: "1", QUIXOS_TRACE_PROFILE: "1" },
});
console.error(`Capture details: ${path.join(output, "capture.log")}`);
capture.child.stderr?.on("data", (chunk) => appendFileSync(path.join(output, "capture.log"), chunk));
const captured = await capture.catch((error) => {
if (typeof error.stdout === "string" && error.stdout.trim().startsWith("{")) return { stdout: error.stdout };
throw error;
});
const converged = JSON.parse(captured.stdout) as Awaited<ReturnType<typeof convergeAuthoring>>;
timings.captureMs = Math.round(performance.now() - started);
console.error(`[${new Date().toISOString()}] Check: source captured in ${(timings.captureMs / 1000).toFixed(1)}s`);
if (!converged.candidate) {
report.phase = converged.worklist.find((entry) => entry.phase !== "dependency")?.phase ?? "convergence";
throw new Error(
converged.worklist.map((entry) => `${entry.directory} [${entry.phase}]: ${entry.message}`).join("\n"),
);
}
report.commit = converged.candidate.commit;
report.phase = "verification";
await progress();
const buildStarted = performance.now();
console.error(
`[${new Date().toISOString()}] Check: immutable Nix ${options.contractOnly ? "contract" : "verification"}; build output: ${path.join(output, "nix.log")}`,
);
try {
report.artifactPath = await buildImmutableCandidate(
converged.candidate,
resource.kind,
path.join(output, "nix.log"),
options.contractOnly,
(phase, ms) => {
timings[phase] = ms;
},
resource.kind === "workspace" ? context.workspaceId : undefined,
);
} finally {
timings.immutableCheckMs = Math.round(performance.now() - buildStarted);
console.error(
`[${new Date().toISOString()}] Check: immutable phase ended after ${(timings.immutableCheckMs / 1000).toFixed(1)}s`,
);
}
const candidateText = await fs.readFile(path.join(report.artifactPath, "candidate.json"), "utf8");
await fs.writeFile(path.join(output, "candidate.json"), candidateText);
report.compilation = "passed";
try {
await fs.access(path.join(report.artifactPath, "execution-plan.json"));
report.runtime = "execution";
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
if (resource.kind === "workspace" && !options.contractOnly && report.runtime === "execution") {
report.phase = "semantic-review";
if (options.reviews && !options.baseline)
throw new Error("Execution semantic reviews require an explicit --baseline candidate");
const baseline = options.baseline
? (JSON.parse(await fs.readFile(options.baseline, "utf8")) as WorkspaceRevision)
: null;
const receipts = options.reviews
? (JSON.parse(await fs.readFile(options.reviews, "utf8")) as EvolutionReview[])
: [];
const review = reviewSemanticMajors(baseline, JSON.parse(candidateText), receipts);
await fs.writeFile(path.join(output, "semantic-review.json"), JSON.stringify(review, null, 2));
report.blockers.push(...review.blockers);
} else if (resource.kind === "workspace" && !options.contractOnly) {
report.phase = "evolution";
await progress();
let baseline = options.baseline;
if (!baseline) {
try {
const host = JSON.parse(await fs.readFile("/etc/quixos/workspace-source.json", "utf8"));
if ((await fs.realpath(host.workbenchRoot)) === context.workbench)
baseline = JSON.parse(
await fs.readFile(path.join(host.runtimeClosureRoot, "manifest.json"), "utf8"),
).workspacePlanPath;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
}
const before = baseline ? (JSON.parse(await fs.readFile(baseline, "utf8")) as WorkspaceRevision) : null;
const reviews = options.reviews
? (JSON.parse(await fs.readFile(options.reviews, "utf8")) as EvolutionReview[])
: [];
const evolution = planEvolution(before, JSON.parse(candidateText), { reviews });
await fs.writeFile(path.join(output, "evolution.json"), JSON.stringify(evolution, null, 2));
report.blockers.push(...evolution.blockers);
report.migrationRequired = evolution.migrationRequired;
report.activationReadiness = evolution.blockers.length
? "blocked"
: evolution.migrationRequired.length
? "migration-required"
: "preserve";
if (evolution.migrationRequired.length)
report.blockers.push(
`Explicit migration required for: ${evolution.migrationRequired.join(", ")}. Compilation passed; supply a migration path before cutover.`,
);
}
if (!report.blockers.length) report.phase = options.contractOnly ? "contract-only" : "checked";
} catch (error) {
report.blockers.push(String(error instanceof Error ? error.message : error));
}
timings.totalMs = Math.round(performance.now() - started);
Object.assign(report, { timings });
await progress(false);
if (options.contractOnly) return report;
const records = path.join(context.workbench, ".quixos/checks");
await fs.mkdir(records, { recursive: true });
const remember = async (value: typeof report) => {
const filename = path.join(records, checkRecordName(value.directory)),
temporary = `${filename}.${randomUUID()}.tmp`;
await fs.writeFile(temporary, JSON.stringify(value, null, 2), { flag: "wx", mode: 0o600 });
await fs.rename(temporary, filename);
};
if (report.artifactPath && report.compilation === "passed" && !report.blockers.length) {
const graph = JSON.parse(await fs.readFile(path.join(report.artifactPath, "graph.json"), "utf8"));
for (const checked of graph.resources) {
const managed = context.resources.find(
(entry) => entry.kind === checked.kind && entry.source?.repository === checked.source.repository,
);
if (managed && managed.directory !== directory)
await remember({
...report,
directory: managed.directory,
commit: checked.source.commit,
blockers: [],
phase: "checked",
});
}
}
await remember(report);
return report;
}
@@ -0,0 +1,119 @@
import { readFile, realpath } from "node:fs/promises";
import path from "node:path";
import { loadQuixosLock, type GitSource } from "../resource-lock/index.js";
export type AuthoringResource = {
kind: "workspace" | "interface" | "package" | "build";
directory: string;
resourceId?: string;
source?: GitSource;
};
/** Registration, not directory co-location, defines the editable selection. */
export async function authoringContext(start: string) {
let workbench = await realpath(start);
for (;;) {
let text: string | undefined;
try {
text = await readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
if (text !== undefined) {
const graph = JSON.parse(text) as { resources: AuthoringResource[]; workspaceId?: string };
if (!graph || typeof graph !== "object" || Array.isArray(graph))
throw new Error("Managed resource inventory is malformed");
if (
graph.workspaceId !== undefined &&
(typeof graph.workspaceId !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9:._-]{0,255}$/.test(graph.workspaceId))
)
throw new Error("Managed workbench has an invalid Workspace identity");
if (!Array.isArray(graph.resources)) throw new Error("Managed resource inventory is malformed");
const resources: AuthoringResource[] = [{ kind: "workspace", directory: "root" }];
const identities = new Set<string>(),
directories = new Set<string>(["root"]);
for (const [index, entry] of graph.resources.entries()) {
const invalid = (reason: string): never => {
throw new Error(`Invalid managed resource registration at entry ${index + 1}: ${reason}`);
};
if (
!entry ||
typeof entry !== "object" ||
Array.isArray(entry) ||
!["interface", "package", "build"].includes(entry.kind)
)
invalid("unsupported resource kind");
if (
entry.resourceId !== undefined &&
(typeof entry.resourceId !== "string" ||
entry.resourceId.length < 1 ||
entry.resourceId.length > 256 ||
entry.resourceId.includes("\0"))
)
invalid("resource identity must be a bounded nonempty identifier");
const source = entry.source;
if (
!source ||
typeof source !== "object" ||
Array.isArray(source) ||
source.resolver !== "git" ||
typeof source.repository !== "string" ||
!source.repository ||
source.repository.length > 4096 ||
source.repository.includes("\0") ||
typeof source.commit !== "string" ||
!/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(source.commit)
)
invalid("expected an immutable Git source");
// Git transport policy belongs to the source resolver, including its
// maintained local/file fixtures; registration validates only its shape.
if (
typeof entry.directory !== "string" ||
!entry.directory ||
entry.directory.length > 4096 ||
entry.directory.includes("\0")
)
invalid("directory must be a bounded path");
// Bootstrap intentionally records stable aliases for native histories.
// Compare physical paths on BOTH sides, then expose the same canonical
// workbench-relative selection for alias and physical working dirs.
let directory: string;
try {
directory = path.relative(workbench, await realpath(path.resolve(workbench, entry.directory)));
} catch {
invalid("registered directory is unavailable");
}
if (!/^resources\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(directory!))
invalid("registered directory must resolve beneath workbench resources");
const identity = `${entry.kind}\0${entry.resourceId ?? source!.repository}`;
if (identities.has(identity) || directories.has(directory!)) {
throw new Error(`Multiple editable selections for ${directory!}`);
}
identities.add(identity);
directories.add(directory!);
resources.push({
kind: entry.kind,
directory: directory!,
resourceId: entry.resourceId,
source: entry.source,
});
}
return {
workbench,
workspaceId: graph.workspaceId,
resources,
async baseline() {
const result = await loadQuixosLock(path.join(workbench, "root/quixos.lock"));
if (!result.ok)
throw new Error(
`Workspace source baseline is invalid: ${result.diagnostics.map((d) => d.message).join("; ")}`,
);
return result.lock.quixos;
},
};
}
const parent = path.dirname(workbench);
if (parent === workbench) throw new Error("Not in a managed workbench; select one with --workbench DIRECTORY");
workbench = parent;
}
}
@@ -0,0 +1,322 @@
import { readFile, writeFile, rename, rm, realpath } from "node:fs/promises";
import path from "node:path";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { randomUUID } from "node:crypto";
import { authoringContext } from "./authoring-context.js";
import { managedResourcePath } from "./managed-source-path.js";
import { snapshotCommit } from "./checked-build.js";
import { profile } from "./profile.js";
import {
loadQuixosLock,
parseQuixosLockDocument,
formatQuixosLockDocument,
retentionTagForCommit,
type GitSource,
} from "../resource-lock/index.js";
const execFile = promisify(callback);
// Bound independent repository operations. One visit owns each repository;
// all visits finish before the final concurrent-edit validation pass.
const concurrency = 4;
async function mapConcurrent<T, R>(values: readonly T[], action: (value: T) => Promise<R>): Promise<R[]> {
const results = new Array<R>(values.length);
let next = 0;
await Promise.all(
Array.from({ length: Math.min(concurrency, values.length) }, async () => {
while (next < values.length) {
const index = next++;
results[index] = await action(values[index]);
}
}),
);
return results;
}
const limiter = () => {
let running = 0;
const waiting: Array<() => void> = [];
return async <T>(action: () => Promise<T>): Promise<T> => {
await new Promise<void>((resolve) => {
if (running < concurrency) {
running++;
resolve();
} else waiting.push(resolve);
});
try {
return await action();
} finally {
const next = waiting.shift();
if (next) next();
else running--;
}
};
};
const command = async (cwd: string, executable: string, args: string[]) => {
const start = performance.now();
// Do not log arguments: transports may contain credentials. Source identities
// remain in the normal checked result, not in this timing channel.
const label = `${path.basename(cwd)} ${executable} ${args[0]}`;
try {
return (
await execFile(executable, args, {
cwd,
maxBuffer: 4 * 1024 * 1024,
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_SUBTREE_PUBLISH: "0", GIT_TERMINAL_PROMPT: "0" },
})
).stdout.trim();
} finally {
if (process.env.QUIXOS_TRACE_CAPTURE === "1")
console.error(`[${new Date().toISOString()}] Capture: ${label}: ${Math.round(performance.now() - start)}ms`);
}
};
const identity = (kind: string, repository: string) => `${kind}\0${repository}`;
export type AuthoringBlocker = {
directory: string;
phase: "resolution" | "dependency" | "source" | "publication" | "concurrent-edit";
message: string;
};
/** Source retention only. Neither successful convergence nor an empty source
* worklist grants typechecking, semantic review or activation approval.
* Caller serializes coordinators; package authors may still be editing. */
export async function convergeAuthoring(start: string, target = "root") {
const started = performance.now();
const context = await authoringContext(start);
const graphFile = path.join(context.workbench, ".quixos/resource-graph.json");
const graphBefore = await readFile(graphFile, "utf8");
const graph = JSON.parse(graphBefore);
// Discovery resolves outer guest aliases. Persist those same physical
// selections, without accepting symlinks within the managed workbench.
for (const resource of graph.resources)
resource.directory = await managedResourcePath(context.workbench, resource.directory);
const blockers: AuthoringBlocker[] = [];
const nodes = new Map<string, { directory: string; kind: string; source: GitSource; dependencies: string[] }>();
const selected = new Map<string, string>();
const inventory = await mapConcurrent(context.resources, async (entry) => {
const root = path.join(context.workbench, entry.directory);
let repository = entry.source?.repository;
try {
if ((await realpath(root)) !== root) throw new Error(`Managed checkout crosses a symlink: ${entry.directory}`);
// Transport rewrites must not become committed source identities.
const origin = await command(root, "git", ["config", "--get", "remote.origin.url"]);
if (repository && origin !== repository)
throw new Error(`Origin differs from registered source for ${entry.directory}`);
repository ??= origin;
} catch (error) {
blockers.push({ directory: entry.directory, phase: "source", message: String(error).slice(0, 2000) });
if (!repository) throw error; // The root has no separate registered source.
}
return { ...entry, repository };
});
for (const { repository, ...entry } of inventory) {
const key = identity(entry.kind, repository);
if (selected.has(key)) throw new Error(`More than one editable checkout for ${repository}`);
selected.set(key, entry.directory);
nodes.set(entry.directory, {
...entry,
source: { resolver: "git", repository, commit: entry.source?.commit ?? "" },
dependencies: [],
});
}
await mapConcurrent([...nodes.values()], async (node) => {
try {
const lock = await loadQuixosLock(path.join(context.workbench, node.directory, "quixos.lock"));
if (!lock.ok) throw new Error(lock.diagnostics.map((d) => `${d.fileName}: ${d.message}`).join("\n"));
node.dependencies = [
...new Set(
lock.lock.resources.flatMap((entry) => {
const directory = selected.get(identity(entry.kind, entry.source.repository));
return directory ? [directory] : [];
}),
),
];
} catch (error) {
blockers.push({ directory: node.directory, phase: "resolution", message: String(error) });
}
});
// Detect cycles before starting concurrent futures: two roots awaiting each
// other must fail clearly rather than deadlocking on their cached promises.
const scanned = new Set<string>();
const detectCycles = (directory: string, parents: string[] = []) => {
if (parents.includes(directory)) {
blockers.push({
directory,
phase: "dependency",
message: `Source dependency cycle: ${[...parents, directory].join(" -> ")}`,
});
return;
}
if (scanned.has(directory)) return;
for (const dependency of nodes.get(directory)!.dependencies) detectCycles(dependency, [...parents, directory]);
scanned.add(directory);
};
for (const directory of nodes.keys()) detectCycles(directory);
const complete = new Map<string, GitSource>();
const pending = new Map<string, Promise<boolean>>();
const capture = limiter();
const visited = new Set<string>();
if (!nodes.has(target)) throw new Error(`Not a registered repository: ${target}`);
const visit = (directory: string): Promise<boolean> => {
visited.add(directory);
const existing = pending.get(directory);
if (existing) return existing;
const result = (async () => {
if (blockers.some((entry) => entry.directory === directory)) return false;
const node = nodes.get(directory)!;
const ready = await Promise.all(node.dependencies.map(visit));
const unavailable = node.dependencies.find((_, index) => !ready[index]);
if (unavailable) {
blockers.push({ directory, phase: "dependency", message: `Waiting for ${unavailable}` });
return false;
}
return await capture(async () => {
const root = path.join(context.workbench, directory);
let phase: AuthoringBlocker["phase"] = "source";
try {
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
if (!lock.ok) throw new Error("Lock changed during convergence; retry after joining writers");
for (const file of lock.lock.sourceFiles ?? ["quixos.lock"]) {
const filename = path.join(root, file),
before = await readFile(filename, "utf8");
const parsed = parseQuixosLockDocument(before, file);
if (!parsed.ok) throw new Error(`Invalid lock ${file}`);
let changed = false;
for (const dependency of parsed.document.resources) {
const target = selected.get(identity(dependency.kind, dependency.source.repository));
const source = target ? complete.get(target) : undefined;
if (target && !source)
throw new Error(
`Dependencies changed during convergence (${dependency.binding}); join writers and retry`,
);
if (source && source.commit !== dependency.source.commit) {
dependency.source = source;
changed = true;
}
}
if (changed) {
const temporary = `${filename}.${randomUUID()}.tmp`;
try {
await writeFile(temporary, formatQuixosLockDocument(parsed.document), { flag: "wx" });
if ((await readFile(filename, "utf8")) !== before)
throw new Error(`Concurrent edit to ${file}; retry after joining writers`);
await rename(temporary, filename);
} finally {
await rm(temporary, { force: true });
}
}
}
const snapshotStarted = performance.now();
const commit = await snapshotCommit(root);
if (process.env.QUIXOS_TRACE_CAPTURE === "1")
console.error(
`[${new Date().toISOString()}] Capture: ${directory} snapshot: ${Math.round(performance.now() - snapshotStarted)}ms`,
);
phase = "publication";
const ref = retentionTagForCommit(commit);
const remote = await command(root, "git", ["ls-remote", "--refs", node.source.repository, ref]);
if (remote && remote.split(/\s+/)[0] !== commit)
throw new Error(`Conflicting immutable retention ref ${ref}`);
if (!remote) {
await command(root, "git", ["push", node.source.repository, `${commit}:${ref}`]);
if (
(await command(root, "git", ["ls-remote", "--refs", node.source.repository, ref])).split(/\s+/)[0] !==
commit
)
throw new Error("Published source retention was not observed");
}
complete.set(directory, { ...node.source, commit });
} catch (error) {
blockers.push({ directory, phase, message: String(error).slice(0, 4000) });
}
return complete.has(directory);
});
})();
pending.set(directory, result);
return result;
};
// Include newly created, not-yet-imported resources, then the root.
if (target === "root") await Promise.all([...nodes.keys()].filter((d) => d !== "root").map(visit));
await visit(target);
for (let index = blockers.length - 1; index >= 0; index--)
if (!visited.has(blockers[index].directory)) blockers.splice(index, 1);
const recheckStarted = performance.now();
await mapConcurrent([...complete], async ([directory, source]) => {
try {
if ((await snapshotCommit(path.join(context.workbench, directory))) !== source.commit)
throw new Error("Source advanced while converging; join writers and retry");
} catch (error) {
blockers.push({ directory, phase: "concurrent-edit", message: String(error) });
}
});
profile("capture-recheck", recheckStarted, { repositories: complete.size });
// Persist successful selections even if another repository is still broken.
// Recovery must not depend on all parents succeeding in the same invocation.
const replacements = new Map<string, string>();
for (const resource of graph.resources) {
const source = complete.get(resource.directory);
if (!source) continue;
const key = `${resource.kind}\0${source.repository}\0${source.commit}`;
replacements.set(resource.key, key);
if (resource.source.commit !== source.commit) delete resource.revisionId;
resource.source = source;
resource.key = key;
}
for (const resource of graph.resources)
for (const dependency of resource.dependencies ?? []) {
dependency.resourceKey = replacements.get(dependency.resourceKey) ?? dependency.resourceKey;
}
for (const direct of graph.directResources ?? [])
direct.resourceKey = replacements.get(direct.resourceKey) ?? direct.resourceKey;
// Inventory is a projection of actual locks, including newly added/removed
// imports. Never require a successful parent compilation to repair it.
for (const [directory] of complete) {
const lock = await loadQuixosLock(path.join(context.workbench, directory, "quixos.lock"));
if (!lock.ok) continue;
const dependencies = lock.lock.resources.map((dependency) => ({
binding: `${dependency.kind}\0${dependency.binding}`,
resourceKey: `${dependency.kind}\0${dependency.source.repository}\0${dependency.source.commit}`,
}));
if (directory === "root") {
graph.quixos = lock.lock.quixos;
graph.directResources = lock.lock.resources.map((dependency, index) => ({
kind: dependency.kind,
binding: dependency.binding,
resourceKey: dependencies[index].resourceKey,
...(selected.has(identity(dependency.kind, dependency.source.repository))
? { directory: selected.get(identity(dependency.kind, dependency.source.repository)) }
: {}),
}));
} else {
const resource = graph.resources.find((entry: { directory: string }) => entry.directory === directory);
if (resource) resource.dependencies = dependencies;
}
}
const graphAfter = JSON.stringify(graph, null, 2) + "\n";
if (graphBefore !== graphAfter) {
const temporary = `${graphFile}.${randomUUID()}.tmp`;
try {
await writeFile(temporary, graphAfter, { flag: "wx", mode: 0o600 });
if ((await readFile(graphFile, "utf8")) !== graphBefore)
throw new Error(
"Managed inventory changed during convergence; source is retained, retry after joining writers",
);
await rename(temporary, graphFile);
} finally {
await rm(temporary, { force: true });
}
}
profile("capture", started, { repositories: nodes.size, snapshots: complete.size * 2, target });
return {
workbench: context.workbench,
converged: blockers.length === 0,
candidate: blockers.length ? null : (complete.get(target) ?? null),
retained: [...complete]
.sort(([left], [right]) => left.localeCompare(right))
.map(([directory, source]) => ({ directory, source })),
worklist: blockers,
verificationEvidence: false,
activated: false,
};
}
@@ -0,0 +1,115 @@
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { realpath } from "node:fs/promises";
import path from "node:path";
import { parseQx, walkSyntax } from "./source.js";
import { authoringContext } from "./authoring-context.js";
import { readQxSource } from "./source-loader.js";
import { loadQuixosLock } from "../resource-lock/index.js";
const execFile = promisify(callback);
const git = async (root: string, args: string[]) =>
(
await execFile("git", ["-C", root, ...args], {
maxBuffer: 8 * 1024 * 1024,
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
})
).stdout;
const message = (error: unknown) => String(error instanceof Error ? error.message : error).slice(0, 2000);
/** Syntax-only contract inspection is deliberately NOT verification evidence.
* Each file can recover independently; current valid files always win. */
export async function inspectAuthoringRepository(root: string, historyLimit = 100) {
const names = (await git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]))
.split("\0")
.filter((name) => name.endsWith(".qx"));
if (names.length > 128)
throw new Error("Repository inspection exceeds 128 QX files; split the resource into smaller repositories");
const files = [];
for (const name of [...new Set(names)].sort()) {
let source = "",
errors: unknown[] = [],
revision: string | null = null;
try {
source = await readQxSource(root, name);
if (source.length > 262144) throw new Error(`Inspection file exceeds 256 KiB: ${name}`);
errors = parseQx(source, name).diagnostics;
} catch (error) {
errors = [{ message: message(error) }];
}
const currentErrors = errors;
if (errors.length) {
// Git can traverse jj's immutable commit DAG without mutating/snapshotting @.
const head = await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"], {
cwd: root,
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1" },
}).then(
(r) => r.stdout.trim(),
() => "HEAD",
);
const commits = await git(root, ["rev-list", `--max-count=${historyLimit}`, head, "--", name]).catch(() => "");
for (const commit of commits.trim().split("\n").filter(Boolean)) {
const historical = await git(root, ["show", `${commit}:${name}`]).catch(() => null);
if (historical === null || historical.length > 262144) continue;
if (!parseQx(historical, name).diagnostics.length) {
source = historical;
revision = commit;
errors = [];
break;
}
}
}
const syntax = errors.length ? null : parseQx(source, name);
files.push({
file: name,
status: errors.length ? "unavailable" : revision ? "historical" : "current",
revision,
currentErrors: currentErrors.slice(0, 20),
omittedErrors: Math.max(0, currentErrors.length - 20),
declarations: syntax
? [...walkSyntax(syntax.root)]
.filter((node) =>
/^(?:(?:interface|package|atom|state|edge|method|function|event|conformance|presentation)\w*Decl|module(?:Member|Export))$/.test(
node.kind,
),
)
.map((node) => ({ kind: node.kind, source: source.slice(node.start, node.end) }))
: [],
});
}
return { verificationEvidence: false as const, resolutionChecked: false as const, files };
}
export async function inspectWorkbench(start: string, selector?: string) {
const context = await authoringContext(start);
if (!selector || selector === ".") {
const relative = path.relative(context.workbench, await realpath(start));
selector =
context.resources.find((entry) => relative === entry.directory || relative.startsWith(entry.directory + path.sep))
?.directory ?? "root";
}
const lock = await loadQuixosLock(path.join(context.workbench, "root/quixos.lock"));
const aliases = lock.ok ? lock.lock.resources.filter((entry) => entry.binding === selector) : [];
const selected = context.resources.filter(
(entry) =>
!selector ||
selector === entry.directory ||
selector === entry.resourceId ||
selector === path.basename(entry.directory) ||
aliases.some((alias) => alias.kind === entry.kind && alias.source.repository === entry.source?.repository),
);
if (!selected.length) throw new Error(`No registered resource matches ${selector}`);
if (selector && selected.length > 1)
throw new Error(`Ambiguous resource ${selector}; use its resource ID or directory`);
const resources = [];
for (const entry of selected) {
try {
const root = path.join(context.workbench, entry.directory);
if ((await realpath(root)) !== root) throw new Error("Managed checkout crosses a symlink");
resources.push({ ...entry, ...(await inspectAuthoringRepository(root)) });
} catch (error) {
resources.push({ ...entry, error: message(error) });
}
}
return { workbench: context.workbench, verificationEvidence: false, resources };
}
@@ -0,0 +1,119 @@
import fs from "node:fs/promises";
import path from "node:path";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { authoringContext } from "./authoring-context.js";
import { inspectAuthoringRepository } from "./authoring-inspect.js";
import { checkRecordName } from "./authoring-check.js";
import { loadQuixosLock } from "../resource-lock/index.js";
import { checkerIdentity } from "./checked-build.js";
const execFile = promisify(callback);
export async function authoringWorklist(start: string) {
const context = await authoringContext(start);
const entries: { directory: string; resourceId?: string; phase: string; message: string; next: string }[] = [];
const dependencies = new Map<string, string[]>();
for (const resource of context.resources) {
const root = path.join(context.workbench, resource.directory);
const add = (phase: string, message: string) =>
entries.push({
directory: resource.directory,
resourceId: resource.resourceId,
phase,
message,
next:
phase === "syntax"
? `qx-workspace inspect ${resource.directory}`
: phase === "evolution"
? "Inspect evolution.json in the check output; resolve its named migration/review requirements before cutover"
: `cd ${resource.directory} && qx-workspace check`,
});
try {
if ((await fs.realpath(root)) !== root) throw new Error("Registered checkout crosses a symlink");
const inspected = await inspectAuthoringRepository(root);
for (const file of inspected.files)
if (file.currentErrors.length)
add(
"syntax",
`${file.file}: ${JSON.stringify(file.currentErrors)}${file.status === "historical" ? `; historical contract available at ${file.revision}` : ""}`,
);
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
if (!lock.ok)
add("resolution", lock.diagnostics.map((entry) => `${entry.fileName}: ${entry.message}`).join("\n"));
else
dependencies.set(
resource.directory,
lock.lock.resources.flatMap((dependency) => {
const selected = context.resources.find(
(entry) => entry.kind === dependency.kind && entry.source?.repository === dependency.source.repository,
);
if (selected?.source && selected.source.commit !== dependency.source.commit)
add("propagation", `Dependency ${dependency.binding} has advanced; check will repin it automatically`);
return selected ? [selected.directory] : [];
}),
);
let record;
try {
record = JSON.parse(
await fs.readFile(
path.join(context.workbench, ".quixos/checks", checkRecordName(resource.directory)),
"utf8",
),
);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
if (!record?.commit) {
if (record?.blockers?.length) add(record.phase, record.blockers.join("\n"));
else add("unchecked", "No immutable candidate check recorded yet");
continue;
}
if (record.checker !== checkerIdentity()) add("unchecked", "The installed checker changed since the last check");
const env = { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1" };
const commit = (
await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"], {
cwd: root,
env,
})
).stdout.trim();
const dirty = await execFile("git", ["diff", "--quiet", "--no-ext-diff", record.commit, "--"], {
cwd: root,
}).then(
() => false,
() => true,
);
const untracked = (await execFile("git", ["ls-files", "--others", "--exclude-standard"], { cwd: root })).stdout;
if (commit !== record.commit || dirty || untracked)
add("unchecked", `Edits are newer than the last check (${record.commit.slice(0, 12)})`);
else if (record.blockers?.length) add(record.phase, record.blockers.join("\n"));
} catch (error) {
add("inspection", String(error).slice(0, 3000));
}
}
// Fixed-point propagation, independent of registration order.
const blocked = new Set(entries.map((entry) => entry.directory));
let changed = true;
while (changed) {
changed = false;
for (const [directory, required] of dependencies)
if (!blocked.has(directory)) {
const waiting = required.filter((dependency) => blocked.has(dependency));
if (waiting.length) {
blocked.add(directory);
changed = true;
entries.push({
directory,
phase: "dependency",
message: `Waiting for ${waiting.join(", ")}`,
next: "Resolve the named repositories, then rerun check",
});
}
}
}
return {
workbench: context.workbench,
verificationEvidence: false,
worklist: entries,
note: "Derived authoring guidance, not activation approval. Independent repositories can be delegated separately; join writers before a root check.",
};
}
+265
View File
@@ -0,0 +1,265 @@
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { execFile as execFileCallback } from "node:child_process";
import { promisify } from "node:util";
import { createHash } from "node:crypto";
import { compileWorkspaceRepository, compileCapabilityResourceRepository } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import {
contentDigest,
planEvolution,
type EvolutionReview,
type WorkspaceRevision,
} from "../capability-model/index.js";
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
import { snapshotCommit, checkoutCommit, buildCheckedPackage } from "./checked-build.js";
const execFile = promisify(execFileCallback);
const bytesDigest = (value: Uint8Array) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
export const localResourceSnapshots = async (
root: string,
filename?: string,
): Promise<{ resources: { kind: string; repository: string; commit: string; directory: string }[] }> => {
if (filename) {
const document = JSON.parse(await fs.readFile(filename, "utf8"));
return {
resources: document.resources.map((entry: { directory: string }) => ({
...entry,
directory: path.resolve(path.dirname(filename), entry.directory),
})),
};
}
let directory = await fs.realpath(root);
for (;;) {
let graphText: string | undefined;
try {
graphText = await fs.readFile(path.join(directory, ".quixos/resource-graph.json"), "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
if (graphText !== undefined) {
const graph = JSON.parse(graphText);
const resources = [];
for (const entry of graph.resources) {
const location = await fs.realpath(path.resolve(directory, entry.directory));
if (!location.startsWith(`${directory}/resources/`))
throw new Error("Workbench resource escapes managed directory");
resources.push({ kind: entry.kind, ...entry.source, directory: location });
}
return { resources };
}
const parent = path.dirname(directory);
if (parent === directory) return { resources: [] };
directory = parent;
}
};
/** Copy actual authoring files without snapshotting jj or creating a Git commit. */
export const snapshotRepository = async (source: string, destination: string) => {
const root = await fs.realpath(source);
const files = async () =>
(
await execFile("git", ["-C", root, "ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
maxBuffer: 16 * 1024 * 1024,
})
).stdout
.split("\0")
.filter(Boolean)
.sort();
const names = [...new Set(await files())];
if (names.length > 50_000) throw new Error("Candidate source exceeds 50000 files");
const contents: { name: string; digest: string; mode: number }[] = [];
let bytes = 0;
await fs.mkdir(destination, { recursive: true, mode: 0o700 });
for (const name of names) {
if (path.isAbsolute(name) || name.split(/[\\/]/).some((part) => part === ".." || part === ".git" || part === ".jj"))
throw new Error("Invalid candidate source path");
const file = path.join(root, name);
let metadata;
try {
metadata = await fs.lstat(file);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
throw error;
}
if (!metadata.isFile() || metadata.isSymbolicLink() || !(await fs.realpath(file)).startsWith(`${root}${path.sep}`))
throw new Error(`Candidate source must be a regular file: ${name}`);
const data = await fs.readFile(file);
bytes += data.length;
if (bytes > 128 * 1024 * 1024) throw new Error("Candidate source exceeds 128 MiB");
contents.push({ name, digest: bytesDigest(data), mode: metadata.mode & 0o777 });
await fs.mkdir(path.dirname(path.join(destination, name)), { recursive: true });
await fs.writeFile(path.join(destination, name), data, { flag: "wx", mode: metadata.mode & 0o777 });
}
if (JSON.stringify([...new Set(await files())]) !== JSON.stringify(names))
throw new Error("Source files changed during candidate snapshot");
for (const entry of contents)
if (bytesDigest(await fs.readFile(path.join(root, entry.name))) !== entry.digest)
throw new Error(`Source changed during candidate snapshot: ${entry.name}`);
return { source: root, directory: destination, treeDigest: contentDigest(contents), files: contents };
};
// Local mirrors accelerate resolution, but only their committed locked trees
// may stand in for published dependencies. Never relabel dirty files as a pin.
async function committedResolver(root: string, temporary: string, filename?: string, publishedOnly = false) {
const map = publishedOnly ? { resources: [] } : await localResourceSnapshots(root, filename);
const resources = [];
for (const [index, entry] of map.resources.entries()) {
const directory = path.join(temporary, `dependency-${index}`);
await checkoutCommit(entry.directory, entry.commit, directory);
resources.push({ ...entry, directory });
}
const snapshotMap = path.join(temporary, "snapshots.json");
await fs.writeFile(snapshotMap, JSON.stringify({ resources }));
return createGitCapabilityResolver({ checkoutRoot: path.join(temporary, "resolved"), snapshotMap });
}
export const checkResourceCandidate = async (options: {
root: string;
output: string;
kind: "package" | "interface";
source: { repository: string; commit: string };
snapshotMap?: string;
publishedOnly?: boolean;
}) => {
await fs.mkdir(options.output, { mode: 0o700 });
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-resource-check-"));
const blockers: string[] = [];
let treeDigest: string | undefined, commit: string | undefined, artifactPath: string | undefined;
try {
commit = await snapshotCommit(options.root);
treeDigest = (await snapshotRepository(options.root, path.join(temporary, "observed"))).treeDigest;
const root = path.join(temporary, "source");
await checkoutCommit(options.root, commit, root);
const resolveResource = await committedResolver(
options.root,
temporary,
options.snapshotMap,
options.publishedOnly,
);
const compiled = await compileCapabilityResourceRepository({
rootDirectory: root,
kind: options.kind,
source: { resolver: "git", repository: options.source.repository, commit },
resolveResource,
});
if (compiled.resource.kind === "package") {
const schema = path.join(temporary, "bindings.json");
await fs.writeFile(schema, JSON.stringify(bindingSchema(compiled)));
artifactPath = await buildCheckedPackage(root, schema, compiled.resource.revision.revisionId);
}
if ((await snapshotCommit(options.root)) !== commit)
throw new Error("Source changed during verification; run the check again");
await fs.writeFile(path.join(options.output, "candidate.json"), JSON.stringify(compiled.resource, null, 2));
} catch (error) {
blockers.push(error instanceof Error ? error.message : String(error));
} finally {
await fs.rm(temporary, { recursive: true, force: true });
}
const result = {
candidateOnly: true,
activationEvidence: false,
commit,
treeDigest,
artifactPath,
blockers,
note: "Checked immutable candidate; cutover independently checks current migration/review requirements. No publication or activation performed.",
};
await fs.writeFile(path.join(options.output, "report.json"), JSON.stringify(result, null, 2));
return result;
};
export const checkWorkspaceCandidate = async (options: {
root: string;
output: string;
snapshotMap?: string;
baseline?: string;
reviews?: string;
}) => {
await fs.mkdir(options.output, { mode: 0o700 });
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-workspace-check-"));
const blockers: string[] = [],
checks: { packageRevisionId: string; artifactPath: string }[] = [];
let commit: string | undefined;
try {
commit = await snapshotCommit(options.root);
for (const entry of (await localResourceSnapshots(options.root, options.snapshotMap)).resources) {
const current = await snapshotCommit(entry.directory);
const tree = async (revision: string) =>
(await execFile("git", ["rev-parse", `${revision}^{tree}`], { cwd: entry.directory })).stdout.trim();
if ((await tree(current)) !== (await tree(entry.commit)))
throw new Error(
`Edited resource is not in the root's locked candidate: ${entry.directory}. Check that resource, then run qx-workspace resource upgrade --publish to propagate its revision.`,
);
}
const root = path.join(temporary, "source");
await checkoutCommit(options.root, commit, root);
const resolveResource = await committedResolver(options.root, temporary, options.snapshotMap);
const compiled = await compileWorkspaceRepository({
rootDirectory: root,
sourceRootCommit: commit,
resolveResource,
});
const baseline = options.baseline
? (JSON.parse(await fs.readFile(options.baseline, "utf8")) as WorkspaceRevision)
: null;
const reviews = options.reviews
? (JSON.parse(await fs.readFile(options.reviews, "utf8")) as EvolutionReview[])
: [];
const evolution = planEvolution(baseline, compiled.workspace, { reviews });
blockers.push(...evolution.blockers);
for (const resource of compiled.capabilityResources.filter((entry) => entry.kind === "package")) {
// Per-package recursive schema, identical to host activation, not unrelated
// workspace declarations that would unnecessarily invalidate build caches.
const candidate = await compileCapabilityResourceRepository({
rootDirectory: resource.directory,
kind: "package",
source: resource.source,
resolveResource,
});
if (candidate.resource.kind !== "package") throw Error("Expected checked package contract");
const schema = path.join(temporary, "bindings.json");
await fs.writeFile(
schema,
JSON.stringify(
specializeBindingSchema(bindingSchema(candidate), compiled.workspace, candidate.resource.revision.revisionId),
),
);
const artifactPath = await buildCheckedPackage(
resource.directory,
schema,
candidate.resource.revision.revisionId,
);
checks.push({ packageRevisionId: candidate.resource.revision.revisionId, artifactPath });
}
if ((await snapshotCommit(options.root)) !== commit)
throw new Error("Source changed during verification; run the check again");
const result = {
schemaVersion: 1,
candidateOnly: true,
activationEvidence: false,
commit,
evolution,
checks,
blockers,
note: "Checks the committed root and its exact locked dependencies. Resource edits must be verified and repinned before they enter this candidate. No publication or activation performed.",
};
await fs.writeFile(path.join(options.output, "candidate.json"), JSON.stringify(compiled.workspace, null, 2));
await fs.writeFile(path.join(options.output, "report.json"), JSON.stringify(result, null, 2));
return result;
} catch (error) {
const result = {
schemaVersion: 1,
candidateOnly: true,
activationEvidence: false,
commit,
checks,
blockers: [...blockers, error instanceof Error ? error.message : String(error)],
};
await fs.writeFile(path.join(options.output, "report.json"), JSON.stringify(result, null, 2));
return result;
} finally {
await fs.rm(temporary, { recursive: true, force: true });
}
};
+234
View File
@@ -0,0 +1,234 @@
import fs from "node:fs/promises";
import path from "node:path";
import { execFile as callback, spawn } from "node:child_process";
import { createWriteStream } from "node:fs";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
const execFile = promisify(callback);
const environment = () => ({ ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", GIT_TERMINAL_PROMPT: "0" });
export const checkerIdentity = () =>
process.env.QUIXOS_CHECK_GENERATOR ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
/** Checking snapshots jj, but never publishes or activates the working copy. */
export async function snapshotCommit(root: string): Promise<string> {
const run = async (...args: string[]) =>
(await execFile("jj", args, { cwd: root, env: environment() })).stdout.trim();
// A single normal log snapshots the working copy and observes both values
// from that exact revision. Separate status/log processes add no evidence.
const [conflict, commit] = (await run("log", "--no-graph", "-r", "@", "-T", 'conflict ++ "\\n" ++ commit_id')).split(
"\n",
);
if (conflict !== "false") throw new Error("Resolve source conflicts before verification");
if (!commit || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit))
throw new Error("Verification requires an exact jj commit");
await execFile("git", ["diff", "--exit-code", "--no-ext-diff", "--no-textconv", commit, "--"], {
cwd: root,
env: environment(),
});
const { stdout } = await execFile("git", ["ls-files", "--others", "--exclude-standard", "-z"], { cwd: root });
if (stdout) throw new Error("Source contains files not captured by jj; inspect jj tracking before verification");
return commit;
}
/** Use Git's actual committed tree, never dirty overlays labelled as old pins. */
export async function checkoutCommit(root: string, commit: string, destination: string) {
await fs.mkdir(destination, { recursive: true });
const archive = await execFile("git", ["archive", "--format=tar", commit], {
cwd: root,
encoding: "buffer",
maxBuffer: 128 * 1024 * 1024,
});
await new Promise<void>((resolve, reject) => {
const child = spawn("tar", ["-xf", "-", "-C", destination], { stdio: ["pipe", "ignore", "pipe"] });
let error = "";
child.stderr.on("data", (chunk) => {
error += chunk;
});
child.on("error", reject);
child.on("close", (code) =>
code === 0 ? resolve() : reject(new Error(`Cannot extract committed source: ${error}`)),
);
child.stdin.on("error", reject);
child.stdin.end(archive.stdout);
});
}
/** Shared by provisional checks, template publication and host activation.
* The Nix derivation is the cached check; its metadata is internal provenance,
* not a certificate authored or approved by the workspace agent.
*/
export async function buildCheckedPackage(source: string, schema: string, packageRevisionId: string): Promise<string> {
const generator =
process.env.QUIXOS_CHECK_GENERATOR ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
if (!/^\/nix\/store\/[^/]+$/.test(generator))
throw new Error("Run verification with the installed Quixos tooling (its exact Nix checker is required)");
const builder = path.join(generator, "share/checked-package.nix");
await fs.access(builder);
return await new Promise((resolve, reject) => {
const child = spawn(
"nix",
[
"build",
"--impure",
"--file",
builder,
"--argstr",
"source",
source,
"--argstr",
"schema",
schema,
"--argstr",
"generator",
generator,
"--argstr",
"packageRevisionId",
packageRevisionId,
"--no-link",
"--print-out-paths",
"-L",
],
{ env: environment(), stdio: ["ignore", "pipe", "inherit"] },
);
let output = "";
child.stdout.on("data", (chunk) => {
output += chunk;
});
child.on("error", reject);
child.on("close", (code) => {
const artifact = output.trim();
if (code !== 0 || !/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+$/.test(artifact))
reject(new Error(`Checked Nix build failed (${code}); see build diagnostics above`));
else resolve(artifact);
});
});
}
/** Exact remote source DAG; the Nix checker owns schema construction and all
* nested fetches. Keep build noise in a named log, with a bounded failure tail. */
export async function buildImmutableCandidate(
source: { repository: string; commit: string },
kind: "workspace" | "interface" | "package" | "build",
logFile: string,
contractOnly = false,
observe?: (phase: "nixEvaluationMs" | "nixBuildMs", ms: number) => void,
workspaceId?: string,
platformSource?: { repository: string; commit: string; directory: string },
): Promise<string> {
if (workspaceId !== undefined && (kind !== "workspace" || !/^[a-zA-Z0-9][a-zA-Z0-9:._-]{0,255}$/.test(workspaceId)))
throw new Error("Instance binding requires a valid Workspace identity and a Workspace root");
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(source.commit))
throw new Error("An immutable candidate requires an exact commit");
const url = new URL(source.repository);
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash)
throw new Error("Candidate origin must be credential-free HTTPS");
if (
platformSource &&
(Object.keys(platformSource).sort().join(",") !== "commit,directory,repository" ||
!/^[a-f0-9]{40}$/.test(platformSource.commit) ||
!/^\/nix\/store\/[a-z0-9]{32}-[^/\s]+$/.test(platformSource.directory))
)
throw Error("Invalid immutable platform source input");
const generator =
process.env.QUIXOS_CHECK_GENERATOR ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
if (!/^\/nix\/store\/[^/]+$/.test(generator)) throw new Error("Use the installed Quixos checker");
const builder = path.join(generator, "share/checked-candidate.nix");
await fs.access(builder);
const log = createWriteStream(logFile, { flags: "wx", mode: 0o600 });
await new Promise<void>((resolve, reject) => {
log.once("open", () => resolve());
log.once("error", reject);
});
let logBytes = 0;
const run = async (executable: string, args: string[]) =>
await new Promise<string>((resolve, reject) => {
let output = "",
tail = "",
failure: Error | undefined;
const child = spawn(executable, args, { env: environment(), stdio: ["ignore", "pipe", "pipe"] });
const failedLog = (error: Error) => {
failure = error;
child.kill();
};
log.on("error", failedLog);
child.stdout.on("data", (chunk) => {
if (failure) return;
if (Buffer.byteLength(output) + chunk.length > 65536)
failedLog(new Error("Candidate checker output exceeds 64 KiB"));
else output += chunk;
});
child.stderr.on("data", (chunk) => {
if (failure) return;
logBytes += chunk.length;
if (logBytes > 16 * 1024 * 1024) {
failedLog(new Error("Candidate checker log exceeds 16 MiB"));
return;
}
log.write(chunk);
tail = (tail + String(chunk)).slice(-6000);
});
child.on("error", (error) => {
failure = error;
});
child.on("close", (code) => {
log.off("error", failedLog);
if (failure) reject(failure);
else if (code !== 0) reject(new Error(`Candidate Nix check failed (${code}). Full log: ${logFile}\n${tail}`));
else resolve(output.trim());
});
});
const timed = async (phase: "nixEvaluationMs" | "nixBuildMs", action: () => Promise<string>) => {
const started = performance.now();
try {
return await action();
} finally {
observe?.(phase, Math.round(performance.now() - started));
}
};
let gcDirectory: string | undefined;
try {
// Evaluation includes import-from-derivation builds (locks and contracts).
// Build the resulting derivation directly, without evaluating the DAG twice.
// Keep the derivation alive across the two processes, including concurrent
// GC. This temporary root is removed on success and on verification failure.
gcDirectory = await fs.mkdtemp(path.join(path.dirname(logFile), ".gc-root-"));
const gcRoot = path.join(gcDirectory, "candidate");
const derivation = await timed("nixEvaluationMs", async () => {
await run("nix-instantiate", [
builder,
"--argstr",
"repository",
source.repository,
"--argstr",
"commit",
source.commit,
"--argstr",
"kind",
kind,
"--argstr",
"generator",
generator,
"--arg",
"contractOnly",
contractOnly ? "true" : "false",
...(workspaceId === undefined ? [] : ["--argstr", "workspaceId", workspaceId]),
...(platformSource ? ["--argstr", "platformSourceJSON", JSON.stringify(platformSource)] : []),
"--add-root",
gcRoot,
"--indirect",
]);
return await fs.readlink(gcRoot);
});
if (!/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+\.drv$/.test(derivation))
throw new Error("Nix returned an invalid candidate derivation");
const artifact = await timed("nixBuildMs", () =>
run("nix", ["build", derivation + "^*", "--no-link", "--print-out-paths", "-L"]),
);
if (!/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+$/.test(artifact))
throw new Error("Nix returned an invalid candidate artifact");
return artifact;
} finally {
await new Promise<void>((resolve) => log.end(resolve));
if (gcDirectory) await fs.rm(gcDirectory, { recursive: true, force: true });
}
}
@@ -0,0 +1,40 @@
import { readFile } from "node:fs/promises";
import type { CheckedResourceResolver } from "./assembly.js";
import type { CapabilityResource } from "./parser.js";
/** Internal compiler inputs produced by checked-candidate.nix, not authoring
* receipts. The Nix composition owns their derivations and exact toolchain.
* Ordinary compiler callers omit this map and run all checks directly. */
export async function loadCheckedResources(filename?: string): Promise<CheckedResourceResolver | undefined> {
if (!filename) return undefined;
const entries = JSON.parse(await readFile(filename, "utf8")) as Array<{
kind: string;
repository: string;
commit: string;
artifact: string;
}>;
const resources = new Map<string, string>();
for (const entry of entries) {
const key = `${entry.kind}\0${entry.repository}\0${entry.commit}`;
if (resources.has(key)) throw new Error("Repeated checked resource input");
if (!/^\/nix\/store\/[^/]+$/.test(entry.artifact))
throw new Error("Checked resources must be immutable Nix outputs");
resources.set(key, entry.artifact);
}
return async (kind, source, lock, dependencies) => {
const artifact = resources.get(`${kind}\0${source.repository}\0${source.commit}`);
if (!artifact) return undefined;
const graph = JSON.parse(await readFile(`${artifact}/graph.json`, "utf8"));
const expected = [...dependencies].map(([binding, node]) => ({ binding, resourceKey: node.key }));
if (
graph.quixos.commit !== lock.quixos.commit ||
graph.quixos.repository !== lock.quixos.repository ||
graph.root.kind !== kind ||
graph.root.source.repository !== source.repository ||
graph.root.source.commit !== source.commit ||
JSON.stringify(graph.root.dependencies) !== JSON.stringify(expected)
)
throw new Error("Checked resource input does not match its locked dependency graph");
return JSON.parse(await readFile(`${artifact}/candidate.json`, "utf8")) as CapabilityResource;
};
}
+55 -27
View File
@@ -2,21 +2,23 @@
import { readFile } from "node:fs/promises";
import process from "node:process";
import { compileCapabilitySource } from "./parser.js";
import { compileCapabilityResourceSource, compileCapabilitySource } from "./parser.js";
import { capabilityId } from "../capability-model/types.js";
import { compileWorkspaceRevision } from "../capability-model/validation.js";
const usage = `usage: quixos-capability-compile [--check]
const usage = `usage: quixos-capability-compile [--check] [--resource]
[--workspace-id ID] [--workspace-revision-id ID]
[--source-root-commit GIT_REV] <workspace.qx | ->
Parses, lowers, and validates a Quixos capability workspace. By default the
checked semantic workspace revision is written as JSON. --check emits no JSON.
Parses, lowers, and validates a Quixos capability workspace. --resource instead
expects one standalone interface or package document. By default the checked
semantic workspace revision or resource is written as JSON. --check emits no JSON.
The identity overrides instantiate a checked built-in/template assembly for one
real workspace; both must be supplied together.`;
const parseArgs = (args: string[]) => {
let checkOnly = false;
let resource = false;
let workspaceId: string | undefined;
let workspaceRevisionId: string | undefined;
let sourceRootCommit: string | undefined;
@@ -24,6 +26,7 @@ const parseArgs = (args: string[]) => {
for (let index = 0; index < args.length; index += 1) {
const argument = args[index]!;
if (argument === "--check") checkOnly = true;
else if (argument === "--resource") resource = true;
else if (argument === "--workspace-id") workspaceId = args[++index];
else if (argument === "--workspace-revision-id") workspaceRevisionId = args[++index];
else if (argument === "--source-root-commit") sourceRootCommit = args[++index];
@@ -31,10 +34,14 @@ const parseArgs = (args: string[]) => {
else if (argument.startsWith("-")) throw new Error(usage);
else positional.push(argument);
}
if (positional.length !== 1 || Boolean(workspaceId) !== Boolean(workspaceRevisionId)) {
if (
positional.length !== 1 ||
Boolean(workspaceId) !== Boolean(workspaceRevisionId) ||
(resource && Boolean(workspaceId || workspaceRevisionId || sourceRootCommit))
) {
throw new Error(usage);
}
return { checkOnly, workspaceId, workspaceRevisionId, sourceRootCommit, fileName: positional[0]! };
return { checkOnly, resource, workspaceId, workspaceRevisionId, sourceRootCommit, fileName: positional[0]! };
};
const main = async () => {
@@ -43,13 +50,7 @@ const main = async () => {
process.stdout.write(`${usage}\n`);
return;
}
const {
checkOnly,
workspaceId,
workspaceRevisionId,
sourceRootCommit,
fileName,
} = parseArgs(args);
const { checkOnly, resource, workspaceId, workspaceRevisionId, sourceRootCommit, fileName } = parseArgs(args);
const source =
fileName === "-"
? await new Promise<string>((resolve, reject) => {
@@ -59,27 +60,54 @@ const main = async () => {
process.stdin.on("error", reject);
})
: await readFile(fileName, "utf8");
const result = compileCapabilitySource(source, fileName);
if (!result.ok) {
for (const diagnostic of result.diagnostics) {
const reportDiagnostics = (
diagnostics: readonly {
fileName: string;
line: number;
column: number;
phase: string;
code: string;
message: string;
path?: string;
}[],
) => {
for (const diagnostic of diagnostics) {
const location = diagnostic.line
? `${diagnostic.fileName}:${diagnostic.line}:${diagnostic.column + 1}`
: `${diagnostic.fileName}${diagnostic.path ? `:${diagnostic.path}` : ""}`;
process.stderr.write(
`${location}: ${diagnostic.phase} ${diagnostic.code}: ${diagnostic.message}\n`,
);
process.stderr.write(`${location}: ${diagnostic.phase} ${diagnostic.code}: ${diagnostic.message}\n`);
}
process.exitCode = 1;
};
if (resource) {
const result = compileCapabilityResourceSource(source, {
source: {
repository: "https://compiler.invalid/resource.git",
commit: "0000000000000000000000000000000000000000",
},
fileName,
});
if (!result.ok) {
reportDiagnostics(result.diagnostics);
return;
}
if (!checkOnly) process.stdout.write(`${JSON.stringify(result.resource, null, 2)}\n`);
return;
}
const workspace = workspaceId && workspaceRevisionId
? {
...result.workspace,
workspaceId: capabilityId.workspace(workspaceId),
id: capabilityId.workspaceRevision(workspaceRevisionId),
...(sourceRootCommit ? { sourceRootCommit } : {}),
}
: { ...result.workspace, ...(sourceRootCommit ? { sourceRootCommit } : {}) };
const result = compileCapabilitySource(source, fileName);
if (!result.ok) {
reportDiagnostics(result.diagnostics);
return;
}
const workspace =
workspaceId && workspaceRevisionId
? {
...result.workspace,
workspaceId: capabilityId.workspace(workspaceId),
id: capabilityId.workspaceRevision(workspaceRevisionId),
...(sourceRootCommit ? { sourceRootCommit } : {}),
}
: { ...result.workspace, ...(sourceRootCommit ? { sourceRootCommit } : {}) };
const instantiated = compileWorkspaceRevision(workspace);
if (!instantiated.ok) {
throw new Error(instantiated.issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n"));
+52
View File
@@ -0,0 +1,52 @@
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
const execFile = promisify(callback);
export const nixLiteral = (value: string): string => JSON.stringify(value).replace(/\$\{/g, "\\${");
/** Recognize the deliberately small scaffold edit surface, never evaluate Nix.
* Authors may replace it with arbitrary Nix; then scaffolding declines the edit. */
export async function componentEntries(source: string): Promise<Record<string, { entry: string; export: string }>> {
const { stdout } = await execFile("nix-instantiate", ["--parse", "--expr", source], { maxBuffer: 1024 * 1024 });
const tokens = stdout.match(/"(?:\\.|[^"\\])*"|[A-Za-z_][A-Za-z0-9_-]*|[{}=;]/g) ?? [];
if (stdout.replace(/"(?:\\.|[^"\\])*"|[A-Za-z_][A-Za-z0-9_-]*|[{}=;]|\s+/g, ""))
throw Error("components.nix is outside the supported literal-entry shape");
let cursor = 0;
const expect = (token: string) => {
if (tokens[cursor++] !== token) throw Error("components.nix is outside the supported literal-entry shape");
};
const string = () => {
const token = tokens[cursor++];
if (!token?.startsWith('"')) throw Error("Component entries must use literal strings");
const json = token.replace(/(\\*)\$\{/g, (_match, escapes: string) => {
if (escapes.length % 2 === 0) throw Error("Component entries must use literal strings");
return escapes.slice(1) + "${";
});
return JSON.parse(json) as string;
};
const entries: Record<string, { entry: string; export: string }> = Object.create(null);
expect("{");
while (tokens[cursor] !== "}") {
const id = tokens[cursor]?.startsWith('"') ? string() : tokens[cursor++];
if (!id) throw Error("Missing component identity");
if (Object.hasOwn(entries, id)) throw Error("Duplicate component entry");
expect("=");
expect("{");
const properties: Record<string, string> = Object.create(null);
while (tokens[cursor] !== "}") {
const key = tokens[cursor]?.startsWith('"') ? string() : tokens[cursor++];
if (!key || !["entry", "export"].includes(key) || Object.hasOwn(properties, key))
throw Error("Unsupported component entry property");
expect("=");
properties[key] = string();
expect(";");
}
expect("}");
expect(";");
if (!properties.entry || !properties.export) throw Error("Component entry needs entry and export");
entries[id] = { entry: properties.entry, export: properties.export };
}
expect("}");
if (cursor !== tokens.length || !/\n}\s*$/.test(source))
throw Error("components.nix is outside the supported scaffold shape");
return entries;
}
@@ -0,0 +1,100 @@
import fs from "node:fs/promises";
import path from "node:path";
import { parseQx, walkSyntax } from "./source.js";
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
import type { StructuralRequest } from "./structural-plan.js";
import { componentEntries, nixLiteral } from "./component-nix.js";
/** Add one ordinary component entry without regenerating existing application code. */
export async function scaffoldComponent(
root: string,
prefix: string,
spec: ScaffoldRecipe,
): Promise<StructuralRequest> {
if (!spec.declaration || !spec.name || !/^[A-Z][A-Za-z0-9_]*$/.test(spec.name) || !spec.id)
throw Error("Component scaffold requires a PascalCase name, ID and QX declaration");
const source = `package Draft id "draft" revision "draft@1" { ${spec.declaration} }`;
const syntax = parseQx(source);
const declarations = [...walkSyntax(syntax.root)].filter((node) => node.kind === "moduleExport");
const pkg = syntax.root.children.find((node) => node.kind === "packageResourceDecl");
if (
syntax.diagnostics.length ||
syntax.root.children.length !== 1 ||
declarations.length !== 1 ||
pkg?.children.filter((node) => node.kind === "packageExport").length !== 1
)
throw Error("Expected exactly one component declaration");
const declaration = declarations[0]!;
const identifier = declaration.children.find((node) => node.kind === "identifier");
const id = declaration.children.find((node) => node.kind === "stringLiteral");
if (
!identifier ||
source.slice(identifier.start, identifier.end) !== spec.name ||
!id ||
JSON.parse(source.slice(id.start, id.end)) !== spec.id
)
throw Error("Component declaration name/ID must match its registration");
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
const existing = parseQx(authored);
const parent = existing.root.children.find((node) => node.kind === "packageResourceDecl");
if (existing.diagnostics.length || !parent) throw Error("Fix package.qx before adding a component");
for (const item of parent.children.filter((node) => node.kind === "packageExport").flatMap((node) => node.children)) {
const name = item.children.find((node) => node.kind === "identifier");
const literal = item.children.find((node) => node.kind === "stringLiteral");
if (
(name && authored.slice(name.start, name.end) === spec.name) ||
(literal && JSON.parse(authored.slice(literal.start, literal.end)) === spec.id)
)
throw Error("Component requires a unique name and ID");
}
const filename = "components.nix";
const before = await fs.readFile(path.join(root, prefix, filename), "utf8");
const entries = await componentEntries(before);
if (!entries || typeof entries !== "object" || Array.isArray(entries) || Object.hasOwn(entries, spec.id))
throw Error("Invalid or duplicate component entry");
const insertion = ` ${nixLiteral(spec.id)} = { entry = ${nixLiteral(`src/components/${spec.name}.tsx`)}; export = ${nixLiteral(spec.name)}; };\n`;
const after = before.replace(/\n}(\s*)$/, (_match, trailing) => "\n" + insertion + "}" + trailing);
const parentID = parent.children.find((node) => node.kind === "stringLiteral")!;
const configFile = prefix + "quixos.check.json";
const configBefore = await fs.readFile(path.join(root, configFile), "utf8");
const config = JSON.parse(configBefore);
config.queryFrontends = [...new Set([...(config.queryFrontends ?? []), "react"])];
const preparationFile = prefix + "quixos.react.json";
let preparationBefore: string | undefined;
try {
preparationBefore = await fs.readFile(path.join(root, preparationFile), "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
const preparation = preparationBefore ? JSON.parse(preparationBefore) : { modules: {} };
if (Object.hasOwn(preparation.modules, spec.name)) throw Error("Duplicate presentation preparation");
preparation.modules[spec.name] = { inputs: [] };
const preparationText = JSON.stringify(preparation, null, 2) + "\n";
return {
kind: "package",
source: spec.source,
resourceRoot: spec.directory,
validation: "syntax",
files: [
preparationBefore === undefined
? { file: preparationFile, create: preparationText }
: { file: preparationFile, expected: preparationBefore, replace: preparationText },
{ file: configFile, expected: configBefore, replace: JSON.stringify(config, null, 2) + "\n" },
{
file: prefix + "package.qx",
edits: [
{
operation: "append",
parent: { kind: "packageResourceDecl", id: JSON.parse(authored.slice(parentID.start, parentID.end)) },
source: spec.declaration,
},
],
},
{ file: prefix + filename, expected: before, replace: after },
{
file: prefix + `src/components/${spec.name}.tsx`,
create: `import type { Modules } from "../gen/client.js";\n\n// Declare initial query inputs in quixos.react.json; use interfaces for actions.\nexport const ${spec.name}: Modules[${JSON.stringify(spec.name)}] = ({subject}) => <p>{subject.object}</p>;\n`,
},
],
};
}
@@ -0,0 +1,43 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
import type { StructuralRequest } from "./structural-plan.js";
export async function scaffoldController(
root: string,
prefix: string,
spec: ScaffoldRecipe,
): Promise<StructuralRequest> {
const entry = { id: spec.id, ...spec.controller };
const fields = ["id", "subject", "snapshot", "remote", "input"];
if (
Object.keys(entry).some((key) => ![...fields, "registry"].includes(key)) ||
(entry.registry !== undefined && typeof entry.registry !== "string") ||
fields.some((key) => typeof (entry as Record<string, unknown>)[key] !== "string") ||
!["unit", "value"].includes(entry.input ?? "")
)
throw Error("Controller requires id, subject, snapshot, remote and input (unit/value)");
const file = prefix + "quixos.controllers.json";
let before: string | undefined;
try {
before = await fs.readFile(path.join(root, file), "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
const catalog = before ? JSON.parse(before) : { schemaVersion: 1, controllers: [] };
if (
catalog.schemaVersion !== 1 ||
!Array.isArray(catalog.controllers) ||
catalog.controllers.length >= 1000 ||
catalog.controllers.some((c: { id: string }) => c.id === entry.id)
)
throw Error("Invalid catalog or duplicate controller ID");
catalog.controllers.push(entry);
const content = JSON.stringify(catalog, null, 2) + "\n";
return {
kind: "package",
source: spec.source,
resourceRoot: spec.directory,
validation: "syntax",
files: [before === undefined ? { file, create: content } : { file, expected: before, replace: content }],
};
}
@@ -0,0 +1,97 @@
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { randomUUID } from "node:crypto";
import { mkdir, mkdtemp, lstat, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { materializeWorkspaceSources, workspaceSourcePin, type WorkspaceSourcePin } from "./source-closure.js";
const exec = promisify(callback);
/** Developer workflow, not an ordinary Workspace Agent's implicit permission
* to publish against canonical repositories. Creates independent Git/jj copies,
* with instance binding outside committed source. No Graph is started here.
*/
export async function prepareDevelopmentSources(options: {
source: WorkspaceSourcePin;
directory: string;
cacheDirectory: string;
locations?: Record<string, string>;
workspaceId?: string;
}) {
const source = workspaceSourcePin(options.source);
if (options.workspaceId !== undefined && !/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(options.workspaceId))
throw Error("Development source binding requires a Workspace UUID");
const destination = path.resolve(options.directory);
const cache = path.resolve(options.cacheDirectory);
if (cache === destination || cache.startsWith(destination + path.sep))
throw Error("The retained source cache must live outside the disposable workbench");
if (
await lstat(destination).catch((error: NodeJS.ErrnoException) => {
if (error.code !== "ENOENT") throw error;
return null;
})
)
throw Error("Choose a new development workbench directory; existing checkouts are never overwritten");
const closure = await materializeWorkspaceSources({
source,
directory: options.cacheDirectory,
locations: options.locations,
});
const origins = new Set<string>();
for (const entry of closure.resources) {
const identity = `${entry.kind}:${entry.repository}`;
if (origins.has(identity)) throw Error("Multiple revisions of one repository require explicit editable selection");
origins.add(identity);
}
const parent = path.dirname(destination);
await mkdir(parent, { recursive: true });
const staging = await mkdtemp(path.join(parent, ".development-sources-"));
// This local association does not create or grant access to a hosted Graph.
const workspaceId = options.workspaceId ?? randomUUID();
try {
const resources = [];
for (const entry of closure.resources) {
const relative = entry.kind === "workspace" ? "root" : "resources/" + entry.directory;
const checkout = path.join(staging, relative);
await mkdir(path.dirname(checkout), { recursive: true });
await exec("git", [
"clone",
"--no-hardlinks",
"--",
path.join(path.resolve(options.cacheDirectory), entry.directory),
checkout,
]);
await exec("git", ["remote", "set-url", "origin", entry.repository], { cwd: checkout });
await exec("git", ["checkout", "--detach", entry.commit], { cwd: checkout });
await exec("jj", ["git", "init", "--colocate"], {
cwd: checkout,
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_SUBTREE_PUBLISH: "0" },
});
await exec("jj", ["describe", "-m", "Develop canonical application source"], {
cwd: checkout,
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_SUBTREE_PUBLISH: "0" },
});
if (entry.kind !== "workspace")
resources.push({
kind: entry.kind,
directory: relative,
source: { resolver: "git", repository: entry.repository, commit: entry.commit },
});
}
await mkdir(path.join(staging, ".quixos"), { mode: 0o700 });
await writeFile(
path.join(staging, ".quixos/resource-graph.json"),
JSON.stringify({ formatVersion: 1, workspaceId, resources }, null, 2) + "\n",
{ mode: 0o600 },
);
await writeFile(
path.join(staging, ".quixos/development-source.json"),
JSON.stringify({ source, workspaceId }, null, 2) + "\n",
{ mode: 0o600 },
);
await rename(staging, destination);
return { directory: destination, source, workspaceId, repositories: closure.resources.length };
} finally {
await rm(staging, { recursive: true, force: true });
}
}
@@ -0,0 +1,208 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
import type { StructuralRequest } from "./structural-plan.js";
import { parseQx, walkSyntax } from "./source.js";
import { addImplementation } from "./implementation-edit.js";
import { formatQuixosLock } from "../resource-lock/index.js";
import { javascriptTarget } from "../execution-world/native-backends.js";
const json = (value: unknown) => JSON.stringify(value, null, 2) + "\n";
const q = (value: string) => JSON.stringify(value);
const nixString = (value: string) => q(value).replaceAll("${", "\\${");
export async function isExecutionScaffold(root: string, prefix: string): Promise<boolean> {
try {
return JSON.parse(await fs.readFile(path.join(root, prefix, "quixos.check.json"), "utf8")).backend === "execution";
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
throw error;
}
}
/** Insert ordinary authored code; declarations remain the only export inventory. */
export async function executionScaffold(
root: string,
command: string,
spec: ScaffoldRecipe,
prefix: string,
nixSource: (source: NonNullable<ScaffoldRecipe["tools"]>["helpers"]) => string,
): Promise<StructuralRequest> {
if (!spec.name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) || !spec.id)
throw new Error("Execution scaffold requires a simple name and an ID");
if (spec.initialFiles && command !== "package") throw new Error("Initial files are only for new packages");
const files: StructuralRequest["files"] = [];
const create = (file: string, content: string) => files.push({ file: prefix + file, create: content });
if (command === "package") {
if (!spec.tools || !spec.revision) throw new Error("Execution package requires exact tool sources and a revision");
create(
"package.qx",
`symbol JavaScript id ${q(javascriptTarget)};\npackage ${spec.name} id ${q(spec.id)} revision ${q(spec.revision)} {\n}\n`,
);
create(
"quixos.lock",
formatQuixosLock({ formatVersion: 1, quixos: { resolver: "git", ...spec.tools.quixos }, resources: [] }),
);
create("quixos.check.json", json({ backend: "execution" }));
create("components.nix", "{\n}\n");
create(
"package.json",
json({
name: `@quixos/${spec.name.toLowerCase()}`,
private: true,
type: "module",
packageManager: "yarn@4.18.0",
scripts: { typecheck: "qx-workspace check" },
}),
);
create(
".gitignore",
"node_modules/\ndist/\nsrc/gen/\n.quixos/\nresult\n.yarn/cache/\n.yarn/install-state.gz\n.yarn/unplugged/\n",
);
create(
".yarnrc.yml",
'enableScripts: true\n\ngenerateDefaultNix: false\n\nindividualNixPackaging: true\n\nnodeLinker: node-modules\n\nsupportedArchitectures:\n os:\n - current\n - linux\n cpu:\n - current\n - x64\n - arm64\n libc:\n - current\n - glibc\n\nplugins:\n - checksum: 271de9a785321a37c564b8e2a6b5cd689850b20572d5fee8ec0a6b8e7a5d4cfb59e533524af2b4a537e00da12828abd062eaec6978556e3dd9149a020db2591b\n path: .yarn/plugins/yarn-plugin-nixify.cjs\n spec: "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/yarn-plugin-nixify-patched/raw/commit/4528fdd20b30d869262443b3f044549810e75fb8/dist/yarn-plugin-nixify.js"\n',
);
for (const lane of ["browser", "server", "remote"])
create(
`src/${lane}.ts`,
`import { createRegistry } from "./gen/${lane === "remote" ? "remote" : "qx"}.js";\nexport const registry = createRegistry({});\n`,
);
create(
"flake.nix",
`{
inputs.protocol.url = ${nixString(nixSource(spec.tools.protocol))};
inputs.nixpkgs.follows = "protocol/nixpkgs";
inputs.flake-utils.follows = "protocol/flake-utils";
inputs.helpers = { url = ${nixString(nixSource(spec.tools.helpers))}; flake = false; };
outputs = { nixpkgs, flake-utils, helpers, ... }:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs { inherit system; };
camino = import (helpers + "/quixos-package-helpers.nix");
in {
devShells.default = (import (helpers + "/quixos-package-helpers.nix")).mkCaminoYarnShell { inherit pkgs; };
quixosPackages.checkedArtifacts = context:
let
pkgs = camino.packageSet { inherit nixpkgs system context; };
contract = builtins.fromJSON (builtins.readFile context.buildContract);
package = contract.definition.package;
portable = builtins.filter (entry: entry.execution.kind != "remote") package.exports;
remote = builtins.filter (entry: entry.execution.kind == "remote") package.exports;
modules = import ./components.nix;
dependencies = if builtins.pathExists ./yarn-project.nix
then camino.mkCaminoYarnDependencies { inherit pkgs; src = ./.; } else null;
in camino.assembleCaminoPackage {
inherit pkgs context;
artifacts =
pkgs.lib.optional (portable != []) (camino.mkCaminoTypeScriptPackage {
inherit pkgs context dependencies; src = ./.;
entry = "src/server.ts"; entries.browser = "src/browser.ts";
targets = pkgs.lib.unique (map (target: if target == "target:org.quixos.execution.javascript@1" then "browser" else if target == "server" then "server" else throw "Unsupported TypeScript backend target") (pkgs.lib.concatMap (entry: entry.execution.targets) portable));
}) ++ pkgs.lib.optional (remote != []) (camino.mkCaminoTypeScriptService {
inherit pkgs context dependencies; src = ./.; entry = "src/remote.ts";
}) ++ pkgs.lib.optional (modules != {}) (camino.mkCaminoReactPackage {
inherit pkgs context dependencies modules; src = ./.;
});
};
});
}
`,
);
create(
"README.md",
`# ${spec.name}\n\nThis execution package exports checked browser/server modules and a process service.\nUse scaffold function with an explicit execution contract to add a typed handler.\nSnapshot/transaction targets choose browser.ts and/or server.ts; remote handlers\nuse remote.ts. Shared helpers are ordinary imports. Generated bindings are build\noutputs, not authored source. The Nix composition supplies the canonical package contract\nand explicit runtime dependencies to checkedArtifacts.\n\nRun qx-workspace check from this package to build it independently, or from a\nworkspace to also check installed bindings. Activate the resulting report separately through the workspace\nhost. A source-only structural check is not runtime qualification.\n`,
);
} else if (command === "function") {
if (!spec.declaration)
throw new Error("Execution functions require a declaration with explicit execution effect and targets");
const wrapped = `package Scaffold id "scaffold" revision "scaffold@1" { ${spec.declaration} }`;
const parsed = parseQx(wrapped);
const pkg = parsed.root.children.find((node) => node.kind === "packageResourceDecl");
const exports = [...walkSyntax(parsed.root)].filter((node) =>
["packageFunctionExport", "packageOperationExport"].includes(node.kind),
);
const declaration = exports[0];
if (
parsed.diagnostics.length ||
exports.length !== 1 ||
!declaration ||
parsed.root.children.length !== 1 ||
pkg?.children.filter((node) => node.kind === "packageExport").length !== 1 ||
pkg?.children.some((node) => !["identifier", "stringLiteral", "packageExport"].includes(node.kind))
)
throw new Error("Expected one function or operation export with an execution contract");
const text = (node: { start: number; end: number }) => wrapped.slice(node.start, node.end);
const name = declaration.children.find((node) => node.kind === "identifier");
const id = declaration.children.find((node) => node.kind === "stringLiteral");
const execution = declaration.children.find((node) => node.kind === "executionClause");
if (!name || text(name) !== spec.name || !id || JSON.parse(text(id)) !== spec.id || !execution)
throw new Error("Declaration name/ID must match; an explicit execution contract is required");
const tokens = parsed.tokens
.filter((token) => !token.trivia && token.start >= execution.start && token.end <= execution.end)
.map((token) => token.text);
const remote = tokens[1] === "remote";
const declaredTargets = remote
? ["remote"]
: [...walkSyntax(execution)].filter((node) => node.kind === "identifier").map(text);
const targets = declaredTargets.length ? declaredTargets : ["server"];
if (
!targets.length ||
new Set(targets).size !== targets.length ||
targets.some((target) => !(remote ? ["remote"] : ["JavaScript", "server"]).includes(target))
)
throw new Error("This TypeScript scaffold supports the JavaScript target, server, and remote service handlers");
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
const syntax = parseQx(authored);
const packageNode = syntax.root.children.find((node) => node.kind === "packageResourceDecl");
if (syntax.diagnostics.length || !packageNode) throw new Error("Fix package.qx before adding a handler");
for (const node of [...walkSyntax(packageNode)].filter((node) =>
["packageFunctionExport", "packageOperationExport", "packageConstructorExport"].includes(node.kind),
)) {
const identifier = node.children.find((child) => child.kind === "identifier");
const literal = node.children.find((child) => child.kind === "stringLiteral");
if (
(identifier && authored.slice(identifier.start, identifier.end) === spec.name) ||
(literal && JSON.parse(authored.slice(literal.start, literal.end)) === spec.id)
)
throw new Error("New export requires a unique name and ID");
}
const packageID = packageNode.children.find((node) => node.kind === "stringLiteral")!;
files.push({
file: prefix + "package.qx",
edits: [
{
operation: "append",
parent: { kind: "packageResourceDecl", id: JSON.parse(authored.slice(packageID.start, packageID.end)) },
source: spec.declaration,
},
],
});
create(
`src/impl/${spec.name}.ts`,
`import type { ${remote ? "Remote" : "Portable"}Implementation } from "../gen/${remote ? "remote" : "qx"}.js";\nexport const handler: ${remote ? "Remote" : "Portable"}Implementation[${q(spec.name)}] = async (_context) => { throw new Error(${q("Implement " + spec.name)}); };\n`,
);
for (const target of targets) {
const file = prefix + `src/${target === "JavaScript" ? "browser" : target}.ts`;
const before = await fs.readFile(path.join(root, file), "utf8");
files.push({
file,
expected: before,
replace: addImplementation(before, "createRegistry", spec.name, `./impl/${spec.name}.js`),
});
}
} else
throw new Error("Execution packages use ordinary factory transactions; migration scaffolding is not supported");
if (spec.initialFiles) {
for (const [file, content] of Object.entries(spec.initialFiles)) {
if (
typeof content !== "string" ||
!/^(?:package\.qx|(?:package|components|flake|yarn-project)\.nix|package\.json|yarn\.lock|\.yarnrc\.yml|\.gitignore|\.yarn\/plugins\/[A-Za-z0-9_.-]+\.cjs|quixos\.controllers\.json|README\.md|(?:src|queries)\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_.-]+)$/.test(
file,
) ||
file.split("/").some((part) => part === ".." || part === "." || part === "gen")
)
throw new Error("Not an initial authored execution file: " + file);
const previous = files.findIndex((entry) => entry.file === prefix + file);
if (previous >= 0) files.splice(previous, 1);
create(file, content);
}
}
return { kind: "package", source: spec.source, resourceRoot: spec.directory, validation: "syntax", files };
}
+61
View File
@@ -0,0 +1,61 @@
import { spawn } from "node:child_process";
export class FileLockBusyError extends Error {}
/** Kernel-owned lock: a crashed coordinator cannot leave a stale ownership file.
* The persistent file is just an inode; EOF releases the helper's lock. */
export async function withFileLock<T>(
filename: string,
work: () => Promise<T>,
{ nonblocking = false }: { nonblocking?: boolean } = {},
): Promise<T> {
const child = spawn(
"flock",
[
"--exclusive",
...(nonblocking ? ["--nonblock"] : ["--timeout", "120"]),
"--conflict-exit-code",
"75",
filename,
process.execPath,
"-e",
'process.stdout.write("locked\\n"); process.stdin.resume();',
],
{ stdio: ["pipe", "pipe", "pipe"] },
);
let diagnostics = "";
child.stdin.on("error", () => {
/* acquisition/exit handling reports helper failure */
});
child.stderr.on("data", (chunk) => {
diagnostics = (diagnostics + String(chunk)).slice(-2000);
});
const closed = new Promise<void>((resolve) => {
child.once("close", () => resolve());
});
try {
await new Promise<void>((resolve, reject) => {
let output = "";
child.once("error", reject);
child.once("exit", (code) => {
reject(
code === 75
? new FileLockBusyError(
nonblocking
? "Another process owns this lock"
: "Timed out after 120 seconds waiting for another authoring command; inspect that command before retrying",
)
: new Error(`Cannot acquire authoring lock: ${diagnostics}`),
);
});
child.stdout.on("data", (chunk) => {
output += chunk;
if (output.includes("locked\n")) resolve();
});
});
return await work();
} finally {
child.stdin.end();
await closed;
}
}
File diff suppressed because one or more lines are too long
@@ -1,183 +1,311 @@
WORKSPACE=1
ATOM=2
INTERFACE=3
INTERFACES=4
PACKAGE=5
VALUE=6
RELATION=7
OPERATION=8
FUNCTION=9
CONSTRUCTOR=10
CONSTRUCTS=11
CONFORM=12
AS=13
BIND=14
TO=15
PRIVATE=16
SHARED=17
STATE=18
EDGE=19
PROJECTION=20
WITH=21
ON=22
POLICY=23
DEFAULT=24
SOURCE=25
REPOSITORY=26
COMMIT=27
REVISION=28
ID=29
DOC=30
MODE=31
EMITS=32
RECEIVER=33
REQUIRES=34
ANY=35
GET=36
SET=37
WATCH=38
START=39
STOP=40
READ=41
WRITE=42
RESOLVE=43
CONNECT=44
DISCONNECT=45
CALL=46
WATCH_START=47
WATCH_STOP=48
SUBSCRIBE=49
UNSUBSCRIBE=50
OPTIMISTIC_REGISTER=51
CRDT=52
OPTIONAL_ONE=53
EXACTLY_ONE=54
MANY_UNIQUE=55
MANY=56
ORDERED=57
UNIT=58
WATCH_HANDLE=59
MESSAGE=60
ATOM_REF=61
INTERFACE_REF=62
OPTIONAL=63
LIST=64
BOOL=65
BYTES=66
DOUBLE=67
INT32=68
INT64=69
STRING=70
UINT32=71
UINT64=72
TRUE=73
FALSE=74
NULL=75
ARROW=76
COLON=77
SEMI=78
COMMA=79
DOT=80
LBRACE=81
RBRACE=82
LBRACK=83
RBRACK=84
LPAREN=85
RPAREN=86
LT=87
GT=88
INTEGER=89
JSON_NUMBER=90
IDENTIFIER=91
STRING_LITERAL=92
LINE_COMMENT=93
BLOCK_COMMENT=94
WS=95
'workspace'=1
'atom'=2
'interface'=3
'interfaces'=4
'package'=5
'value'=6
'relation'=7
'operation'=8
'function'=9
'constructor'=10
'constructs'=11
'conform'=12
'as'=13
'bind'=14
'to'=15
'private'=16
'shared'=17
'state'=18
'edge'=19
'projection'=20
'with'=21
'on'=22
'policy'=23
'default'=24
'source'=25
'repository'=26
'commit'=27
'revision'=28
'id'=29
'doc'=30
'mode'=31
'emits'=32
'receiver'=33
'requires'=34
'any'=35
'get'=36
'set'=37
'watch'=38
'start'=39
'stop'=40
'read'=41
'write'=42
'resolve'=43
'connect'=44
'disconnect'=45
'call'=46
'watch-start'=47
'watch-stop'=48
'subscribe'=49
'unsubscribe'=50
'optimistic-register'=51
'crdt'=52
'optional-one'=53
'exactly-one'=54
'many-unique'=55
'many'=56
'ordered'=57
'unit'=58
'watch-handle'=59
'message'=60
'atom-ref'=61
'interface-ref'=62
'optional'=63
'list'=64
'bool'=65
'bytes'=66
'double'=67
'int32'=68
'int64'=69
'string'=70
'uint32'=71
'uint64'=72
'true'=73
'false'=74
'null'=75
'->'=76
':'=77
';'=78
','=79
'.'=80
'{'=81
'}'=82
'['=83
']'=84
'('=85
')'=86
'<'=87
'>'=88
EDIT=1
MODULE=2
CONTRACT=3
PRESENTATION=4
SUBJECT=5
FOR=6
CREATION=7
VARIANT=8
PREPARES=9
INITIALIZE=10
WORKSPACE=11
EXECUTION=12
SNAPSHOT=13
TRANSACTION=14
REMOTE=15
TARGETS=16
PORTABLE=17
REPLICA=18
LOCAL=19
FROM=20
RESULT_BYTES=21
RPC_CALLS=22
DEADLINE_MS=23
QUERY=24
REFLECTION=25
SPECIALIZE=26
ROOT=27
DOCUMENT=28
FRAGMENTS=29
VIEW=30
MAX=31
ALLOW=32
POLL=33
QUERYABLE=34
RPC=35
QUERY_REASON=36
TYPE=37
OBJECT=38
STORABLE=39
IMPLEMENTS=40
REF=41
FRAGMENT=42
IMPORT=43
EXTERNAL=44
SYMBOL=45
ATOM=46
INTERFACE=47
INTERFACES=48
PACKAGE=49
VALUE=50
FIELD=51
RELATION=52
OPERATION=53
FUNCTION=54
CONSTRUCTOR=55
CONSTRUCTS=56
INPUT=57
CONFORM=58
AS=59
BIND=60
STATIC=61
TO=62
PRIVATE=63
SHARED=64
STATE=65
EDGE=66
PROJECTION=67
WITH=68
USING=69
ACCEPT=70
VIA=71
MATERIALIZE=72
IF=73
ABSENT=74
ON=75
POLICY=76
DEFAULT=77
SOURCE=78
REPOSITORY=79
COMMIT=80
REVISION=81
SEMANTIC_MAJOR=82
ON_DELETE=83
LIFECYCLE=84
JOURNAL=85
SUBSCRIPTION=86
RETAIN_OTHER=87
KEYED=88
PUBLIC_TRAVERSAL=89
ID=90
DOC=91
MODE=92
EMITS=93
RECEIVER=94
REQUIRES=95
ANY=96
GET=97
SET=98
WATCH=99
START=100
STOP=101
READ=102
WRITE=103
RESOLVE=104
CONNECT=105
DISCONNECT=106
CALL=107
WATCH_START=108
WATCH_STOP=109
SUBSCRIBE=110
UNSUBSCRIBE=111
REGISTER=112
CRDT=113
OPTIONAL_ONE=114
EXACTLY_ONE=115
MANY_UNIQUE=116
MANY=117
ORDERED=118
UNIT=119
WATCH_HANDLE=120
MESSAGE=121
ATOM_REF=122
INTERFACE_REF=123
OPTIONAL=124
LIST=125
RECORD=126
BOOL=127
BYTES=128
DOUBLE=129
INT32=130
INT64=131
STRING=132
UINT32=133
UINT64=134
TRUE=135
FALSE=136
NULL=137
ARROW=138
COLON=139
SEMI=140
COMMA=141
DOT=142
LBRACE=143
RBRACE=144
LBRACK=145
RBRACK=146
LPAREN=147
RPAREN=148
LT=149
GT=150
AMP=151
EQUAL=152
INTEGER=153
JSON_NUMBER=154
IDENTIFIER=155
STRING_LITERAL=156
LINE_COMMENT=157
BLOCK_COMMENT=158
WS=159
'edit'=1
'module'=2
'contract'=3
'presentation'=4
'subject'=5
'for'=6
'creation'=7
'variant'=8
'prepares'=9
'initialize'=10
'workspace'=11
'execution'=12
'snapshot'=13
'transaction'=14
'remote'=15
'targets'=16
'portable'=17
'replica'=18
'local'=19
'from'=20
'result-bytes'=21
'rpc-calls'=22
'deadline-ms'=23
'query'=24
'reflection'=25
'specialize'=26
'root'=27
'document'=28
'fragments'=29
'view'=30
'max'=31
'allow'=32
'poll'=33
'queryable'=34
'rpc'=35
'query-reason'=36
'type'=37
'object'=38
'storable'=39
'implements'=40
'ref'=41
'fragment'=42
'import'=43
'external'=44
'symbol'=45
'atom'=46
'interface'=47
'interfaces'=48
'package'=49
'value'=50
'field'=51
'relation'=52
'operation'=53
'function'=54
'constructor'=55
'constructs'=56
'input'=57
'conform'=58
'as'=59
'bind'=60
'static'=61
'to'=62
'private'=63
'shared'=64
'state'=65
'edge'=66
'projection'=67
'with'=68
'using'=69
'accept'=70
'via'=71
'materialize'=72
'if'=73
'absent'=74
'on'=75
'policy'=76
'default'=77
'source'=78
'repository'=79
'commit'=80
'revision'=81
'semantic-major'=82
'on-delete'=83
'lifecycle'=84
'journal'=85
'subscription'=86
'retain-other'=87
'keyed'=88
'public-traversal'=89
'id'=90
'doc'=91
'mode'=92
'emits'=93
'receiver'=94
'requires'=95
'any'=96
'get'=97
'set'=98
'watch'=99
'start'=100
'stop'=101
'read'=102
'write'=103
'resolve'=104
'connect'=105
'disconnect'=106
'call'=107
'watch-start'=108
'watch-stop'=109
'subscribe'=110
'unsubscribe'=111
'register'=112
'crdt'=113
'optional-one'=114
'exactly-one'=115
'many-unique'=116
'many'=117
'ordered'=118
'unit'=119
'watch-handle'=120
'message'=121
'atom-ref'=122
'interface-ref'=123
'optional'=124
'list'=125
'record'=126
'bool'=127
'bytes'=128
'double'=129
'int32'=130
'int64'=131
'string'=132
'uint32'=133
'uint64'=134
'true'=135
'false'=136
'null'=137
'->'=138
':'=139
';'=140
','=141
'.'=142
'{'=143
'}'=144
'['=145
']'=146
'('=147
')'=148
'<'=149
'>'=150
'&'=151
'='=152
File diff suppressed because one or more lines are too long
@@ -1,183 +1,311 @@
WORKSPACE=1
ATOM=2
INTERFACE=3
INTERFACES=4
PACKAGE=5
VALUE=6
RELATION=7
OPERATION=8
FUNCTION=9
CONSTRUCTOR=10
CONSTRUCTS=11
CONFORM=12
AS=13
BIND=14
TO=15
PRIVATE=16
SHARED=17
STATE=18
EDGE=19
PROJECTION=20
WITH=21
ON=22
POLICY=23
DEFAULT=24
SOURCE=25
REPOSITORY=26
COMMIT=27
REVISION=28
ID=29
DOC=30
MODE=31
EMITS=32
RECEIVER=33
REQUIRES=34
ANY=35
GET=36
SET=37
WATCH=38
START=39
STOP=40
READ=41
WRITE=42
RESOLVE=43
CONNECT=44
DISCONNECT=45
CALL=46
WATCH_START=47
WATCH_STOP=48
SUBSCRIBE=49
UNSUBSCRIBE=50
OPTIMISTIC_REGISTER=51
CRDT=52
OPTIONAL_ONE=53
EXACTLY_ONE=54
MANY_UNIQUE=55
MANY=56
ORDERED=57
UNIT=58
WATCH_HANDLE=59
MESSAGE=60
ATOM_REF=61
INTERFACE_REF=62
OPTIONAL=63
LIST=64
BOOL=65
BYTES=66
DOUBLE=67
INT32=68
INT64=69
STRING=70
UINT32=71
UINT64=72
TRUE=73
FALSE=74
NULL=75
ARROW=76
COLON=77
SEMI=78
COMMA=79
DOT=80
LBRACE=81
RBRACE=82
LBRACK=83
RBRACK=84
LPAREN=85
RPAREN=86
LT=87
GT=88
INTEGER=89
JSON_NUMBER=90
IDENTIFIER=91
STRING_LITERAL=92
LINE_COMMENT=93
BLOCK_COMMENT=94
WS=95
'workspace'=1
'atom'=2
'interface'=3
'interfaces'=4
'package'=5
'value'=6
'relation'=7
'operation'=8
'function'=9
'constructor'=10
'constructs'=11
'conform'=12
'as'=13
'bind'=14
'to'=15
'private'=16
'shared'=17
'state'=18
'edge'=19
'projection'=20
'with'=21
'on'=22
'policy'=23
'default'=24
'source'=25
'repository'=26
'commit'=27
'revision'=28
'id'=29
'doc'=30
'mode'=31
'emits'=32
'receiver'=33
'requires'=34
'any'=35
'get'=36
'set'=37
'watch'=38
'start'=39
'stop'=40
'read'=41
'write'=42
'resolve'=43
'connect'=44
'disconnect'=45
'call'=46
'watch-start'=47
'watch-stop'=48
'subscribe'=49
'unsubscribe'=50
'optimistic-register'=51
'crdt'=52
'optional-one'=53
'exactly-one'=54
'many-unique'=55
'many'=56
'ordered'=57
'unit'=58
'watch-handle'=59
'message'=60
'atom-ref'=61
'interface-ref'=62
'optional'=63
'list'=64
'bool'=65
'bytes'=66
'double'=67
'int32'=68
'int64'=69
'string'=70
'uint32'=71
'uint64'=72
'true'=73
'false'=74
'null'=75
'->'=76
':'=77
';'=78
','=79
'.'=80
'{'=81
'}'=82
'['=83
']'=84
'('=85
')'=86
'<'=87
'>'=88
EDIT=1
MODULE=2
CONTRACT=3
PRESENTATION=4
SUBJECT=5
FOR=6
CREATION=7
VARIANT=8
PREPARES=9
INITIALIZE=10
WORKSPACE=11
EXECUTION=12
SNAPSHOT=13
TRANSACTION=14
REMOTE=15
TARGETS=16
PORTABLE=17
REPLICA=18
LOCAL=19
FROM=20
RESULT_BYTES=21
RPC_CALLS=22
DEADLINE_MS=23
QUERY=24
REFLECTION=25
SPECIALIZE=26
ROOT=27
DOCUMENT=28
FRAGMENTS=29
VIEW=30
MAX=31
ALLOW=32
POLL=33
QUERYABLE=34
RPC=35
QUERY_REASON=36
TYPE=37
OBJECT=38
STORABLE=39
IMPLEMENTS=40
REF=41
FRAGMENT=42
IMPORT=43
EXTERNAL=44
SYMBOL=45
ATOM=46
INTERFACE=47
INTERFACES=48
PACKAGE=49
VALUE=50
FIELD=51
RELATION=52
OPERATION=53
FUNCTION=54
CONSTRUCTOR=55
CONSTRUCTS=56
INPUT=57
CONFORM=58
AS=59
BIND=60
STATIC=61
TO=62
PRIVATE=63
SHARED=64
STATE=65
EDGE=66
PROJECTION=67
WITH=68
USING=69
ACCEPT=70
VIA=71
MATERIALIZE=72
IF=73
ABSENT=74
ON=75
POLICY=76
DEFAULT=77
SOURCE=78
REPOSITORY=79
COMMIT=80
REVISION=81
SEMANTIC_MAJOR=82
ON_DELETE=83
LIFECYCLE=84
JOURNAL=85
SUBSCRIPTION=86
RETAIN_OTHER=87
KEYED=88
PUBLIC_TRAVERSAL=89
ID=90
DOC=91
MODE=92
EMITS=93
RECEIVER=94
REQUIRES=95
ANY=96
GET=97
SET=98
WATCH=99
START=100
STOP=101
READ=102
WRITE=103
RESOLVE=104
CONNECT=105
DISCONNECT=106
CALL=107
WATCH_START=108
WATCH_STOP=109
SUBSCRIBE=110
UNSUBSCRIBE=111
REGISTER=112
CRDT=113
OPTIONAL_ONE=114
EXACTLY_ONE=115
MANY_UNIQUE=116
MANY=117
ORDERED=118
UNIT=119
WATCH_HANDLE=120
MESSAGE=121
ATOM_REF=122
INTERFACE_REF=123
OPTIONAL=124
LIST=125
RECORD=126
BOOL=127
BYTES=128
DOUBLE=129
INT32=130
INT64=131
STRING=132
UINT32=133
UINT64=134
TRUE=135
FALSE=136
NULL=137
ARROW=138
COLON=139
SEMI=140
COMMA=141
DOT=142
LBRACE=143
RBRACE=144
LBRACK=145
RBRACK=146
LPAREN=147
RPAREN=148
LT=149
GT=150
AMP=151
EQUAL=152
INTEGER=153
JSON_NUMBER=154
IDENTIFIER=155
STRING_LITERAL=156
LINE_COMMENT=157
BLOCK_COMMENT=158
WS=159
'edit'=1
'module'=2
'contract'=3
'presentation'=4
'subject'=5
'for'=6
'creation'=7
'variant'=8
'prepares'=9
'initialize'=10
'workspace'=11
'execution'=12
'snapshot'=13
'transaction'=14
'remote'=15
'targets'=16
'portable'=17
'replica'=18
'local'=19
'from'=20
'result-bytes'=21
'rpc-calls'=22
'deadline-ms'=23
'query'=24
'reflection'=25
'specialize'=26
'root'=27
'document'=28
'fragments'=29
'view'=30
'max'=31
'allow'=32
'poll'=33
'queryable'=34
'rpc'=35
'query-reason'=36
'type'=37
'object'=38
'storable'=39
'implements'=40
'ref'=41
'fragment'=42
'import'=43
'external'=44
'symbol'=45
'atom'=46
'interface'=47
'interfaces'=48
'package'=49
'value'=50
'field'=51
'relation'=52
'operation'=53
'function'=54
'constructor'=55
'constructs'=56
'input'=57
'conform'=58
'as'=59
'bind'=60
'static'=61
'to'=62
'private'=63
'shared'=64
'state'=65
'edge'=66
'projection'=67
'with'=68
'using'=69
'accept'=70
'via'=71
'materialize'=72
'if'=73
'absent'=74
'on'=75
'policy'=76
'default'=77
'source'=78
'repository'=79
'commit'=80
'revision'=81
'semantic-major'=82
'on-delete'=83
'lifecycle'=84
'journal'=85
'subscription'=86
'retain-other'=87
'keyed'=88
'public-traversal'=89
'id'=90
'doc'=91
'mode'=92
'emits'=93
'receiver'=94
'requires'=95
'any'=96
'get'=97
'set'=98
'watch'=99
'start'=100
'stop'=101
'read'=102
'write'=103
'resolve'=104
'connect'=105
'disconnect'=106
'call'=107
'watch-start'=108
'watch-stop'=109
'subscribe'=110
'unsubscribe'=111
'register'=112
'crdt'=113
'optional-one'=114
'exactly-one'=115
'many-unique'=116
'many'=117
'ordered'=118
'unit'=119
'watch-handle'=120
'message'=121
'atom-ref'=122
'interface-ref'=123
'optional'=124
'list'=125
'record'=126
'bool'=127
'bytes'=128
'double'=129
'int32'=130
'int64'=131
'string'=132
'uint32'=133
'uint64'=134
'true'=135
'false'=136
'null'=137
'->'=138
':'=139
';'=140
','=141
'.'=142
'{'=143
'}'=144
'['=145
']'=146
'('=147
')'=148
'<'=149
'>'=150
'&'=151
'='=152
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -3,22 +3,46 @@ import { AbstractParseTreeVisitor } from "antlr4ng";
import { DocumentContext } from "./QuixosCapabilityParser.js";
import { FragmentDeclContext } from "./QuixosCapabilityParser.js";
import { SourceImportDeclContext } from "./QuixosCapabilityParser.js";
import { WorkspaceDeclContext } from "./QuixosCapabilityParser.js";
import { WorkspaceItemContext } from "./QuixosCapabilityParser.js";
import { ResourceImportDeclContext } from "./QuixosCapabilityParser.js";
import { ExternalAtomDeclContext } from "./QuixosCapabilityParser.js";
import { ExternalInterfaceDeclContext } from "./QuixosCapabilityParser.js";
import { SymbolDeclContext } from "./QuixosCapabilityParser.js";
import { ResourcePreambleContext } from "./QuixosCapabilityParser.js";
import { AtomDeclContext } from "./QuixosCapabilityParser.js";
import { InterfaceDeclContext } from "./QuixosCapabilityParser.js";
import { SourceBlockContext } from "./QuixosCapabilityParser.js";
import { InterfaceResourceDeclContext } from "./QuixosCapabilityParser.js";
import { TypeParametersContext } from "./QuixosCapabilityParser.js";
import { TypeParameterContext } from "./QuixosCapabilityParser.js";
import { InterfaceTypeContext } from "./QuixosCapabilityParser.js";
import { TypeArgumentsContext } from "./QuixosCapabilityParser.js";
import { TypeArgumentContext } from "./QuixosCapabilityParser.js";
import { TypeAliasDeclContext } from "./QuixosCapabilityParser.js";
import { InterfaceMemberContext } from "./QuixosCapabilityParser.js";
import { ModuleMemberContext } from "./QuixosCapabilityParser.js";
import { OperationMemberContext } from "./QuixosCapabilityParser.js";
import { ValueMemberContext } from "./QuixosCapabilityParser.js";
import { ValueMemberOperationContext } from "./QuixosCapabilityParser.js";
import { RelationshipMemberContext } from "./QuixosCapabilityParser.js";
import { QueryReadContractContext } from "./QuixosCapabilityParser.js";
import { RelationshipOperationContext } from "./QuixosCapabilityParser.js";
import { TargetConstraintContext } from "./QuixosCapabilityParser.js";
import { PackageDeclContext } from "./QuixosCapabilityParser.js";
import { PackageResourceDeclContext } from "./QuixosCapabilityParser.js";
import { PackageExportContext } from "./QuixosCapabilityParser.js";
import { ModuleExportContext } from "./QuixosCapabilityParser.js";
import { PresentationDeclContext } from "./QuixosCapabilityParser.js";
import { PackageQueryContext } from "./QuixosCapabilityParser.js";
import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js";
import { QueryClauseContext } from "./QuixosCapabilityParser.js";
import { FragmentVariableContext } from "./QuixosCapabilityParser.js";
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
import { AcceptanceClauseContext } from "./QuixosCapabilityParser.js";
import { PreparationClauseContext } from "./QuixosCapabilityParser.js";
import { ExecutionClauseContext } from "./QuixosCapabilityParser.js";
import { SelectedOperationsContext } from "./QuixosCapabilityParser.js";
import { PackageConstructorExportContext } from "./QuixosCapabilityParser.js";
import { EventClauseContext } from "./QuixosCapabilityParser.js";
import { OperationModeContext } from "./QuixosCapabilityParser.js";
@@ -35,17 +59,27 @@ import { StoragePolicyContext } from "./QuixosCapabilityParser.js";
import { EdgeDeclContext } from "./QuixosCapabilityParser.js";
import { EdgeEndpointContext } from "./QuixosCapabilityParser.js";
import { ConformanceDeclContext } from "./QuixosCapabilityParser.js";
import { ConformanceSourceArgumentsContext } from "./QuixosCapabilityParser.js";
import { ConformanceItemContext } from "./QuixosCapabilityParser.js";
import { ModuleBindingDeclContext } from "./QuixosCapabilityParser.js";
import { InitializationDeclContext } from "./QuixosCapabilityParser.js";
import { InitializationFieldContext } from "./QuixosCapabilityParser.js";
import { CreationOwnerMappingContext } from "./QuixosCapabilityParser.js";
import { StateFieldBindingDeclContext } from "./QuixosCapabilityParser.js";
import { RelationshipMaterializationDeclContext } from "./QuixosCapabilityParser.js";
import { OperationBindingDeclContext } from "./QuixosCapabilityParser.js";
import { MemberOperationRefContext } from "./QuixosCapabilityParser.js";
import { OperationNameContext } from "./QuixosCapabilityParser.js";
import { OperationProviderContext } from "./QuixosCapabilityParser.js";
import { AcceptanceBindingContext } from "./QuixosCapabilityParser.js";
import { StatePrimitiveContext } from "./QuixosCapabilityParser.js";
import { EdgePrimitiveContext } from "./QuixosCapabilityParser.js";
import { DependencyBindingBlockContext } from "./QuixosCapabilityParser.js";
import { DependencyBindingContext } from "./QuixosCapabilityParser.js";
import { ConstructorBindingDeclContext } from "./QuixosCapabilityParser.js";
import { ValueTypeContext } from "./QuixosCapabilityParser.js";
import { RecordFieldContext } from "./QuixosCapabilityParser.js";
import { VariantCaseContext } from "./QuixosCapabilityParser.js";
import { ScalarTypeContext } from "./QuixosCapabilityParser.js";
import { CardinalityContext } from "./QuixosCapabilityParser.js";
import { JsonLiteralContext } from "./QuixosCapabilityParser.js";
@@ -70,6 +104,18 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitDocument?: (ctx: DocumentContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.fragmentDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitFragmentDecl?: (ctx: FragmentDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.sourceImportDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitSourceImportDecl?: (ctx: SourceImportDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.workspaceDecl`.
* @param ctx the parse tree
@@ -82,6 +128,36 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitWorkspaceItem?: (ctx: WorkspaceItemContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.resourceImportDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitResourceImportDecl?: (ctx: ResourceImportDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.externalAtomDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitExternalAtomDecl?: (ctx: ExternalAtomDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.externalInterfaceDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitExternalInterfaceDecl?: (ctx: ExternalInterfaceDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.symbolDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitSymbolDecl?: (ctx: SymbolDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.resourcePreamble`.
* @param ctx the parse tree
* @return the visitor result
*/
visitResourcePreamble?: (ctx: ResourcePreambleContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.atomDecl`.
* @param ctx the parse tree
@@ -89,23 +165,59 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
*/
visitAtomDecl?: (ctx: AtomDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.interfaceDecl`.
* Visit a parse tree produced by `QuixosCapabilityParser.interfaceResourceDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitInterfaceDecl?: (ctx: InterfaceDeclContext) => Result;
visitInterfaceResourceDecl?: (ctx: InterfaceResourceDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.sourceBlock`.
* Visit a parse tree produced by `QuixosCapabilityParser.typeParameters`.
* @param ctx the parse tree
* @return the visitor result
*/
visitSourceBlock?: (ctx: SourceBlockContext) => Result;
visitTypeParameters?: (ctx: TypeParametersContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.typeParameter`.
* @param ctx the parse tree
* @return the visitor result
*/
visitTypeParameter?: (ctx: TypeParameterContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.interfaceType`.
* @param ctx the parse tree
* @return the visitor result
*/
visitInterfaceType?: (ctx: InterfaceTypeContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.typeArguments`.
* @param ctx the parse tree
* @return the visitor result
*/
visitTypeArguments?: (ctx: TypeArgumentsContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.typeArgument`.
* @param ctx the parse tree
* @return the visitor result
*/
visitTypeArgument?: (ctx: TypeArgumentContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.typeAliasDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitTypeAliasDecl?: (ctx: TypeAliasDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.interfaceMember`.
* @param ctx the parse tree
* @return the visitor result
*/
visitInterfaceMember?: (ctx: InterfaceMemberContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.moduleMember`.
* @param ctx the parse tree
* @return the visitor result
*/
visitModuleMember?: (ctx: ModuleMemberContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.operationMember`.
* @param ctx the parse tree
@@ -130,6 +242,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitRelationshipMember?: (ctx: RelationshipMemberContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.queryReadContract`.
* @param ctx the parse tree
* @return the visitor result
*/
visitQueryReadContract?: (ctx: QueryReadContractContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.relationshipOperation`.
* @param ctx the parse tree
@@ -143,17 +261,53 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
*/
visitTargetConstraint?: (ctx: TargetConstraintContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.packageDecl`.
* Visit a parse tree produced by `QuixosCapabilityParser.packageResourceDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPackageDecl?: (ctx: PackageDeclContext) => Result;
visitPackageResourceDecl?: (ctx: PackageResourceDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.packageExport`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPackageExport?: (ctx: PackageExportContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.moduleExport`.
* @param ctx the parse tree
* @return the visitor result
*/
visitModuleExport?: (ctx: ModuleExportContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.presentationDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPresentationDecl?: (ctx: PresentationDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.packageQuery`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPackageQuery?: (ctx: PackageQueryContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.packageQuerySpecialization`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPackageQuerySpecialization?: (ctx: PackageQuerySpecializationContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.queryClause`.
* @param ctx the parse tree
* @return the visitor result
*/
visitQueryClause?: (ctx: QueryClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.fragmentVariable`.
* @param ctx the parse tree
* @return the visitor result
*/
visitFragmentVariable?: (ctx: FragmentVariableContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.packageOperationExport`.
* @param ctx the parse tree
@@ -166,6 +320,30 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitPackageFunctionExport?: (ctx: PackageFunctionExportContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.acceptanceClause`.
* @param ctx the parse tree
* @return the visitor result
*/
visitAcceptanceClause?: (ctx: AcceptanceClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.preparationClause`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPreparationClause?: (ctx: PreparationClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.executionClause`.
* @param ctx the parse tree
* @return the visitor result
*/
visitExecutionClause?: (ctx: ExecutionClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.selectedOperations`.
* @param ctx the parse tree
* @return the visitor result
*/
visitSelectedOperations?: (ctx: SelectedOperationsContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.packageConstructorExport`.
* @param ctx the parse tree
@@ -262,12 +440,54 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitConformanceDecl?: (ctx: ConformanceDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.conformanceSourceArguments`.
* @param ctx the parse tree
* @return the visitor result
*/
visitConformanceSourceArguments?: (ctx: ConformanceSourceArgumentsContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.conformanceItem`.
* @param ctx the parse tree
* @return the visitor result
*/
visitConformanceItem?: (ctx: ConformanceItemContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.moduleBindingDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitModuleBindingDecl?: (ctx: ModuleBindingDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.initializationDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitInitializationDecl?: (ctx: InitializationDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.initializationField`.
* @param ctx the parse tree
* @return the visitor result
*/
visitInitializationField?: (ctx: InitializationFieldContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.creationOwnerMapping`.
* @param ctx the parse tree
* @return the visitor result
*/
visitCreationOwnerMapping?: (ctx: CreationOwnerMappingContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.stateFieldBindingDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitStateFieldBindingDecl?: (ctx: StateFieldBindingDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.relationshipMaterializationDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitRelationshipMaterializationDecl?: (ctx: RelationshipMaterializationDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.operationBindingDecl`.
* @param ctx the parse tree
@@ -292,6 +512,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitOperationProvider?: (ctx: OperationProviderContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.acceptanceBinding`.
* @param ctx the parse tree
* @return the visitor result
*/
visitAcceptanceBinding?: (ctx: AcceptanceBindingContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.statePrimitive`.
* @param ctx the parse tree
@@ -328,6 +554,18 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitValueType?: (ctx: ValueTypeContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.recordField`.
* @param ctx the parse tree
* @return the visitor result
*/
visitRecordField?: (ctx: RecordFieldContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.variantCase`.
* @param ctx the parse tree
* @return the visitor result
*/
visitVariantCase?: (ctx: VariantCaseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.scalarType`.
* @param ctx the parse tree
+419
View File
@@ -0,0 +1,419 @@
import {
TypeSubstitution,
GenericTypeError,
instantiateInterface,
appliedInterfaceId,
valueType,
type AtomId,
type ClosedTypeArgument,
type GenericTypeEnvironment,
type InterfaceApplicationExpression,
type InterfaceRevision,
type ObjectTypeExpression,
type TypeArgumentExpression,
type TypeParameter,
type ValueAliasDefinition,
type ValueType,
type ValueTypeExpression,
} from "../capability-model/index.js";
import type {
InterfaceTypeContext,
TargetConstraintContext,
TypeArgumentsContext,
TypeParametersContext,
TypeAliasDeclContext,
ValueTypeContext,
} from "./generated/QuixosCapabilityParser.js";
const literal = (context: { getText(): string }) => JSON.parse(context.getText()) as string;
/** Lexical authoring scope; only `value`/`target` return installed types. */
export class GenericSourceTypes {
readonly interfaces = new Map<string, InterfaceRevision>();
readonly definitions = new Map<string, InterfaceRevision>();
readonly applications = new Map<string, InterfaceRevision>();
readonly aliases = new Map<string, ValueAliasDefinition>();
parameters = new Map<string, TypeParameter>();
bindings = new Map<string, ClosedTypeArgument>();
self?: AtomId;
private active: string[] = [];
private applicationCount = 0;
constructor(readonly atoms: Map<string, AtomId>) {}
environment(): GenericTypeEnvironment {
return {
arguments: this.bindings,
aliases: this.aliases,
self: this.self,
applyInterface: (id, args) => this.apply(id, args).revisionId,
// Obligations are retained on the application and discharged by workspace
// validation, where all atom conformances are known (not source order).
implementsInterface: () => true,
};
}
register(name: string, definition: InterfaceRevision) {
this.interfaces.set(name, definition);
this.definitions.set(definition.revisionId, definition);
}
/** Check authored applications even when nobody has instantiated this template yet. */
validateTemplate(definition: InterfaceRevision) {
const template = definition.template;
if (!template) return;
const aliases = new Map((template.aliases ?? []).map((alias) => [alias.id, alias]));
const parametersById = new Map(
[...template.parameters, ...(template.aliases ?? []).flatMap((alias) => alias.parameters)].map((parameter) => [
parameter.id,
parameter,
]),
);
const replace = (node: unknown, arguments_: Map<string, TypeArgumentExpression>): unknown => {
if (!node || typeof node !== "object") return node;
if ("kind" in node && node.kind === "parameter" && "parameterId" in node) {
const arg = arguments_.get(String(node.parameterId));
if (arg) return arg.kind === "value" ? arg.type : arg.target;
}
if (Array.isArray(node)) return node.map((entry) => replace(entry, arguments_));
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, replace(value, arguments_)]));
};
const implies = (
actual: InterfaceApplicationExpression,
required: InterfaceApplicationExpression,
seen = new Set<string>(),
): boolean => {
const key = JSON.stringify(actual);
if (key === JSON.stringify(required)) return true;
if (seen.has(key) || seen.size > 128) return false;
seen.add(key);
const contract = this.definitions.get(actual.definitionId);
if (!contract?.template)
return (contract?.requiredInterfaces ?? []).includes(required.definitionId) && required.arguments.length === 0;
const args = new Map(
contract.template.parameters.map((parameter, index) => [parameter.id, actual.arguments[index]]),
);
return contract.template.requires.some((parent) =>
implies(replace(parent, args) as InterfaceApplicationExpression, required, seen),
);
};
const storable = (type: ValueTypeExpression, depth = 0): boolean => {
if (depth > 128)
throw new GenericTypeError(
"type-complexity-limit",
definition.displayName,
"Storable alias expansion exceeds depth limit",
);
if (type.kind === "parameter") {
const parameter = parametersById.get(type.parameterId);
return parameter?.kind === "value" && Boolean(parameter.storable);
}
if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1);
if (type.kind === "record") return Object.values(type.fields).every((field) => storable(field, depth + 1));
if (type.kind === "variant") return Object.values(type.cases).every((entry) => storable(entry.value, depth + 1));
if (type.kind === "alias") {
const alias = aliases.get(type.definitionId);
if (!alias || alias.parameters.length !== type.arguments.length) return false;
return storable(
replace(
alias.body,
new Map(alias.parameters.map((parameter, index) => [parameter.id, type.arguments[index]])),
) as ValueTypeExpression,
depth + 1,
);
}
return type.kind === "scalar" || (type.kind === "builtin" && type.name === "unit");
};
let remaining = 10000;
const visit = (node: unknown, path: string, depth = 0): void => {
if (depth > 128 || --remaining < 0)
throw new GenericTypeError("type-complexity-limit", path, "Type exceeds the depth or expansion budget");
if (!node || typeof node !== "object") return;
if ("definitionId" in node && "arguments" in node) {
const application = node as InterfaceApplicationExpression | Extract<ValueTypeExpression, { kind: "alias" }>;
const alias = "kind" in application && application.kind === "alias";
const target = alias ? aliases.get(application.definitionId) : this.definitions.get(application.definitionId);
if (!target)
throw new GenericTypeError(
alias ? "unknown-alias" : "unknown-interface",
path,
`Unknown definition ${application.definitionId}`,
);
if ("template" in target && target.template?.usesSelf) template.usesSelf = true;
const parameters = "parameters" in target ? target.parameters : (target.template?.parameters ?? []);
if (parameters.length !== application.arguments.length)
throw new GenericTypeError(
"type-arity",
path,
`Expected ${parameters.length} type arguments, received ${application.arguments.length}`,
);
parameters.forEach((parameter, index) => {
if (parameter.kind !== application.arguments[index].kind)
throw new GenericTypeError(
"parameter-kind",
path,
`Expected ${parameter.kind}, received ${application.arguments[index].kind}`,
);
const argument = application.arguments[index];
if (parameter.kind === "value" && parameter.storable && argument.kind === "value" && !storable(argument.type))
throw new GenericTypeError(
"non-storable-argument",
path,
"Generic application cannot prove its value argument is storable",
);
if (parameter.kind === "object" && argument.kind === "object" && argument.target.kind === "parameter") {
const offered = parametersById.get(argument.target.parameterId);
const mapping = new Map(parameters.map((p, i) => [p.id, application.arguments[i]]));
for (const bound of parameter.implements) {
const required = replace(bound, mapping) as InterfaceApplicationExpression;
if (offered?.kind !== "object" || !offered.implements.some((evidence) => implies(evidence, required)))
throw new GenericTypeError(
"unsatisfied-bound",
path,
`Object parameter does not prove ${required.definitionId}`,
);
}
}
});
}
for (const [name, child] of Object.entries(node)) visit(child, `${path}.${name}`, depth + 1);
};
visit(template, definition.displayName);
const active = new Set<string>();
const done = new Set<string>();
const checkAlias = (id: string) => {
if (done.has(id)) return;
if (active.has(id))
throw new GenericTypeError("recursive-alias", id, `Recursive value alias: ${[...active, id].join(" -> ")}`);
active.add(id);
const walk = (node: unknown): void => {
if (!node || typeof node !== "object") return;
if ("kind" in node && node.kind === "alias")
checkAlias((node as ValueAliasDefinition["body"] & { definitionId: string }).definitionId);
Object.values(node).forEach(walk);
};
walk(aliases.get(id)?.body);
active.delete(id);
done.add(id);
};
for (const id of aliases.keys()) checkAlias(id);
}
apply(id: string, arguments_: readonly ClosedTypeArgument[]): InterfaceRevision {
const definition = this.definitions.get(id);
if (!definition) throw new GenericTypeError("unknown-interface", id, "Unknown interface definition");
const application = {
definitionId: definition.revisionId,
source: definition.source,
arguments: [...arguments_],
...(definition.template?.usesSelf ? { self: this.self } : {}),
};
const appliedId = definition.template ? appliedInterfaceId(application) : definition.revisionId;
const cached = this.applications.get(appliedId);
if (cached) return cached;
if (this.active.includes(id))
throw new GenericTypeError(
"recursive-application",
id,
`Expanding recursive interface application: ${[...this.active, id].join(" -> ")}`,
);
if (++this.applicationCount > 10000)
throw new GenericTypeError("type-complexity-limit", id, "Too many interface applications");
this.active.push(id);
if (definition.template)
this.applications.set(appliedId, {
interfaceId: definition.interfaceId,
revisionId: appliedId,
displayName: definition.displayName,
source: definition.source,
members: [],
application,
});
try {
const obligations: NonNullable<InterfaceRevision["argumentRequirements"]> = [];
const instance = instantiateInterface(definition, arguments_, {
...this.environment(),
implementsInterface: (target, required) => {
obligations.push({ target, required });
return true;
},
});
if (instance === definition) return definition;
instance.argumentRequirements = obligations;
this.applications.set(instance.revisionId, instance);
this.definitions.set(instance.revisionId, instance);
return instance;
} catch (error) {
this.applications.delete(appliedId);
throw error;
} finally {
this.active.pop();
}
}
interface(context: InterfaceTypeContext): InterfaceApplicationExpression {
return this.interfaceByName(context.identifier().getText(), context.typeArguments());
}
interfaceByName(name: string, arguments_: TypeArgumentsContext | null): InterfaceApplicationExpression {
const definition = this.interfaces.get(name);
if (!definition) throw new GenericTypeError("unknown-interface", name, "Unknown interface");
return { definitionId: definition.revisionId, arguments: this.arguments(arguments_) };
}
arguments(context: TypeArgumentsContext | null): TypeArgumentExpression[] {
return (context?.typeArgument() ?? []).map((argument): TypeArgumentExpression => {
if (argument.INTERFACE())
return {
kind: "object",
target: { kind: "application", application: this.interface(argument.interfaceType()!) },
};
if (argument.ATOM()) return { kind: "object", target: this.atom(argument.identifier()!.getText()) };
if (argument.OBJECT()) return { kind: "object", target: this.objectParameter(argument.identifier()!.getText()) };
const type = argument.valueType()!;
if (type.getText() === "Self") return { kind: "object", target: { kind: "self" } };
const parameter = type.identifier() && this.parameters.get(type.identifier()!.getText());
if (parameter?.kind === "object" && !type.typeArguments())
return { kind: "object", target: { kind: "parameter", parameterId: parameter.id } };
return { kind: "value", type: this.expression(type) };
});
}
private atom(name: string): ObjectTypeExpression {
if (name === "Self") return { kind: "self" };
const atomId = this.atoms.get(name);
if (!atomId) throw new GenericTypeError("unknown-atom", name, "Unknown atom");
return { kind: "atom", atomId };
}
private objectParameter(name: string): ObjectTypeExpression {
if (name === "Self") return { kind: "self" };
const parameter = this.parameters.get(name);
if (!parameter || parameter.kind !== "object")
throw new GenericTypeError("parameter-kind", name, "Expected an object parameter");
return { kind: "parameter", parameterId: parameter.id };
}
targetExpression(context: TargetConstraintContext): ObjectTypeExpression {
const name = context.identifier().getText();
if (context.ATOM()) return this.atom(name);
if (context.OBJECT()) return this.objectParameter(name);
return { kind: "application", application: this.interfaceByName(name, context.typeArguments()) };
}
expression(context: ValueTypeContext): ValueTypeExpression {
if (context.VARIANT()) {
const entries = context.variantCase().map(
(entry) =>
[
entry.identifier().getText(),
{
id: literal(entry.stringLiteral()),
value: this.expression(entry.valueType()),
},
] as const,
);
if (
new Set(entries.map(([label]) => label)).size !== entries.length ||
new Set(entries.map(([, entry]) => entry.id)).size !== entries.length
)
throw new GenericTypeError("duplicate-variant-case", "variant", "Variant labels and case IDs must be unique");
return { kind: "variant", id: literal(context.stringLiteral()!), cases: Object.fromEntries(entries) };
}
if (context.scalarType())
return {
kind: "scalar",
name: context.scalarType()!.getText() as Extract<ValueType, { kind: "scalar" }>["name"],
};
if (context.UNIT()) return valueType.unit;
if (context.WATCH_HANDLE()) return valueType.watchHandle;
if (context.MESSAGE()) return valueType.message(literal(context.stringLiteral()!));
if (context.OPTIONAL() || context.LIST())
return { kind: context.LIST() ? "list" : "optional", value: this.expression(context.valueType()!) };
if (context.RECORD()) {
const fields = context
.recordField()
.map((field) => [field.identifier().getText(), this.expression(field.valueType())] as const);
if (new Set(fields.map(([name]) => name)).size !== fields.length)
throw new GenericTypeError("duplicate-field", "record", "Duplicate record field");
return { kind: "record", fields: Object.fromEntries(fields) };
}
const name = context.identifier()!.getText();
if (name === "change" && context.typeArguments()) {
const args = this.arguments(context.typeArguments());
if (args.length !== 1 || args[0]!.kind !== "value")
throw new GenericTypeError("invalid-change-type", name, "change<T> requires one materialized value type");
return { kind: "change", value: args[0]!.type };
}
if (context.ATOM_REF()) return { kind: "object-ref", expectation: this.atom(name) };
if (context.INTERFACE_REF())
return {
kind: "object-ref",
expectation: { kind: "application", application: this.interfaceByName(name, context.typeArguments()) },
};
if (context.REF()) return { kind: "object-ref", expectation: this.objectParameter(name) };
const parameter = this.parameters.get(name);
if (parameter) {
if (parameter.kind !== "value" || context.typeArguments())
throw new GenericTypeError("parameter-kind", name, "Expected a value parameter; object parameters need ref<T>");
return { kind: "parameter", parameterId: parameter.id };
}
if (!this.aliases.has(name)) throw new GenericTypeError("unknown-type", name, "Unknown value type");
return { kind: "alias", definitionId: name, arguments: this.arguments(context.typeArguments()) };
}
value(context: ValueTypeContext): ValueType {
return new TypeSubstitution(this.environment()).value(this.expression(context));
}
target(context: TargetConstraintContext) {
return new TypeSubstitution(this.environment()).object(this.targetExpression(context));
}
declareParameters(context: TypeParametersContext | null, owner: string): TypeParameter[] {
const entries = context?.typeParameter() ?? [];
const result: TypeParameter[] = entries.map((parameter, index) => {
const name = parameter.identifier().getText();
if (name === "Self" || this.parameters.has(name))
throw new GenericTypeError("duplicate-parameter", owner, `Duplicate or reserved parameter ${name}`);
const declaration: TypeParameter = parameter.VALUE()
? {
id: `${owner}/parameter/${index}`,
name,
kind: "value",
...(parameter.STORABLE() ? { storable: true } : {}),
}
: { id: `${owner}/parameter/${index}`, name, kind: "object", implements: [] };
this.parameters.set(name, declaration);
return declaration;
});
entries.forEach((entry, index) => {
const parameter = result[index];
if (parameter.kind === "object")
parameter.implements = entry.interfaceType().map((bound) => this.interface(bound));
});
return result;
}
declareAliases(contexts: readonly TypeAliasDeclContext[]) {
for (const context of contexts) {
const name = context.identifier().getText();
if (this.aliases.has(name)) throw new GenericTypeError("duplicate-alias", name, "Duplicate type alias");
this.aliases.set(name, { id: name, parameters: [], body: valueType.unit });
}
for (const context of contexts) {
const name = context.identifier().getText();
const previous = this.parameters;
this.parameters = new Map();
try {
this.aliases.set(name, {
id: name,
parameters: this.declareParameters(context.typeParameters(), JSON.stringify(["alias", name])),
body: this.expression(context.valueType()),
});
} finally {
this.parameters = previous;
}
}
}
}
+126
View File
@@ -0,0 +1,126 @@
import childProcess from "node:child_process";
import crypto from "node:crypto";
import { mkdir, mkdtemp, readFile, realpath, rename, rm, stat } from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import type { CapabilityRepositoryResolver } from "./assembly.js";
const execFile = promisify(childProcess.execFile);
const sourceKey = (kind: string, repository: string, commit: string) =>
`${kind}\0${repository}\0${commit.toLowerCase()}`;
const checkoutName = (kind: string, repository: string, commit: string) => {
const digest = crypto
.createHash("sha256")
.update(sourceKey(kind, repository, commit))
.digest("hex")
.slice(0, 24);
return `${kind}-${digest}`;
};
export const createGitCapabilityResolver = async (options: {
checkoutRoot: string;
snapshotMap?: string;
snapshotOnly?: boolean;
}): Promise<CapabilityRepositoryResolver> => {
await mkdir(options.checkoutRoot, { recursive: true });
const checkoutRoot = await realpath(options.checkoutRoot);
const snapshots = new Map<string, string>();
if (options.snapshotMap) {
const snapshotMapPath = await realpath(options.snapshotMap);
const document = JSON.parse(await readFile(snapshotMapPath, "utf8")) as {
resources?: Array<{
kind: string;
repository: string;
commit: string;
directory: string;
}>;
};
for (const entry of document.resources ?? []) {
if (entry.kind !== "interface" && entry.kind !== "package" && entry.kind !== "build") {
throw new Error(`Snapshot map has unsupported resource kind ${entry.kind}`);
}
const key = sourceKey(entry.kind, entry.repository, entry.commit);
if (snapshots.has(key)) throw new Error(`Snapshot map repeats ${key}`);
const directory = path.resolve(path.dirname(snapshotMapPath), entry.directory);
snapshots.set(key, await realpath(directory));
}
}
const checkouts = new Map<string, Promise<{ directory: string }>>();
return async (source, kind) => {
const key = sourceKey(kind, source.repository, source.commit);
const snapshot = snapshots.get(key);
if (snapshot) return { directory: snapshot };
if (options.snapshotOnly) throw new Error(`No offline snapshot for ${kind} ${source.repository}@${source.commit}`);
const existing = checkouts.get(key);
if (existing) return await existing;
const pending = (async () => {
const directory = path.join(checkoutRoot, checkoutName(kind, source.repository, source.commit));
const verify = async (checkout: string) => {
const { stdout } = await execFile("git", ["-C", checkout, "rev-parse", "HEAD"]);
if (stdout.trim().toLowerCase() !== source.commit.toLowerCase()) {
throw new Error(
`Locked commit mismatch for ${source.repository}: wanted ${source.commit}, fetched ${stdout.trim()}`,
);
}
const { stdout: changes } = await execFile("git", [
"-C",
checkout,
"status",
"--porcelain",
"--untracked-files=all",
]);
if (changes.trim()) throw new Error(`Dependency checkout was modified: ${checkout}`);
};
// Only complete, checked clones become visible under the deterministic name.
// Concurrent resolvers may fetch independently, but cannot observe a partial clone.
if (
await stat(directory).then(
() => true,
(error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") return false;
throw error;
},
)
) {
await verify(directory);
return { directory };
}
const staging = await mkdtemp(path.join(checkoutRoot, ".fetch-"));
const checkout = path.join(staging, "checkout");
try {
await execFile("git", [
"-c",
"advice.detachedHead=false",
"clone",
"--depth",
"1",
"--single-branch",
"--branch",
`quixos-reachability/${source.commit.toLowerCase()}`,
source.repository,
checkout,
]);
await verify(checkout);
try {
await rename(checkout, directory);
} catch (error) {
if (!["EEXIST", "ENOTEMPTY"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error;
await verify(directory);
}
} finally {
await rm(staging, { recursive: true, force: true });
}
return { directory };
})();
checkouts.set(key, pending);
try {
return await pending;
} catch (error) {
checkouts.delete(key);
throw error;
}
};
};
@@ -0,0 +1,54 @@
import { parse } from "@babel/parser";
type Node = { type: string; start: number; end: number; [key: string]: unknown };
const node = (value: unknown): value is Node =>
!!value && typeof value === "object" && typeof (value as Node).type === "string";
/** One requested insertion into ordinary authored TypeScript, not regeneration.
* Ambiguous/custom wiring is left alone with an actionable error. */
export function addImplementation(
text: string,
factory: "createRuntime" | "serveMigration" | "createRegistry",
key: string,
importPath: string,
migrationOnly = false,
): string {
const ast = parse(text, { sourceType: "module", plugins: ["typescript"] });
const objects: Node[] = [];
const names = new Set<string>();
const visit = (value: unknown) => {
if (Array.isArray(value)) {
value.forEach(visit);
return;
}
if (!node(value)) return;
if (value.type === "Identifier") names.add(String(value.name));
if (
value.type === "CallExpression" &&
node(value.callee) &&
value.callee.type === "Identifier" &&
value.callee.name === factory &&
Array.isArray(value.arguments) &&
value.arguments.length === 1 &&
node(value.arguments[0]) &&
value.arguments[0].type === "ObjectExpression"
)
objects.push(value.arguments[0]);
Object.values(value).forEach(visit);
};
visit(ast);
if (objects.length !== 1)
throw new Error(
`Cannot safely add implementation: expected one ${factory}({...}) literal. Wire the handler in your authored server code instead.`,
);
const object = objects[0];
if (
(object.properties as Node[]).some((p) => node(p.key) && !p.computed && (p.key.name === key || p.key.value === key))
)
throw new Error(`Implementation already exists for ${key}`);
let alias = "qxImplementation";
for (let index = 1; names.has(alias); index++) alias = `qxImplementation${index}`;
const value = migrationOnly ? 'async () => { throw new Error("Migration-only export"); }' : alias;
const insertion = object.start + 1;
const edited = text.slice(0, insertion) + `\n ${JSON.stringify(key)}: ${value},\n` + text.slice(insertion);
return (migrationOnly ? "" : `import {handler as ${alias}} from ${JSON.stringify(importPath)};\n`) + edited;
}
+4
View File
@@ -1 +1,5 @@
export * from "./parser.js";
export * from "./assembly.js";
export * from "./source.js";
export * from "./source-loader.js";
export * from "./scaffold.js";
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env node
// Data only: never load files, resolve repositories, or evaluate code.
import { parseQx } from "./source.js";
import { parseQuixosLockDocument } from "../resource-lock/parser.js";
import { instantiateWorkspaceIdentity } from "./structural-edits.js";
let input = "";
for await (const chunk of process.stdin) {
input += chunk;
if (Buffer.byteLength(input) > 6 * 1024 * 1024) throw new Error("Inspection input exceeds limit");
}
const document = JSON.parse(input);
if (!Array.isArray(document) && document?.operation === "instantiate-workspace") {
if (
typeof document.source !== "string" ||
document.source.length > 262144 ||
typeof document.workspaceId !== "string"
)
throw new Error("Invalid template identity request");
process.stdout.write(JSON.stringify({ source: instantiateWorkspaceIdentity(document.source, document.workspaceId) }));
process.exit(0);
}
const files: { path: string; source: string }[] = document;
if (!Array.isArray(files) || files.length > 128) throw new Error("Too many source files");
const result = files.map(({ path, source }) => {
if (typeof path !== "string" || typeof source !== "string" || source.length > 262144)
throw new Error("Invalid source input");
if (path.endsWith(".qx")) {
const parsed = parseQx(source, path);
return { path, root: parsed.root, diagnostics: parsed.diagnostics };
}
return { path, lock: parseQuixosLockDocument(source, path) };
});
process.stdout.write(JSON.stringify(result));
@@ -0,0 +1,19 @@
import { realpath } from "node:fs/promises";
import path from "node:path";
/** Registration may name the workbench through its guest alias. Only that
* outer alias is permitted: resources and each checkout remain physical,
* directly contained directories. Persist the resulting root-relative path.
*/
export async function managedResourcePath(workbench: string, directory: string): Promise<string> {
const root = await realpath(workbench);
const selected = path.resolve(root, directory);
const name = path.basename(selected);
const resources = path.dirname(selected);
if (path.basename(resources) !== "resources" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name))
throw new Error(`Repository is outside the managed workbench: ${directory}`);
const relative = `resources/${name}`;
if ((await realpath(path.dirname(resources))) !== root || (await realpath(selected)) !== path.join(root, relative))
throw new Error(`Managed repository path crosses a symlink or escapes the workbench: ${directory}`);
return relative;
}
+27
View File
@@ -0,0 +1,27 @@
import fs from "node:fs/promises";
import path from "node:path";
import { contentDigest } from "../capability-model/evolution.js";
import { validateMigrationCatalog, type MigrationCatalog } from "../capability-model/migrations.js";
import { applyStructure, planStructure } from "./structural-plan.js";
/** Explicitly acknowledge edited migration code. This does not change retained
* contracts or grant activation approval; the immutable checker verifies it. */
export async function sealMigrations(directory: string) {
const root = await fs.realpath(directory);
const file = "quixos.migrations.json";
const before = await fs.readFile(path.join(root, file), "utf8");
const catalog = JSON.parse(before) as MigrationCatalog;
for (const migration of catalog.migrations) {
const implementation = await fs.realpath(path.join(root, migration.implementation.file));
if (!implementation.startsWith(root + path.sep)) throw new Error("Migration implementation escapes package");
migration.implementation.digest = contentDigest(await fs.readFile(implementation, "utf8"));
}
validateMigrationCatalog(catalog);
return applyStructure(
await planStructure(root, {
kind: "package",
validation: "syntax",
files: [{ file, expected: before, replace: JSON.stringify(catalog, null, 2) + "\n" }],
}),
);
}
File diff suppressed because it is too large Load Diff
+522
View File
@@ -0,0 +1,522 @@
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { randomUUID } from "node:crypto";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { loadQuixosLock, parseQuixosLockDocument } from "../resource-lock/index.js";
import { contentDigest } from "../capability-model/evolution.js";
import { planStructure, applyStructure, type StructuralRequest } from "./structural-plan.js";
import { snapshotRepository } from "./candidate-check.js";
import {
compileWorkspaceRepository,
compileCapabilityResourceRepository,
type ResolvedCapabilityResource,
} from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import { snapshotCommit, buildImmutableCandidate } from "./checked-build.js";
import {
planEvolution,
reviewSemanticMajors,
type WorkspaceRevision,
type EvolutionReview,
} from "../capability-model/index.js";
import { withFileLock } from "./file-lock.js";
import { managedResourcePath } from "./managed-source-path.js";
const execFile = promisify(callback);
type Source = { repository: string; commit: string };
export type UpgradeNode = { kind: "workspace" | "package" | "interface" | "build"; directory: string; source: Source };
export type UpgradeSpec = {
nodes: UpgradeNode[];
quixos?: Source;
baseline?: string;
reviews?: string;
bootstrap?: boolean;
};
type NodePlan = UpgradeNode & { treeDigest: string; dependencies: string[]; lockFiles: string[] };
export type UpgradePlan = { schemaVersion: 1; workbench: string; spec: UpgradeSpec; nodes: NodePlan[]; digest: string };
type Step = {
directory: string;
phase: "editing" | "prepared" | "refactor" | "checked" | "publishing" | "published";
commit?: string;
treeDigest?: string;
structuralJournal?: string;
structuralPlan?: Awaited<ReturnType<typeof planStructure>>;
};
type Journal = { schemaVersion: 1; plan: UpgradePlan; steps: Step[] };
const sourceKey = (node: { kind: string; source: Source }) =>
JSON.stringify([node.kind, node.source.repository, node.source.commit]);
const command = async (cwd: string, tool: string, args: string[]) =>
(
await execFile(tool, args, {
cwd,
maxBuffer: 16 * 1024 * 1024,
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_SUBTREE_PUBLISH: "0" },
})
).stdout.trim();
const validSource = (value: Source) => {
const url = new URL(value.repository);
if (
url.protocol !== "https:" ||
url.username ||
url.password ||
url.search ||
url.hash ||
!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value.commit)
)
throw new Error("Upgrade sources must be exact credential-free HTTPS revisions");
};
const location = async (root: string, directory: string) => {
if (directory !== "root" && !/^resources\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(directory))
throw new Error("Upgrade target must be a managed root/resource repository");
const resolved = await fs.realpath(path.join(root, directory));
if (resolved !== path.join(root, directory)) throw new Error("Upgrade target crosses a symlink");
return resolved;
};
const treeDigest = async (root: string) => {
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-upgrade-tree-"));
try {
return (await snapshotRepository(root, temporary)).treeDigest;
} finally {
await fs.rm(temporary, { recursive: true, force: true });
}
};
const writeJournal = async (filename: string, journal: unknown) => {
const temp = `${filename}.${randomUUID()}.tmp`;
const handle = await fs.open(temp, "wx", 0o600);
try {
await handle.writeFile(JSON.stringify(journal, null, 2));
await handle.sync();
} finally {
await handle.close();
}
await fs.rename(temp, filename);
const directory = await fs.open(path.dirname(filename), "r");
try {
await directory.sync();
} finally {
await directory.close();
}
};
export const discoverUpgradeSpec = async (workbenchPath: string): Promise<UpgradeSpec> => {
const workbench = await fs.realpath(workbenchPath);
const graph = JSON.parse(await fs.readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"));
const root = await location(workbench, "root");
const nodes: UpgradeNode[] = [
{
kind: "workspace",
directory: "root",
source: {
repository: await command(root, "git", ["config", "--get", "remote.origin.url"]),
commit: await command(root, "jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"]),
},
},
...(await Promise.all(
graph.resources.map(
async (entry: { kind: "interface" | "package" | "build"; directory: string; source: Source }) => ({
kind: entry.kind,
source: entry.source,
directory: await managedResourcePath(workbench, entry.directory),
}),
),
)),
];
let baseline: string | undefined;
try {
const host = JSON.parse(await fs.readFile("/etc/quixos/workspace-source.json", "utf8"));
if ((await fs.realpath(host.workbenchRoot)) === (await fs.realpath(workbench)))
baseline = JSON.parse(
await fs.readFile(path.join(host.runtimeClosureRoot, "manifest.json"), "utf8"),
).workspacePlanPath;
} catch (error) {
if (!["ENOENT", "EACCES"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error;
}
return { nodes, baseline };
};
/** Read-only source plan. Repositories are selected explicitly, including any
* parallel versions of the same resource; no guesses at a floating 'latest'. */
export const planPinUpgrades = async (workbenchPath: string, spec: UpgradeSpec): Promise<UpgradePlan> => {
const workbench = await fs.realpath(workbenchPath);
if (
!Array.isArray(spec.nodes) ||
!spec.nodes.length ||
spec.nodes.length > 100 ||
spec.nodes.filter((node) => node.kind === "workspace").length !== 1
)
throw new Error("Upgrade graph requires one workspace and at most 100 repositories");
if (spec.quixos) validSource(spec.quixos);
const keys = new Map<string, string>();
for (const node of spec.nodes) {
validSource(node.source);
if (!["workspace", "package", "interface", "build"].includes(node.kind) || keys.has(sourceKey(node)))
throw new Error("Duplicate/invalid upgrade resource identity");
keys.set(sourceKey(node), node.directory);
}
if (new Set(spec.nodes.map((node) => node.directory)).size !== spec.nodes.length)
throw new Error("Upgrade directories must be distinct");
const nodes: NodePlan[] = [];
for (const node of spec.nodes) {
const root = await location(workbench, node.directory);
if ((await command(root, "git", ["config", "--get", "remote.origin.url"])) !== node.source.repository)
throw new Error(`Upgrade origin differs from selected source: ${node.directory}`);
const loaded = await loadQuixosLock(path.join(root, "quixos.lock"));
if (!loaded.ok)
throw new Error(
`Invalid lock in ${node.directory}: ${loaded.diagnostics.map((entry) => entry.message).join("; ")}`,
);
const dependencies = loaded.lock.resources
.map((resource) => keys.get(sourceKey(resource)))
.filter((value): value is string => Boolean(value));
nodes.push({
...node,
treeDigest: await treeDigest(root),
dependencies: [...new Set(dependencies)],
lockFiles: loaded.lock.sourceFiles ?? ["quixos.lock"],
});
}
const ordered: NodePlan[] = [],
remaining = [...nodes];
while (remaining.length) {
const index = remaining.findIndex((node) =>
node.dependencies.every((dependency) => ordered.some((entry) => entry.directory === dependency)),
);
if (index < 0) throw new Error("Cyclic source publication graph");
ordered.push(remaining.splice(index, 1)[0]);
}
const workspace = ordered.find((node) => node.kind === "workspace")!;
// Even unreferenced new resources are published before the root.
ordered.splice(ordered.indexOf(workspace), 1);
ordered.push(workspace);
const plan = { schemaVersion: 1 as const, workbench, spec, nodes: ordered };
return { ...plan, digest: contentDigest(plan) };
};
export type UpgradeEffects = {
check(node: NodePlan, root: string, output: string, spec: UpgradeSpec): Promise<void>;
snapshot(root: string): Promise<string>;
publish(root: string, commit: string): Promise<void>;
};
/** Review a successfully built immutable candidate using the same boundary as
* ordinary authoring checks. Execution storage compatibility is checked against
* the current graph at activation; the legacy evolution model is not its schema.
*/
export async function reviewUpgradeCandidate({
artifact,
baseline,
candidate,
reviews = [],
}: {
artifact: string;
baseline: WorkspaceRevision | null;
candidate: WorkspaceRevision;
reviews?: EvolutionReview[];
}) {
let execution = false;
try {
await fs.access(path.join(artifact, "execution-plan.json"));
execution = true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
return execution
? { filename: "semantic-review.json", review: reviewSemanticMajors(baseline, candidate, reviews) }
: { filename: "evolution.json", review: planEvolution(baseline, candidate, { reviews }) };
}
const effects: UpgradeEffects = {
async check(node, root, output, spec) {
if (node.kind === "workspace" && !spec.baseline && !spec.bootstrap)
throw new Error(
"Upgrading a workspace requires its checked active baseline for major-review checks (or explicit bootstrap:true for a new workspace)",
);
// Explicit baseline upgrades use the same immutable Nix checker. Retaining
// an unverified source is safe and must precede a remote flake fetch.
await fs.mkdir(output);
const commit = await snapshotCommit(root);
await effects.publish(root, commit);
const artifact = await buildImmutableCandidate({ ...node.source, commit }, node.kind, path.join(output, "nix.log"));
const candidate = await fs.readFile(path.join(artifact, "candidate.json"), "utf8");
await fs.writeFile(path.join(output, "candidate.json"), candidate);
if (node.kind === "workspace") {
const baseline = spec.baseline
? (JSON.parse(await fs.readFile(spec.baseline, "utf8")) as WorkspaceRevision)
: null;
const reviews = spec.reviews ? (JSON.parse(await fs.readFile(spec.reviews, "utf8")) as EvolutionReview[]) : [];
const { filename, review } = await reviewUpgradeCandidate({
artifact,
baseline,
candidate: JSON.parse(candidate),
reviews,
});
await fs.writeFile(path.join(output, filename), JSON.stringify(review, null, 2));
if (review.blockers.length)
throw new Error(`Refactor required in ${node.directory}: ${review.blockers.join("; ")}`);
}
},
async snapshot(root) {
const commit = await snapshotCommit(root);
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit)) throw new Error("Publication did not resolve an exact commit");
await command(root, "git", ["diff", "--exit-code", "--no-ext-diff", "--no-textconv", commit, "--"]);
const tracked = new Set((await command(root, "git", ["ls-tree", "-r", "--name-only", "-z", commit])).split("\0"));
for (const file of (await command(root, "git", ["ls-files", "--others", "--exclude-standard", "-z"]))
.split("\0")
.filter(Boolean))
if (!tracked.has(file)) throw new Error(`Uncaptured source file ${file}`);
return commit;
},
async publish(root, commit) {
const ref = `refs/tags/quixos-reachability/${commit}`;
const remote = await command(root, "git", ["ls-remote", "--refs", "origin", ref]);
if (remote && remote !== `${commit}\t${ref}`) throw new Error("Immutable publication ref conflict");
if (!remote) await command(root, "git", ["push", "origin", `${commit}:${ref}`]);
if ((await command(root, "git", ["ls-remote", "--refs", "origin", ref])) !== `${commit}\t${ref}`)
throw new Error("Publication response uncertain; retry the same journal");
},
};
/** Explicit --publish only. Append-only remote retention; never moves the
* workspace branch, activates code, or rolls back previously published nodes. */
export const applyPinUpgrades = async (
plan: UpgradePlan,
journalId?: string,
implementation: UpgradeEffects = effects,
options: { acceptEdits?: boolean } = {},
) => {
if (implementation === effects && !plan.spec.baseline && !plan.spec.bootstrap)
throw new Error("Publication requires an active checked baseline or explicit bootstrap:true");
const { digest, ...body } = plan;
if (contentDigest(body) !== digest) throw new Error("Upgrade plan digest mismatch");
const directory = path.join(plan.workbench, ".quixos", "upgrades");
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
if ((await fs.realpath(directory)) !== directory) throw new Error("Upgrade journals must not cross symlinks");
const id = journalId ?? randomUUID();
if (!/^[a-f0-9-]{36}$/.test(id)) throw new Error("Invalid upgrade journal ID");
const filename = path.join(directory, `${id}.json`);
return withFileLock(path.join(directory, "writer.lock"), async () => {
try {
const journal: Journal = journalId
? JSON.parse(await fs.readFile(filename, "utf8"))
: { schemaVersion: 1, plan, steps: [] };
if (journal.plan.digest !== plan.digest) throw new Error("Upgrade journal belongs to another plan");
if (!journalId) await writeJournal(filename, journal);
for (const node of plan.nodes) {
const root = await location(plan.workbench, node.directory);
if ((await command(root, "git", ["config", "--get", "remote.origin.url"])) !== node.source.repository)
throw new Error("Upgrade remote changed after planning");
let step = journal.steps.find((entry) => entry.directory === node.directory);
if (step?.phase === "published") continue;
if (!step) {
if ((await treeDigest(root)) !== node.treeDigest) throw new Error(`Stale upgrade plan: ${node.directory}`);
const files: StructuralRequest["files"] = [];
for (const file of node.lockFiles) {
const parsed = parseQuixosLockDocument(await fs.readFile(path.join(root, file), "utf8"));
if (!parsed.ok) throw new Error("Invalid lock during upgrade");
const edits = [];
for (const resource of parsed.document.resources) {
const dependency = plan.nodes.find((entry) => sourceKey(entry) === sourceKey(resource));
const published =
dependency &&
journal.steps.find((entry) => entry.directory === dependency.directory && entry.phase === "published");
if (published?.commit && published.commit !== resource.source.commit)
edits.push({
operation: "dependency" as const,
kind: resource.kind,
name: resource.binding,
source: { ...resource.source, commit: published.commit },
});
}
if (parsed.document.kind === "root" && plan.spec.quixos)
edits.push({ operation: "quixos-pin" as const, source: plan.spec.quixos });
if (edits.length) files.push({ file, edits });
}
const structuralPlan = files.length
? await planStructure(
root,
{ kind: node.kind, source: node.source, files },
process.env.QUIXOS_SNAPSHOT_MAP,
)
: undefined;
step = {
directory: node.directory,
phase: "editing",
treeDigest: node.treeDigest,
structuralPlan,
structuralJournal: structuralPlan ? randomUUID() : undefined,
};
journal.steps.push(step);
await writeJournal(filename, journal);
}
if (step.phase === "editing") {
if (step.structuralPlan) await applyStructure(step.structuralPlan, step.structuralJournal);
else if ((await treeDigest(root)) !== step.treeDigest)
throw new Error("Source changed before upgrade editing");
step.treeDigest = await treeDigest(root);
step.phase = "prepared";
await writeJournal(filename, journal);
}
if (step.phase === "refactor") {
const current = await treeDigest(root);
if (current !== step.treeDigest && !options.acceptEdits)
throw new Error("Refactored source requires --accept-edits when resuming");
step.treeDigest = current;
step.phase = "prepared";
await writeJournal(filename, journal);
}
if ((await treeDigest(root)) !== step.treeDigest)
throw new Error(`Source changed during upgrade: ${node.directory}; inspect ${filename}`);
if (step.phase === "prepared") {
try {
await implementation.check(
node,
root,
path.join(directory, `${id}-${node.directory.replaceAll("/", "-")}-${randomUUID()}`),
plan.spec,
);
} catch (error) {
step.phase = "refactor";
await writeJournal(filename, journal);
throw error;
}
if ((await treeDigest(root)) !== step.treeDigest) throw new Error("Source changed while checking");
step.phase = "checked";
await writeJournal(filename, journal);
}
if (step.phase === "checked") {
step.commit = await implementation.snapshot(root);
if ((await treeDigest(root)) !== step.treeDigest)
throw new Error("Publication snapshot changed checked files");
step.phase = "publishing";
await writeJournal(filename, journal);
}
await implementation.publish(root, step.commit!);
step.phase = "published";
await writeJournal(filename, journal);
}
// Keep subsequent automatic upgrades associated with the newly published
// identities, without renaming repositories or changing any selected branch.
// Explicit-spec callers without a managed graph retain the journal as their
// source of revisions instead.
const graphFile = path.join(plan.workbench, ".quixos/resource-graph.json");
let graphText: string | undefined;
try {
graphText = await fs.readFile(graphFile, "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
if (graphText !== undefined) {
const previous = JSON.parse(graphText);
const snapshots = await Promise.all(
previous.resources.map(async (entry: { kind: string; source: Source; directory: string }) => ({
kind: entry.kind,
...entry.source,
directory: await location(plan.workbench, await managedResourcePath(plan.workbench, entry.directory)),
})),
);
for (const node of plan.nodes.filter((entry) => entry.kind !== "workspace")) {
const step = journal.steps.find((entry) => entry.directory === node.directory)!;
if ((await treeDigest(await location(plan.workbench, node.directory))) !== step.treeDigest)
throw new Error("Published source changed before workbench graph refresh");
snapshots.push({
kind: node.kind,
repository: node.source.repository,
commit: step.commit,
directory: path.join(plan.workbench, node.directory),
});
}
const unique = [
...new Map(
snapshots.map((entry: { kind: string; repository: string; commit: string }) => [
JSON.stringify([entry.kind, entry.repository, entry.commit]),
entry,
]),
).values(),
];
const snapshotMap = path.join(directory, `${id}-published-snapshots.json`);
await fs.writeFile(snapshotMap, JSON.stringify({ resources: unique }));
const resolveResource = await createGitCapabilityResolver({
checkoutRoot: path.join(directory, `${id}-graph-resources`),
snapshotMap,
snapshotOnly: true,
});
const rootNode = plan.nodes.find((node) => node.kind === "workspace")!;
if (
(await treeDigest(await location(plan.workbench, rootNode.directory))) !==
journal.steps.find((step) => step.directory === rootNode.directory)!.treeDigest
)
throw new Error("Root source changed before workbench graph refresh");
const compiled = await compileWorkspaceRepository({
rootDirectory: await location(plan.workbench, rootNode.directory),
resolveResource,
});
// Managed repositories need not currently be reachable from the workspace.
// Keep them discoverable/checkpointed until explicitly removed by the user.
const resources = new Map<string, ResolvedCapabilityResource>(
compiled.resources.map((node) => [node.key, node]),
);
for (const node of plan.nodes.filter((entry) => entry.kind !== "workspace")) {
const step = journal.steps.find((entry) => entry.directory === node.directory)!;
const source = { resolver: "git" as const, repository: node.source.repository, commit: step.commit! };
const key = `${node.kind}\0${source.repository}\0${source.commit}`;
if (resources.has(key)) continue;
const directory = await location(plan.workbench, node.directory);
const standalone = await compileCapabilityResourceRepository({
rootDirectory: directory,
kind: node.kind as "package" | "interface" | "build",
source,
resolveResource,
});
for (const dependency of standalone.resources) resources.set(dependency.key, dependency);
resources.set(key, {
key,
kind: node.kind as "package" | "interface" | "build",
source,
directory,
lock: standalone.lock,
resource: standalone.resource,
dependencies: standalone.directResources,
});
}
await writeJournal(graphFile, {
formatVersion: 1,
quixos: compiled.lock.quixos,
directResources: [...compiled.directResources.entries()].map(([bindingKey, node]) => {
const [kind, binding] = bindingKey.split("\0");
return { kind, binding, resourceKey: node.key, directory: path.relative(plan.workbench, node.directory) };
}),
resources: [...resources.values()].map((node) => ({
key: node.key,
kind: node.kind,
source: node.source,
directory: path.relative(plan.workbench, node.directory),
...(node.resource.kind === "build"
? {}
: {
resourceId:
node.resource.kind === "interface"
? node.resource.revision.interfaceId
: node.resource.revision.packageId,
revisionId: node.resource.revision.revisionId,
}),
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
binding,
resourceKey: dependency.key,
})),
})),
});
}
return {
id,
journal: filename,
revisions: journal.steps.map((step) => ({ directory: step.directory, commit: step.commit })),
activated: false,
};
} catch (error) {
throw new Error(`${error instanceof Error ? error.message : String(error)}; upgrade journal ${filename}`, {
cause: error,
});
}
});
};
+5
View File
@@ -0,0 +1,5 @@
/** Observations belong in logs, never in content-addressed compiler outputs. */
export const profile = (phase: string, started: number, details: Record<string, unknown> = {}) => {
if (process.env.QUIXOS_TRACE_PROFILE === "1")
console.error(`[qx-profile] ${JSON.stringify({ phase, ms: Math.round(performance.now() - started), ...details })}`);
};
@@ -0,0 +1,18 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import type { PackageQueryGenerator } from "./assembly.js";
/** The build invoker selects this executable explicitly. Never resolve a
* generator by a capability name or execute a module discovered in user QX. */
export async function loadQueryGenerator(filename?: string): Promise<PackageQueryGenerator | undefined> {
if (!filename) return undefined;
const module = await import(pathToFileURL(path.resolve(filename)).href);
if (typeof module.generatePackageQueries !== "function")
throw new Error("Query generator must export generatePackageQueries");
const generator: PackageQueryGenerator = module.generatePackageQueries;
if (module.completeWorkspace !== undefined) {
if (typeof module.completeWorkspace !== "function") throw new Error("completeWorkspace must be a function");
generator.completeWorkspace = module.completeWorkspace;
}
return generator;
}
+79
View File
@@ -0,0 +1,79 @@
import fs from "node:fs/promises";
import path from "node:path";
import { parse } from "graphql";
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
import type { StructuralRequest } from "./structural-plan.js";
import { parseQx, walkSyntax } from "./source.js";
/** Insert the authored query and its document together; placement never changes implicitly. */
export async function scaffoldQuery(root: string, prefix: string, spec: ScaffoldRecipe): Promise<StructuralRequest> {
if (
!spec.name ||
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) ||
!spec.id ||
!spec.declaration ||
typeof spec.queryDocument !== "string"
)
throw new Error("Query scaffold requires name, id, declaration and queryDocument");
const source = `package Draft id "draft" revision "draft@1" { ${spec.declaration} }`;
const syntax = parseQx(source);
const pkg = syntax.root.children.find((node) => node.kind === "packageResourceDecl");
const queries = [...walkSyntax(syntax.root)].filter((node) => node.kind === "packageQuery");
const query = queries[0];
if (
syntax.diagnostics.length ||
syntax.root.children.length !== 1 ||
!pkg ||
queries.length !== 1 ||
!query ||
pkg.children.filter((node) => node.kind === "packageExport").length !== 1 ||
pkg.children.some((node) => !["identifier", "stringLiteral", "packageExport"].includes(node.kind))
)
throw new Error("Expected one QX query declaration");
const identifier = query.children.find((node) => node.kind === "identifier")!;
const literals = query.children
.filter((node) => node.kind === "stringLiteral")
.map((node) => JSON.parse(source.slice(node.start, node.end)) as string);
const [id, document, operation] = literals;
if (source.slice(identifier.start, identifier.end) !== spec.name || id !== spec.id)
throw new Error("Query declaration name/ID must match its registration");
if (!document || !/^queries\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_-]+\.graphql$/.test(document))
throw new Error("Query document must be a contained queries/*.graphql path");
const graphql = parse(spec.queryDocument);
const operations = graphql.definitions.filter((definition) => definition.kind === "OperationDefinition");
if (operations.length !== 1 || operations[0]!.operation !== "query" || operations[0]!.name?.value !== operation)
throw new Error("GraphQL document must contain the declared named query operation");
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
const existing = parseQx(authored);
const parent = existing.root.children.find((node) => node.kind === "packageResourceDecl");
if (existing.diagnostics.length || !parent) throw new Error("Fix package.qx before adding a query");
for (const node of parent.children.filter((node) => node.kind === "packageExport").flatMap((node) => node.children)) {
const name = node.children.find((child) => child.kind === "identifier");
const literal = node.children.find((child) => child.kind === "stringLiteral");
if (
(name && authored.slice(name.start, name.end) === spec.name) ||
(literal && JSON.parse(authored.slice(literal.start, literal.end)) === spec.id)
)
throw new Error("New query requires a unique name and ID");
}
const parentID = parent.children.find((node) => node.kind === "stringLiteral")!;
return {
kind: "package",
source: spec.source,
resourceRoot: spec.directory,
validation: "syntax",
files: [
{
file: prefix + "package.qx",
edits: [
{
operation: "append",
parent: { kind: "packageResourceDecl", id: JSON.parse(authored.slice(parentID.start, parentID.end)) },
source: spec.declaration,
},
],
},
{ file: prefix + document, create: spec.queryDocument },
],
};
}
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env node
import { writeFile } from "node:fs/promises";
import process from "node:process";
import { bindingSchema } from "../bindings/index.js";
import { compileCapabilityResourceRepository, type ResolvedCapabilityResource } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import { loadQueryGenerator } from "./query-generator.js";
import { loadCheckedResources } from "./checked-resources.js";
const usage = `usage: quixos-resource-compile --root DIRECTORY --kind interface|package|build
--repository URL --commit GIT_REV --checkout-root DIRECTORY
[--snapshot-map PATH] [--snapshot-only true] [--graph-out PATH] [--schema-out PATH]
[--query-generator BUILD_MODULE_PATH] [--checked-resources NIX_MAP]
Resolves a standalone resource's recursive dependency-lock graph, validates
its interface.qx or package.qx manifest, and emits the checked resource as JSON.`;
const parseArgs = (args: string[]) => {
const values = new Map<string, string>();
for (let index = 0; index < args.length; index += 2) {
const key = args[index];
const value = args[index + 1];
if (!key?.startsWith("--") || !value) throw new Error(usage);
if (
![
"--root",
"--kind",
"--repository",
"--commit",
"--checkout-root",
"--snapshot-map",
"--snapshot-only",
"--graph-out",
"--schema-out",
"--query-generator",
"--checked-resources",
].includes(key) ||
values.has(key)
)
throw new Error(usage);
values.set(key, value);
}
const rootDirectory = values.get("--root");
const kind = values.get("--kind");
const repository = values.get("--repository");
const commit = values.get("--commit")?.toLowerCase();
const checkoutRoot = values.get("--checkout-root");
if (
!rootDirectory ||
!repository ||
!commit ||
!checkoutRoot ||
(kind !== "interface" && kind !== "package" && kind !== "build")
) {
throw new Error(usage);
}
if (!/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(commit)) {
throw new Error("--commit must be a full Git object ID");
}
if (values.has("--snapshot-only") && values.get("--snapshot-only") !== "true")
throw new Error("--snapshot-only accepts true");
return {
rootDirectory,
kind,
repository,
commit,
checkoutRoot,
snapshotMap: values.get("--snapshot-map"),
snapshotOnly: values.get("--snapshot-only") === "true",
graphOut: values.get("--graph-out"),
schemaOut: values.get("--schema-out"),
queryGenerator: values.get("--query-generator"),
checkedResources: values.get("--checked-resources"),
} as const;
};
const graphEntry = (node: ResolvedCapabilityResource) => ({
key: node.key,
kind: node.kind,
source: node.source,
directory: node.directory,
...(node.resource.kind === "build"
? {}
: {
resourceId:
node.resource.kind === "interface" ? node.resource.revision.interfaceId : node.resource.revision.packageId,
revisionId: node.resource.revision.revisionId,
}),
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
binding,
resourceKey: dependency.key,
})),
});
const main = async () => {
if (process.argv.includes("--help") || process.argv.includes("-h")) {
process.stdout.write(`${usage}\n`);
return;
}
const options = parseArgs(process.argv.slice(2));
const resolveResource = await createGitCapabilityResolver({
checkoutRoot: options.checkoutRoot,
snapshotMap: options.snapshotMap,
snapshotOnly: options.snapshotOnly,
});
const compiled = await compileCapabilityResourceRepository({
rootDirectory: options.rootDirectory,
kind: options.kind,
source: {
resolver: "git",
repository: options.repository,
commit: options.commit,
},
resolveResource,
generateQueries: await loadQueryGenerator(options.queryGenerator),
checkedResource: await loadCheckedResources(options.checkedResources),
});
if (options.graphOut) {
await writeFile(
options.graphOut,
`${JSON.stringify(
{
formatVersion: 1,
quixos: compiled.lock.quixos,
root: graphEntry(
compiled.resources.find(
(node) =>
node.source.repository === options.repository &&
node.source.commit === options.commit &&
node.kind === options.kind,
)!,
),
resources: compiled.resources.map(graphEntry),
},
null,
2,
)}\n`,
);
}
if (options.schemaOut) await writeFile(options.schemaOut, `${JSON.stringify(bindingSchema(compiled), null, 2)}\n`);
process.stdout.write(`${JSON.stringify(compiled.resource, null, 2)}\n`);
};
main().catch((error: unknown) => {
process.stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
process.exitCode = 1;
});
@@ -0,0 +1,65 @@
import type { GitSource } from "../resource-lock/index.js";
import type { StructuralRequest } from "./structural-plan.js";
import { executionScaffold, isExecutionScaffold } from "./execution-scaffold.js";
import { scaffoldController } from "./controller-scaffold.js";
import { scaffoldComponent } from "./component-scaffold.js";
import { scaffoldQuery } from "./query-scaffold.js";
type Source = { repository: string; commit: string };
export type ScaffoldRecipe = {
template?: "typescript-execution" | "typescript-component";
source: Source;
directory?: string;
name?: string;
id?: string;
revision?: string;
declaration?: string;
queryDocument?: string;
initialFiles?: Record<string, string>;
controller?: { registry?: string; subject: string; snapshot: string; remote: string; input: "unit" | "value" };
tools?: { quixos: Source; protocol: Source; helpers: Source; sdk?: Source };
};
const source = (value: Source): GitSource => {
if (!value || typeof value.repository !== "string" || typeof value.commit !== "string")
throw Error("Scaffold requires an exact source identity; use a registered repository");
const url = new URL(value.repository);
if (
url.protocol !== "https:" ||
url.username ||
url.password ||
url.search ||
url.hash ||
!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(value.commit)
)
throw Error("Scaffolds require credential-free HTTPS sources and full exact commits");
return { resolver: "git", ...value };
};
const nixSource = (value: Source) =>
`git+${source(value).repository}?ref=refs/tags/quixos-reachability/${value.commit}&rev=${value.commit}`;
/** One-shot edits; structural planning retains interrupted work and authored files. */
export async function scaffoldRecipe(
root: string,
command: "package" | "function" | "component" | "controller" | "query" | "migration" | "refresh",
spec: ScaffoldRecipe,
): Promise<StructuralRequest> {
if (command === "refresh")
throw Error(
"Scaffold refresh is removed. Edit authored code and run check; add entries with scaffold function/component/query.",
);
if (command === "migration")
throw Error(
"Old-workspace migration scaffolding is retired. Current disposable workspaces do not require migration code.",
);
source(spec.source);
if (spec.directory && !/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(spec.directory))
throw Error("Scaffold directory must be contained");
if (spec.template && !["typescript-execution", "typescript-component"].includes(spec.template))
throw Error("Use typescript-execution or typescript-component; old server/source-string presets are retired");
const prefix = spec.directory ? spec.directory + "/" : "";
if (command !== "package" && !(await isExecutionScaffold(root, prefix)))
throw Error("Scaffolding requires a checked execution package");
if (spec.tools) Object.values(spec.tools).forEach(source);
if (command === "controller") return scaffoldController(root, prefix, spec);
if (command === "component") return scaffoldComponent(root, prefix, spec);
if (command === "query") return scaffoldQuery(root, prefix, spec);
return executionScaffold(root, command, spec, prefix, nixSource);
}
+82
View File
@@ -0,0 +1,82 @@
import { lstat, open, rename, unlink } from "node:fs/promises";
import path from "node:path";
import { randomUUID } from "node:crypto";
import { addWorkspaceImport } from "./source.js";
import { readQxSource } from "./source-loader.js";
import { compileWorkspaceRepository, type CapabilityRepositoryResolver } from "./assembly.js";
export const planAtomScaffold = (workspace: string, name: string, id: string) => {
if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) throw new Error("Atom name must be PascalCase");
if (!id.trim()) throw new Error("Atom ID must not be empty");
const fileName = `${name}.qx`;
return {
before: workspace,
workspace: addWorkspaceImport(workspace, fileName),
fileName,
fragment: `fragment {\n atom ${name} id ${JSON.stringify(id)};\n}\n`,
};
};
/** Validate an in-memory proposal before creating files. Never replace a fragment. */
export const scaffoldAtom = async (options: {
root: string;
name: string;
id: string;
write: boolean;
resolveResource: CapabilityRepositoryResolver;
}) => {
const before = await readQxSource(options.root, "workspace.qx");
const plan = planAtomScaffold(before, options.name, options.id);
const fragmentPath = path.join(options.root, plan.fileName);
try {
await lstat(fragmentPath);
throw new Error(`Scaffold target already exists: ${plan.fileName}`);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
const observed = new Map<string, string>();
await compileWorkspaceRepository({
rootDirectory: options.root,
resolveResource: options.resolveResource,
readSource: async (name) => {
if (name === "workspace.qx") return plan.workspace;
if (name === plan.fileName) return plan.fragment;
const text = await readQxSource(options.root, name);
observed.set(name, text);
return text;
},
});
if (!options.write) return plan;
const workspacePath = path.join(options.root, "workspace.qx");
const temporary = path.join(options.root, `.qx-scaffold-${randomUUID()}.tmp`);
let createdFragment = false;
let createdTemporary = false;
try {
const fragment = await open(fragmentPath, "wx");
createdFragment = true;
try {
await fragment.writeFile(plan.fragment);
} finally {
await fragment.close();
}
const file = await open(temporary, "wx", (await lstat(workspacePath)).mode);
createdTemporary = true;
try {
await file.writeFile(plan.workspace);
} finally {
await file.close();
}
observed.set("workspace.qx", before);
for (const [name, text] of observed) {
if ((await readQxSource(options.root, name)) !== text)
throw new Error(`QX source changed during scaffolding: ${name}`);
}
await rename(temporary, workspacePath);
createdTemporary = false;
createdFragment = false;
} finally {
if (createdTemporary) await unlink(temporary);
if (createdFragment) await unlink(fragmentPath);
}
return plan;
};
+117
View File
@@ -0,0 +1,117 @@
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
import { lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { retentionTagForCommit } from "../resource-lock/index.js";
import { materializeWorkspaceSources, workspaceSourcePin, type WorkspaceSourcePin } from "./source-closure.js";
const exec = promisify(callback);
type SourceBackup = {
formatVersion: 1;
root: WorkspaceSourcePin;
repositories: Array<{ repository: string; commits: string[]; bundle: string; sha256: string }>;
};
async function hash(filename: string) {
const digest = createHash("sha256");
for await (const chunk of createReadStream(filename)) digest.update(chunk);
return digest.digest("hex");
}
/** Source-only backup: no Graph state, checkout instructions or credentials.
* Git bundles retain exact commits and their ancestors without needing a forge.
* Storage/retention of the resulting directory belongs to the operator. */
export async function backupWorkspaceSources(options: {
source: WorkspaceSourcePin;
directory: string;
output: string;
locations?: Record<string, string>;
}): Promise<SourceBackup> {
const output = path.resolve(options.output);
if (
await lstat(output).catch((error: NodeJS.ErrnoException) => {
if (error.code !== "ENOENT") throw error;
return null;
})
)
throw Error("Source backup destination must not exist");
const closure = await materializeWorkspaceSources(options);
const grouped = new Map<string, Map<string, string>>();
for (const entry of closure.resources) {
const commits = grouped.get(entry.repository) ?? new Map<string, string>();
commits.set(entry.commit, path.join(path.resolve(options.directory), entry.directory));
grouped.set(entry.repository, commits);
}
await mkdir(path.dirname(output), { recursive: true });
const staging = await mkdtemp(path.join(path.dirname(output), ".source-backup-"));
const manifest: SourceBackup = { formatVersion: 1, root: closure.root, repositories: [] };
try {
for (const [repository, commits] of [...grouped].sort(([a], [b]) => a.localeCompare(b))) {
const bundle = createHash("sha256").update(repository).digest("hex") + ".bundle";
const gitDirectory = await mkdtemp(path.join(staging, ".git-"));
try {
await exec("git", ["init", "--bare", gitDirectory]);
const refs: string[] = [];
for (const [commit, checkout] of commits) {
const ref = retentionTagForCommit(commit);
await exec("git", ["-C", gitDirectory, "fetch", "--no-tags", "--", checkout, `${ref}:${ref}`]);
refs.push(ref);
}
await exec("git", ["-C", gitDirectory, "bundle", "create", path.join(staging, bundle), ...refs]);
} finally {
await rm(gitDirectory, { recursive: true, force: true });
}
manifest.repositories.push({
repository,
commits: [...commits.keys()].sort(),
bundle,
sha256: await hash(path.join(staging, bundle)),
});
}
await writeFile(path.join(staging, "backup.json"), JSON.stringify(manifest, null, 2) + "\n", { mode: 0o600 });
await rename(staging, output);
return manifest;
} finally {
await rm(staging, { recursive: true, force: true });
}
}
export async function restoreWorkspaceSources(options: { backup: string; directory: string }) {
const backup = path.resolve(options.backup);
const filename = path.join(backup, "backup.json");
const info = await lstat(filename);
if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) throw Error("Invalid source backup manifest");
const manifest = JSON.parse(await readFile(filename, "utf8")) as SourceBackup;
if (
manifest.formatVersion !== 1 ||
!Array.isArray(manifest.repositories) ||
!manifest.repositories.length ||
manifest.repositories.length > 256
)
throw Error("Invalid source backup manifest");
const root = workspaceSourcePin(manifest.root);
const locations: Record<string, string> = {};
for (const entry of manifest.repositories) {
if (!entry || !Array.isArray(entry.commits) || !entry.commits.length || entry.commits.length > 256)
throw Error("Invalid source backup entry");
for (const commit of entry.commits) workspaceSourcePin({ repository: entry.repository, commit });
if (Object.hasOwn(locations, entry.repository)) throw Error("Duplicate source backup repository");
const expected = createHash("sha256").update(entry.repository).digest("hex") + ".bundle";
if (entry.bundle !== expected || !/^[a-f0-9]{64}$/.test(entry.sha256))
throw Error("Invalid source backup bundle identity");
const bundle = path.join(backup, expected);
const stat = await lstat(bundle);
if (!stat.isFile() || stat.isSymbolicLink() || (await hash(bundle)) !== entry.sha256)
throw Error("Source backup bundle integrity mismatch");
locations[entry.repository] = pathToFileURL(bundle).href;
}
if (
!manifest.repositories.some((entry) => entry.repository === root.repository && entry.commits.includes(root.commit))
)
throw Error("Source backup does not retain its root commit");
// Every discovered dependency must use the backup. Never fall through to the
// original forge if a manifest omits a required repository.
return materializeWorkspaceSources({ source: root, directory: options.directory, locations, requireLocations: true });
}
@@ -0,0 +1,18 @@
import { buildImmutableCandidate } from "./checked-build.js";
// Installed platform driver, run only inside the sealed checker's unprivileged
// evaluator. Authored flakes never become the outer evaluator program.
const [repository, commit, log, workspaceId] = process.argv.slice(2);
if (!repository || !commit || !log || !workspaceId || process.argv.length !== 6)
throw Error("source-check-driver REPOSITORY COMMIT LOG WORKSPACE_ID");
process.stdout.write(
(await buildImmutableCandidate(
{ repository, commit },
"workspace",
log,
false,
undefined,
workspaceId,
process.env.QUIXOS_CHECK_PLATFORM_SOURCE ? JSON.parse(process.env.QUIXOS_CHECK_PLATFORM_SOURCE) : undefined,
)) + "\n",
);
+176
View File
@@ -0,0 +1,176 @@
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { createHash, randomUUID } from "node:crypto";
import { mkdir, mkdtemp, lstat, realpath, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadQuixosLock, retentionTagForCommit } from "../resource-lock/index.js";
const exec = promisify(callback);
import { workspaceSourcePin, type WorkspaceSourcePin } from "./source-pin.js";
export { workspaceSourcePin, type WorkspaceSourcePin } from "./source-pin.js";
export type SourceClosureEntry = WorkspaceSourcePin & {
kind: "workspace" | "interface" | "package" | "build";
directory: string;
};
export type WorkspaceSourceClosure = {
formatVersion: 1;
root: WorkspaceSourcePin;
resources: SourceClosureEntry[];
};
/** Trusted transport substitutions preserve source identity. A mirror or bundle
* is explicitly selected, never tried automatically after an origin failure.
* Git authentication stays in the caller's Git configuration, outside metadata.
*/
export async function materializeWorkspaceSources(options: {
source: WorkspaceSourcePin;
directory: string;
locations?: Record<string, string>;
concurrency?: number;
maxRepositories?: number;
requireLocations?: boolean;
/** Optional acquisition boundary, enforced before fetching each dependency.
* It limits canonical identity; explicit transport substitutions remain a
* separate trusted operator decision. */
repositoryPrefix?: string;
}): Promise<WorkspaceSourceClosure> {
const source = workspaceSourcePin(options.source);
const prefix = options.repositoryPrefix ? new URL(options.repositoryPrefix) : null;
if (
prefix &&
(prefix.protocol !== "https:" ||
prefix.username ||
prefix.password ||
prefix.search ||
prefix.hash ||
!prefix.pathname.endsWith("/") ||
/[%\\\\]/.test(prefix.pathname))
)
throw Error("Source repository prefix requires a credential-free HTTPS directory URL");
const concurrency = options.concurrency ?? 4,
maxRepositories = options.maxRepositories ?? 256;
if (
!Number.isInteger(concurrency) ||
concurrency < 1 ||
concurrency > 16 ||
!Number.isInteger(maxRepositories) ||
maxRepositories < 1 ||
maxRepositories > 4096
)
throw Error("Invalid source closure limits");
const locations = new Map(Object.entries(options.locations ?? {}));
for (const [canonical, location] of locations) {
workspaceSourcePin({ repository: canonical, commit: source.commit });
const url = new URL(location);
if (
!["https:", "file:"].includes(url.protocol) ||
url.username ||
url.password ||
url.search ||
url.hash ||
(url.protocol === "file:" && url.hostname !== "")
)
throw Error("Invalid source transport substitution");
}
await mkdir(options.directory, { recursive: true, mode: 0o700 });
const directory = await realpath(options.directory);
const entries = new Map<string, SourceClosureEntry>();
const pending: SourceClosureEntry[] = [];
const enqueue = (pin: WorkspaceSourcePin, kind: SourceClosureEntry["kind"]) => {
pin = workspaceSourcePin(pin);
const repository = new URL(pin.repository);
if (
prefix &&
(repository.origin !== prefix.origin ||
!repository.pathname.startsWith(prefix.pathname) ||
/[%\\\\]/.test(repository.pathname))
)
throw Error(`Source repository is outside the acquisition boundary: ${pin.repository}`);
if (options.requireLocations && !locations.has(pin.repository))
throw Error(`Source backup omits required repository: ${pin.repository}`);
const key = `${kind}\0${pin.repository}\0${pin.commit}`;
if (entries.has(key)) return;
if (entries.size >= maxRepositories) throw Error("Workspace source closure exceeds repository limit");
const name = `${kind}-${createHash("sha256").update(key).digest("hex").slice(0, 24)}`;
const entry = { ...pin, kind, directory: name };
entries.set(key, entry);
pending.push(entry);
};
enqueue(source, "workspace");
const git = async (cwd: string, args: string[]) =>
(
await exec("git", args, {
cwd,
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
maxBuffer: 4 * 1024 * 1024,
})
).stdout.trim();
const checkout = async (entry: SourceClosureEntry) => {
const destination = path.join(directory, entry.directory);
const verify = async () => {
const info = await lstat(destination);
if (!info.isDirectory() || info.isSymbolicLink() || (await realpath(destination)) !== destination)
throw Error("Source checkout must be a contained directory");
if ((await git(destination, ["rev-parse", "HEAD"])) !== entry.commit)
throw Error(`Source commit mismatch: ${entry.repository}`);
if (await git(destination, ["status", "--porcelain", "--untracked-files=all"]))
throw Error(`Source checkout was modified: ${entry.repository}`);
};
const existing = await lstat(destination).catch((error: NodeJS.ErrnoException) => {
if (error.code !== "ENOENT") throw error;
return null;
});
if (!existing) {
const staging = await mkdtemp(path.join(directory, ".fetch-"));
try {
await git(staging, ["init"]);
await git(staging, ["remote", "add", "origin", entry.repository]);
const ref = retentionTagForCommit(entry.commit);
// Full history supports normal jj development and independent bundles.
const location = locations.get(entry.repository) ?? entry.repository;
await git(staging, [
"fetch",
"--no-tags",
"--",
location.startsWith("file:") ? fileURLToPath(location) : location,
`${ref}:${ref}`,
]);
await git(staging, ["checkout", "--detach", entry.commit]);
try {
await rename(staging, destination);
} catch (error) {
if (!["EEXIST", "ENOTEMPTY"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error;
}
} finally {
await rm(staging, { recursive: true, force: true });
}
}
await verify();
const lock = await loadQuixosLock(path.join(destination, "quixos.lock"));
if (!lock.ok)
throw Error(`Invalid source lock in ${entry.repository}: ${lock.diagnostics.map((d) => d.message).join("; ")}`);
return lock.lock.resources;
};
// Bounded breadth-first batches resolve diamonds once and avoid recursive
// promise deadlocks. Completion order cannot affect the recorded closure.
while (pending.length) {
const batch = pending.splice(0, concurrency);
const results = await Promise.allSettled(batch.map(checkout));
for (const result of results) {
if (result.status === "rejected") throw result.reason;
for (const resource of result.value) enqueue(resource.source, resource.kind);
}
}
const closure: WorkspaceSourceClosure = {
formatVersion: 1,
root: source,
resources: [...entries.values()].sort((a, b) =>
`${a.kind}:${a.repository}@${a.commit}`.localeCompare(`${b.kind}:${b.repository}@${b.commit}`),
),
};
const temporary = path.join(directory, `.closure-${randomUUID()}.json`);
await writeFile(temporary, JSON.stringify(closure, null, 2) + "\n", { flag: "wx", mode: 0o600 });
await rename(temporary, path.join(directory, "sources.json"));
return closure;
}
+106
View File
@@ -0,0 +1,106 @@
import { lstat, readFile } from "node:fs/promises";
import path from "node:path";
import { parseQx, validateQxImportPath, walkSyntax } from "./source.js";
export const readQxSource = async (root: string, name: string) => {
validateQxImportPath(name);
return readRepositorySource(root, name);
};
/** Refuse symlinks at every component, including the final source file. */
export const readRepositorySource = async (root: string, name: string) => {
if (!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$/.test(name))
throw new Error(`Invalid repository-relative source path: ${name}`);
let current = path.resolve(root);
const segments = name.split("/");
for (const [index, segment] of segments.entries()) {
current = path.join(current, segment);
const stat = await lstat(current);
if (stat.isSymbolicLink() || (index === segments.length - 1 ? !stat.isFile() : !stat.isDirectory()))
throw new Error(`Sources must be ordinary files beneath ordinary directories: ${name}`);
}
return readFile(current, "utf8");
};
/** Local imports share one workspace scope; paths are always repository-relative. */
export const resolveQxSources = async (read: (name: string) => Promise<string>, entry = "workspace.qx") => {
const files = new Map<string, string>();
const active: string[] = [];
const visited = new Set<string>();
const segments: { start: number; end: number; fileName: string; sourceStart: number }[] = [];
let source = "";
const append = (fileName: string, text: string, sourceStart: number) => {
segments.push({ start: source.length, end: source.length + text.length, fileName, sourceStart });
source += text;
};
const visit = async (name: string, root: boolean) => {
validateQxImportPath(name);
if (active.includes(name)) throw new Error(`QX import cycle: ${[...active, name].join(" -> ")}`);
if (visited.has(name)) return;
const text = await read(name);
const syntax = parseQx(text, name);
if (syntax.diagnostics.length)
throw new Error(syntax.diagnostics.map((d) => `${name}:${d.line}:${d.column + 1}: ${d.message}`).join("\n"));
const declaration = syntax.root.children[0]!;
if (declaration.kind !== (root ? "workspaceDecl" : "fragmentDecl"))
throw new Error(`${name}: expected ${root ? "workspace" : "fragment"} document`);
files.set(name, text);
active.push(name);
visited.add(name);
const braces = syntax.tokens.filter((token) => !token.trivia);
let cursor = root ? 0 : braces.find((token) => token.kind === "LBRACE")!.end;
const end = root ? text.length : braces.filter((token) => token.kind === "RBRACE").at(-1)!.start;
for (const node of walkSyntax(declaration)) {
if (node.kind !== "sourceImportDecl") continue;
append(name, text.slice(cursor, node.start), cursor);
const literal = node.children.find((child) => child.kind === "stringLiteral")!;
// Separators prevent adjacent tokens/comments joining across files.
append(name, "\n", node.start);
await visit(JSON.parse(text.slice(literal.start, literal.end)), false);
append(name, "\n", node.end);
cursor = node.end;
}
append(name, text.slice(cursor, end), cursor);
active.pop();
};
await visit(entry, true);
return {
source,
sourceFiles: [...files.keys()],
files,
originalPosition(line: number, column: number) {
const lines = source.split("\n");
const offset =
lines.slice(0, line - 1).reduce((sum, value) => sum + value.length + 1, 0) +
[...(lines[line - 1] ?? "")].slice(0, column).join("").length;
const segment = segments.find((entry) => entry.start <= offset && entry.end > offset);
if (!segment) return { fileName: entry, line, column };
const prefix = files.get(segment.fileName)!.slice(0, segment.sourceStart + offset - segment.start);
return {
fileName: segment.fileName,
line: prefix.split("\n").length,
column: prefix.length - prefix.lastIndexOf("\n") - 1,
};
},
};
};
export const loadQxSources = (root: string) => resolveQxSources((name) => readQxSource(root, name));
/** The same validated closure used by compilation, for installation and pinning. */
export const qxSourceManifest = async (root: string) => {
const resolved = await loadQxSources(root);
const syntax = parseQx(resolved.source);
const imports = [...walkSyntax(syntax.root)]
.filter((node) => node.kind === "resourceImportDecl")
.map((node) => {
const name = node.children.find((child) => child.kind === "identifier")!;
const kind = syntax.tokens.some(
(token) => token.start >= node.start && token.end <= node.end && token.kind === "INTERFACE",
)
? ("interface" as const)
: ("package" as const);
return { kind, binding: resolved.source.slice(name.start, name.end) };
});
return { sourceFiles: resolved.sourceFiles, imports };
};
+48
View File
@@ -0,0 +1,48 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { loadQuixosLock, parseQuixosLockDocument, resolveQuixosLock } from "../resource-lock/index.js";
import { editStructure } from "./structural-edits.js";
import { workspaceSourcePin, type WorkspaceSourcePin } from "./source-closure.js";
/** Inspect/rewrite the root and its declared imports only, using the same lock
* grammar as compilation. Return proposed file contents; publishing them and
* selecting a Graph remain the caller's separate responsibilities. */
export async function workspaceSourceLocks(directory: string, input: unknown = []) {
if (!Array.isArray(input) || input.length > 256) throw Error("Invalid source lock replacements");
const replacements = new Map<string, WorkspaceSourcePin>();
const key = (source: WorkspaceSourcePin) => `${source.repository}\0${source.commit}`;
for (const entry of input) {
if (!entry || typeof entry !== "object") throw Error("Invalid source lock replacement");
const from = workspaceSourcePin(entry.from),
to = workspaceSourcePin(entry.to);
if (replacements.has(key(from))) throw Error("Duplicate source lock replacement");
replacements.set(key(from), to);
}
const lock = await loadQuixosLock(path.join(directory, "quixos.lock"));
if (!lock.ok) throw Error(`Invalid source lock: ${lock.diagnostics.map((d) => d.message).join("; ")}`);
const files = new Map<string, string>();
for (const filename of lock.lock.sourceFiles!) {
let source = await readFile(path.join(directory, filename), "utf8");
const parsed = parseQuixosLockDocument(source, filename);
if (!parsed.ok) throw Error("Source lock changed during inspection");
for (const dependency of parsed.document.resources) {
const replacement = replacements.get(key(dependency.source));
if (replacement)
source = editStructure(source, {
operation: "dependency",
kind: dependency.kind,
name: dependency.binding,
source: replacement,
});
}
files.set(filename, source);
}
const checked = await resolveQuixosLock(files.get("quixos.lock")!, async (filename) => {
const source = files.get(filename);
if (source === undefined) throw Error("Source lock import changed during inspection");
return source;
});
if (!checked.ok)
throw Error(`Invalid rewritten source locks: ${checked.diagnostics.map((d) => d.message).join("; ")}`);
return { lock: checked.lock, files: [...files].map(([path, source]) => ({ path, source })) };
}
+18
View File
@@ -0,0 +1,18 @@
export type WorkspaceSourcePin = { repository: string; commit: string };
export function workspaceSourcePin(value: unknown): WorkspaceSourcePin {
if (!value || typeof value !== "object" || Array.isArray(value)) throw Error("Invalid Workspace source pin");
const pin = value as WorkspaceSourcePin;
const url = new URL(pin.repository);
if (
url.protocol !== "https:" ||
url.username ||
url.password ||
url.search ||
url.hash ||
typeof pin.commit !== "string" ||
!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(pin.commit)
)
throw Error("Workspace source pins require credential-free HTTPS and an exact commit");
return { repository: pin.repository, commit: pin.commit };
}
+156
View File
@@ -0,0 +1,156 @@
import { ParserRuleContext } from "antlr4ng";
import { parseDocument } from "./parser.js";
import { QuixosCapabilityParser } from "./generated/QuixosCapabilityParser.js";
/** Offsets and columns use UTF-16, as do JavaScript and LSP. End is exclusive. */
export type SourceRange = { start: number; end: number };
export type SourceEdit = SourceRange & { text: string };
export type SyntaxNode = SourceRange & { kind: string; children: SyntaxNode[] };
export const parseQx = (source: string, fileName = "<memory>") => {
const parsed = parseDocument(source, fileName);
// ANTLR indexes Unicode code points; editors index UTF-16 code units.
const offsets = [0];
for (const character of source) offsets.push(offsets[offsets.length - 1]! + character.length);
const offset = (index: number) => offsets[Math.max(0, index)] ?? source.length;
const node = (context: ParserRuleContext): SyntaxNode => ({
kind: QuixosCapabilityParser.ruleNames[context.ruleIndex]!,
start: offset(context.start?.start ?? 0),
end: offset((context.stop?.stop ?? -1) + 1),
children: context.children.flatMap((child) => (child instanceof ParserRuleContext ? [node(child)] : [])),
});
return {
source,
fileName,
root: node(parsed.tree),
diagnostics: parsed.diagnostics.map((diagnostic) => {
const line = source.split("\n")[diagnostic.line - 1] ?? "";
return { ...diagnostic, column: [...line].slice(0, diagnostic.column).join("").length };
}),
tokens: parsed.tokens
.getTokens()
.filter((token) => token.type !== -1)
.map((token) => ({
kind: QuixosCapabilityParser.symbolicNames[token.type] ?? "token",
start: offset(token.start),
end: offset(token.stop + 1),
text: token.text ?? "",
trivia: token.channel !== 0,
})),
};
};
/** Edits refer to one immutable source snapshot; overlapping edits are errors. */
export const applySourceEdits = (source: string, edits: readonly SourceEdit[]) => {
const sorted = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
let end = 0;
let previousStart = -1;
let result = "";
for (const edit of sorted) {
if (
!Number.isInteger(edit.start) ||
!Number.isInteger(edit.end) ||
edit.start < end ||
edit.start === previousStart ||
edit.end < edit.start ||
edit.end > source.length
) {
throw new Error("Invalid or overlapping source edits");
}
result += source.slice(end, edit.start) + edit.text;
end = edit.end;
previousStart = edit.start;
}
return result + source.slice(end);
};
export const walkSyntax = function* (node: SyntaxNode): Generator<SyntaxNode> {
yield node;
for (const child of node.children) yield* walkSyntax(child);
};
export const lintQx = (source: string, fileName = "<memory>") => {
const syntax = parseQx(source, fileName);
const diagnostics = syntax.diagnostics.map((entry) => ({ ...entry, severity: "error" as "error" | "warning" }));
if (diagnostics.length) return diagnostics;
const seen = new Set<string>();
for (const node of walkSyntax(syntax.root)) {
if (node.kind !== "sourceImportDecl") continue;
const literal = node.children.find((child) => child.kind === "stringLiteral")!;
const importPath: string = JSON.parse(source.slice(literal.start, literal.end));
let message: string | undefined;
let code = "invalid-source-import";
try {
validateQxImportPath(importPath);
} catch (error) {
message = (error as Error).message;
}
if (!message && seen.has(importPath)) {
code = "duplicate-source-import";
message = `Repeated local import ${importPath}`;
}
seen.add(importPath);
if (message) {
const prefix = source.slice(0, node.start);
diagnostics.push({
phase: "syntax",
code,
message,
fileName,
line: prefix.split("\n").length,
column: prefix.length - prefix.lastIndexOf("\n") - 1,
severity: code === "duplicate-source-import" ? "warning" : "error",
});
}
}
return diagnostics;
};
/** Conservative formatter: indentation only, preserving strings and comments verbatim. */
export const formatQx = (source: string) => {
const syntax = parseQx(source);
if (syntax.diagnostics.length) throw new Error("Cannot format QX with syntax errors");
const edits: SourceEdit[] = [];
let depth = 0;
let lineStart = 0;
for (const token of syntax.tokens) {
if (token.kind === "WS") continue;
lineStart = source.lastIndexOf("\n", token.start - 1) + 1;
if (/^[ \t]*$/.test(source.slice(lineStart, token.start))) {
const indentation = " ".repeat(Math.max(0, depth - (token.kind === "RBRACE" ? 1 : 0)));
if (source.slice(lineStart, token.start) !== indentation)
edits.push({ start: lineStart, end: token.start, text: indentation });
}
if (!token.trivia) {
if (token.kind === "LBRACE") depth++;
if (token.kind === "RBRACE") depth--;
}
}
return applySourceEdits(source, edits);
};
export const addWorkspaceImport = (source: string, importPath: string) => {
validateQxImportPath(importPath);
const syntax = parseQx(source);
if (syntax.diagnostics.length || syntax.root.children[0]?.kind !== "workspaceDecl")
throw new Error("Expected a syntactically valid workspace");
for (const node of walkSyntax(syntax.root)) {
if (node.kind === "sourceImportDecl") {
const literal = node.children.find((child) => child.kind === "stringLiteral")!;
if (JSON.parse(source.slice(literal.start, literal.end)) === importPath) return source;
}
}
const brace = syntax.tokens.find((token) => token.kind === "LBRACE")!;
const newline = source.includes("\r\n") ? "\r\n" : "\n";
return applySourceEdits(source, [
{ start: brace.end, end: brace.end, text: `${newline} import ${JSON.stringify(importPath)};` },
]);
};
export const validateQxImportPath = (value: string) => {
if (
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*\.qx$/.test(value) ||
value.split("/").some((part) => part === "." || part === "..")
)
throw new Error(`Invalid repository-relative QX import path: ${value}`);
};
+266
View File
@@ -0,0 +1,266 @@
import { CharStream, CommonTokenStream } from "antlr4ng";
import { QuixosLockLexer } from "../resource-lock/generated/QuixosLockLexer.js";
import { QuixosLockParser } from "../resource-lock/generated/QuixosLockParser.js";
import { parseQuixosLockDocument } from "../resource-lock/parser.js";
import { parseQx, walkSyntax, applySourceEdits, type SyntaxNode } from "./source.js";
export type StructuralSelector = { kind: string; id?: string; name?: string; names?: string[] };
export type StructuralEdit =
| { operation: "append"; parent: StructuralSelector; source: string }
| { operation: "replace"; target: StructuralSelector; source: string }
| { operation: "remove"; target: StructuralSelector }
| { operation: "import"; kind: "interface" | "package"; name: string }
| { operation: "semantic-major"; target: StructuralSelector; major: number }
| { operation: "conformance-id"; target: StructuralSelector; id: string }
| { operation: "quixos-pin"; source: { repository: string; commit: string } }
| {
operation: "dependency";
kind: "interface" | "package" | "build";
name: string;
source: { repository: string; commit: string } | null;
};
// Deliberately exclude valueType/identifier/stringLiteral: callers operate on
// declaration structure, not arbitrary token offsets or lockfile text patches.
const selectable = new Set([
"workspaceDecl",
"fragmentDecl",
"interfaceResourceDecl",
"packageResourceDecl",
"atomDecl",
"valueMember",
"moduleMember",
"moduleExport",
"relationshipMember",
"operationMember",
"packageOperationExport",
"packageFunctionExport",
"packageConstructorExport",
"conformanceDecl",
"stateDecl",
"edgeDecl",
"constructorBindingDecl",
"resourceImportDecl",
"sourceImportDecl",
"operationBindingDecl",
]);
/** Bind only the template root identity; schema/atom identities are reusable.
* The revision's real identity is derived from its containing commit at compile time. */
export const instantiateWorkspaceIdentity = (source: string, workspaceId: string): string => {
if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(workspaceId))
throw new Error("Workspace identity must be a UUID");
const syntax = parseQx(source);
if (syntax.diagnostics.length) throw new Error("Cannot instantiate a malformed template workspace");
const declaration = syntax.root.children.find((node) => node.kind === "workspaceDecl");
const literals = declaration?.children.filter((node) => node.kind === "stringLiteral");
if (!literals || literals.length !== 3) throw new Error("Template must contain a workspace declaration");
return applySourceEdits(source, [
{ ...literals[0], text: JSON.stringify(workspaceId) },
{ ...literals[1], text: JSON.stringify(`workspace-revision:${workspaceId}:source`) },
]);
};
const select = (
source: string,
selector: StructuralSelector,
): { node: SyntaxNode; syntax: ReturnType<typeof parseQx> } => {
if (!selectable.has(selector.kind)) throw new Error(`Unsupported structural selector ${selector.kind}`);
const syntax = parseQx(source);
if (syntax.diagnostics.length) throw new Error("Cannot scaffold syntactically invalid QX");
const matches = [...walkSyntax(syntax.root)].filter((node) => {
if (node.kind !== selector.kind) return false;
if (
selector.name &&
!node.children.some(
(child) => child.kind === "identifier" && source.slice(child.start, child.end) === selector.name,
)
)
return false;
if (
selector.names &&
JSON.stringify(
node.children
.filter((child) => child.kind === "identifier")
.map((child) => source.slice(child.start, child.end)),
) !== JSON.stringify(selector.names)
)
return false;
if (selector.id) {
// Only an explicit ID field counts, not a coincidentally equal revision,
// default value, nested declaration, or comment.
const tokens = syntax.tokens.filter(
(token) => !token.trivia && token.start >= node.start && token.end <= node.end,
);
const literal = node.children.find(
(child) =>
child.kind === "stringLiteral" &&
tokens.some((token, index) => token.start === child.start && tokens[index - 1]?.kind === "ID"),
);
if (!literal || JSON.parse(source.slice(literal.start, literal.end)) !== selector.id) return false;
}
return true;
});
if (matches.length !== 1) throw new Error(`Structural selector must resolve exactly once (found ${matches.length})`);
return { node: matches[0], syntax };
};
/** Comment-preserving structural edits; every result is parsed before returning. */
export const editStructure = (source: string, edit: StructuralEdit): string => {
if (edit.operation === "quixos-pin") {
const parsed = parseQuixosLockDocument(source);
if (!parsed.ok || parsed.document.kind !== "root") throw new Error("Quixos pins belong in a valid root lockfile");
const parser = new QuixosLockParser(new CommonTokenStream(new QuixosLockLexer(CharStream.fromString(source))));
const entries = parser.document().quixosEntry();
if (entries.length !== 1) throw new Error("Expected one Quixos source declaration");
const literals = entries[0].quixosSourceBlock().stringLiteral();
const offsets = [0];
for (const character of source) offsets.push(offsets[offsets.length - 1] + character.length);
const result = applySourceEdits(source, [
{
start: offsets[literals[0].start!.start],
end: offsets[literals[0].stop!.stop + 1],
text: JSON.stringify(edit.source.repository),
},
{
start: offsets[literals[literals.length - 1].start!.start],
end: offsets[literals[literals.length - 1].stop!.stop + 1],
text: JSON.stringify(edit.source.commit),
},
]);
const checked = parseQuixosLockDocument(result);
if (!checked.ok)
throw new Error(`Invalid Quixos pin: ${checked.diagnostics.map((entry) => entry.message).join("; ")}`);
return result;
}
if (edit.operation === "import") {
if (!["interface", "package"].includes(edit.kind) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(edit.name))
throw new Error("Invalid resource import");
const syntax = parseQx(source);
if (syntax.diagnostics.length) throw new Error("Cannot scaffold invalid QX");
const text = `import ${edit.kind} ${edit.name};`;
if (
[...walkSyntax(syntax.root)].some(
(entry) =>
entry.kind === "resourceImportDecl" && source.slice(entry.start, entry.end).replace(/\s+/g, " ") === text,
)
)
return source;
const root = syntax.root.children[0];
const position = ["workspaceDecl", "fragmentDecl"].includes(root.kind)
? syntax.tokens.find((token) => token.kind === "LBRACE")!.end
: root.start;
const result = applySourceEdits(source, [{ start: position, end: position, text: `\n${text}\n` }]);
if (parseQx(result).diagnostics.length) throw new Error("Invalid resource import position");
return result;
}
if (edit.operation === "dependency") {
const parsed = parseQuixosLockDocument(source);
if (!parsed.ok) throw new Error("Cannot scaffold an invalid lockfile");
if (!["interface", "package", "build"].includes(edit.kind) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(edit.name))
throw new Error("Invalid dependency selector");
const parser = new QuixosLockParser(new CommonTokenStream(new QuixosLockLexer(CharStream.fromString(source))));
const tree = parser.document();
const offsets = [0];
for (const character of source) offsets.push(offsets[offsets.length - 1] + character.length);
const entries = tree
.resourceEntry()
.filter((entry) => entry.resourceKind().getText() === edit.kind && entry.identifier().getText() === edit.name);
if (entries.length > 1) throw new Error("Ambiguous dependency selector");
const entry = entries[0];
const replacement = edit.source
? `${edit.kind} ${edit.name} source {\n repository ${JSON.stringify(edit.source.repository)};\n commit ${JSON.stringify(edit.source.commit)};\n}`
: "";
if (!entry && !edit.source) throw new Error("Cannot remove an absent dependency");
const start = entry ? offsets[entry.start!.start] : offsets[tree.RBRACE().symbol.start];
const end = entry ? offsets[entry.stop!.stop + 1] : start;
const result = applySourceEdits(source, [{ start, end, text: entry ? replacement : `${replacement}\n` }]);
const checked = parseQuixosLockDocument(result);
if (!checked.ok)
throw new Error(`Invalid dependency change: ${checked.diagnostics.map((entry) => entry.message).join("; ")}`);
return result;
}
const { node, syntax } = select(source, edit.operation === "append" ? edit.parent : edit.target);
let result: string;
if (edit.operation === "semantic-major") {
if (
!["conformanceDecl", "packageResourceDecl"].includes(node.kind) ||
!Number.isSafeInteger(edit.major) ||
edit.major < 1
)
throw new Error("Semantic major requires a package/conformance and positive integer");
const tokens = syntax.tokens.filter((token) => !token.trivia && token.start >= node.start && token.end <= node.end);
const marker = tokens.findIndex((token) => token.kind === "SEMANTIC_MAJOR");
const value = marker < 0 ? undefined : tokens[marker + 1];
const brace = tokens.find((token) => token.kind === "LBRACE")!;
result = applySourceEdits(source, [
{
start: value?.start ?? brace.start,
end: value?.end ?? brace.start,
text: value ? String(edit.major) : `semantic-major ${edit.major} `,
},
]);
} else if (edit.operation === "conformance-id") {
if (node.kind !== "conformanceDecl" || !edit.id)
throw new Error("Identity enrollment requires a conformance and stable ID");
const existing = node.children.find((entry) => entry.kind === "stringLiteral");
if (existing) {
if (JSON.parse(source.slice(existing.start, existing.end)) !== edit.id)
throw new Error("Cannot change an enrolled conformance identity; create a new conformance explicitly");
return source;
}
const identifiers = node.children.filter((entry) => entry.kind === "identifier");
const targetArguments = node.children.find((entry) => entry.kind === "typeArguments");
const position = targetArguments?.end ?? identifiers[identifiers.length - 1].end;
result = applySourceEdits(source, [{ start: position, end: position, text: ` id ${JSON.stringify(edit.id)}` }]);
} else if (edit.operation === "append") {
const closing = syntax.tokens.find((token) => token.kind === "RBRACE" && token.end === node.end);
if (!closing) throw new Error("Append requires a declaration with a body");
result = applySourceEdits(source, [{ start: closing.start, end: closing.start, text: `\n${edit.source}\n` }]);
} else {
// A resource parser node includes imports/external declarations preceding
// its header. Replacing the declaration must not delete that preamble.
const identifier = node.children.find((child) => child.kind === "identifier");
const header =
["packageResourceDecl", "interfaceResourceDecl"].includes(node.kind) && identifier
? syntax.tokens
.filter(
(token) =>
token.start >= node.start &&
token.end <= identifier.start &&
token.kind === (node.kind === "packageResourceDecl" ? "PACKAGE" : "INTERFACE"),
)
.at(-1)?.start
: undefined;
const wrapper =
edit.operation === "remove" && ["stateDecl", "edgeDecl"].includes(node.kind)
? [...walkSyntax(syntax.root)]
.filter(
(entry) =>
["conformanceItem", "sharedAttachmentDecl"].includes(entry.kind) &&
entry.start <= node.start &&
entry.end >= node.end,
)
.sort((a, b) => a.end - a.start - (b.end - b.start))[0]
: undefined;
result = applySourceEdits(source, [
{
start: wrapper?.start ?? header ?? node.start,
end: wrapper?.end ?? node.end,
text: edit.operation === "replace" ? edit.source : "",
},
]);
}
const checked = parseQx(result);
if (checked.diagnostics.length)
throw new Error(`Invalid structural change: ${checked.diagnostics.map((entry) => entry.message).join("; ")}`);
return result;
};
export const scaffoldResourceSource = (kind: "interface" | "package", name: string, id: string, revision: string) => {
if (!/^[A-Z][A-Za-z0-9]*$/.test(name) || !id || !revision)
throw new Error("Resource scaffold requires a PascalCase name and explicit identities");
const source = `${kind} ${name} id ${JSON.stringify(id)} revision ${JSON.stringify(revision)} {\n}\n`;
if (parseQx(source).diagnostics.length) throw new Error("Invalid resource scaffold");
return source;
};
+327
View File
@@ -0,0 +1,327 @@
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { randomUUID } from "node:crypto";
import { contentDigest } from "../capability-model/evolution.js";
import { editStructure, type StructuralEdit } from "./structural-edits.js";
import { snapshotRepository, localResourceSnapshots } from "./candidate-check.js";
import { compileWorkspaceRepository, compileCapabilityResourceRepository } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import { bindingSchema, generateTypeScriptBindings } from "../bindings/index.js";
import { parseQx } from "./source.js";
import { parseQuixosLockDocument } from "../resource-lock/index.js";
import { withFileLock } from "./file-lock.js";
export type StructuralRequest = {
kind: "workspace" | "interface" | "package" | "build";
source?: { repository: string; commit: string };
resourceRoot?: string;
validation?: "syntax" | "resource-graph";
files: (
| { file: string; edits: StructuralEdit[] }
| { file: string; create: string }
| { file: string; generated: string }
| { file: string; expected: string; replace: string }
)[];
};
type Change = { file: string; before: string | null; after: string; mode: number };
type Journal = { schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[] };
const safeFile = (file: string) => {
if (
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc\.yml|\.yarn\/plugins\/[A-Za-z0-9_-][A-Za-z0-9_.-]*\.cjs|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|graphql|lock|ts|tsx|css|mjs|json|nix|txtpb|md))$/.test(
file,
) ||
file.split("/").some((part) => [".git", ".jj", ".quixos", "node_modules"].includes(part))
)
throw new Error(`Unsafe scaffold path ${file}`);
};
const read = async (root: string, file: string): Promise<string | null> => {
safeFile(file);
const target = path.join(root, file);
try {
const metadata = await fs.lstat(target);
if (!metadata.isFile() || metadata.isSymbolicLink() || !(await fs.realpath(target)).startsWith(`${root}/`))
throw new Error(`Scaffold target is not a contained regular file: ${file}`);
return await fs.readFile(target, "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw error;
}
};
const containedParent = async (root: string, file: string) => {
let current = root;
for (const part of file.split("/").slice(0, -1)) {
current = path.join(current, part);
await fs.mkdir(current).catch((error: NodeJS.ErrnoException) => {
if (error.code !== "EEXIST") throw error;
});
const metadata = await fs.lstat(current);
if (!metadata.isDirectory() || metadata.isSymbolicLink())
throw new Error("Scaffold parent must be a real directory");
}
};
const durableJson = async (file: string, value: unknown) => {
const temporary = `${file}.${randomUUID()}.tmp`;
const handle = await fs.open(temporary, "wx", 0o600);
try {
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`);
await handle.sync();
} finally {
await handle.close();
}
await fs.rename(temporary, file);
const directory = await fs.open(path.dirname(file), "r");
try {
await directory.sync();
} finally {
await directory.close();
}
};
/** Validate the entire edited resource graph in a private snapshot before writes. */
export const planStructure = async (rootPath: string, request: StructuralRequest, snapshotMap?: string) => {
if (request.validation && !["syntax", "resource-graph"].includes(request.validation))
throw new Error("Unknown structural validation mode");
const root = await fs.realpath(rootPath);
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-structure-"));
try {
const snapshot = await snapshotRepository(root, path.join(temporary, "source"));
const observed = await Promise.all(
snapshot.files.map(async ({ name }) => ({
file: name,
digest: contentDigest(await fs.readFile(path.join(snapshot.directory, name), "utf8")),
})),
);
const changes: Change[] = [];
if (!Array.isArray(request.files) || !request.files.length || request.files.length > 100)
throw new Error("Structural plan requires 1–100 files");
for (const input of request.files) {
safeFile(input.file);
if (changes.some((entry) => entry.file === input.file)) throw new Error("Repeated structural file target");
const before = await read(root, input.file);
let after: string;
if ("create" in input) {
if (before !== null || typeof input.create !== "string")
throw new Error("Scaffold creation cannot replace an existing file");
after = input.create;
} else if ("replace" in input) {
if (before !== input.expected || typeof input.replace !== "string")
throw new Error(`Stale imperative edit: ${input.file}`);
after = input.replace;
} else if ("generated" in input) {
const generated = (text: string) =>
text.startsWith("// Generated by qx-scaffold-v1\n") ||
text.startsWith("# Generated by qx-scaffold-v1\n") ||
(() => {
try {
return JSON.parse(text).generatedBy === "qx-scaffold-v1";
} catch {
return false;
}
})();
if (
typeof input.generated !== "string" ||
!generated(input.generated) ||
(before !== null && !generated(before))
)
throw new Error("Only scaffold-owned generated files may be regenerated");
after = input.generated;
} else {
if (before === null || !Array.isArray(input.edits))
throw new Error("Structural edit requires an existing source");
after = input.edits.reduce(editStructure, before);
}
if (Buffer.byteLength(after) > 1024 * 1024) throw new Error("Scaffold file exceeds 1 MiB");
const mode = before === null ? 0o644 : (await fs.stat(path.join(root, input.file))).mode & 0o777;
changes.push({ file: input.file, before, after, mode });
await containedParent(snapshot.directory, input.file);
await fs.writeFile(path.join(snapshot.directory, input.file), after);
}
if (request.validation === "syntax") {
for (const change of changes) {
if (change.file.endsWith(".qx") && parseQx(change.after, change.file).diagnostics.length)
throw new Error(`Invalid QX syntax in ${change.file}`);
if (path.basename(change.file) === "quixos.lock" && !parseQuixosLockDocument(change.after, change.file).ok)
throw new Error(`Invalid lock syntax in ${change.file}`);
}
} else {
const localMap = path.join(temporary, "local-resources.json");
await fs.writeFile(localMap, JSON.stringify(await localResourceSnapshots(root, snapshotMap)));
const resolveResource = await createGitCapabilityResolver({
checkoutRoot: path.join(temporary, "resources"),
snapshotMap: localMap,
});
if (
request.resourceRoot &&
!/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(request.resourceRoot)
)
throw new Error("Resource root must be a contained relative directory");
const resourceRoot = path.join(snapshot.directory, request.resourceRoot ?? "");
if (request.kind === "workspace")
await compileWorkspaceRepository({ rootDirectory: resourceRoot, resolveResource });
else if (["package", "interface", "build"].includes(request.kind) && request.source) {
const compiled = await compileCapabilityResourceRepository({
rootDirectory: resourceRoot,
kind: request.kind as "package" | "interface" | "build",
source: { resolver: "git", ...request.source },
resolveResource,
});
if (compiled.resource.kind === "package") {
const configurationSource = await read(resourceRoot, "quixos.check.json");
const configuration = configurationSource === null ? undefined : JSON.parse(configurationSource);
// Execution bindings are immutable build outputs owned by checkedArtifacts,
// not source edits produced by a structural preview.
const artifacts =
!configuration || configuration.backend === "execution"
? []
: [
{
file: configuration.bindingOutput as string,
after: generateTypeScriptBindings(
bindingSchema(compiled),
compiled.resource.revision.revisionId,
configuration.options,
),
},
];
for (const artifact of artifacts) {
const file = request.resourceRoot ? `${request.resourceRoot}/${artifact.file}` : artifact.file;
safeFile(file);
if (Buffer.byteLength(artifact.after) > 1024 * 1024)
throw new Error("Generated scaffold file exceeds 1 MiB");
const before = await read(root, file);
if (
before !== null &&
!before.startsWith("// Generated by quixos-codegen-ts.") &&
(() => {
try {
return JSON.parse(before).generatedBy !== "qx-scaffold-v1";
} catch {
return true;
}
})()
)
throw new Error(`Refusing to overwrite hand-authored generated artifact ${file}`);
const previous = changes.find((entry) => entry.file === file);
if (previous) previous.after = artifact.after;
else changes.push({ file, before, after: artifact.after, mode: 0o644 });
}
}
} else throw new Error("Resource plans require kind and exact authored source identity");
}
if (changes.length > 100) throw new Error("Structural plan including generated artifacts exceeds 100 files");
// Validation may fetch dependencies; reject edits made while it was running.
for (const entry of changes)
if ((await read(root, entry.file)) !== entry.before)
throw new Error(`Source changed while planning: ${entry.file}`);
for (const entry of observed)
if (contentDigest(await fs.readFile(path.join(root, entry.file), "utf8")) !== entry.digest)
throw new Error(`Validation input changed while planning: ${entry.file}`);
return {
root,
changes,
observed,
digest: contentDigest(changes),
validation: request.validation ?? ("resource-graph" as const),
};
} finally {
await fs.rm(temporary, { recursive: true, force: true });
}
};
/** Replay only exact before/after states. A crash never loses the original text. */
const replayStructure = async (rootPath: string, id: string) => {
const root = await fs.realpath(rootPath);
if (!/^[a-f0-9-]{36}$/.test(id)) throw new Error("Invalid scaffold journal ID");
const journalPath = path.join(root, ".quixos", "scaffolds", `${id}.json`);
const journal = JSON.parse(await fs.readFile(journalPath, "utf8")) as Journal;
if (journal.schemaVersion !== 1 || journal.root !== root || journal.id !== id)
throw new Error("Scaffold journal identity mismatch");
for (const entry of journal.changes) {
const current = await read(root, entry.file);
if (current !== entry.before && current !== entry.after)
throw new Error(
`Scaffold conflicts with newer edits: ${entry.file}; original text is retained in ${journalPath}`,
);
}
if (journal.phase === "complete") return { id, journalPath, phase: journal.phase };
for (const entry of journal.changes) {
if ((await read(root, entry.file)) === entry.after) continue;
await containedParent(root, entry.file);
const target = path.join(root, entry.file);
const temporary = `${target}.qx-${randomUUID()}.tmp`;
const handle = await fs.open(temporary, "wx", entry.mode);
try {
await handle.writeFile(entry.after);
await handle.sync();
} finally {
await handle.close();
}
if (entry.before === null) {
// link is atomic and fails if another author created the destination.
await fs.link(temporary, target);
await fs.unlink(temporary);
} else {
if ((await read(root, entry.file)) !== entry.before)
throw new Error(`Source changed during scaffold: ${entry.file}`);
await fs.rename(temporary, target);
}
const directory = await fs.open(path.dirname(target), "r");
try {
await directory.sync();
} finally {
await directory.close();
}
}
journal.phase = "complete";
await durableJson(journalPath, journal);
return { id, journalPath, phase: journal.phase };
};
const withStructureLock = async <T>(root: string, work: () => Promise<T>) => {
await containedParent(root, ".quixos/scaffolds/placeholder.json");
const lock = path.join(root, ".quixos", "scaffolds", "writer.lock");
return withFileLock(lock, work);
};
export const resumeStructure = async (rootPath: string, id: string) => {
const root = await fs.realpath(rootPath);
return withStructureLock(root, () => replayStructure(root, id));
};
export const applyStructure = async (plan: Awaited<ReturnType<typeof planStructure>>, id: string = randomUUID()) =>
withStructureLock(plan.root, async () => {
if (!/^[a-f0-9-]{36}$/.test(id)) throw new Error("Invalid scaffold journal ID");
const directory = path.join(plan.root, ".quixos", "scaffolds");
try {
const existing = JSON.parse(await fs.readFile(path.join(directory, `${id}.json`), "utf8")) as Journal;
if (existing.root !== plan.root || contentDigest(existing.changes) !== plan.digest)
throw new Error("Scaffold journal identity conflict");
for (const entry of plan.observed)
if (
!plan.changes.some((change) => change.file === entry.file) &&
contentDigest(await fs.readFile(path.join(plan.root, entry.file), "utf8")) !== entry.digest
)
throw new Error(`Stale scaffold validation input: ${entry.file}`);
return replayStructure(plan.root, id);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
// An unfinished journal must be recovered before another structural mutation.
for (const file of await fs.readdir(directory))
if (file.endsWith(".json")) {
const prior = JSON.parse(await fs.readFile(path.join(directory, file), "utf8")) as Journal;
if (prior.phase !== "complete") throw new Error(`Unfinished scaffold ${prior.id}; resume it first`);
}
for (const entry of plan.changes)
if ((await read(plan.root, entry.file)) !== entry.before) throw new Error(`Stale scaffold plan: ${entry.file}`);
for (const entry of plan.observed)
if (contentDigest(await fs.readFile(path.join(plan.root, entry.file), "utf8")) !== entry.digest)
throw new Error(`Stale scaffold validation input: ${entry.file}`);
await durableJson(path.join(directory, `${id}.json`), {
schemaVersion: 1,
id,
root: plan.root,
phase: "prepared",
changes: plan.changes,
} satisfies Journal);
return replayStructure(plan.root, id);
});
+646
View File
@@ -0,0 +1,646 @@
#!/usr/bin/env node
import { readFile, writeFile, mkdtemp, rm, realpath } from "node:fs/promises";
import { parseQx, formatQx, lintQx } from "./source.js";
import { scaffoldAtom } from "./scaffold.js";
import { loadQxSources, qxSourceManifest } from "./source-loader.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import { planEvolution } from "../capability-model/index.js";
import { snapshotRepository } from "./candidate-check.js";
import { planPinUpgrades, applyPinUpgrades, discoverUpgradeSpec, type UpgradeSpec } from "./pin-upgrades.js";
import path from "node:path";
import os from "node:os";
import { spawnSync } from "node:child_process";
import { planStructure, applyStructure, resumeStructure, type StructuralRequest } from "./structural-plan.js";
import { scaffoldRecipe, type ScaffoldRecipe } from "./scaffold-recipes.js";
import { checkBundleSources } from "../bindings/bundle-policy.js";
import { sealMigrations } from "./migration-seal.js";
import { generatePackageDescriptor } from "../bindings/index.js";
import { buildCheckedPackage, buildImmutableCandidate, snapshotCommit } from "./checked-build.js";
import { formatQuixosLock, loadQuixosLock, parseQuixosLockDocument } from "../resource-lock/index.js";
import { walkSyntax } from "./source.js";
import { inspectWorkbench } from "./authoring-inspect.js";
import { authoringContext } from "./authoring-context.js";
import { convergeAuthoring } from "./authoring-converge.js";
import { checkAuthoring } from "./authoring-check.js";
import { authoringWorklist } from "./authoring-worklist.js";
const authorSource = async (root: string) => {
const result = spawnSync("git", ["config", "--get", "remote.origin.url"], { cwd: root, encoding: "utf8" });
if (result.error || result.status !== 0) throw new Error("Managed resource has no origin");
return { repository: result.stdout.trim(), commit: await snapshotCommit(root) };
};
const readSpec = async (value: string) => {
if (value === "-") {
let input = "";
for await (const chunk of process.stdin) {
input += chunk;
if (input.length > 1024 * 1024) throw new Error("Scaffold specification exceeds 1 MiB");
}
return JSON.parse(input);
}
return JSON.parse(value.trimStart().startsWith("{") ? value : await readFile(value, "utf8"));
};
const planSummary = (plan: Awaited<ReturnType<typeof planStructure>>) => ({
root: plan.root,
validation: plan.validation,
changes: plan.changes.map((change) => ({
file: change.file,
beforeBytes: change.before?.length ?? 0,
afterBytes: change.after?.length ?? 0,
})),
note: "Structural plan only, not implementation verification. Run qx-workspace check while iterating.",
});
const main = async () => {
const [command, ...args] = process.argv.slice(2);
if (command === "source-locks") {
if (!args[0] || (args.length !== 1 && (args.length !== 3 || args[1] !== "--replacements")))
throw Error("source-locks DIRECTORY [--replacements JSON_FILE]");
const { workspaceSourceLocks } = await import("./source-locks.js");
const replacements = args[2] ? JSON.parse(await readFile(args[2], "utf8")) : [];
process.stdout.write(JSON.stringify(await workspaceSourceLocks(path.resolve(args[0]), replacements)) + "\n");
return;
}
if (["source-checkout", "source-develop", "source-backup", "source-restore"].includes(command!)) {
const args = ["source", command.slice("source-".length), ...process.argv.slice(3)];
const { materializeWorkspaceSources } = await import("./source-closure.js");
if (!args[2]) throw Error("source requires a pinned source JSON file");
const flags = new Map<string, string>();
for (let i = 3; i < args.length; i += 2) {
if (
![
"--dir",
...(args[1] !== "restore" ? ["--locations"] : []),
...(args[1] === "checkout" ? ["--repository-prefix"] : []),
...(args[1] === "develop" ? ["--cache", "--workspace-id"] : []),
...(args[1] === "backup" ? ["--output"] : []),
].includes(args[i]) ||
!args[i + 1] ||
flags.has(args[i])
)
throw Error("Invalid source command options; use qx-workspace help source");
flags.set(args[i], args[i + 1]);
}
if (!flags.get("--dir")) throw Error("source requires --dir DIRECTORY");
if (args[1] === "restore") {
const { restoreWorkspaceSources } = await import("./source-backup.js");
const result = await restoreWorkspaceSources({ backup: args[2], directory: path.resolve(flags.get("--dir")!) });
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
return;
}
const locationsFile = flags.get("--locations");
const options = {
source: JSON.parse(await readFile(args[2], "utf8")),
directory: path.resolve(flags.get("--dir")!),
...(locationsFile ? { locations: JSON.parse(await readFile(locationsFile, "utf8")) } : {}),
...(flags.has("--repository-prefix") ? { repositoryPrefix: flags.get("--repository-prefix") } : {}),
};
let result;
if (args[1] === "develop") {
if (!flags.get("--cache")) throw Error("source develop requires --cache DIRECTORY");
const { prepareDevelopmentSources } = await import("./development-sources.js");
result = await prepareDevelopmentSources({
...options,
cacheDirectory: path.resolve(flags.get("--cache")!),
...(flags.has("--workspace-id") ? { workspaceId: flags.get("--workspace-id")! } : {}),
});
} else if (args[1] === "backup") {
if (!flags.get("--output")) throw Error("source backup requires --output DIRECTORY");
const { backupWorkspaceSources } = await import("./source-backup.js");
result = await backupWorkspaceSources({ ...options, output: path.resolve(flags.get("--output")!) });
} else result = await materializeWorkspaceSources(options);
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
return;
}
if (command === "source-manifest") {
if (args.length !== 1) throw new Error("source-manifest WORKSPACE_ROOT");
process.stdout.write(JSON.stringify(await qxSourceManifest(args[0])) + "\n");
return;
}
if (command === "scaffold-validate-qx") {
if (args.length !== 1) throw new Error("scaffold-validate-qx SOURCES_JSON");
const sources = JSON.parse(args[0]);
if (!Array.isArray(sources) || sources.length > 100) throw new Error("Expected at most 100 scaffold sources");
for (const entry of sources) {
if (
!entry ||
typeof entry.file !== "string" ||
typeof entry.source !== "string" ||
entry.source.length > 1024 * 1024
)
throw new Error("Invalid scaffold source");
const diagnostics = parseQx(entry.source, entry.file).diagnostics;
if (diagnostics.length)
throw new Error(diagnostics.map((d) => `${entry.file}:${d.line}:${d.column + 1}: ${d.message}`).join("\n"));
}
process.stdout.write('{"syntaxValid":true,"verificationEvidence":false}\n');
return;
}
if (command === "scaffold-placement-binding") {
if (args.length !== 1) throw new Error("scaffold-placement-binding ROOT");
const { source } = await loadQxSources(args[0]);
const syntax = parseQx(source);
const text = (n: { start: number; end: number }) => source.slice(n.start, n.end);
const matches: string[] = [];
for (const edge of walkSyntax(syntax.root)) {
if (edge.kind !== "edgeDecl") continue;
for (const endpoint of edge.children.filter((n) => n.kind === "edgeEndpoint")) {
const target = endpoint.children.find((n) => n.kind === "targetConstraint");
if (
!target ||
!syntax.tokens.some((t) => t.start >= target.start && t.end <= target.end && t.kind === "INTERFACE") ||
!target.children.some((n) => n.kind === "identifier" && text(n) === "WebStudioPlaceable")
)
continue;
const edgeName = text(edge.children.find((n) => n.kind === "identifier")!);
const projection = text(endpoint.children.find((n) => n.kind === "identifier")!);
matches.push(`bind placements.resolve to edge ${edgeName}.${projection}.resolve;`);
}
}
if (matches.length !== 1)
throw new Error(
`Expected one canvas placement edge for WebStudioPlaceable; found ${matches.length}. Configure the workspace canvas before adding a bundle.`,
);
process.stdout.write(JSON.stringify({ binding: matches[0] }) + "\n");
return;
}
if (command === "package-identity") {
if (args.length !== 1) throw new Error("usage: quixos-qx package-identity PACKAGE_QX");
const authored = await readFile(args[0], "utf8");
const syntax = parseQx(authored);
const declarations = [...walkSyntax(syntax.root)].filter((node) => node.kind === "packageResourceDecl");
if (syntax.diagnostics.length || declarations.length !== 1)
throw new Error("Expected one valid package declaration");
const literals = declarations[0].children.filter((node) => node.kind === "stringLiteral");
process.stdout.write(
JSON.stringify({
id: JSON.parse(authored.slice(literals[0].start, literals[0].end)),
revision: JSON.parse(authored.slice(literals[1].start, literals[1].end)),
}) + "\n",
);
return;
}
if (command === "package-descriptor") {
if (args.length !== 2) throw new Error("usage: quixos-qx package-descriptor SCHEMA REVISION_ID");
process.stdout.write(generatePackageDescriptor(JSON.parse(await readFile(args[0], "utf8")), args[1]));
return;
}
if (command === "migration-seal") {
if (args.length !== 1) throw new Error("usage: quixos-qx migration-seal PACKAGE_DIRECTORY");
process.stdout.write(JSON.stringify(await sealMigrations(args[0])) + "\n");
return;
}
if (command === "bundle-policy") {
if (args.length !== 1) throw new Error("usage: quixos-qx bundle-policy SOURCE_DIRECTORY");
await checkBundleSources(args[0]);
return;
}
if (command === "worklist" && !args.includes("--help")) {
if (args.length !== 1) throw new Error("usage: quixos-qx worklist WORKBENCH");
process.stdout.write(`${JSON.stringify(await authoringWorklist(args[0]), null, 2)}\n`);
return;
}
if (["author-check", "author-contract"].includes(command) && !args.includes("--help")) {
const [root, output, ...flags] = args;
if (!root || !output)
throw new Error("usage: quixos-qx author-check ROOT OUTPUT [--baseline FILE] [--reviews FILE]");
const options: { baseline?: string; reviews?: string } = {};
for (let index = 0; index < flags.length; index += 2) {
if (!flags[index + 1]) throw new Error("Missing check option value");
if (flags[index] === "--baseline") options.baseline = flags[index + 1];
else if (flags[index] === "--reviews") options.reviews = flags[index + 1];
else throw new Error(`Unknown check option ${flags[index]}`);
}
const result = await checkAuthoring(root, output, { ...options, contractOnly: command === "author-contract" });
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
if (result.blockers.length) process.exitCode = 1;
return;
}
if (["converge", "_converge"].includes(command) && !args.includes("--help")) {
if (!args.length || args.length > 2)
throw new Error("usage: quixos-qx converge WORKBENCH [REGISTERED_DIRECTORY] (join package writers first)");
const context = await authoringContext(args[0]);
if (command === "converge") {
console.error(`[${new Date().toISOString()}] Capture: waiting for coordinator lock (120s limit)`);
const result = spawnSync(
"flock",
[
"--exclusive",
"--timeout",
"120",
"--conflict-exit-code",
"75",
path.join(context.workbench, ".quixos/converge.lock"),
process.execPath,
process.argv[1],
"_converge",
context.workbench,
...(args[1] ? [args[1]] : []),
],
{ stdio: "inherit" },
);
if (result.error) throw result.error;
if (result.status === 75)
process.stderr.write(
"Timed out after 120 seconds waiting for source capture; inspect the active coordinator. No build lock is held.\n",
);
process.exitCode = result.status ?? 1;
return;
}
console.error(`[${new Date().toISOString()}] Capture: coordinator lock acquired`);
const result = await convergeAuthoring(context.workbench, args[1]);
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
if (!result.converged) process.exitCode = 1;
return;
}
if (command === "check-committed" && !args.includes("--help")) {
const [kind, repository, commit, log, ...extra] = args;
if (
!["workspace", "interface", "package", "build"].includes(kind) ||
!log ||
(extra.length !== 0 && (kind !== "workspace" || extra.length !== 2 || extra[0] !== "--workspace-id" || !extra[1]))
)
throw new Error(
"usage: quixos-qx check-committed workspace|interface|package|build REPOSITORY COMMIT LOG_FILE [--workspace-id ID]",
);
process.stdout.write(
`${await buildImmutableCandidate({ repository, commit }, kind as "workspace" | "interface" | "package" | "build", log, false, undefined, extra[1])}\n`,
);
return;
}
if (!command || command === "--help" || args.includes("--help")) {
process.stdout.write(
"quixos-qx: author-check, author-contract, converge, worklist, inspect, resources, check-committed, source-baseline, scaffold-package, scaffold-interface, scaffold-build, scaffold-function, scaffold-query, scaffold-dependency, scaffold-structure, scaffold-resume, pin-upgrade, parse, lint, format\n" +
"inspect WORKBENCH [RESOURCE] shows provisional contracts, with explicit historical fallback; never verification evidence.\n" +
"resources WORKBENCH lists registered editable repositories. Use qx-workspace for the workspace authoring workflow.\n",
);
return;
}
if (command === "inspect" || command === "resources") {
if (!args[0] || args.length > (command === "inspect" ? 2 : 1))
throw new Error(`usage: quixos-qx ${command} WORKBENCH${command === "inspect" ? " [RESOURCE]" : ""}`);
const result = command === "inspect" ? await inspectWorkbench(args[0], args[1]) : await authoringContext(args[0]);
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return;
}
if (command === "source-baseline") {
if (args.length !== 1) throw new Error("usage: quixos-qx source-baseline WORKBENCH");
process.stdout.write(`${JSON.stringify(await (await authoringContext(args[0])).baseline())}\n`);
return;
}
if (command === "scaffold-dependency") {
const [root, kind, name, ...remaining] = args;
let repository: string | undefined, commit: string | undefined;
const flags = [...remaining];
if (flags.length && !flags[0].startsWith("--")) {
repository = flags.shift();
commit = flags.shift();
} else if (root && ["interface", "package", "build"].includes(kind) && name) {
const context = await authoringContext(root);
const alias = await realpath(path.join(context.workbench, `${kind}s`, name)).catch(() => null);
const matches = context.resources.filter(
(entry) =>
entry.kind === kind &&
(entry.resourceId === name ||
path.basename(entry.directory) === name ||
path.join(context.workbench, entry.directory) === alias),
);
if (matches.length !== 1 || !matches[0].source)
throw new Error(`Select exactly one registered ${kind} with qx-workspace resources; no match for ${name}`);
const selected = matches[0];
let source = selected.source!;
if (flags.includes("--write")) {
const retained = spawnSync("quixos-qx", ["converge", context.workbench, selected.directory], {
encoding: "utf8",
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1" },
});
if (retained.error || retained.status !== 0)
throw new Error(
`Dependency source needs attention: ${retained.error?.message ?? retained.stdout ?? retained.stderr}`,
);
source = JSON.parse(retained.stdout).candidate;
}
repository = source.repository;
commit = source.commit;
}
if (
!root ||
!["interface", "package", "build"].includes(kind) ||
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name ?? "") ||
!repository ||
!commit ||
flags.some((flag) => flag !== "--write")
)
throw new Error(
"usage: quixos-qx scaffold-dependency ROOT interface|package|build NAME REPOSITORY COMMIT [--write]",
);
const resourceKind = kind as "package" | "interface" | "build";
let entrypoint: "workspace" | "package" | "interface" | "build" | undefined;
for (const candidate of ["workspace", "package", "interface"] as const) {
try {
await readFile(path.join(root, `${candidate}.qx`));
if (entrypoint) throw new Error("Ambiguous repository entrypoint");
entrypoint = candidate;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
}
if (!entrypoint) {
const context = await authoringContext(root);
const selected = context.resources.find(
(entry) => path.join(context.workbench, entry.directory) === path.resolve(root),
);
if (selected?.kind !== "build") throw new Error("No QX repository entrypoint");
entrypoint = "build";
}
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
if (!lock.ok) throw new Error("Invalid resource lock");
let target = "quixos.lock";
for (const file of lock.lock.sourceFiles ?? ["quixos.lock"]) {
const parsed = parseQuixosLockDocument(await readFile(path.join(root, file), "utf8"));
if (parsed.ok && parsed.document.resources.some((entry) => entry.kind === kind && entry.binding === name))
target = file;
}
const request: StructuralRequest = {
kind: entrypoint,
source: await authorSource(root),
validation: "syntax",
files: [
...(entrypoint !== "build" && resourceKind !== "build"
? [{ file: `${entrypoint}.qx`, edits: [{ operation: "import" as const, kind: resourceKind, name }] }]
: []),
{
file: target,
edits: [{ operation: "dependency", kind: resourceKind, name, source: { repository, commit } }],
},
],
};
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
process.stdout.write(
`${JSON.stringify({ ...planSummary(plan), applied: flags.includes("--write") ? await applyStructure(plan) : undefined }, null, 2)}\n`,
);
return;
}
if (command === "scaffold-interface" || command === "scaffold-build") {
const isBuild = command === "scaffold-build";
const [root, specFile, ...flags] = args;
if (!root || !specFile || flags.some((flag) => flag !== "--write"))
throw new Error("usage: quixos-qx scaffold-interface|scaffold-build ROOT SPEC_JSON [--write]");
const spec = (await readSpec(specFile)) as ScaffoldRecipe;
if (
!spec.name ||
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) ||
(!isBuild && (!spec.id || !spec.revision)) ||
!spec.tools?.quixos
)
throw new Error(
isBuild
? "Build scaffold requires name and Quixos toolchain source"
: "Interface scaffold requires name, id, revision and Quixos toolchain source",
);
const request: StructuralRequest = {
kind: isBuild ? "build" : "interface",
// Like package scaffolding, declaration creation is provisional. Imports
// can be attached next; the normal check verifies the complete graph.
validation: "syntax",
source: spec.source,
files: [
...(!isBuild
? [
{
file: "interface.qx",
create:
spec.declaration ??
`interface ${spec.name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n}\n`,
},
]
: [
{
file: "README.md",
create: `# ${spec.name}\n\nManaged build dependency. Consumers select sources or Nix outputs through context.dependencies.\n`,
},
]),
{
file: "quixos.lock",
create: formatQuixosLock({
formatVersion: 1,
quixos: { resolver: "git", ...spec.tools.quixos },
resources: [],
}),
},
{ file: ".gitignore", create: ".quixos/\n" },
],
};
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
process.stdout.write(
`${JSON.stringify({ ...planSummary(plan), applied: flags.includes("--write") ? await applyStructure(plan) : undefined }, null, 2)}\n`,
);
return;
}
if (command === "build-package") {
if (args.length !== 3)
throw new Error("usage: quixos-qx build-package COMMITTED_SOURCE SCHEMA PACKAGE_REVISION_ID");
process.stdout.write(`${await buildCheckedPackage(args[0], args[1], args[2])}\n`);
return;
}
if (command === "source-digest") {
if (!args[0] || args.length !== 1) throw new Error("usage: quixos-qx source-digest ROOT");
const temporary = await mkdtemp(path.join(os.tmpdir(), "qx-source-digest-"));
try {
process.stdout.write(`${(await snapshotRepository(args[0], temporary)).treeDigest}\n`);
} finally {
await rm(temporary, { recursive: true, force: true });
}
return;
}
if (command === "pin-upgrade") {
const [workbench, specFile, ...flags] = args;
if (!workbench || !specFile)
throw new Error("usage: quixos-qx pin-upgrade WORKBENCH SPEC_JSON [--publish] [--resume UUID]");
let publish = false,
acceptEdits = false,
resume: string | undefined;
for (let index = 0; index < flags.length; index++) {
if (flags[index] === "--publish") publish = true;
else if (flags[index] === "--accept-edits") acceptEdits = true;
else if (flags[index] === "--resume" && /^[a-f0-9-]{36}$/.test(flags[index + 1] ?? "")) resume = flags[++index];
else throw new Error(`Unknown pin-upgrade option ${flags[index]}`);
}
if (acceptEdits && !resume) throw new Error("--accept-edits requires an existing refactor journal (--resume)");
const plan = resume
? JSON.parse(await readFile(path.join(workbench, ".quixos/upgrades", `${resume}.json`), "utf8")).plan
: await planPinUpgrades(
workbench,
specFile === "auto"
? await discoverUpgradeSpec(workbench)
: (JSON.parse(await readFile(specFile, "utf8")) as UpgradeSpec),
);
if (resume && path.resolve(workbench) !== plan.workbench)
throw new Error("Upgrade journal belongs to another workbench");
process.stdout.write(
`${JSON.stringify(publish ? await applyPinUpgrades(plan, resume, undefined, { acceptEdits }) : plan, null, 2)}\n`,
);
return;
}
if (command === "scaffold-refresh")
throw new Error(
"Refresh is no longer required: edit declarations and typed implementation wiring, then run qx-workspace check. Dependencies are provided by checkedArtifacts.",
);
if (command === "scaffold-install")
throw Error("Execution dependencies are supplied by checkedArtifacts; run workspace-root check instead");
if (
[
"scaffold-package",
"scaffold-function",
"scaffold-query",
"scaffold-component",
"scaffold-controller",
"scaffold-migration",
].includes(command)
) {
const [root, specFile, ...flags] = args;
let spec: ScaffoldRecipe;
if (command === "scaffold-function" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(specFile ?? "")) {
const authored = parseQx(await readFile(path.join(root, "package.qx"), "utf8"));
const declarationNode = [...walkSyntax(authored.root)].find((node) => node.kind === "packageResourceDecl");
const nameNode = declarationNode?.children.find((node) => node.kind === "identifier");
if (!nameNode) throw new Error("Expected a package declaration");
const name = authored.source.slice(nameNode.start, nameNode.end);
spec = { source: await authorSource(root), name: specFile, id: `export:${name}:${specFile}` };
const declaration = flags.indexOf("--declaration");
if (declaration >= 0) {
if (!flags[declaration + 1]) throw new Error("--declaration requires a QX declaration file");
spec.declaration = await readFile(flags[declaration + 1], "utf8");
const parsed = parseQx(`package Draft id "package:draft" revision "package:draft@1" { ${spec.declaration} }`);
if (parsed.diagnostics.length) throw new Error(parsed.diagnostics.map((d) => d.message).join("\n"));
const exported = [...walkSyntax(parsed.root)].filter((node) =>
["packageOperationExport", "packageFunctionExport", "packageConstructorExport"].includes(node.kind),
);
if (exported.length !== 1)
throw new Error("--declaration must contain exactly one function, operation or constructor export");
const literal = exported[0].children.find((node) => node.kind === "stringLiteral");
if (!literal) throw new Error("Declaration requires an authored export ID");
spec.id = JSON.parse(parsed.source.slice(literal.start, literal.end));
flags.splice(declaration, 2);
}
} else spec = (await readSpec(specFile)) as ScaffoldRecipe;
if (
!root ||
!specFile ||
flags.some((flag) => !["--write", "--install"].includes(flag)) ||
(flags.includes("--install") && !flags.includes("--write"))
)
throw new Error("usage: quixos-qx scaffold-package|function|query|component|controller ROOT SPEC_JSON [--write]");
const plan =
command === "scaffold-install"
? undefined
: await planStructure(
root,
await scaffoldRecipe(
root,
command.slice(9) as "package" | "function" | "query" | "component" | "controller" | "migration",
spec,
),
process.env.QUIXOS_SNAPSHOT_MAP,
);
if (flags.includes("--install"))
throw Error(
"Execution dependencies are supplied by checkedArtifacts; run workspace-root check instead of scaffold install",
);
const applied = plan && flags.includes("--write") ? await applyStructure(plan) : undefined;
process.stdout.write(
`${JSON.stringify({ ...(plan ? planSummary(plan) : { installed: true }), applied }, null, 2)}\n`,
);
return;
}
if (command === "scaffold-structure") {
const [root, spec, ...flags] = args;
if (!root || !spec || flags.some((flag) => flag !== "--write"))
throw new Error("usage: quixos-qx scaffold-structure ROOT SPEC_JSON [--write]");
const request = (await readSpec(spec)) as StructuralRequest;
if (request.kind !== "workspace" && !request.source) request.source = await authorSource(root);
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
const applied = flags.includes("--write") ? await applyStructure(plan) : undefined;
process.stdout.write(`${JSON.stringify({ ...planSummary(plan), applied }, null, 2)}\n`);
return;
}
if (command === "scaffold-resume") {
const [root, id, ...extra] = args;
if (!root || !id || extra.length) throw new Error("usage: quixos-qx scaffold-resume ROOT JOURNAL_ID");
process.stdout.write(`${JSON.stringify(await resumeStructure(root, id), null, 2)}\n`);
return;
}
if (["check", "check-resource"].includes(command))
throw new Error(
"Use qx-workspace check in the registered repository; handwritten source/snapshot-map candidates are no longer an authoring check path",
);
if (command === "evolution") {
const [baseline, candidate, reviews, ...extra] = args;
if (!baseline || !candidate || extra.length)
throw new Error("usage: quixos-qx evolution BASELINE_JSON CANDIDATE_JSON [REVIEWS_JSON]");
const before = baseline === "none" ? null : JSON.parse(await readFile(baseline, "utf8"));
const after = JSON.parse(await readFile(candidate, "utf8"));
const decisions = reviews ? JSON.parse(await readFile(reviews, "utf8")) : [];
if (!Array.isArray(decisions)) throw new Error("Reviews must be an array");
process.stdout.write(`${JSON.stringify(planEvolution(before, after, { reviews: decisions }), null, 2)}\n`);
return;
}
if (command === "scaffold-atom") {
const [root, name, id, ...flags] = args;
if (!root || !name || !id || flags.some((flag) => flag !== "--write"))
throw new Error("usage: quixos-qx scaffold-atom ROOT NAME ID [--write]");
const resolveResource = await createGitCapabilityResolver({ checkoutRoot: `${root}/.quixos/resource-checkouts` });
const plan = await scaffoldAtom({ root, name, id, write: flags.includes("--write"), resolveResource });
process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
return;
}
if (command === "format" && (args[0] === "--check" || args[0] === "--write")) {
const check = args[0] === "--check",
files = args.slice(1);
if (!files.length || files.some((file) => file.startsWith("-")))
throw new Error("usage: quixos-qx format --check|--write FILE...");
for (const file of files) {
try {
const source = await readFile(file, "utf8"),
formatted = formatQx(source);
if (formatted === source) continue;
if (check) {
console.error(`Needs formatting: ${file}`);
process.exitCode = 1;
} else {
await writeFile(file, formatted);
console.log(`Formatted ${file}`);
}
} catch (error) {
console.error(`${file}: ${error instanceof Error ? error.message : error}`);
process.exitCode = 1;
}
}
return;
}
const [file, flag, ...rest] = args;
if (
!file ||
rest.length ||
(flag && flag !== "--write") ||
!["parse", "lint", "format"].includes(command ?? "") ||
(flag && command !== "format")
)
throw new Error("usage: quixos-qx parse|lint|format FILE [--write (format only)]");
const source = await readFile(file, "utf8");
if (command === "format") {
const formatted = formatQx(source);
if (flag) await writeFile(file, formatted);
else process.stdout.write(formatted);
} else {
const result = command === "parse" ? parseQx(source, file) : lintQx(source, file);
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
if (Array.isArray(result) ? result.some((entry) => entry.severity === "error") : result.diagnostics.length)
process.exitCode = 1;
}
};
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env node
import { readFile, writeFile } from "node:fs/promises";
import process from "node:process";
import { compileWorkspaceRepository } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import { loadQueryGenerator } from "./query-generator.js";
import { loadCheckedResources } from "./checked-resources.js";
import { profile } from "./profile.js";
import { compileExecutionPlan, packageBuildContexts } from "../execution-world/index.js";
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
import {
planEvolution,
runtimeContracts,
type EvolutionReview,
type WorkspaceRevision,
} from "../capability-model/index.js";
const usage = `usage: quixos-workspace-compile --root DIRECTORY --checkout-root DIRECTORY
[--snapshot-map PATH] [--graph-out PATH] [--plan-out PATH] [--workspace-id ID] [--workspace-revision-id ID]
[--source-root-commit GIT_REV] [--baseline PLAN_JSON] [--evolution-out PATH]
[--reviews REVIEW_JSON] [--schemas-out PATH] [--execution-plan-out PATH] [--build-contexts-out PATH]
[--query-generator BUILD_MODULE_PATH] [--checked-resources NIX_MAP]
Resolves a workspace's recursive resource-lock graph, clones every exact
resource revision, validates standalone interface/package manifests, and emits
the checked workspace plan as JSON.`;
const parseArgs = (args: string[]) => {
const values = new Map<string, string>();
for (let index = 0; index < args.length; index += 2) {
const key = args[index];
const value = args[index + 1];
if (!key?.startsWith("--") || !value) throw new Error(usage);
values.set(key, value);
}
const rootDirectory = values.get("--root");
const checkoutRoot = values.get("--checkout-root");
if (!rootDirectory || !checkoutRoot) throw new Error(usage);
return {
rootDirectory,
checkoutRoot,
graphOut: values.get("--graph-out"),
planOut: values.get("--plan-out"),
schemasOut: values.get("--schemas-out"),
executionPlanOut: values.get("--execution-plan-out"),
buildContextsOut: values.get("--build-contexts-out"),
snapshotMap: values.get("--snapshot-map"),
workspaceId: values.get("--workspace-id"),
workspaceRevisionId: values.get("--workspace-revision-id"),
sourceRootCommit: values.get("--source-root-commit"),
baseline: values.get("--baseline"),
evolutionOut: values.get("--evolution-out"),
reviews: values.get("--reviews"),
queryGenerator: values.get("--query-generator"),
checkedResources: values.get("--checked-resources"),
};
};
const main = async () => {
if (process.argv.includes("--help") || process.argv.includes("-h")) {
process.stdout.write(`${usage}\n`);
return;
}
const options = parseArgs(process.argv.slice(2));
const resolveResource = await createGitCapabilityResolver({
checkoutRoot: options.checkoutRoot,
snapshotMap: options.snapshotMap,
});
const assemblyStarted = performance.now();
const assembled = await compileWorkspaceRepository({
rootDirectory: options.rootDirectory,
workspaceId: options.workspaceId,
workspaceRevisionId: options.workspaceRevisionId,
sourceRootCommit: options.sourceRootCommit,
resolveResource,
generateQueries: await loadQueryGenerator(options.queryGenerator),
checkedResource: await loadCheckedResources(options.checkedResources),
});
profile("workspace-assembly", assemblyStarted, { resources: assembled.resources.length });
if (options.graphOut) {
await writeFile(
options.graphOut,
`${JSON.stringify(
{
formatVersion: 1,
workspaceId: assembled.workspace.workspaceId,
quixos: assembled.lock.quixos,
directResources: [...assembled.directResources.entries()].map(([bindingKey, node]) => {
const [kind, binding] = bindingKey.split("\0");
return { kind, binding, resourceKey: node.key, directory: node.directory };
}),
resources: assembled.resources.map((node) => ({
key: node.key,
kind: node.kind,
source: node.source,
directory: node.directory,
...(node.resource.kind === "build"
? {}
: {
resourceId:
node.resource.kind === "interface"
? node.resource.revision.interfaceId
: node.resource.revision.packageId,
revisionId: node.resource.revision.revisionId,
}),
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
binding,
resourceKey: dependency.key,
})),
})),
},
null,
2,
)}\n`,
);
}
const candidate = { ...assembled.workspace, executionContracts: runtimeContracts(assembled.workspace) };
if (options.executionPlanOut || options.buildContextsOut) {
const executionStarted = performance.now();
const plan = compileExecutionPlan(assembled.workspace);
// Machine-consumed immutable metadata should not spend its bounded input
// reservation on indentation. Its parsed allocation is charged separately.
if (options.executionPlanOut) await writeFile(options.executionPlanOut, `${JSON.stringify(plan)}\n`);
if (options.buildContextsOut)
await writeFile(options.buildContextsOut, JSON.stringify(packageBuildContexts(plan.definition.bindingSchema)));
profile("execution-plan-and-contexts", executionStarted);
}
if (options.schemasOut) {
const schemasStarted = performance.now();
const schemas: Record<string, unknown> = {};
for (const node of assembled.capabilityResources.filter((entry) => entry.kind === "package")) {
const closure = new Map<string, typeof node>();
const visit = (entry: typeof node) => {
if (closure.has(entry.key) || entry.kind === "build") return;
closure.set(entry.key, entry);
entry.dependencies.forEach(visit);
};
visit(node);
if (node.resource.kind !== "package") continue;
schemas[node.resource.revision.revisionId] = specializeBindingSchema(
bindingSchema({ resources: [...closure.values()] }),
candidate,
node.resource.revision.revisionId,
);
}
await writeFile(options.schemasOut, JSON.stringify(schemas));
profile("package-schemas", schemasStarted);
}
if (options.evolutionOut) {
const baseline = options.baseline
? (JSON.parse(await readFile(options.baseline, "utf8")) as WorkspaceRevision)
: null;
const reviews = options.reviews ? (JSON.parse(await readFile(options.reviews, "utf8")) as EvolutionReview[]) : [];
if (!Array.isArray(reviews)) throw new Error("Review file must contain an array");
await writeFile(
options.evolutionOut,
`${JSON.stringify(planEvolution(baseline, candidate, { reviews }), null, 2)}\n`,
);
}
if (options.planOut) await writeFile(options.planOut, `${JSON.stringify(candidate)}\n`);
else process.stdout.write(`${JSON.stringify(candidate, null, 2)}\n`);
};
main().catch((error: unknown) => {
process.stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
process.exitCode = 1;
});
+208
View File
@@ -0,0 +1,208 @@
import { createHash } from "node:crypto";
import {
capabilityId,
type AtomId,
type Conformance,
type InterfaceRevision,
type InterfaceRevisionId,
} from "./types.js";
import {
GenericTypeError,
TypeSubstitution,
bindTypeParameters,
canonicalTypeArgument,
type ClosedTypeArgument,
type GenericTypeEnvironment,
type InterfaceApplicationExpression,
type TypeParameter,
} from "./generics.js";
/** Compiler input only. Runtime plans contain ordinary closed conformances. */
export interface ConformanceRule {
id: string;
parameters: TypeParameter[];
source: InterfaceApplicationExpression;
target: InterfaceApplicationExpression;
}
export interface RuleApplication {
rule: ConformanceRule;
atomId: AtomId;
sourceId: InterfaceRevisionId;
targetId: InterfaceRevisionId;
bindings: Map<string, ClosedTypeArgument>;
id: Conformance["id"];
}
const parameterId = (argument: ConformanceRule["source"]["arguments"][number]) => {
const value = argument.kind === "value" ? argument.type : argument.target;
return value.kind === "parameter" ? value.parameterId : undefined;
};
const hasParameter = (node: unknown): boolean =>
!!node &&
typeof node === "object" &&
(("kind" in node && node.kind === "parameter") || Object.values(node).some(hasParameter));
export function validateConformanceRules(
rules: readonly ConformanceRule[],
definitions: ReadonlyMap<string, InterfaceRevision>,
) {
const ids = new Set<string>();
const graph = new Map<string, ConformanceRule[]>();
for (const rule of rules) {
if (ids.has(rule.id)) throw new GenericTypeError("duplicate-rule-id", rule.id, `Duplicate rule ID ${rule.id}`);
ids.add(rule.id);
for (const application of [
rule.source,
rule.target,
...rule.parameters.flatMap((p) => (p.kind === "object" ? p.implements : [])),
]) {
const definition = definitions.get(application.definitionId);
if (!definition)
throw new GenericTypeError("unknown-interface", rule.id, `Unknown interface ${application.definitionId}`);
const parameters = definition.template?.parameters ?? [];
if (parameters.length !== application.arguments.length)
throw new GenericTypeError("type-arity", rule.id, `Wrong arity for ${definition.displayName}`);
parameters.forEach((p, i) => {
if (p.kind !== application.arguments[i].kind)
throw new GenericTypeError(
"parameter-kind",
rule.id,
`Wrong argument kind for ${definition.displayName}.${p.name}`,
);
});
}
const captured = new Set<string>();
for (const argument of rule.source.arguments) {
const id = parameterId(argument);
if (id) captured.add(id);
else if (hasParameter(argument))
throw new GenericTypeError(
"unsupported-rule-pattern",
rule.id,
"Capture whole source arguments, not nested type patterns",
);
}
for (const p of rule.parameters)
if (!captured.has(p.id))
throw new GenericTypeError(
"uncaptured-rule-parameter",
rule.id,
`Parameter ${p.name} must occur in the source application`,
);
graph.set(rule.source.definitionId, [...(graph.get(rule.source.definitionId) ?? []), rule]);
}
const visited = new Set<string>();
const active = new Set<string>();
const visit = (id: string, trail: string[]) => {
if (active.has(id))
throw new GenericTypeError(
"recursive-conformance-rule",
trail.at(-1) ?? id,
`Recursive conformance rules: ${trail.join(" -> ")}`,
);
if (visited.has(id)) return;
active.add(id);
for (const rule of graph.get(id) ?? []) visit(rule.target.definitionId, [...trail, rule.id]);
active.delete(id);
visited.add(id);
};
for (const id of graph.keys()) visit(id, []);
}
/** Monotone closure: bounds only enable applications, never invent evidence. */
export function expandConformanceRules(
rules: readonly ConformanceRule[],
initial: readonly Pick<Conformance, "atomId" | "interfaceRevisionId">[],
definitions: ReadonlyMap<string, InterfaceRevision>,
environment: (atom: AtomId, bindings: Map<string, ClosedTypeArgument>) => GenericTypeEnvironment,
): RuleApplication[] {
validateConformanceRules(rules, definitions);
const key = (atom: string, iface: string) => JSON.stringify([atom, iface]);
const facts = new Map(initial.map((f) => [key(f.atomId, f.interfaceRevisionId), f]));
const applications = new Map<string, RuleApplication>();
const implies = (actual: InterfaceRevisionId, required: InterfaceRevisionId, seen = new Set<string>()): boolean => {
if (actual === required) return true;
if (seen.has(actual)) return false;
seen.add(actual);
return (definitions.get(actual)?.requiredInterfaces ?? []).some((id) => implies(id, required, seen));
};
const bySource = new Map<string, ConformanceRule[]>();
for (const rule of [...rules].sort((a, b) => a.id.localeCompare(b.id)))
bySource.set(rule.source.definitionId, [...(bySource.get(rule.source.definitionId) ?? []), rule]);
let changed = true;
while (changed) {
changed = false;
for (const fact of [...facts.values()].sort((a, b) =>
key(a.atomId, a.interfaceRevisionId).localeCompare(key(b.atomId, b.interfaceRevisionId)),
)) {
const definition = definitions.get(fact.interfaceRevisionId);
if (!definition) continue;
for (const rule of bySource.get(definition.application?.definitionId ?? definition.revisionId) ?? []) {
const arguments_ = definition.application?.arguments ?? [];
const bindings = new Map<string, ClosedTypeArgument>();
const env = environment(fact.atomId, bindings);
const substitution = new TypeSubstitution(env);
if (
!rule.source.arguments.every((pattern, i) => {
const actual = arguments_[i];
if (!actual || actual.kind !== pattern.kind) return false;
const id = parameterId(pattern);
if (!id) return canonicalTypeArgument(substitution.argument(pattern)) === canonicalTypeArgument(actual);
const previous = bindings.get(id);
if (previous && canonicalTypeArgument(previous) !== canonicalTypeArgument(actual)) return false;
bindings.set(id, actual);
return true;
})
)
continue;
try {
bindTypeParameters(
rule.parameters,
rule.parameters.map((p) => bindings.get(p.id)!),
{
...env,
implementsInterface: (target, required) =>
target.kind === "atom"
? facts.has(key(target.atomId, required))
: implies(target.interfaceRevisionId, required),
},
rule.id,
);
} catch (error) {
if (error instanceof GenericTypeError && ["unsatisfied-bound", "non-storable-argument"].includes(error.code))
continue;
throw error;
}
const applicationKey = JSON.stringify([rule.id, fact.atomId, fact.interfaceRevisionId]);
if (applications.has(applicationKey)) continue;
if (applications.size >= 10000)
throw new GenericTypeError(
"rule-expansion-limit",
rule.id,
"Conformance rule expansion exceeds 10000 applications",
);
const targetId = substitution.application(rule.target);
const id = capabilityId.conformance(
`conformance-rule:sha256:${createHash("sha256")
.update(
JSON.stringify(["quixos-conformance-rule-v1", rule.id, fact.atomId, fact.interfaceRevisionId, targetId]),
)
.digest("hex")}`,
);
applications.set(applicationKey, {
rule,
atomId: fact.atomId,
sourceId: fact.interfaceRevisionId,
targetId,
bindings,
id,
});
const targetKey = key(fact.atomId, targetId);
if (!facts.has(targetKey)) {
facts.set(targetKey, { atomId: fact.atomId, interfaceRevisionId: targetId });
changed = true;
}
}
}
}
return [...applications.values()];
}
+128
View File
@@ -0,0 +1,128 @@
import type { InterfaceRevision, PackageController, WorkspaceRevision } from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
import { canonicalJson } from "./evolution.js";
export function validateControllers(
controllers: readonly PackageController[],
interfaces: readonly InterfaceRevision[],
prefix: string,
queries: readonly import("../query/types.js").CheckedQuery[] = [],
): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [],
ids = new Set<string>();
const fail = (path: string, message: string) => issues.push({ code: "invalid-controller" as const, path, message });
for (const [index, controller] of controllers.entries()) {
const path = `${prefix}[${index}]`;
if (!controller.id.trim() || ids.has(controller.id))
fail(`${path}.id`, "Controller identities must be nonempty and unique within the package");
ids.add(controller.id);
if (controller.registry !== undefined) {
const query = queries.find((query) => query.declaration.id === controller.registry);
const selected = (entries: import("../query/types.js").QuerySelection[] | undefined, name: string) =>
entries?.find((entry) => entry.name === name && entry.key === name && !entry.conditions.length);
const root = selected(query?.selection, "root"),
objects = selected(root?.selection, "objects"),
entries = selected(objects?.selection, "entries"),
node = selected(entries?.selection, "node"),
qx = selected(node?.selection, "_camino"),
ref = selected(qx?.selection, "ref");
const all = objects?.arguments.all;
if (
!query ||
query.declaration.root.kind !== "workspace-interface" ||
query.declaration.root.interface !== controller.subject ||
query.declaration.placement?.kind === "local" ||
query.effects.some((effect) => effect.execution === "rpc-permitted") ||
query.variables.kind !== "record" ||
Object.keys(query.variables.fields).length ||
!ref ||
all?.kind !== "literal" ||
!/^\d+$/.test(String(all.value)) ||
!Number.isSafeInteger(Number(all.value)) ||
Number(all.value) < 1 ||
Number(all.value) > 64
)
fail(
`${path}.registry`,
"Controller registry requires a native/portable workspace query over its subject, no variables, and root.objects(all: 1..64).entries.node._camino.ref without aliases or conditional selections",
);
}
const iface = interfaces.find((i) => i.revisionId === controller.subject && !i.template);
if (!iface) {
fail(`${path}.subject`, "Controller subject must be an imported closed interface");
continue;
}
const operations = iface.members.flatMap((m) => m.operations);
const snapshot = operations.find((o) => o.id === controller.snapshot),
remote = operations.find((o) => o.id === controller.remote);
const unit = (value: unknown) => canonicalJson(value) === canonicalJson({ kind: "builtin", name: "unit" });
if (
!snapshot ||
snapshot.scope === "class" ||
snapshot.mode !== "call" ||
snapshot.execution?.kind !== "snapshot" ||
!snapshot.execution.targets.includes("server") ||
!unit(snapshot.inputType)
)
fail(`${path}.snapshot`, "Controller snapshot must be a server snapshot object call with unit input");
if (
!remote ||
remote.scope === "class" ||
remote.mode !== "call" ||
(remote.execution?.kind ?? "remote") !== "remote"
)
fail(`${path}.remote`, "Controller target must be a remote object call");
if (controller.input !== "unit" && controller.input !== "value")
fail(`${path}.input`, "Controller input must be unit or value");
else if (
remote &&
(controller.input === "unit"
? !unit(remote.inputType)
: snapshot && canonicalJson(snapshot.outputType) !== canonicalJson(remote.inputType))
)
fail(`${path}.input`, "Controller input mapping does not match the remote operation input");
}
return issues;
}
export function validateWorkspaceControllers(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
return workspace.packageImports.flatMap((pkg, index) =>
validateControllers(
pkg.controllers ?? [],
workspace.interfaceImports,
`packageImports[${index}].controllers`,
pkg.checkedQueries,
),
);
}
/** The file is declarative metadata, not an executable registration callback. */
export function parseControllerCatalog(
text: string,
interfaces: readonly InterfaceRevision[],
queries: readonly import("../query/types.js").CheckedQuery[] = [],
): PackageController[] {
if (Buffer.byteLength(text) > 1048576) throw new Error("Controller catalog exceeds 1 MiB");
const value: unknown = JSON.parse(text);
const record = (value: unknown): value is Record<string, unknown> =>
!!value && typeof value === "object" && !Array.isArray(value);
if (
!record(value) ||
value.schemaVersion !== 1 ||
Object.keys(value).some((k) => !["schemaVersion", "controllers"].includes(k)) ||
!Array.isArray(value.controllers) ||
value.controllers.length > 1000
)
throw new Error("Invalid controller catalog: expected schemaVersion 1 and at most 1000 controllers");
const fields = ["id", "subject", "snapshot", "remote", "input"];
for (const entry of value.controllers)
if (
!record(entry) ||
Object.keys(entry).some((key) => ![...fields, "registry"].includes(key)) ||
(entry.registry !== undefined && typeof entry.registry !== "string") ||
fields.some((key) => typeof entry[key] !== "string")
)
throw new Error("Invalid controller entry: expected exactly id, subject, snapshot, remote, input strings");
const controllers = value.controllers as PackageController[];
const issues = validateControllers(controllers, interfaces, "controllers", queries);
if (issues.length) throw new Error(issues.map((i) => `${i.path}: ${i.message}`).join("\n"));
return controllers;
}
+568
View File
@@ -0,0 +1,568 @@
import { createHash } from "node:crypto";
import { setImmediate } from "node:timers/promises";
import type { Binding, Conformance, DependencyBinding, PersistentAttachment, WorkspaceRevision } from "./types.js";
import { validateWorkspaceRevision } from "./validation.js";
/** Content hashing is independent of JSON object insertion order, not array order. */
const compareText = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
export const canonicalJson = (value: unknown): string => {
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (typeof value === "object" && value !== null) {
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
throw new Error("Expected a plain JSON object");
return `{${Object.entries(value)
.filter(([, entry]) => entry !== undefined)
.sort(([a], [b]) => compareText(a, b))
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
.join(",")}}`;
}
throw new Error(`Cannot hash non-JSON value: ${typeof value}`);
};
export const contentDigest = (value: unknown) =>
`sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}`;
/** Same canonical bytes as contentDigest, without monopolizing a live host's
* event loop while verifying large immutable metadata. No validation is skipped.
* This is for asynchronous loading, not a different build/wire identity. */
export async function contentDigestAsync(value: unknown): Promise<string> {
// An explicit stack visits each node once; recursive generator delegation
// otherwise forwards every token through every ancestor of a deep plan.
type Frame = { kind: "value" | "array" | "object"; value: unknown; index?: number; keys?: string[]; wrote?: boolean };
const hash = createHash("sha256"),
stack: Frame[] = [{ kind: "value", value }],
active = new WeakSet<object>();
let buffer = "",
sliceStarted = performance.now(),
steps = 0;
const append = (part: string) => {
buffer += part;
if (buffer.length >= 65536) {
hash.update(buffer);
buffer = "";
}
};
while (stack.length) {
const frame = stack[stack.length - 1]!;
if (frame.kind === "value") {
const value = frame.value;
if (value !== null && typeof value === "object") {
if (active.has(value)) throw new Error("Cannot hash cyclic JSON values");
active.add(value);
frame.index = 0;
if (Array.isArray(value)) {
frame.kind = "array";
append("[");
} else {
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
throw new Error("Expected a plain JSON object");
frame.kind = "object";
frame.keys = Object.keys(value).sort(compareText);
frame.wrote = false;
append("{");
}
} else {
append(canonicalJson(value));
stack.pop();
}
} else if (frame.kind === "array") {
const array = frame.value as unknown[];
if (frame.index === array.length) {
append("]");
active.delete(array);
stack.pop();
} else {
const index = frame.index!;
frame.index = index + 1;
if (index) append(",");
// Match canonicalJson's Array.map behavior for sparse arrays too.
if (index in array) stack.push({ kind: "value", value: array[index] });
}
} else {
const object = frame.value as Record<string, unknown>;
if (frame.index === frame.keys!.length) {
append("}");
active.delete(object);
stack.pop();
} else {
const index = frame.index!;
frame.index = index + 1;
const key = frame.keys![index]!,
entry = object[key];
if (entry !== undefined) {
if (frame.wrote) append(",");
frame.wrote = true;
append(JSON.stringify(key) + ":");
stack.push({ kind: "value", value: entry });
}
}
}
if (++steps % 512 === 0 && performance.now() - sliceStarted >= 8) {
await setImmediate();
sliceStarted = performance.now();
}
}
hash.update(buffer);
return `sha256:${hash.digest("hex")}`;
}
const semantic = (value: unknown): unknown => {
if (Array.isArray(value)) return value.map(semantic);
if (value && typeof value === "object")
return Object.fromEntries(
Object.entries(value)
.filter(([key, entry]) => entry !== undefined && key !== "displayName" && key !== "documentation")
// These are authored data/maps, not schema nodes. A user field literally
// named displayName or documentation is semantic and must stay in the hash.
.map(([key, entry]) => [key, key === "defaultValue" || key === "fields" ? entry : semantic(entry)]),
);
return value;
};
const sorted = <T>(entries: readonly T[], key: (entry: T) => string) =>
[...entries].sort((a, b) => compareText(key(a), key(b)));
export const conformanceIdentity = (entry: Conformance): string =>
entry.id ?? `legacy:${entry.atomId}:${entry.interfaceRevisionId}`;
export type StorageContract = {
id: string;
ownerId: string;
kind: "state" | "edge";
digest: string;
definition: unknown;
};
/** Automatic evolution preserves values; it never interprets migration code or
* guesses that a new nominal message descriptor means the same representation. */
export const storageChangeRequiresMigration = (
previous: StorageContract | undefined,
next: StorageContract | undefined,
oldAtomIds: ReadonlySet<string>,
): boolean => {
if (!next) return true;
const after = next.definition as PersistentAttachment;
if (!previous) {
if (after.kind === "state")
return (
oldAtomIds.has(after.attachedTo) && after.defaultValue === undefined && after.valueType.kind !== "optional"
);
return after.endpoints.some(
(endpoint) =>
endpoint.cardinality === "exactly-one" &&
(endpoint.constraint.kind !== "atom" || oldAtomIds.has(endpoint.constraint.atomId)),
);
}
if (previous.ownerId !== next.ownerId || previous.kind !== next.kind) return true;
const before = previous.definition as PersistentAttachment;
if (before.kind === "state" && after.kind === "state") {
// Capture materializes old defaults, so changing a default affects only
// newly constructed objects, not existing sparse state.
const { defaultValue: _beforeDefault, ...beforeStorage } = before;
const { defaultValue: _afterDefault, ...afterStorage } = after;
return canonicalJson(beforeStorage) !== canonicalJson(afterStorage);
}
return canonicalJson(before) !== canonicalJson(after);
};
export const storageContracts = (workspace: WorkspaceRevision): StorageContract[] => {
const result: StorageContract[] = [];
const add = (attachment: PersistentAttachment, ownerId: string) => {
const definition = semantic(attachment);
result.push({
id: attachment.id,
ownerId,
kind: attachment.kind,
definition,
digest: contentDigest({ ownerId, definition }),
});
};
for (const attachment of workspace.sharedAttachments) add(attachment, "legacy:workspace");
for (const conformance of workspace.conformances)
for (const attachment of conformance.privateAttachments) add(attachment, conformanceIdentity(conformance));
return sorted(result, (entry) => entry.id);
};
type GraphNode = { value: unknown; dependencies: Set<string>; reviewProviders: Set<string> };
export type RuntimeContract = {
groupId: string;
packageId: string;
packageRevisionId: string;
digest: string;
reviewProviders: string[];
dependencies: Array<{ id: string; digest: string }>;
};
/** Build only outbound execution dependencies. Incoming callers never retain or invalidate a provider. */
export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[] => {
const nodes = new Map<string, GraphNode>();
const node = (key: string, value: unknown) => {
const result = { value: semantic(value), dependencies: new Set<string>(), reviewProviders: new Set<string>() };
nodes.set(key, result);
return result;
};
const conformanceKey = (atom: string, iface: string) => `conformance:${atom}:${iface}`;
for (const storage of storageContracts(workspace)) node(`attachment:${storage.id}`, storage);
for (const iface of workspace.interfaceImports)
node(`interface:${iface.revisionId}`, {
...iface,
members: sorted(iface.members, (entry) => entry.id).map((entry) => ({
...entry,
operations: sorted(entry.operations, (operation) => operation.id),
})),
});
for (const pkg of workspace.packageImports)
node(`package:${pkg.revisionId}`, {
...pkg,
semanticMajor: pkg.semanticMajor ?? 1,
exports: sorted(pkg.exports, (entry) => entry.id).map((entry) => ({
...entry,
dependencyPorts: sorted(entry.dependencyPorts, (port) => port.id),
})),
});
const dependency = (parent: GraphNode, binding: DependencyBinding, atomId: string, reviews?: Set<string>) => {
if (binding.kind === "query") {
const pkg = workspace.packageImports.find((pkg) => pkg.revisionId === binding.packageRevisionId);
const query = pkg?.checkedQueries?.find((query) => query.declaration.id === binding.queryId);
if (!query) throw new Error(`Missing checked query contract ${binding.packageRevisionId}:${binding.queryId}`);
const key = `query:${binding.packageRevisionId}:${binding.queryId}`;
const queryNode = nodes.get(key) ?? node(key, query);
parent.dependencies.add(key);
for (const effect of query.effects) {
queryNode.dependencies.add(`interface:${effect.interfaceRevisionId}`);
for (const conformance of workspace.conformances.filter(
(entry) => entry.interfaceRevisionId === effect.interfaceRevisionId,
))
queryNode.dependencies.add(conformanceKey(conformance.atomId, conformance.interfaceRevisionId));
}
}
if (binding.kind === "state") parent.dependencies.add(`attachment:${binding.slotId}`);
if (binding.kind === "edge") parent.dependencies.add(`attachment:${binding.edgeTypeId}`);
if (binding.kind === "creation") {
for (const owner of binding.owners)
parent.dependencies.add(conformanceKey(binding.atomId, owner.interfaceRevisionId));
}
if (binding.kind === "constructor") {
parent.dependencies.add(`constructor:${binding.atomId}`);
const ctor = workspace.constructors.find((entry) => entry.atomId === binding.atomId);
const pkg = workspace.packageImports.find((entry) => entry.revisionId === ctor?.packageRevisionId);
if (pkg) reviews?.add(pkg.packageId);
}
if (binding.kind !== "constructor" && binding.via) parent.dependencies.add(`attachment:${binding.via.edgeTypeId}`);
if (binding.kind === "interface") {
parent.dependencies.add(`interface:${binding.interfaceRevisionId}`);
// An edge traversal may select any matching target. Conservatively include every possible witness.
for (const conformance of workspace.conformances) {
if (
conformance.interfaceRevisionId === binding.interfaceRevisionId &&
(binding.via || conformance.atomId === atomId)
) {
parent.dependencies.add(conformanceKey(conformance.atomId, conformance.interfaceRevisionId));
reviews?.add(conformanceIdentity(conformance));
for (const operation of conformance.operationBindings) {
let provider: Binding | undefined = operation.binding;
const visited = new Set<string>();
while (provider?.kind === "interface") {
const forward: Extract<Binding, { kind: "interface" }> = provider;
const key = `${provider.interfaceRevisionId}\0${provider.operationId}`;
if (visited.has(key)) throw new Error("Forwarding cycle in evolution contract");
visited.add(key);
const target = workspace.conformances.find(
(c) => c.atomId === conformance.atomId && c.interfaceRevisionId === forward.interfaceRevisionId,
);
if (!target) throw new Error("Missing forwarding conformance in evolution contract");
reviews?.add(conformanceIdentity(target));
provider = target.operationBindings.find((entry) => entry.operationId === forward.operationId)?.binding;
}
if (provider?.kind !== "package") continue;
const revisionId = provider.packageRevisionId;
const pkg = workspace.packageImports.find((entry) => entry.revisionId === revisionId);
if (pkg) reviews?.add(pkg.packageId);
}
}
}
}
};
const binding = (parent: GraphNode, value: Binding, atomId: string, context: unknown) => {
if (value.kind === "lifecycle") return;
if (value.kind === "journal" || value.kind === "subscription") {
dependency(parent, { kind: "state", slotId: value.slotId }, atomId);
return;
}
if (value.kind !== "package") {
dependency(parent, value, atomId);
return;
}
const packageNode = nodes.get(`package:${value.packageRevisionId}`)!;
parent.dependencies.add(`package:${value.packageRevisionId}`);
const normalized = { ...value, dependencies: sorted(value.dependencies, (entry) => entry.portId) };
const key = `binding:${contentDigest({ atomId, context, value: semantic(normalized) })}`;
const bound = node(key, { atomId, context, binding: normalized });
packageNode.dependencies.add(key);
for (const port of value.dependencies) dependency(bound, port.binding, atomId, packageNode.reviewProviders);
};
for (const conformance of workspace.conformances) {
const parent = node(conformanceKey(conformance.atomId, conformance.interfaceRevisionId), {
id: conformanceIdentity(conformance),
semanticMajor: conformance.semanticMajor ?? 1,
atomId: conformance.atomId,
interfaceRevisionId: conformance.interfaceRevisionId,
operations: sorted(conformance.operationBindings, (entry) => entry.operationId),
...(conformance.moduleBindings?.length
? { modules: sorted(conformance.moduleBindings, (entry) => entry.memberId) }
: {}),
...(conformance.initialization
? { initialization: sorted(conformance.initialization, (entry) => entry.input) }
: {}),
materializations: sorted(conformance.relationshipMaterializations, (entry) => entry.memberId),
});
parent.dependencies.add(`interface:${conformance.interfaceRevisionId}`);
for (const module of conformance.moduleBindings ?? [])
parent.dependencies.add(`package:${module.packageRevisionId}`);
const contract = workspace.interfaceImports.find((entry) => entry.revisionId === conformance.interfaceRevisionId);
for (const required of contract?.requiredInterfaces ?? [])
parent.dependencies.add(conformanceKey(conformance.atomId, required));
for (const attachment of conformance.privateAttachments) parent.dependencies.add(`attachment:${attachment.id}`);
for (const operation of conformance.operationBindings)
binding(parent, operation.binding, conformance.atomId, {
conformanceId: conformanceIdentity(conformance),
semanticMajor: conformance.semanticMajor ?? 1,
operationId: operation.operationId,
});
for (const materialization of conformance.relationshipMaterializations) {
parent.dependencies.add(`constructor:${materialization.constructorAtomId}`);
parent.dependencies.add(`attachment:${materialization.edgeTypeId}`);
}
}
for (const constructor of workspace.constructors) {
const parent = node(`constructor:${constructor.atomId}`, constructor);
binding(parent, { kind: "package", ...constructor }, constructor.atomId, { constructor: constructor.atomId });
}
const counts = new Map<string, number>();
for (const pkg of workspace.packageImports) counts.set(pkg.packageId, (counts.get(pkg.packageId) ?? 0) + 1);
return sorted(
workspace.packageImports.map((pkg): RuntimeContract => {
const visited = new Set<string>();
const walk = (key: string) => {
if (visited.has(key)) return;
const entry = nodes.get(key);
if (!entry) throw new Error(`Unresolved execution dependency ${key}`);
visited.add(key);
for (const target of entry.dependencies) walk(target);
};
walk(`package:${pkg.revisionId}`);
const dependencies = [...visited].sort().map((id) => ({ id, digest: contentDigest(nodes.get(id)!.value) }));
return {
groupId: counts.get(pkg.packageId) === 1 ? pkg.packageId : `${pkg.packageId}#${pkg.revisionId}`,
packageId: pkg.packageId,
packageRevisionId: pkg.revisionId,
digest: contentDigest(dependencies),
reviewProviders: [...nodes.get(`package:${pkg.revisionId}`)!.reviewProviders].sort(),
dependencies,
};
}),
(entry) => entry.groupId,
);
};
export type EvolutionReview = {
requirementDigest: string;
decision: "changed" | "accepted-unchanged";
rationale: string;
agentId: string;
};
export type ReviewRequirement = {
consumerId: string;
providerId: string;
oldMajor: number;
newMajor: number;
requirementDigest: string;
};
export type RuntimeAction = {
groupId: string;
action: "keep" | "start" | "replace" | "retire";
previous?: RuntimeContract;
candidate?: RuntimeContract;
reasons: string[];
};
export type EvolutionReport = {
schemaVersion: 1;
baselineDigest: string | null;
candidateDigest: string;
checkerVersion: string;
runtimeActions: RuntimeAction[];
storageChanges: Array<{
id: string;
kind: "add" | "remove" | "change";
requiresMigration: boolean;
previous?: StorageContract;
candidate?: StorageContract;
}>;
migrationRequired: string[];
reviews: Array<ReviewRequirement & { accepted: boolean }>;
packageChecks: Array<{ groupId: string; contractDigest: string }>;
blockers: string[];
};
export const planEvolution = (
baseline: WorkspaceRevision | null,
candidate: WorkspaceRevision,
options: { reviews?: EvolutionReview[]; allowLegacy?: boolean } = {},
): EvolutionReport => {
const issues = validateWorkspaceRevision(candidate);
if (issues.length)
throw new Error(
`Invalid candidate workspace:\n${issues.map((entry) => `${entry.path}: ${entry.message}`).join("\n")}`,
);
if (baseline && baseline.workspaceId !== candidate.workspaceId)
throw new Error("Cannot evolve a different workspace");
const candidateDigest = contentDigest(candidate);
const checkerVersion = "quixos-evolution-v1";
const blockers: string[] = [];
if (!options.allowLegacy) {
if (candidate.sharedAttachments.length)
blockers.push("Assign legacy workspace-shared attachments to explicit conformance owners");
for (const entry of candidate.conformances)
if (!entry.id)
blockers.push(`Conformance ${entry.atomId} as ${entry.interfaceRevisionId} requires an authored ID`);
}
const previousRuntimes = new Map((baseline ? runtimeContracts(baseline) : []).map((entry) => [entry.groupId, entry]));
const nextRuntimes = new Map(runtimeContracts(candidate).map((entry) => [entry.groupId, entry]));
const runtimeActions: RuntimeAction[] = [...new Set([...previousRuntimes.keys(), ...nextRuntimes.keys()])]
.sort()
.map((groupId) => {
const previous = previousRuntimes.get(groupId),
next = nextRuntimes.get(groupId);
const before = new Map(previous?.dependencies.map((entry) => [entry.id, entry.digest]));
const after = new Map(next?.dependencies.map((entry) => [entry.id, entry.digest]));
const reasons = [...new Set([...before.keys(), ...after.keys()])]
.sort()
.filter((id) => before.get(id) !== after.get(id));
return {
groupId,
action: !previous ? "start" : !next ? "retire" : previous.digest === next.digest ? "keep" : "replace",
...(previous ? { previous } : {}),
...(next ? { candidate: next } : {}),
reasons,
};
});
const beforeStorage = new Map((baseline ? storageContracts(baseline) : []).map((entry) => [entry.id, entry]));
const afterStorage = new Map(storageContracts(candidate).map((entry) => [entry.id, entry]));
const storageChanges: EvolutionReport["storageChanges"] = [];
for (const id of [...new Set([...beforeStorage.keys(), ...afterStorage.keys()])].sort()) {
const previous = beforeStorage.get(id),
next = afterStorage.get(id);
if (previous?.digest !== next?.digest)
storageChanges.push({
id,
kind: !previous ? "add" : !next ? "remove" : "change",
requiresMigration: storageChangeRequiresMigration(
previous,
next,
new Set(baseline?.atoms.map((atom) => atom.id) ?? []),
),
...(previous ? { previous } : {}),
...(next ? { candidate: next } : {}),
});
}
const semanticReview = assessSemanticReviews(baseline, candidate, options.reviews ?? [], checkerVersion, [
...nextRuntimes.values(),
]);
const reviews = semanticReview.reviews;
blockers.push(...semanticReview.blockers);
return {
schemaVersion: 1,
baselineDigest: baseline ? contentDigest(baseline) : null,
candidateDigest,
checkerVersion,
runtimeActions,
storageChanges,
migrationRequired: storageChanges.filter((entry) => entry.requiresMigration).map((entry) => entry.id),
reviews,
packageChecks: runtimeActions
.filter((entry) => entry.candidate && entry.action !== "keep")
.map((entry) => ({ groupId: entry.groupId, contractDigest: entry.candidate!.digest })),
blockers,
};
};
/** Review dependency contracts, without making any storage or process-placement decision. */
function assessSemanticReviews(
baseline: WorkspaceRevision | null,
candidate: WorkspaceRevision,
receipts: readonly EvolutionReview[],
checkerVersion: string,
consumers: readonly RuntimeContract[],
): Pick<EvolutionReport, "reviews" | "blockers"> {
const blockers: string[] = [];
const providers = (workspace: WorkspaceRevision) => [
...workspace.packageImports.map((entry) => ({
id: entry.packageId as string,
revision: entry.revisionId as string,
major: entry.semanticMajor ?? 1,
node: `package:${entry.revisionId}`,
digest: contentDigest(entry),
})),
...workspace.conformances.map((entry) => ({
id: conformanceIdentity(entry),
revision: contentDigest(entry),
major: entry.semanticMajor ?? 1,
node: `conformance:${entry.atomId}:${entry.interfaceRevisionId}`,
digest: contentDigest(entry),
})),
];
const oldProviders = baseline ? providers(baseline) : [];
const reviews: EvolutionReport["reviews"] = [];
for (const provider of providers(candidate)) {
const old = oldProviders.filter((entry) => entry.id === provider.id);
if (old.length > 1) {
blockers.push(`Ambiguous semantic-major lineage for ${provider.id}`);
continue;
}
if (!old[0] || old[0].major === provider.major) continue;
if (provider.major < old[0].major) blockers.push(`Semantic major decreases for ${provider.id}`);
for (const consumer of consumers) {
if (consumer.packageId === provider.id || !consumer.reviewProviders.includes(provider.id)) continue;
const requirement = {
consumerId: consumer.groupId,
providerId: provider.id,
oldMajor: old[0].major,
newMajor: provider.major,
};
const requirementDigest = contentDigest({
...requirement,
oldProvider: old[0].digest,
newProvider: provider.digest,
consumer: consumer.digest,
checkerVersion,
});
const accepted = receipts.some(
(entry) =>
entry.requirementDigest === requirementDigest &&
["changed", "accepted-unchanged"].includes(entry.decision) &&
entry.rationale.trim() &&
entry.agentId.trim(),
);
reviews.push({ ...requirement, requirementDigest, accepted });
if (!accepted) blockers.push(`Semantic-major review required: ${consumer.groupId} consumes ${provider.id}`);
}
}
return { reviews, blockers };
}
export function reviewSemanticMajors(
baseline: WorkspaceRevision | null,
candidate: WorkspaceRevision,
receipts: readonly EvolutionReview[] = [],
) {
for (const workspace of baseline ? [baseline, candidate] : [candidate]) {
const issues = validateWorkspaceRevision(workspace);
if (issues.length)
throw new Error("Invalid semantic-review workspace: " + issues.map((issue) => issue.message).join("; "));
}
if (baseline && baseline.workspaceId !== candidate.workspaceId)
throw new Error("Cannot review a different workspace");
const checkerVersion = "quixos-semantic-review-v1";
const report = assessSemanticReviews(baseline, candidate, receipts, checkerVersion, runtimeContracts(candidate));
return {
checkerVersion,
baselineDigest: baseline ? contentDigest(baseline) : null,
candidateDigest: contentDigest(candidate),
...report,
};
}
+178
View File
@@ -0,0 +1,178 @@
import type { ExecutionContract, InterfaceOperation, WorkspaceRevision } from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
import { resolveForwardedBinding } from "./forwarding.js";
/** Validate declared effects and targets; this is not a JavaScript purity proof. */
export function validateExecutionContracts(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [];
const fail = (path: string, message: string) => issues.push({ code: "invalid-execution-contract", path, message });
const interfaces = new Map(workspace.interfaceImports.map((i) => [i.revisionId, i]));
const packages = new Map(workspace.packageImports.map((p) => [p.revisionId, p]));
const operations = (id: InterfaceOperation["id"], revision: WorkspaceRevision["interfaceImports"][number]) =>
(revision.template?.members ?? revision.members).flatMap((m) => m.operations).find((o) => o.id === id);
const check = (contract: ExecutionContract | undefined, path: string) => {
if (!contract || contract.kind === "remote") return;
if (
!["snapshot", "transaction"].includes(contract.kind) ||
!Array.isArray(contract.targets) ||
!contract.targets.length ||
contract.targets.some((t) => typeof t !== "string" || !t.trim()) ||
new Set(contract.targets).size !== contract.targets.length
)
fail(path, "Portable execution requires a nonempty set of distinct target names");
};
const supports = (
requested: ExecutionContract | undefined,
provided: ExecutionContract | undefined,
path: string,
) => {
if (!requested || requested.kind === "remote") return;
if (!provided || provided.kind === "remote") {
fail(path, "Portable execution depends on a remote-only operation");
return;
}
if (requested.kind === "snapshot" && provided.kind !== "snapshot")
fail(path, "Snapshot execution cannot perform a transaction");
};
for (const revision of workspace.interfaceImports)
for (const member of revision.template?.members ?? revision.members) {
for (const operation of member.operations) {
check(operation.execution, `${revision.revisionId}/${operation.id}`);
if (operation.execution && operation.execution.kind !== "remote" && operation.mode !== "call")
fail(
`${revision.revisionId}/${operation.id}`,
"Watch lifecycle operations are remote; portable reads use snapshots",
);
}
if (member.kind !== "operation" && member.kind !== "module" && member.queryRead?.execution === "portable") {
const getter = member.operations.find((o) => o.displayName === (member.kind === "value" ? "get" : "resolve"));
if (getter?.execution?.kind !== "snapshot")
fail(`${revision.revisionId}/${member.id}`, "Queryable portable members require a snapshot getter");
}
}
for (const revision of workspace.packageImports) {
for (const entry of [...revision.exports, ...(revision.genericExports ?? [])]) {
const path = `${revision.revisionId}/${entry.id}`;
check(entry.execution, path);
for (const port of entry.dependencyPorts) {
const requirement = port.requirement;
const portPath = `${path}/${port.id}`;
if (requirement.kind === "interface") {
const contract = interfaces.get(
"interfaceRevisionId" in requirement
? requirement.interfaceRevisionId
: requirement.application.definitionId,
);
const selected = requirement.operations;
if (requirement.receiver === "input") {
if (!entry.execution || entry.execution.kind === "remote" || !selected?.length)
fail(portPath, "Reference-selected calls require portable execution and an explicit selection");
for (const id of selected ?? []) {
const operation = contract && operations(id, contract);
if (!operation?.execution || operation.execution.kind === "remote" || operation.scope === "class")
fail(portPath, "Reference-selected dependencies require portable object operations");
}
}
if (selected && (!selected.length || new Set(selected).size !== selected.length))
fail(portPath, "Selected interface operations must be nonempty and distinct");
for (const id of selected ?? [])
if (contract && !operations(id, contract)) fail(portPath, `Unknown selected operation ${id}`);
if (entry.execution && entry.execution.kind !== "remote") {
if (!selected) fail(portPath, "Portable interface ports must select their required operations");
for (const id of selected ?? [])
if (contract) supports(entry.execution, operations(id, contract)?.execution, portPath);
}
}
if (!entry.execution || entry.execution.kind === "remote") continue;
if (requirement.kind === "state" || requirement.kind === "edge") {
const read = requirement.kind === "state" ? "read" : "resolve";
for (const primitive of requirement.primitives) {
if (primitive.startsWith("watch"))
fail(portPath, "Portable operations cannot acquire watch lifecycle primitives");
else if (entry.execution.kind === "snapshot" && primitive !== read)
fail(portPath, "Snapshot operations cannot acquire write primitives");
}
} else if (requirement.kind === "query") {
const query = packages.get(requirement.packageRevisionId)?.queries?.find((q) => q.id === requirement.queryId);
if (query?.placement?.kind !== "local")
fail(portPath, "Portable operations require a local query dependency");
} else if (requirement.kind === "creation") {
if (entry.execution.kind !== "transaction") fail(portPath, "Object creation requires transaction execution");
} else if (requirement.kind === "reflection") {
if (requirement.selectors.some((selector) => selector !== "presentations"))
fail(portPath, "Only checked presentation metadata is portable");
} else if (requirement.kind !== "interface")
fail(portPath, `Portable execution does not support ${requirement.kind} dependencies`);
}
}
for (const query of revision.queries ?? [])
if (query.placement?.kind === "local") {
const sourceQueryId = query.placement.sourceQueryId;
const source = revision.queries?.find((q) => q.id === sourceQueryId);
if (source?.placement?.kind !== "replica")
fail(`${revision.revisionId}/${query.id}`, "Local queries must name a replica query in the same package");
}
}
for (const conformance of workspace.conformances) {
const revision = interfaces.get(conformance.interfaceRevisionId);
if (!revision) continue;
for (const entry of conformance.operationBindings) {
const operation = operations(entry.operationId, revision);
if (!operation) continue;
const path = `${conformance.atomId}/${revision.revisionId}/${entry.operationId}`;
let binding;
try {
binding = resolveForwardedBinding(
workspace,
conformance.atomId,
revision.revisionId,
entry.operationId,
).binding;
} catch {
continue;
} // Ordinary binding validation reports unresolved references and cycles.
if (entry.binding.kind === "interface") {
const target = interfaces.get(entry.binding.interfaceRevisionId);
if (target) supports(operation.execution, operations(entry.binding.operationId, target)?.execution, path);
}
if (binding.kind === "lifecycle") {
if (
operation.execution?.kind !== "transaction" ||
operation.execution.targets.length !== 1 ||
operation.execution.targets[0] !== "server"
)
fail(
path,
"Deletion requires an explicitly server-only transaction; partial replicas cannot enumerate incoming references",
);
continue;
}
if (!operation.execution || operation.execution.kind === "remote") continue;
if (binding.kind === "journal") {
const expected = binding.primitive === "publish" ? "transaction" : "snapshot";
if (operation.execution.kind !== expected) fail(path, "Native journal operation requires the matching effect");
continue;
}
if (binding.kind === "subscription") {
const expected = binding.primitive === "status" ? "snapshot" : "transaction";
if (
operation.execution.kind !== expected ||
(binding.primitive !== "status" && operation.execution.targets.some((target) => target !== "server"))
)
fail(path, "Subscription controls require authoritative execution; status is a portable snapshot");
continue;
}
if (binding.kind === "package") {
const provider = packages.get(binding.packageRevisionId)?.exports.find((e) => e.id === binding.exportId);
supports(operation.execution, provider?.execution, path);
} else {
const read = binding.kind === "state" ? "read" : "resolve";
if (binding.primitive.startsWith("watch"))
fail(path, "Portable operations cannot bind watch lifecycle primitives");
else if (operation.execution.kind === "snapshot" && binding.primitive !== read)
fail(path, "Snapshot operations cannot bind write primitives");
}
}
}
return issues;
}
+36
View File
@@ -0,0 +1,36 @@
import type {
AtomId,
Binding,
Conformance,
InterfaceRevisionId,
OperationBinding,
OperationId,
WorkspaceRevision,
} from "./types.js";
export type ConcreteBinding = Exclude<Binding, { kind: "interface" }>;
export type ResolvedBinding = { conformance: Conformance; entry: OperationBinding; binding: ConcreteBinding };
/** Follow public operations on the same receiver, never private attachment names. */
export function resolveForwardedBinding(
workspace: Pick<WorkspaceRevision, "conformances">,
atomId: AtomId,
interfaceRevisionId: InterfaceRevisionId,
operationId: OperationId,
): ResolvedBinding {
const visited = new Set<string>();
for (;;) {
const key = `${interfaceRevisionId}\0${operationId}`;
if (visited.has(key)) throw new Error(`Forwarding cycle at ${interfaceRevisionId}/${operationId}`);
visited.add(key);
const conformance = workspace.conformances.find(
(c) => c.atomId === atomId && c.interfaceRevisionId === interfaceRevisionId,
);
const entry = conformance?.operationBindings.find((b) => b.operationId === operationId);
if (!conformance || !entry)
throw new Error(`Missing forwarding target ${atomId} as ${interfaceRevisionId}/${operationId}`);
if (entry.binding.kind !== "interface") return { conformance, entry, binding: entry.binding };
interfaceRevisionId = entry.binding.interfaceRevisionId;
operationId = entry.binding.operationId;
}
}
+159
View File
@@ -0,0 +1,159 @@
import { createHash } from "node:crypto";
import {
capabilityId,
type PackageExport,
type PackageExportId,
type DependencyPort,
type PackageRevision,
} from "./types.js";
import {
TypeSubstitution,
GenericTypeError,
bindTypeParameters,
canonicalTypeArgument,
type TypeParameter,
type ValueTypeExpression,
type ObjectTypeExpression,
type InterfaceApplicationExpression,
type GenericTypeEnvironment,
type ClosedTypeArgument,
type ValueAliasDefinition,
} from "./generics.js";
export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
requirement:
| Extract<DependencyPort["requirement"], { kind: "query" | "reflection" }>
| {
kind: "state";
valueType: ValueTypeExpression;
primitives: Extract<DependencyPort["requirement"], { kind: "state" }>["primitives"];
}
| {
kind: "edge";
target: ObjectTypeExpression;
cardinality: Extract<DependencyPort["requirement"], { kind: "edge" }>["cardinality"];
keyType?: "string" | "boolean" | "int64";
primitives: Extract<DependencyPort["requirement"], { kind: "edge" }>["primitives"];
}
| {
kind: "interface";
application: InterfaceApplicationExpression;
operations?: import("./types.js").OperationId[];
receiver?: "input";
}
| { kind: "creation"; target: ObjectTypeExpression; inputType: ValueTypeExpression }
| { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression };
};
export interface GenericPackageExport {
execution?: import("./types.js").ExecutionContract;
id: PackageExportId;
displayName: string;
parameters: TypeParameter[];
kind: "operation" | "function";
inputType: ValueTypeExpression;
outputType: ValueTypeExpression;
eventType?: ValueTypeExpression;
mode?: Extract<PackageExport, { kind: "operation" }>["mode"];
receiverRequirement:
| { kind: "any-object" }
| { kind: "target"; target: ObjectTypeExpression }
| { kind: "interfaces"; interfaces: InterfaceApplicationExpression[] };
dependencyPorts: GenericDependencyPort[];
aliases: ValueAliasDefinition[];
}
/** A closed manifest is a Nix build input, never a mutable source checkout. */
export const specializePackageExport = (
pkg: PackageRevision,
definition: GenericPackageExport,
arguments_: ClosedTypeArgument[],
environment: GenericTypeEnvironment,
): PackageExport => {
const lexical = { ...environment, aliases: new Map(definition.aliases.map((alias) => [alias.id, alias])) };
const bindings = bindTypeParameters(definition.parameters, arguments_, lexical, definition.displayName);
const substitution = new TypeSubstitution({ ...lexical, arguments: bindings });
const digest = createHash("sha256")
.update(
JSON.stringify([
"quixos-package-specialization-v2",
pkg.revisionId,
definition.id,
arguments_.map(canonicalTypeArgument),
environment.self ?? null,
]),
)
.digest("hex");
const ports: DependencyPort[] = definition.dependencyPorts.map((port) => {
const requirement = port.requirement;
switch (requirement.kind) {
case "reflection":
case "query":
return { ...port, requirement };
case "state":
return { ...port, requirement: { ...requirement, valueType: substitution.value(requirement.valueType) } };
case "edge":
return { ...port, requirement: { ...requirement, target: substitution.object(requirement.target) } };
case "interface":
return {
...port,
requirement: {
kind: "interface",
interfaceRevisionId: substitution.application(requirement.application),
...(requirement.operations ? { operations: requirement.operations } : {}),
...(requirement.receiver ? { receiver: requirement.receiver } : {}),
},
};
case "creation":
case "constructor": {
const target = substitution.object(requirement.target);
if (target.kind !== "atom")
throw new GenericTypeError(
"constructor-target",
port.displayName,
"Constructor ports require a concrete atom argument, not an interface view",
);
return {
...port,
requirement: {
kind: requirement.kind,
atomId: target.atomId,
inputType: substitution.value(requirement.inputType),
},
};
}
}
});
const common = {
...(definition.execution ? { execution: structuredClone(definition.execution) } : {}),
id: capabilityId.packageExport(`export-application:sha256:${digest}`),
displayName: `${definition.displayName}$${digest}`,
inputType: substitution.value(definition.inputType),
outputType: substitution.value(definition.outputType),
dependencyPorts: ports,
application: {
exportId: definition.id,
arguments: structuredClone(arguments_),
...(environment.self ? { self: environment.self } : {}),
},
};
if (definition.kind === "function") return { ...common, kind: "function" };
const receiver = definition.receiverRequirement;
const target = receiver.kind === "target" ? substitution.object(receiver.target) : undefined;
return {
...common,
kind: "operation",
mode: definition.mode!,
...(definition.eventType ? { eventType: substitution.value(definition.eventType) } : {}),
receiverRequirement: target
? target.kind === "atom"
? { kind: "exact-atom", atomId: target.atomId }
: { kind: "all-interfaces", interfaceRevisionIds: [target.interfaceRevisionId] }
: receiver.kind === "interfaces"
? {
kind: "all-interfaces",
interfaceRevisionIds: receiver.interfaces.map((value) => substitution.application(value)),
}
: { kind: "any-object" },
};
};
+457
View File
@@ -0,0 +1,457 @@
import { createHash } from "node:crypto";
import {
capabilityId,
type AtomId,
type InterfaceMember,
type InterfaceOperation,
type InterfaceRevision,
type InterfaceRevisionId,
type ObjectExpectation,
type SourceRevision,
type ValueType,
} from "./types.js";
/** Authoring-only expressions. Installed ValueType deliberately has no variable case. */
export type ValueTypeExpression =
| Extract<ValueType, { kind: "builtin" | "scalar" | "message" }>
| { kind: "parameter"; parameterId: string }
| { kind: "record"; fields: Record<string, ValueTypeExpression> }
| { kind: "variant"; id: string; cases: Record<string, { id: string; value: ValueTypeExpression }> }
| { kind: "list" | "optional" | "change"; value: ValueTypeExpression }
| { kind: "object-ref"; expectation: ObjectTypeExpression }
| { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] };
export type ObjectTypeExpression =
| { kind: "atom"; atomId: AtomId }
| { kind: "interface"; interfaceRevisionId: InterfaceRevisionId }
| { kind: "parameter"; parameterId: string }
| { kind: "self" }
| { kind: "application"; application: InterfaceApplicationExpression };
export type TypeArgumentExpression =
| { kind: "value"; type: ValueTypeExpression }
| { kind: "object"; target: ObjectTypeExpression };
export type ClosedTypeArgument = { kind: "value"; type: ValueType } | { kind: "object"; target: ObjectExpectation };
export interface InterfaceApplicationExpression {
definitionId: InterfaceRevisionId;
arguments: TypeArgumentExpression[];
}
export type TypeParameter =
| { id: string; name: string; kind: "value"; storable?: boolean }
| { id: string; name: string; kind: "object"; implements: InterfaceApplicationExpression[] };
export interface ValueAliasDefinition {
id: string;
parameters: TypeParameter[];
body: ValueTypeExpression;
}
export interface GenericInterfaceTemplate {
parameters: TypeParameter[];
members: InterfaceMember<ValueTypeExpression, ObjectTypeExpression>[];
requires: InterfaceApplicationExpression[];
usesSelf: boolean;
aliases?: ValueAliasDefinition[];
}
/** Instantiation produces the existing closed runtime IR, with explicit provenance. */
export const instantiateInterface = (
definition: InterfaceRevision,
arguments_: readonly ClosedTypeArgument[],
environment: GenericTypeEnvironment,
): InterfaceRevision => {
const template = definition.template;
if (!template) {
if (arguments_.length) fail("type-arity", definition.displayName, "Non-generic interface takes no type arguments");
return definition;
}
const lexicalEnvironment = {
...environment,
aliases: new Map((template.aliases ?? []).map((alias) => [alias.id, alias])),
};
const argumentsMap = bindTypeParameters(template.parameters, arguments_, lexicalEnvironment, definition.displayName);
if (template.usesSelf && !environment.self)
fail("unbound-self", definition.displayName, "Self requires an implementing atom");
const substitution = new TypeSubstitution({ ...lexicalEnvironment, arguments: argumentsMap });
const operation = (entry: InterfaceOperation<ValueTypeExpression>): InterfaceOperation => ({
...entry,
inputType: substitution.value(entry.inputType, `${definition.displayName}.${entry.displayName}.input`),
outputType: substitution.value(entry.outputType, `${definition.displayName}.${entry.displayName}.output`),
eventType: entry.eventType
? substitution.value(entry.eventType, `${definition.displayName}.${entry.displayName}.event`)
: undefined,
});
const members: InterfaceMember[] = template.members.map((member) => {
const common = { id: member.id, displayName: member.displayName, operations: member.operations.map(operation) };
switch (member.kind) {
case "module":
return {
...common,
kind: "module",
contract: member.contract,
...(member.subject ? { subject: substitution.object(member.subject, member.displayName) } : {}),
};
case "value":
return {
...common,
kind: "value",
...(member.nativeField ? { nativeField: true as const } : {}),
...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}),
valueType: substitution.value(member.valueType, member.displayName),
};
case "relationship":
return {
...common,
kind: "relationship",
...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}),
target: substitution.object(member.target, member.displayName),
cardinality: member.cardinality,
ordered: member.ordered,
...(member.keyType ? { keyType: member.keyType } : {}),
};
case "operation":
return {
...common,
kind: "operation",
inputType: substitution.value(member.inputType, member.displayName),
outputType: substitution.value(member.outputType, member.displayName),
};
}
});
const application: AppliedInterfaceIdentity = {
definitionId: definition.revisionId,
source: definition.source,
arguments: structuredClone([...arguments_]),
...(template.usesSelf ? { self: environment.self } : {}),
};
return {
interfaceId: definition.interfaceId,
revisionId: appliedInterfaceId(application),
displayName: definition.displayName,
source: definition.source,
members,
application,
requiredInterfaces: template.requires.map((required) => substitution.application(required)),
};
};
export interface GenericTypeEnvironment {
arguments: ReadonlyMap<string, ClosedTypeArgument>;
self?: AtomId;
aliases?: ReadonlyMap<string, ValueAliasDefinition>;
/** Resolves only checked declarations, never a caller-supplied runtime type string. */
applyInterface: (definitionId: InterfaceRevisionId, arguments_: readonly ClosedTypeArgument[]) => InterfaceRevisionId;
/** Proof in the candidate, not an authorization grant. */
implementsInterface: (target: ObjectExpectation, required: InterfaceRevisionId) => boolean;
/** External codecs must explicitly declare reference-free persistence support. */
storableMessage?: (descriptorId: string) => boolean;
}
export class GenericTypeError extends Error {
constructor(
readonly code: string,
readonly path: string,
message: string,
) {
super(`${path}: ${message}`);
this.name = "GenericTypeError";
}
}
const fail = (code: string, path: string, message: string): never => {
throw new GenericTypeError(code, path, message);
};
/** One budget across nested aliases/substitutions, including concrete arguments. */
class Budget {
private remaining = 10000;
enter(path: string, depth: number) {
if (depth > 128 || --this.remaining < 0)
fail("type-complexity-limit", path, "Type exceeds the depth or expansion budget");
}
}
export const isStorableType = (
type: ValueType,
storableMessage: (descriptorId: string) => boolean = () => false,
): boolean => {
const budget = new Budget();
const visit = (value: ValueType, depth: number): boolean => {
budget.enter("storable", depth);
switch (value.kind) {
case "scalar":
return true;
case "builtin":
return value.name === "unit";
case "message":
return storableMessage(value.descriptorId);
case "object-ref":
return false;
case "list":
case "optional":
return visit(value.value, depth + 1);
case "change":
return false;
case "record":
return Object.values(value.fields).every((field) => visit(field, depth + 1));
case "variant":
return Object.values(value.cases).every((entry) => visit(entry.value, depth + 1));
}
};
return visit(type, 0);
};
/** Canonical closed-type encoding, independent of record insertion order. */
export const canonicalTypeArgument = (argument: ClosedTypeArgument): string => {
const budget = new Budget();
const target = (value: ObjectExpectation): unknown => {
switch (value.kind) {
case "atom":
return ["atom", value.atomId];
case "interface":
return ["interface", value.interfaceRevisionId];
default:
return fail("unresolved-type", "argument", "Expected a closed object target");
}
};
const type = (value: ValueType, depth: number): unknown => {
budget.enter("argument", depth);
switch (value.kind) {
case "builtin":
case "scalar":
return [value.kind, value.name];
case "message":
return ["message", value.descriptorId];
case "variant":
return [
"variant",
value.id,
Object.keys(value.cases)
.sort()
.map((label) => [label, value.cases[label].id, type(value.cases[label].value, depth + 1)]),
];
case "object-ref":
return ["object-ref", target(value.expectation)];
case "change":
case "list":
case "optional":
return [value.kind, type(value.value, depth + 1)];
case "record":
return [
"record",
Object.keys(value.fields)
.sort()
.map((key) => [key, type(value.fields[key], depth + 1)]),
];
default:
return fail("unresolved-type", "argument", "Expected a closed value type");
}
};
switch (argument.kind) {
case "value":
return JSON.stringify(["value", type(argument.type, 0)]);
case "object":
return JSON.stringify(["object", target(argument.target)]);
default:
return fail("invalid-kind", "argument", "Expected a value or object type argument");
}
};
export interface AppliedInterfaceIdentity {
definitionId: InterfaceRevisionId;
source: SourceRevision;
arguments: ClosedTypeArgument[];
/** Only include Self when it is actually part of the closed contract. */
self?: AtomId;
}
export const appliedInterfaceId = (application: AppliedInterfaceIdentity): InterfaceRevisionId => {
const encoding = JSON.stringify([
"quixos-applied-interface-v2",
application.definitionId,
application.arguments.map(canonicalTypeArgument),
application.self ?? null,
]);
return capabilityId.interfaceRevision(
`interface-application:sha256:${createHash("sha256").update(encoding).digest("hex")}`,
);
};
export class TypeSubstitution {
private readonly budget = new Budget();
private readonly aliases: string[] = [];
constructor(private environment: GenericTypeEnvironment) {}
argument(expression: TypeArgumentExpression, path = "argument", depth = 0): ClosedTypeArgument {
this.budget.enter(path, depth);
switch (expression.kind) {
case "value":
return { kind: "value", type: this.value(expression.type, path, depth + 1) };
case "object":
return { kind: "object", target: this.object(expression.target, path, depth + 1) };
default:
return fail("invalid-kind", path, "Expected a value or object type argument");
}
}
application(expression: InterfaceApplicationExpression, path = "interface", depth = 0): InterfaceRevisionId {
this.budget.enter(path, depth);
return this.environment.applyInterface(
expression.definitionId,
expression.arguments.map((argument, index) => this.argument(argument, `${path}.arguments[${index}]`, depth + 1)),
);
}
object(expression: ObjectTypeExpression, path = "target", depth = 0): ObjectExpectation {
this.budget.enter(path, depth);
switch (expression.kind) {
case "atom":
return { kind: "atom", atomId: expression.atomId };
case "interface":
return { kind: "interface", interfaceRevisionId: expression.interfaceRevisionId };
case "self":
return this.environment.self
? { kind: "atom", atomId: this.environment.self }
: fail("unbound-self", path, "Self requires an implementing atom");
case "parameter": {
const argument = this.environment.arguments.get(expression.parameterId);
if (!argument) return fail("unbound-parameter", path, `Unbound parameter ${expression.parameterId}`);
if (argument.kind !== "object")
return fail("parameter-kind", path, `Parameter ${expression.parameterId} is a value, not an object target`);
canonicalTypeArgument(argument);
return structuredClone(argument.target);
}
case "application":
return { kind: "interface", interfaceRevisionId: this.application(expression.application, path, depth + 1) };
default:
return fail("invalid-type", path, "Unknown object type expression");
}
}
value(expression: ValueTypeExpression, path = "type", depth = 0): ValueType {
this.budget.enter(path, depth);
switch (expression.kind) {
case "variant":
return {
kind: "variant",
id: expression.id,
cases: Object.fromEntries(
Object.entries(expression.cases).map(([label, entry]) => [
label,
{ id: entry.id, value: this.value(entry.value, `${path}.${label}`, depth + 1) },
]),
),
};
case "builtin":
return { kind: "builtin", name: expression.name };
case "scalar":
return { kind: "scalar", name: expression.name };
case "message":
return { kind: "message", descriptorId: expression.descriptorId };
case "change":
case "list":
case "optional":
return { kind: expression.kind, value: this.value(expression.value, `${path}.${expression.kind}`, depth + 1) };
case "record":
return {
kind: "record",
fields: Object.fromEntries(
Object.entries(expression.fields).map(([name, field]) => [
name,
this.value(field, `${path}.${name}`, depth + 1),
]),
),
};
case "object-ref":
return { kind: "object-ref", expectation: this.object(expression.expectation, `${path}.ref`, depth + 1) };
case "parameter": {
const argument = this.environment.arguments.get(expression.parameterId);
if (!argument) return fail("unbound-parameter", path, `Unbound parameter ${expression.parameterId}`);
if (argument.kind !== "value")
return fail("parameter-kind", path, `Parameter ${expression.parameterId} is an object target; use ref<T>`);
canonicalTypeArgument(argument);
return this.value(argument.type, path, depth + 1);
}
case "alias": {
const alias = this.environment.aliases?.get(expression.definitionId);
if (!alias) return fail("unknown-alias", path, `Unknown type alias ${expression.definitionId}`);
if (this.aliases.includes(alias.id))
return fail("recursive-alias", path, `Recursive value alias: ${[...this.aliases, alias.id].join(" -> ")}`);
const arguments_ = expression.arguments.map((argument, index) =>
this.argument(argument, `${path}.arguments[${index}]`, depth + 1),
);
const bindings = bindTypeParameters(alias.parameters, arguments_, this.environment, path);
this.aliases.push(alias.id);
try {
// Reuse this expansion budget/stack; lexical parameter maps are restored.
const previous = this.environment;
this.environment = { ...previous, arguments: bindings };
try {
return this.value(alias.body, `${path}.${alias.id}`, depth + 1);
} finally {
this.environment = previous;
}
} finally {
this.aliases.pop();
}
}
default:
return fail("invalid-type", path, "Unknown value type expression");
}
}
}
export const bindTypeParameters = (
parameters: readonly TypeParameter[],
arguments_: readonly ClosedTypeArgument[],
environment: GenericTypeEnvironment,
path = "parameters",
): ReadonlyMap<string, ClosedTypeArgument> => {
if (parameters.length !== arguments_.length)
fail("type-arity", path, `Expected ${parameters.length} type arguments, received ${arguments_.length}`);
const bindings = new Map<string, ClosedTypeArgument>();
const names = new Set<string>();
for (const [index, parameter] of parameters.entries()) {
if (
!parameter.id ||
!parameter.name ||
parameter.name === "Self" ||
bindings.has(parameter.id) ||
names.has(parameter.name)
)
fail("duplicate-parameter", path, `Invalid or duplicate parameter ${parameter.name}`);
names.add(parameter.name);
const argument = arguments_[index];
if (parameter.kind !== argument.kind)
fail("parameter-kind", `${path}.${parameter.name}`, `Expected ${parameter.kind}, received ${argument.kind}`);
canonicalTypeArgument(argument);
bindings.set(parameter.id, structuredClone(argument));
}
const substitution = new TypeSubstitution({ ...environment, arguments: bindings });
for (const parameter of parameters) {
const argument = bindings.get(parameter.id)!;
if (
parameter.kind === "value" &&
argument.kind === "value" &&
parameter.storable &&
!isStorableType(argument.type, environment.storableMessage)
)
fail(
"non-storable-argument",
`${path}.${parameter.name}`,
"State values cannot contain managed references or unsupported transport values",
);
if (parameter.kind === "object" && argument.kind === "object") {
for (const bound of parameter.implements) {
const required = substitution.application(bound, `${path}.${parameter.name}.implements`);
if (!environment.implementsInterface(argument.target, required))
fail("unsatisfied-bound", `${path}.${parameter.name}`, `Object target does not implement ${required}`);
}
}
}
return bindings;
};
+5
View File
@@ -1,2 +1,7 @@
export * from "./types.js";
export * from "./validation.js";
export * from "./evolution.js";
export * from "./migrations.js";
export * from "./generics.js";
export * from "./generic-packages.js";
export * from "./modules.js";
+139
View File
@@ -0,0 +1,139 @@
import type {
BoundDependency,
Conformance,
DependencyPort,
InitializationField,
ValueType,
WorkspaceRevision,
} from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
import { cardinalityValueType, valueTypesEqual } from "./validation.js";
/** Creation grants seed inputs on new objects, never general private-state authority. */
export function validateInitialization(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [];
const fail = (path: string, message: string) => issues.push({ code: "invalid-initialization", path, message });
const ownerKey = (owner: Conformance) => `${owner.atomId}/${owner.interfaceRevisionId}`;
const shapes = new Map<string, ValueType>();
const fieldType = (owner: Conformance, field: InitializationField): ValueType | undefined => {
const binding = field.binding;
const attachment = owner.privateAttachments.find(
(a) => a.kind === binding.kind && a.id === (binding.kind === "state" ? binding.slotId : binding.edgeTypeId),
);
if (!attachment) {
fail(ownerKey(owner), `Initialization input ${field.input} must name an attachment owned by this conformance`);
return;
}
if (attachment.kind === "state") {
if (["journal", "subscription"].includes(attachment.storagePolicy.kind)) {
fail(ownerKey(owner), "Native journals start empty and cannot be seeded as state");
return;
}
return attachment.valueType;
}
if (binding.kind !== "edge") return;
const index = attachment.endpoints.findIndex((e) => e.projectionId === binding.projectionId);
const endpoint = attachment.endpoints[index];
const target = attachment.endpoints[1 - index];
if (!endpoint || !target || endpoint.constraint.kind !== "atom" || endpoint.constraint.atomId !== owner.atomId) {
fail(ownerKey(owner), `Initialization input ${field.input} must name an edge projection on the newborn atom`);
return;
}
if (target.keyType) {
fail(
ownerKey(owner),
"Initialize a keyed relationship through its keyed projection; inverse seeds cannot supply a map key",
);
return;
}
if (endpoint.cardinality === "many" || endpoint.cardinality === "many-unique") {
const fields: Record<string, ValueType> = {
key: { kind: "scalar", name: "string" },
target: cardinalityValueType(target.constraint, "exactly-one"),
};
if (endpoint.keyType)
fields.mapKey = { kind: "scalar", name: endpoint.keyType === "boolean" ? "bool" : endpoint.keyType };
return { kind: "list", value: { kind: "record", fields } };
}
return cardinalityValueType(target.constraint, endpoint.cardinality);
};
for (const owner of workspace.conformances) {
if (!owner.initialization) continue;
const fields: Record<string, ValueType> = Object.create(null);
const destinations = new Set<string>();
for (const field of owner.initialization) {
const path = `${ownerKey(owner)}/${field.input}`;
if (!field.input || Object.hasOwn(fields, field.input))
fail(path, "Initialization input names must be nonempty and distinct");
const destination = JSON.stringify(field.binding);
if (destinations.has(destination)) fail(path, "An attachment may have only one initialization input");
destinations.add(destination);
const type = fieldType(owner, field);
if (type) fields[field.input] = type;
}
shapes.set(ownerKey(owner), { kind: "record", fields });
}
const checkBindings = (ports: DependencyPort[], dependencies: BoundDependency[], path: string) => {
for (const dependency of dependencies) {
if (dependency.binding.kind !== "creation") continue;
const binding = dependency.binding;
const requirement = ports.find((p) => p.id === dependency.portId)?.requirement;
if (requirement?.kind !== "creation") continue; // General binding validation reports the kind mismatch.
const portPath = `${path}/${dependency.portId}`;
if (binding.atomId !== requirement.atomId) fail(portPath, `Creation port requires atom ${requirement.atomId}`);
const initialized = new Set<string>();
const supplied = new Set<string>();
for (const mapping of binding.owners) {
const key = `${binding.atomId}/${mapping.interfaceRevisionId}`;
if (supplied.has(key)) fail(portPath, `Duplicate initialization owner ${key}`);
supplied.add(key);
const owner = workspace.conformances.find((o) => ownerKey(o) === key);
const shape = shapes.get(key);
if (!owner || !shape) {
fail(portPath, `Owner ${key} has no declared initialization mapping`);
continue;
}
let seed: ValueType | undefined = requirement.inputType;
for (const component of mapping.inputPath) seed = seed?.kind === "record" ? seed.fields[component] : undefined;
if (!seed || !valueTypesEqual(seed, shape))
fail(
portPath,
`Seed input${mapping.inputPath.map((p) => `.${p}`).join("")} does not match initialization inputs of ${key}`,
);
for (const field of owner.initialization!)
if (field.binding.kind === "state") initialized.add(field.binding.slotId);
}
const states = [
...workspace.sharedAttachments,
...workspace.conformances.flatMap((o) => o.privateAttachments),
].filter((a) => a.kind === "state" && a.attachedTo === binding.atomId);
for (const state of states)
if (
state.kind === "state" &&
!["journal", "subscription"].includes(state.storagePolicy.kind) &&
state.defaultValue === undefined &&
!initialized.has(state.id)
)
fail(portPath, `Required state ${state.id} has no default or declared seed mapping`);
}
};
const exports = new Map(
workspace.packageImports.flatMap((p) => p.exports.map((e) => [`${p.revisionId}/${e.id}`, e] as const)),
);
for (const owner of workspace.conformances)
for (const operation of owner.operationBindings) {
if (operation.binding.kind !== "package") continue;
const entry = exports.get(`${operation.binding.packageRevisionId}/${operation.binding.exportId}`);
if (entry)
checkBindings(
entry.dependencyPorts,
operation.binding.dependencies,
`${ownerKey(owner)}/${operation.operationId}`,
);
}
for (const constructor of workspace.constructors) {
const entry = exports.get(`${constructor.packageRevisionId}/${constructor.exportId}`);
if (entry) checkBindings(entry.dependencyPorts, constructor.dependencies, `constructor/${constructor.atomId}`);
}
return issues;
}
+131
View File
@@ -0,0 +1,131 @@
import {
valueType,
type JournalPrimitive,
type InterfaceOperation,
type InterfaceRevision,
type ObjectExpectation,
type OperationId,
type ValueType,
} from "./types.js";
import { valueTypesEqual } from "./validation.js";
export const subscriptionStatusType: ValueType = {
kind: "record",
fields: {
status: valueType.string,
generation: valueType.uint64,
cursor: valueType.uint64,
failure: {
kind: "optional",
value: {
kind: "record",
fields: { publication: valueType.string, code: valueType.string, message: valueType.string },
},
},
},
};
/** Native subscription configuration is selected by the checked attach contract.
* No library/interface ID or handler method name is reserved by Camino. */
export function subscriptionContract(
payload: ValueType,
parameters: ValueType,
attach: InterfaceOperation,
interfaces: readonly InterfaceRevision[],
): { source: ObjectExpectation; handler: ObjectExpectation; operation: OperationId } {
const fail = (message: string): never => {
throw new Error(message);
};
if (
attach.mode !== "call" ||
attach.scope === "class" ||
!valueTypesEqual(attach.outputType, valueType.unit) ||
attach.inputType.kind !== "record"
)
return fail("Subscription attachment must be an object call returning unit");
const fields = attach.inputType.fields;
if (
Object.keys(fields).sort().join(",") !== "handler,parameters,source,start" ||
fields.source?.kind !== "object-ref" ||
fields.handler?.kind !== "object-ref" ||
fields.handler.expectation.kind !== "interface" ||
!valueTypesEqual(fields.parameters!, parameters)
)
return fail("Attachment needs typed source/handler references, matching parameters, and a start policy");
const start = fields.start;
if (
start?.kind !== "variant" ||
Object.keys(start.cases).sort().join(",") !== "afterCursor,future,retained" ||
!valueTypesEqual(start.cases.future!.value, valueType.unit) ||
!valueTypesEqual(start.cases.retained!.value, valueType.unit) ||
!valueTypesEqual(start.cases.afterCursor!.value, valueType.string)
)
return fail("Subscription start requires future:unit, retained:unit and afterCursor:string variant cases");
const handler = fields.handler.expectation;
const expected: ValueType = { kind: "record", fields: { event: payload, parameters } };
const candidates =
interfaces
.find((iface) => iface.revisionId === handler.interfaceRevisionId)
?.members.flatMap((member) => member.operations)
.filter(
(operation) =>
operation.mode === "call" &&
operation.scope !== "class" &&
operation.execution?.kind === "transaction" &&
operation.execution.targets.includes("server") &&
valueTypesEqual(operation.inputType, expected) &&
valueTypesEqual(operation.outputType, valueType.unit),
) ?? [];
if (candidates.length !== 1)
return fail("Handler interface must select one unambiguous server transaction from {event, parameters} to unit");
return { source: fields.source.expectation, handler, operation: candidates[0]!.id };
}
/** Journals persist checked values, including managed references. They cannot
* persist process handles, opaque messages or edit commands. */
export function journalPayloadType(type: ValueType): boolean {
switch (type.kind) {
case "scalar":
case "object-ref":
return true;
case "builtin":
return type.name === "unit";
case "list":
case "optional":
return journalPayloadType(type.value);
case "record":
return Object.values(type.fields).every(journalPayloadType);
case "variant":
return Object.values(type.cases).every((entry) => journalPayloadType(entry.value));
default:
return false;
}
}
export function journalPrimitiveSignature(
payload: ValueType,
primitive: JournalPrimitive,
): Pick<InterfaceOperation, "mode" | "inputType" | "outputType"> {
const optional = (value: ValueType): ValueType => ({ kind: "optional", value });
if (primitive === "publish")
return {
mode: "call",
inputType: payload,
outputType: { kind: "record", fields: { publication: valueType.string } },
};
if (primitive === "head") return { mode: "call", inputType: valueType.unit, outputType: optional(valueType.uint64) };
return {
mode: "call",
inputType: { kind: "record", fields: { after: optional(valueType.string), limit: valueType.uint32 } },
outputType: {
kind: "record",
fields: {
entries: {
kind: "list",
value: { kind: "record", fields: { publication: valueType.string, position: valueType.uint64, payload } },
},
next: optional(valueType.string),
asOfHead: optional(valueType.uint64),
},
},
};
}
+156
View File
@@ -0,0 +1,156 @@
import { contentDigest } from "./evolution.js";
import type { PersistentAttachment } from "./types.js";
/** Portable storage shape: local owner/slot/projection identities are supplied
* by the consuming workspace's explicit bindings, never baked into this hash. */
export const migrationPortContract = (attachment: PersistentAttachment): unknown => {
if (attachment.kind === "state")
return {
kind: "state",
valueType: attachment.valueType,
storagePolicy: attachment.storagePolicy,
...(attachment.defaultValue === undefined ? {} : { defaultValue: attachment.defaultValue }),
};
const endpoint = (value: (typeof attachment.endpoints)[number]) => ({
constraint: value.constraint,
cardinality: value.cardinality,
ordered: value.ordered,
onDelete: value.onDelete ?? "restrict",
retainOther: value.retainOther ?? false,
...(value.keyType ? { keyType: value.keyType } : {}),
...(value.publicTraversal ? { publicTraversal: true } : {}),
});
return { kind: "edge", first: endpoint(attachment.endpoints[0]), second: endpoint(attachment.endpoints[1]) };
};
export type MigrationDeclaration = {
id: string;
scopeId: string;
from: string;
to: string;
implementation: { exportId: string; file: string; digest: string };
predecessors: string[];
ports: {
name: string;
view: "old" | "new";
access: ("read" | "write" | "create" | "edge")[];
contractDigest: string;
}[];
preservesOldReaders?: boolean;
preservesOldWriters?: boolean;
};
export type MigrationCatalog = {
schemaVersion: 1;
contracts: Record<string, unknown>;
migrations: MigrationDeclaration[];
};
export const validateMigrationCatalog = (value: unknown, exportIds?: ReadonlySet<string>): MigrationCatalog => {
if (!value || typeof value !== "object" || Array.isArray(value))
throw new Error("Migration catalog must be an object");
const catalog = value as MigrationCatalog;
if (
catalog.schemaVersion !== 1 ||
!catalog.contracts ||
Array.isArray(catalog.contracts) ||
!Array.isArray(catalog.migrations)
)
throw new Error("Unsupported migration catalog");
for (const [digest, contract] of Object.entries(catalog.contracts))
if (contentDigest(contract) !== digest) throw new Error(`Migration contract digest mismatch: ${digest}`);
const ids = new Set<string>();
for (const migration of catalog.migrations) {
if (!migration.id || !migration.scopeId || ids.has(migration.id))
throw new Error("Migration IDs must be stable and unique");
ids.add(migration.id);
if (!catalog.contracts[migration.from] || !catalog.contracts[migration.to] || migration.from === migration.to)
throw new Error(`Migration ${migration.id} requires distinct retained source and target contracts`);
if (!migration.implementation?.exportId || !/^sha256:[0-9a-f]{64}$/.test(migration.implementation.digest))
throw new Error(`Migration ${migration.id} requires an exact implementation digest`);
if (
!migration.implementation.file ||
migration.implementation.file.startsWith("/") ||
migration.implementation.file.split(/[\\/]/).some((part) => !part || part === "." || part === "..")
)
throw new Error("Migration implementation must be a relative package file");
if (exportIds && !exportIds.has(migration.implementation.exportId))
throw new Error(`Migration ${migration.id} refers to an undeclared package export`);
if (!Array.isArray(migration.predecessors) || !Array.isArray(migration.ports))
throw new Error(`Migration ${migration.id} requires predecessors and ports`);
if (new Set(migration.predecessors).size !== migration.predecessors.length)
throw new Error(`Duplicate predecessor in ${migration.id}`);
for (const promise of [migration.preservesOldReaders, migration.preservesOldWriters])
if (promise !== undefined && typeof promise !== "boolean")
throw new Error("Migration compatibility promises must be booleans");
const ports = new Set<string>();
for (const port of migration.ports) {
if (
!port.name ||
ports.has(port.name) ||
!["old", "new"].includes(port.view) ||
!Array.isArray(port.access) ||
!port.access.length ||
port.access.some((access) => !["read", "write", "create", "edge"].includes(access)) ||
!catalog.contracts[port.contractDigest]
)
throw new Error(`Invalid migration port in ${migration.id}`);
if (port.view === "old" && port.access.some((access) => access !== "read"))
throw new Error("Old migration views are read-only");
ports.add(port.name);
}
}
for (const migration of catalog.migrations)
for (const predecessor of migration.predecessors)
if (!ids.has(predecessor)) throw new Error(`Missing retained predecessor ${predecessor}`);
// Catalogs are retained across releases. Reject impossible histories at
// publication/check time, not only when someone tries to select a path.
const remaining = new Map(catalog.migrations.map((entry) => [entry.id, new Set(entry.predecessors)]));
const ready = [...remaining].filter(([, dependencies]) => dependencies.size === 0).map(([id]) => id);
for (let index = 0; index < ready.length; index++) {
remaining.delete(ready[index]);
for (const [id, dependencies] of remaining)
if (dependencies.delete(ready[index]) && dependencies.size === 0) ready.push(id);
}
if (remaining.size) throw new Error(`Cyclic migration predecessors: ${[...remaining.keys()].join(", ")}`);
return catalog;
};
export type MigrationSelection = {
scopeId: string;
from: string;
to: string;
path: string[];
bindings: Record<string, string>;
};
/** Explicit paths, not shortest-path guesses. Receipts identify code plus local scope mapping. */
export const selectMigrationPath = (
catalog: MigrationCatalog,
selection: MigrationSelection,
previousReceipts: ReadonlyMap<string, string> = new Map(),
) => {
validateMigrationCatalog(catalog);
let current = selection.from;
const seen = new Set<string>();
const transitions = [];
for (const id of selection.path) {
const declaration = catalog.migrations.find((entry) => entry.id === id);
if (!declaration || declaration.scopeId !== selection.scopeId || declaration.from !== current || seen.has(id))
throw new Error(`Invalid selected migration transition ${id}`);
for (const predecessor of declaration.predecessors)
if (!seen.has(predecessor) && !previousReceipts.has(predecessor))
throw new Error(`Unsatisfied predecessor ${predecessor}`);
const usedBindings: Record<string, string> = {};
for (const port of declaration.ports) {
if (!selection.bindings[port.name]) throw new Error(`Missing local migration binding ${port.name}`);
usedBindings[port.name] = selection.bindings[port.name];
}
const digest = contentDigest({ declaration, bindings: usedBindings });
const previous = previousReceipts.get(id);
if (previous && previous !== digest)
throw new Error(`Migration identity ${id} was previously used with different code or scope`);
transitions.push({ declaration, bindings: usedBindings, digest, alreadyApplied: Boolean(previous) });
seen.add(id);
current = declaration.to;
}
if (current !== selection.to) throw new Error("Selected migration path does not cover the target storage contract");
return transitions;
};
+82
View File
@@ -0,0 +1,82 @@
import type { InterfaceRevision, ModuleInterfaceMember, PackageModule, WorkspaceRevision } from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
/** Public nominal contract lookup; does not resolve an executable artifact or grant access. */
export function moduleContract(
interfaces: readonly InterfaceRevision[],
module: PackageModule,
): ModuleInterfaceMember | undefined {
const iface = interfaces.find((entry) => entry.revisionId === module.interfaceRevisionId);
if (!iface || iface.template) return undefined;
const member = iface.members.find((entry) => entry.id === module.memberId);
return member?.kind === "module" ? member : undefined;
}
export function validateModules(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [];
const fail = (path: string, message: string) => issues.push({ code: "invalid-module-contract", path, message });
for (const iface of workspace.interfaceImports) {
for (const member of iface.template?.members ?? iface.members) {
if (member.kind !== "module") continue;
const path = `${iface.revisionId}.${member.id}`;
if (!/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+\/[1-9][0-9]*$/.test(member.contract))
fail(path, "Module contracts require a namespaced, versioned identifier");
if (member.operations.length) fail(path, "A module member cannot declare callable operations");
}
}
for (const pkg of workspace.packageImports) {
const ids = new Set<string>(pkg.exports.map((entry) => entry.id));
const names = new Set([
...pkg.exports.map((entry) => entry.displayName),
...(pkg.genericExports ?? []).map((entry) => entry.displayName),
...(pkg.queries ?? []).map((entry) => entry.displayName),
...(pkg.queryTemplates ?? []).map((entry) => entry.declaration.displayName),
]);
for (const module of pkg.modules ?? []) {
const path = `${pkg.revisionId}.${module.id}`;
if (!module.id.trim() || !module.displayName.trim() || ids.has(module.id) || names.has(module.displayName))
fail(path, "Duplicate or empty module identity or package member name");
ids.add(module.id);
names.add(module.displayName);
if (!moduleContract(workspace.interfaceImports, module))
fail(path, "Module export must implement a closed imported module member");
}
}
for (const conformance of workspace.conformances) {
const iface = workspace.interfaceImports.find((entry) => entry.revisionId === conformance.interfaceRevisionId);
if (!iface) continue;
const bound = new Set<string>();
for (const binding of conformance.moduleBindings ?? []) {
const path = `${conformance.id ?? conformance.atomId}.${binding.memberId}`;
const member = iface.members.find((entry) => entry.id === binding.memberId);
const pkg = workspace.packageImports.find((entry) => entry.revisionId === binding.packageRevisionId);
const module = pkg?.modules?.find((entry) => entry.id === binding.moduleId);
if (bound.has(binding.memberId)) fail(path, "Module member is bound more than once");
bound.add(binding.memberId);
if (
member?.kind !== "module" ||
!module ||
module.interfaceRevisionId !== iface.revisionId ||
module.memberId !== member.id
) {
fail(path, "Module binding must implement this exact nominal interface member");
continue;
}
const subject = member.subject;
if (
subject &&
!(subject.kind === "atom"
? subject.atomId === conformance.atomId
: workspace.conformances.some(
(entry) =>
entry.atomId === conformance.atomId && entry.interfaceRevisionId === subject.interfaceRevisionId,
))
)
fail(path, "Implementing atom does not satisfy the module subject contract");
}
for (const member of iface.members)
if (member.kind === "module" && !bound.has(member.id))
fail(`${conformance.id ?? conformance.atomId}.${member.id}`, "Missing module member binding");
}
return issues;
}
+49
View File
@@ -0,0 +1,49 @@
import type { WorkspaceRevision } from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
import { moduleContract } from "./modules.js";
/** Presentation metadata has no graph authority and cannot name arbitrary module URLs. */
export function validatePresentations(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [];
const fail = (path: string, message: string) => issues.push({ code: "invalid-presentation", path, message });
const interfaces = new Map(workspace.interfaceImports.map((entry) => [entry.revisionId, entry]));
const packages = new Map(workspace.packageImports.map((entry) => [entry.revisionId, entry]));
const text = (value: string, path: string) => {
if (!value.trim()) fail(path, "Presentation identities and names must be nonempty");
};
const subject = (id: string, path: string) => {
const contract = interfaces.get(id as WorkspaceRevision["interfaceImports"][number]["revisionId"]);
if (!contract || contract.template) fail(path, `Expected an imported closed subject interface: ${id}`);
};
for (const [index, pkg] of workspace.packageImports.entries()) {
for (const [alias, id] of Object.entries(pkg.clientInterfaces ?? {})) {
text(alias, `packageImports[${index}].clientInterfaces`);
subject(id, `packageImports[${index}].clientInterfaces.${alias}`);
}
}
const ids = new Set<string>(),
names = new Set<string>();
for (const [index, presentation] of (workspace.presentations ?? []).entries()) {
const path = `presentations[${index}]`;
text(presentation.id, `${path}.id`);
text(presentation.displayName, `${path}.displayName`);
if (ids.has(presentation.id) || names.has(presentation.displayName))
fail(path, "Duplicate presentation identity or name");
ids.add(presentation.id);
names.add(presentation.displayName);
subject(presentation.subject, `${path}.subject`);
const module = packages
.get(presentation.packageRevisionId)
?.modules?.find((entry) => entry.id === presentation.componentId);
if (module) {
const contract = moduleContract(workspace.interfaceImports, module);
if (!contract || !["org.quixos.react.subject-only/1", "org.quixos.react.component/2"].includes(contract.contract))
fail(`${path}.componentId`, "Standalone presentation requires a supported React presentation contract");
if (contract?.subject?.kind !== "interface" || contract.subject.interfaceRevisionId !== presentation.subject)
fail(`${path}.subject`, "Component requires its exact declared subject interface");
continue;
}
fail(`${path}.componentId`, "Presentation names no installed package component");
}
return issues;
}
+209 -63
View File
@@ -7,6 +7,7 @@ type OpaqueId<Kind extends string> = string & {
export type WorkspaceId = OpaqueId<"WorkspaceId">;
export type WorkspaceRevisionId = OpaqueId<"WorkspaceRevisionId">;
export type AtomId = OpaqueId<"AtomId">;
export type ConformanceId = OpaqueId<"ConformanceId">;
export type InterfaceId = OpaqueId<"InterfaceId">;
export type InterfaceRevisionId = OpaqueId<"InterfaceRevisionId">;
export type MemberId = OpaqueId<"MemberId">;
@@ -28,12 +29,11 @@ const opaque = <Kind extends string>(value: string) => value as OpaqueId<Kind>;
*/
export const capabilityId = {
workspace: (value: string) => opaque<"WorkspaceId">(value),
workspaceRevision: (value: string) =>
opaque<"WorkspaceRevisionId">(value),
workspaceRevision: (value: string) => opaque<"WorkspaceRevisionId">(value),
atom: (value: string) => opaque<"AtomId">(value),
conformance: (value: string) => opaque<"ConformanceId">(value),
interface: (value: string) => opaque<"InterfaceId">(value),
interfaceRevision: (value: string) =>
opaque<"InterfaceRevisionId">(value),
interfaceRevision: (value: string) => opaque<"InterfaceRevisionId">(value),
member: (value: string) => opaque<"MemberId">(value),
operation: (value: string) => opaque<"OperationId">(value),
slot: (value: string) => opaque<"SlotId">(value),
@@ -51,15 +51,7 @@ export interface SourceRevision {
commit: string;
}
export type ScalarValueTypeName =
| "bool"
| "bytes"
| "double"
| "int32"
| "int64"
| "string"
| "uint32"
| "uint64";
export type ScalarValueTypeName = "bool" | "bytes" | "double" | "int32" | "int64" | "string" | "uint32" | "uint64";
export type ObjectExpectation =
| { kind: "atom"; atomId: AtomId }
@@ -72,9 +64,12 @@ export type ValueType =
| { kind: "builtin"; name: "unit" | "watch-handle" }
| { kind: "scalar"; name: ScalarValueTypeName }
| { kind: "message"; descriptorId: string }
| { kind: "record"; fields: Record<string, ValueType> }
| { kind: "variant"; id: string; cases: Record<string, { id: string; value: ValueType }> }
| { kind: "object-ref"; expectation: ObjectExpectation }
| { kind: "optional"; value: ValueType }
| { kind: "list"; value: ValueType };
| { kind: "list"; value: ValueType }
| { kind: "change"; value: ValueType };
export const valueType = {
unit: { kind: "builtin", name: "unit" } as const,
@@ -109,39 +104,41 @@ export interface AtomDefinition {
documentation?: string;
}
export type InterfaceOperationMode =
| "call"
| "watch-start"
| "watch-stop"
| "subscribe"
| "unsubscribe";
export type InterfaceOperationMode = "call" | "watch-start" | "watch-stop" | "subscribe" | "unsubscribe";
export interface InterfaceOperation {
/** Omission has remote semantics; availability never strengthens an imported contract. */
export type ExecutionContract = { kind: "remote" } | { kind: "snapshot" | "transaction"; targets: string[] };
export interface InterfaceOperation<Type = ValueType> {
execution?: ExecutionContract;
id: OperationId;
displayName: string;
inputType: ValueType;
outputType: ValueType;
inputType: Type;
outputType: Type;
mode: InterfaceOperationMode;
/** Class capabilities have no object receiver and bind free functions. */
scope?: "class";
/** Required for watch-start/subscribe and absent for other modes. */
eventType?: ValueType;
eventType?: Type;
}
interface InterfaceMemberBase {
export type QueryReadContract = { execution: "native" | "rpc-permitted" | "portable" };
interface InterfaceMemberBase<Type = ValueType> {
id: MemberId;
displayName: string;
operations: InterfaceOperation[];
operations: InterfaceOperation<Type>[];
}
export interface ValueInterfaceMember extends InterfaceMemberBase {
export interface ValueInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
kind: "value";
valueType: ValueType;
/** Every operation resolves directly to the same native state slot. */
nativeField?: true;
queryRead?: QueryReadContract;
valueType: Type;
}
export type EdgeCardinality =
| "optional-one"
| "exactly-one"
| "many"
| "many-unique";
export type EdgeCardinality = "optional-one" | "exactly-one" | "many" | "many-unique";
export type EdgeEndpointConstraint =
| { kind: "atom"; atomId: AtomId }
@@ -150,24 +147,40 @@ export type EdgeEndpointConstraint =
interfaceRevisionId: InterfaceRevisionId;
};
export interface RelationshipInterfaceMember extends InterfaceMemberBase {
export interface RelationshipInterfaceMember<
Type = ValueType,
Target = EdgeEndpointConstraint,
> extends InterfaceMemberBase<Type> {
kind: "relationship";
target: EdgeEndpointConstraint;
queryRead?: QueryReadContract;
target: Target;
cardinality: EdgeCardinality;
ordered: boolean;
keyType?: "string" | "boolean" | "int64";
}
/** A named callable capability that is not value or relationship sugar. */
export interface OperationInterfaceMember extends InterfaceMemberBase {
export interface OperationInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
kind: "operation";
inputType: ValueType;
outputType: ValueType;
inputType: Type;
outputType: Type;
}
export type InterfaceMember =
| ValueInterfaceMember
| RelationshipInterfaceMember
| OperationInterfaceMember;
/** A typed code artifact. Module bindings are neither calls nor storage fields. */
export interface ModuleInterfaceMember<
Type = ValueType,
Target = EdgeEndpointConstraint,
> extends InterfaceMemberBase<Type> {
kind: "module";
contract: string;
subject?: Target;
}
export type InterfaceMember<Type = ValueType, Target = EdgeEndpointConstraint> =
| ValueInterfaceMember<Type>
| RelationshipInterfaceMember<Type, Target>
| OperationInterfaceMember<Type>
| ModuleInterfaceMember<Type, Target>;
export interface InterfaceRevision {
interfaceId: InterfaceId;
@@ -175,11 +188,27 @@ export interface InterfaceRevision {
displayName: string;
source: SourceRevision;
members: InterfaceMember[];
/** Authored generic definition; never an executable contract by itself. */
template?: import("./generics.js").GenericInterfaceTemplate;
/** Immutable provenance of a closed generic application. */
application?: import("./generics.js").AppliedInterfaceIdentity;
requiredInterfaces?: InterfaceRevisionId[];
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
}
export interface JournalStorageLimits {
maxEntries: number;
maxBytes: number;
maxPublicationBytes: number;
maxProtectedSubscriptions: number;
}
export type StoragePolicy =
| { kind: "optimistic-register" }
| { kind: "crdt-document"; updateType: ValueType };
| { kind: "register" }
| { kind: "crdt-document"; updateType: ValueType }
| { kind: "journal"; limits: JournalStorageLimits }
| { kind: "subscription"; payload: ValueType };
export type JournalPrimitive = "publish" | "readPage" | "head";
export type SubscriptionPrimitive = "attach" | "status" | "pause" | "resume" | "retire";
export interface StateSlotDefinition {
kind: "state";
@@ -192,11 +221,15 @@ export interface StateSlotDefinition {
}
export interface EdgeEndpoint {
keyType?: "string" | "boolean" | "int64";
publicTraversal?: boolean;
projectionId: EdgeProjectionId;
displayName: string;
constraint: EdgeEndpointConstraint;
cardinality: EdgeCardinality;
ordered: boolean;
onDelete?: "restrict" | "detach" | "cascade-other";
retainOther?: boolean;
}
export interface EdgeDefinition {
@@ -208,18 +241,9 @@ export interface EdgeDefinition {
export type PersistentAttachment = StateSlotDefinition | EdgeDefinition;
export type StatePrimitive =
| "read"
| "write"
| "watch-start"
| "watch-stop";
export type StatePrimitive = "read" | "write" | "edit" | "watch-start" | "watch-stop";
export type EdgePrimitive =
| "resolve"
| "connect"
| "disconnect"
| "watch-start"
| "watch-stop";
export type EdgePrimitive = "resolve" | "connect" | "disconnect" | "watch-start" | "watch-stop";
export type PackageReceiverRequirement =
| { kind: "any-object" }
@@ -230,6 +254,8 @@ export type PackageReceiverRequirement =
};
export type DependencyPortRequirement =
| { kind: "reflection"; selectors: string[] }
| { kind: "query"; packageRevisionId: PackageRevisionId; queryId: string }
| {
kind: "state";
valueType: ValueType;
@@ -239,13 +265,18 @@ export type DependencyPortRequirement =
kind: "edge";
target: EdgeEndpointConstraint;
cardinality: EdgeCardinality;
keyType?: "string" | "boolean" | "int64";
primitives: EdgePrimitive[];
}
| {
kind: "receiver-interface";
kind: "interface";
interfaceRevisionId: InterfaceRevisionId;
/** Exact public operation identities, not implementation export names. */
operations?: OperationId[];
receiver?: "input";
}
| { kind: "constructor"; atomId: AtomId };
| { kind: "creation"; atomId: AtomId; inputType: ValueType }
| { kind: "constructor"; atomId: AtomId; inputType?: ValueType };
export interface DependencyPort {
id: DependencyPortId;
@@ -254,11 +285,18 @@ export interface DependencyPort {
}
interface PackageExportBase {
/** Returns this declared birth, with unchanged operation input as its initializer. */
preparation?: { port: DependencyPortId; key: string };
/** Required authoritative snapshot export in this package. */
acceptanceExportId?: PackageExportId;
execution?: ExecutionContract;
id: PackageExportId;
displayName: string;
inputType: ValueType;
outputType: ValueType;
dependencyPorts: DependencyPort[];
/** Closed adapter served by the same immutable package build. */
application?: { exportId: PackageExportId; arguments: import("./generics.js").ClosedTypeArgument[]; self?: AtomId };
}
export interface PackageOperationExport extends PackageExportBase {
@@ -277,29 +315,93 @@ export interface PackageConstructorExport extends PackageExportBase {
constructsAtom: AtomId;
}
export type PackageExport =
| PackageOperationExport
| PackageFunctionExport
| PackageConstructorExport;
export type PackageExport = PackageOperationExport | PackageFunctionExport | PackageConstructorExport;
export interface PackageModule {
id: string;
displayName: string;
interfaceRevisionId: InterfaceRevisionId;
memberId: MemberId;
}
export interface ModuleBinding {
memberId: MemberId;
packageRevisionId: PackageRevisionId;
moduleId: string;
}
export interface WorkspacePresentation {
id: string;
displayName: string;
subject: InterfaceRevisionId;
packageRevisionId: PackageRevisionId;
componentId: string;
}
export interface PackageRevision {
/** Explicit build consumers own these artifacts. Generic checking and query
* execution attach no semantics to their keys or contents. */
buildArtifacts?: Record<string, unknown>;
modules?: PackageModule[];
/** Closed interface imports exposed to this package's checked component bindings. */
clientInterfaces?: Record<string, InterfaceRevisionId>;
/** Explicit package imports supplying module selections, never the whole workspace. */
clientPackages?: Record<string, PackageRevisionId>;
controllers?: PackageController[];
queries?: import("../query/types.js").QueryDeclaration[];
checkedQueries?: import("../query/types.js").CheckedQuery[];
queryCompositions?: import("../query/composition.js").CheckedQueryComposition[];
queryTemplates?: import("../query/types.js").QueryTemplate[];
genericExports?: import("./generic-packages.js").GenericPackageExport[];
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
migrationCatalog?: import("./migrations.js").MigrationCatalog;
packageId: PackageId;
revisionId: PackageRevisionId;
displayName: string;
source: SourceRevision;
exports: PackageExport[];
/** Author-declared implementation semantics; omitted legacy values mean 1. */
semanticMajor?: number;
}
/** Checked adapter registration. Implementations remain ordinary object capabilities. */
export interface PackageController {
/** Optional opt-in discovery: a bounded checked workspace query in this package. */
registry?: string;
id: string;
subject: InterfaceRevisionId;
snapshot: OperationId;
remote: OperationId;
input: "unit" | "value";
}
export type DependencyBinding =
| { kind: "state"; slotId: SlotId }
| { kind: "reflection"; selectors: string[]; via?: never }
| {
kind: "query";
packageRevisionId: PackageRevisionId;
queryId: string;
scope: "object" | "workspace";
via?: EdgeTraversal;
}
| { kind: "state"; slotId: SlotId; via?: EdgeTraversal }
| {
kind: "edge";
edgeTypeId: EdgeTypeId;
projectionId: EdgeProjectionId;
via?: EdgeTraversal;
}
| {
kind: "receiver-interface";
kind: "interface";
interfaceRevisionId: InterfaceRevisionId;
via?: EdgeTraversal;
receiver?: "input";
}
| {
kind: "creation";
atomId: AtomId;
owners: { interfaceRevisionId: InterfaceRevisionId; inputPath: string[] }[];
via?: never;
}
| { kind: "constructor"; atomId: AtomId };
@@ -308,7 +410,33 @@ export interface BoundDependency {
binding: DependencyBinding;
}
/** Selects the object at the other end of an exactly-one relationship. */
export interface EdgeTraversal {
edgeTypeId: EdgeTypeId;
projectionId: EdgeProjectionId;
}
/**
* Provisional get-or-create recipe for a relationship implementation. The
* host projection is supplied by the relationship's resolve binding; this
* projection originates at the object constructed for the relationship.
*/
export interface RelationshipMaterialization {
memberId: MemberId;
constructorAtomId: AtomId;
edgeTypeId: EdgeTypeId;
constructedProjectionId: EdgeProjectionId;
}
export type Binding =
| { kind: "journal"; slotId: SlotId; primitive: JournalPrimitive }
| { kind: "subscription"; slotId: SlotId; primitive: SubscriptionPrimitive }
| { kind: "lifecycle"; primitive: "delete" }
| {
kind: "interface";
interfaceRevisionId: InterfaceRevisionId;
operationId: OperationId;
}
| {
kind: "state";
slotId: SlotId;
@@ -325,18 +453,34 @@ export type Binding =
packageRevisionId: PackageRevisionId;
exportId: PackageExportId;
dependencies: BoundDependency[];
acceptanceDependencies?: BoundDependency[];
};
export interface OperationBinding {
operationId: OperationId;
binding: Binding;
/** Required for package-backed reads of an RPC-permitted queryable field. */
queryReason?: string;
}
export interface InitializationField {
input: string;
binding: { kind: "state"; slotId: SlotId } | { kind: "edge"; edgeTypeId: EdgeTypeId; projectionId: EdgeProjectionId };
}
export interface Conformance {
/** Source provenance of a compiler-expanded rule, never runtime dispatch input. */
ruleOrigin?: { ruleId: string; sourceInterfaceRevisionId: InterfaceRevisionId; line: number; column: number };
moduleBindings?: ModuleBinding[];
initialization?: InitializationField[];
/** Absent only for legacy assemblies awaiting explicit ownership enrollment. */
id?: ConformanceId;
semanticMajor?: number;
atomId: AtomId;
interfaceRevisionId: InterfaceRevisionId;
privateAttachments: PersistentAttachment[];
operationBindings: OperationBinding[];
relationshipMaterializations: RelationshipMaterialization[];
}
export interface AtomConstructorBinding {
@@ -347,6 +491,8 @@ export interface AtomConstructorBinding {
}
export interface WorkspaceRevision {
presentations?: WorkspacePresentation[];
linkedQueries?: import("../query/link.js").LinkedQuery[];
id: WorkspaceRevisionId;
workspaceId: WorkspaceId;
parentRevisionIds: WorkspaceRevisionId[];
File diff suppressed because it is too large Load Diff
+2 -8
View File
@@ -1,10 +1,6 @@
#!/usr/bin/env node
import fs from "node:fs";
import {
packageDescriptorToJson,
parsePackageDescriptorTextproto,
validatePackageDescriptor,
} from "./descriptor.js";
import { packageDescriptorToJson, parsePackageDescriptorTextproto, validatePackageDescriptor } from "./descriptor.js";
const usage = (): never => {
console.error("Usage: quixos-descriptor-check <descriptor.txtpb>");
@@ -14,9 +10,7 @@ const usage = (): never => {
const path = process.argv[2] ?? usage();
try {
const descriptor = parsePackageDescriptorTextproto(
fs.readFileSync(path, "utf8"),
);
const descriptor = parsePackageDescriptorTextproto(fs.readFileSync(path, "utf8"));
const errors = validatePackageDescriptor(descriptor);
if (errors.length > 0) {
for (const error of errors) {
+5 -15
View File
@@ -14,19 +14,13 @@ const protoPaths = () => {
path.resolve(process.cwd(), "proto"),
path.resolve(process.cwd(), "quixos-protocol/proto"),
];
return [...new Set(candidates)].filter((candidate) =>
fs.existsSync(path.join(candidate, "quixos/package.proto")),
);
return [...new Set(candidates)].filter((candidate) => fs.existsSync(path.join(candidate, "quixos/package.proto")));
};
export const parsePackageDescriptorTextproto = (
text: string,
): PackageDescriptor => {
export const parsePackageDescriptorTextproto = (text: string): PackageDescriptor => {
const paths = protoPaths();
if (paths.length === 0) {
throw new Error(
"QUIXOS_PROTO_PATH must include quixos-protocol/proto to parse package descriptors",
);
throw new Error("QUIXOS_PROTO_PATH must include quixos-protocol/proto to parse package descriptors");
}
const result = childProcess.spawnSync(
"protoc",
@@ -44,9 +38,7 @@ export const parsePackageDescriptorTextproto = (
throw result.error;
}
if (result.status !== 0) {
throw new Error(
`Failed to parse package descriptor textproto:\n${result.stderr.toString().trim()}`,
);
throw new Error(`Failed to parse package descriptor textproto:\n${result.stderr.toString().trim()}`);
}
return fromBinary(PackageDescriptorSchema, result.stdout);
};
@@ -56,9 +48,7 @@ export const packageDescriptorToJson = (descriptor: PackageDescriptor) =>
prettySpaces: 2,
});
export const validatePackageDescriptor = (
descriptor: PackageDescriptor,
): string[] => {
export const validatePackageDescriptor = (descriptor: PackageDescriptor): string[] => {
const errors: string[] = [];
if (!descriptor.packageId) {
errors.push("packageId is required");
+147
View File
@@ -0,0 +1,147 @@
# Closed execution plans and artifacts
This page documents current compiler/runtime representations. Its universal
artifact inventory, React metadata and overlapping world/installation structures
are [known architectural gaps](../../../docs/ARCHITECTURE_STATUS.md), not the
[core package contract](../../../docs/ARCHITECTURE.md). Historical LF task numbers
refer to the original implementation sequence, not an active queue.
`compileExecutionPlan` validates a workspace, links its checked queries, resolves
public forwarding, and emits one versioned plan with an SHA-256 digest. It retains
exact interface/package revisions and generic applications, owner initialization,
storage definitions/profiles, selected operation identities, query placement, and
source provenance. `quixos-workspace-compile --execution-plan-out PATH` writes it.
The compiler emits operation dependencies as a graph. A traversed interface port
retains its edge/projection and exact selected operations; native reads identify
the canonical storage inputs. Cycles do not become infinitely expanded paths.
Execution and admission must apply their runtime depth/work budgets when walking
this graph. The plan includes the full linked GraphQL selections, not just root
traversals or a list of fields.
Local and replica queries must have the same root contract. A local query cannot
introduce a relationship path absent from its replica source; authors expand the
source selection explicitly. Portable getters may require extra canonical fields
and exactly-one dependency traversals, recorded in the dependency graph. Admission
resolves those inputs on the server for selected objects. This never changes the
source query's candidate membership, and does not claim a limited source is a
complete interface extent. An incomplete runtime projection remains a coverage
state, not permission to invent a missing value.
A portable transaction cannot depend on a local presentation query, including
through a selector. Pass explicit references and preconditions as mutation input.
Otherwise an admitted top-N list could select different mutation targets locally
and at acceptance. Remote transactions can implement explicit server workflows.
This restriction does not ban local queries from snapshot selectors. Such query
ports are view-relative: both hosts must use the frame's admitted candidate
universe. An executor must report unavailable coverage when that view is absent,
not substitute a full-server extent. Query-port acquisition remains unavailable
until the query executor implements this rule; emitting its checked dependency
graph is not runtime support.
## Artifacts and world identity
`PackageArtifacts` describes one package output containing a process service and
optional target modules. Implementation IDs are stable manifest keys, separate
from export capability IDs and JavaScript registry names. Each implementation
covers its ABI, export map, entrypoint, and declared asset paths/digests/sizes/MIME.
Its digest excludes its own digest field. Paths must be normalized and relative.
`checkExecutionWorld` verifies plan identity, supported codecs, exact source and
binding schema, service fallback, promised targets, declared exports, and all
supplied file bytes. The reader must confine paths/symlinks to the checked output
and honor its byte limit before allocation. `artifactFileReader` supplies that
adapter for trusted immutable output roots. `loadArtifactWorld` checks candidate
outputs without importing package code. `BrowserArtifactGateway` registers only
declared browser assets and verifies bytes before serving; Web Studio places it
behind workspace authentication. LF-05 still checks actual module
registries/imports and verifies build receipts; a manifest
claim alone does not establish the JavaScript exports or an isolated process.
The returned `camino-world-v1` descriptor includes every declared implementation
and its digest, so executor choice does not change semantics or silently load new
code. Each binding key is the JSON pair `[packageRevisionId, implementationId]`;
there can be both a service and module for the server target. World hashing uses
the core codec's canonical ordering and domain-separated SHA-256 framing. The
monorepo `camino-execution-world` check compares the compiler and core implementations.
No machine-local Nix path enters the world descriptor.
Reusable package compilation now receives a canonical per-package build contract;
exact source provenance and workspace linking remain separate. See
[package build contracts](../../../docs/PACKAGE_BUILD_CONTRACTS.md). Generation,
receipts and checking must agree on that contract; unchanged source alone does
not justify reusing an incompatible binding schema.
## Transaction-port ABI
`quixos-transaction-ports-v1` uses coordinator-issued frame/invocation handles and
monotonic invocation-local sequence numbers. A package receives its export ID,
input, and optional receiver. Port calls name a declared port and its allowed
primitive or exact interface operation; they never select arbitrary private slots,
packages, or coordinator transactions. Creation uses an explicit stable call key. Interface calls into transaction
operations also require a stable key, checked after resolving the selected
operation. The coordinator extends the birth path; it never derives that path
from an asynchronous completion order or the transport sequence number.
The coordinator resolves each port through the installed operation binding,
checks effects/selected methods, validates typed arguments/references, and joins
nested calls to the outer logical transaction. A nested failure poisons the outer domain
transaction even if application code catches it; frame cleanup cannot turn it into
a partial success. Process completion cannot independently commit. The coordinator
owns savepoints, cancellation, frame expiry, and rejection. These enforcement
mechanisms are LF-06/LF-08, not guarantees provided by TypeScript frame types.
The service control vocabulary includes readiness, cancellation, and shutdown;
transport framing and generated adapters are LF-05/LF-08. Portable modules and
services implement the same declared operation contracts. ABI compatibility is
not a statement about package trust, co-location permission, or multi-user isolation.
Portable package bindings are generated with `quixos-codegen-ts` options
`{"portable":{"target":"browser"}}` (or `server`). They expose `Contexts`,
`PortableImplementation`, checked codec declarations, and `createRegistry`.
Handlers import the shared replica core, never Replicache. Stable mutation keys
are mandatory for creation and selected nested transaction calls. Snapshot
contexts expose only their declared read methods; runtime frame checks remain
mandatory even when the TypeScript source passed compilation.
The maintained reader source in `tools/local-first/reader/portable.ts` typechecks
against both generated targets without casts. The root
`camino-portable-bindings` qualification compares Node, Window, Worker and
serialized-port behavior. Its simulated channel tests adapters, not installed
operation acquisition or process isolation; those require the coordinator.
Closed generic package applications use the same portable adapters. The checked
workspace specializes their input/output types and dependency ports before code
generation. Each application has its own export ID and generated handler name
(`name$<specialization digest>`); handlers are concrete, so a string application
and a list-of-int64 application cannot accidentally share an untyped codec.
Common implementation helpers can still be ordinary generic TypeScript functions.
This does not implement universal runtime type application or legacy constructors.
The `camino-portable-bindings` check compiles positive/negative typed consumers and
executes both specializations in Node, Chromium Window, and Worker hosts.
Use `selectArtifactImplementation(checkedManifest, selection)` to choose an
implementation by target, ABI, required export IDs, and optionally its exact ID.
Without an ID, exactly one candidate must match. Missing exports, wrong targets,
and ambiguous choices fail; manifest order never determines execution. This
selects already checked bytes and does not grant permission to run untrusted
package code in process. Multiple selected implementations remain part of the
same checked world's complete artifact inventory.
```ts
const implementation = selectArtifactImplementation(manifest, {
target: "server",
compatibility: "quixos-service-v1",
id: "isolated-service",
exports: requiredExports,
});
```
Consumers use the selected entrypoint, registry export and export coverage from
that manifest. The reader's Nix builder accepts per-package
`implementationSelection = { "package:reader-runtime@1" = {
browser = "browser-module"; server = "server-module"; service = "service";
}; };` overrides. Its fixture requires one implementation per lane covering that
lane's needed exports; the selector itself permits a smaller requested subset.
The installation records the chosen service and module entries. Process lifecycle
and trusted-module placement still belong to the existing runner.
+429
View File
@@ -0,0 +1,429 @@
import {
executionPlanDigestAsync,
type ExecutionPlan,
type PlanPackage,
ExecutionPlanError,
hexDigest,
executionVersions,
} from "./plan.js";
import { createHash } from "node:crypto";
import { canonicalJson } from "../capability-model/evolution.js";
export interface ArtifactFile {
path: string;
digest: string;
bytes: number;
mediaType: string;
}
export interface ArtifactImplementation {
id: string;
target: string;
compatibility: "quixos-service-v1" | "quixos-js-module-v1" | "quixos-restricted-module-v1";
path: string;
ports: "quixos-transaction-ports-v1";
registryExport?: string;
exports: string[];
/** Every executable/imported local asset, including the entrypoint, must be declared. */
files: ArtifactFile[];
contentDigest: string;
}
export interface ArtifactGroup {
schemaVersion: 1;
packageRevisionId: string;
bindingDigest: string;
versions: ExecutionPlan["definition"]["versions"];
implementations: ArtifactImplementation[];
components?: ComponentArtifact[];
}
/** Installation evidence binds a reusable group to the selected exact source.
* Compilation receipts never claim that a cache hit was built at a new pin. */
export interface PackageArtifacts extends ArtifactGroup {
sourceDigest: string;
}
export const componentSharedModules = [
"react",
"react-dom",
"react-dom/client",
"react/jsx-runtime",
"@quixos/camino-react",
"@quixos/camino-react/ui",
] as const;
export interface ComponentArtifact {
id: string;
subject: string;
compatibility: "org.quixos.react.esm/1";
path: string;
exportName: string;
sharedModules: Record<(typeof componentSharedModules)[number], string>;
files: ArtifactFile[];
contentDigest: string;
}
export interface ImplementationSelection {
target: string;
compatibility: ArtifactImplementation["compatibility"];
/** Optional only when exactly one compatible implementation covers these exports. */
id?: string;
exports: readonly string[];
}
/** Select from a checked manifest; selection does not establish package trust or isolation.
* Never resolve ambiguity by manifest order or silently substitute a requested implementation.
*/
export function selectArtifactImplementation(
manifest: PackageArtifacts,
selection: ImplementationSelection,
): ArtifactImplementation {
const candidates = manifest.implementations.filter(
(entry) =>
(selection.id === undefined || entry.id === selection.id) &&
entry.target === selection.target &&
entry.compatibility === selection.compatibility &&
selection.exports.every((id) => entry.exports.includes(id)),
);
if (candidates.length !== 1)
throw new ExecutionPlanError(
candidates.length ? "AMBIGUOUS_IMPLEMENTATION" : "IMPLEMENTATION_UNAVAILABLE",
manifest.packageRevisionId,
`${selection.id ?? "Select an explicit implementation ID"}: expected one ${selection.target}/${selection.compatibility} implementation covering the requested exports; found ${candidates.length}`,
);
return candidates[0]!;
}
export interface CheckedWorld {
codec: "camino-world-v1";
plan: string;
artifacts: { binding: string; target: string; digest: string }[];
versions: Record<string, string>;
}
const fail = (path: string, message: string): never => {
throw new ExecutionPlanError("INVALID_ARTIFACT", path, message);
};
const object = (value: unknown, path: string): Record<string, unknown> => {
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
fail(path, "Expected a plain JSON record");
return value as Record<string, unknown>;
};
const keys = (record: Record<string, unknown>, required: string[], optional: string[], path: string) => {
if (
required.some((key) => !Object.hasOwn(record, key)) ||
Object.keys(record).some((key) => ![...required, ...optional].includes(key))
)
fail(path, "Unexpected or missing fields");
};
const text = (value: unknown, path: string): string => {
if (
typeof value !== "string" ||
!value.length ||
value.length > 4096 ||
value.includes("\0") ||
Buffer.from(value, "utf8").toString("utf8") !== value
)
fail(path, "Expected bounded nonempty text");
return value as string;
};
const digest = (value: unknown, path: string): string => {
if (typeof value !== "string" || !/^[0-9a-f]{64}$/.test(value)) fail(path, "Expected SHA-256 hex");
return value as string;
};
const array = (value: unknown, path: string): unknown[] => {
if (!Array.isArray(value) || value.length > 10000) fail(path, "Expected a bounded array");
return value as unknown[];
};
const unique = (values: string[], path: string) => {
if (new Set(values).size !== values.length) fail(path, "Duplicate identities");
};
export function artifactPath(value: unknown): string {
const path = text(value, "artifact path");
if (
path.startsWith("/") ||
path.includes("\\") ||
path.split("/").some((part) => !part || part === "." || part === "..") ||
/[\u0000-\u001f\u007f?#%]/.test(path)
)
fail(path, "Expected a normalized relative artifact path");
return path;
}
/** No self-reference: the digest covers ABI/export metadata and the digests of every declared file. */
export function implementationDigest(implementation: Omit<ArtifactImplementation, "contentDigest">): string {
return hexDigest({
...implementation,
exports: [...implementation.exports].sort(),
files: [...implementation.files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
});
}
export function componentArtifactDigest(component: Omit<ComponentArtifact, "contentDigest">): string {
return hexDigest({
...component,
files: [...component.files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
});
}
function parseArtifactFiles(value: unknown, path: string): ArtifactFile[] {
const files = array(value, path).map((value): ArtifactFile => {
const file = object(value, path);
keys(file, ["path", "digest", "bytes", "mediaType"], [], path);
if (!Number.isSafeInteger(file.bytes) || (file.bytes as number) < 0 || (file.bytes as number) > 256 * 1024 * 1024)
fail(path, "Artifact file exceeds size bound");
return {
path: artifactPath(file.path),
digest: digest(file.digest, path),
bytes: file.bytes as number,
mediaType: text(file.mediaType, path),
};
});
unique(
files.map((file) => file.path),
path,
);
return files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
}
function parseComponent(value: unknown, index: number): ComponentArtifact {
const path = `components[${index}]`,
entry = object(value, path);
keys(
entry,
["id", "subject", "compatibility", "path", "exportName", "sharedModules", "files", "contentDigest"],
[],
path,
);
if (entry.compatibility !== "org.quixos.react.esm/1") fail(path, "Unsupported component ABI");
const files = parseArtifactFiles(entry.files, `${path}.files`);
const shared = object(entry.sharedModules, `${path}.sharedModules`);
keys(shared, [...componentSharedModules], [], `${path}.sharedModules`);
const sharedModules = Object.fromEntries(
componentSharedModules.map((name) => [name, artifactPath(shared[name])]),
) as ComponentArtifact["sharedModules"];
const entryPath = artifactPath(entry.path),
exportName = text(entry.exportName, path);
if (!/^[$A-Z_a-z][$\w]*$/.test(exportName)) fail(path, "Expected a JavaScript component export name");
for (const modulePath of [entryPath, ...Object.values(sharedModules)])
if (!files.some((file) => file.path === modulePath && file.mediaType === "text/javascript"))
fail(path, "Component entry and shared modules require declared JavaScript files");
const component: ComponentArtifact = {
id: text(entry.id, path),
subject: text(entry.subject, path),
compatibility: "org.quixos.react.esm/1",
path: entryPath,
exportName,
sharedModules,
files,
contentDigest: digest(entry.contentDigest, path),
};
const { contentDigest, ...body } = component;
if (componentArtifactDigest(body) !== contentDigest) fail(path, "Component metadata digest mismatch");
return component;
}
export function parseArtifactGroup(value: unknown): ArtifactGroup {
const record = object(value, "manifest");
keys(
record,
["schemaVersion", "packageRevisionId", "bindingDigest", "versions", "implementations"],
["components"],
"manifest",
);
if (record.schemaVersion !== 1) fail("manifest", "Unsupported artifact manifest version");
const versions = object(record.versions, "versions");
keys(versions, ["value", "reference", "ports", "crdt"], [], "versions");
for (const [key, value] of Object.entries(versions)) text(value, `versions.${key}`);
const implementations = array(record.implementations, "implementations").map(
(value, index): ArtifactImplementation => {
const path = `implementations[${index}]`,
entry = object(value, path);
keys(
entry,
["id", "target", "compatibility", "path", "ports", "exports", "files", "contentDigest"],
["registryExport"],
path,
);
if (
!["quixos-service-v1", "quixos-js-module-v1", "quixos-restricted-module-v1"].includes(
String(entry.compatibility),
)
)
fail(path, "Unsupported artifact ABI");
if (entry.ports !== "quixos-transaction-ports-v1") fail(path, "Unsupported transaction port ABI");
if (
entry.compatibility === "quixos-service-v1" &&
(entry.target !== "server" || entry.registryExport !== undefined)
)
fail(path, "Service must target server and has no JS registry export");
if (entry.compatibility !== "quixos-service-v1") text(entry.registryExport, `${path}.registryExport`);
if (entry.compatibility === "quixos-restricted-module-v1" && entry.target !== "server")
fail(path, "Restricted modules must target server");
const exports = array(entry.exports, `${path}.exports`).map((value) => text(value, `${path}.exports`));
unique(exports, `${path}.exports`);
const files = parseArtifactFiles(entry.files, `${path}.files`);
const entryPath = artifactPath(entry.path);
if (!files.some((file) => file.path === entryPath)) fail(path, "Entrypoint is not covered by the file manifest");
const result: ArtifactImplementation = {
id: text(entry.id, path),
target: text(entry.target, path),
compatibility: entry.compatibility as ArtifactImplementation["compatibility"],
path: entryPath,
ports: "quixos-transaction-ports-v1",
exports: exports.sort(),
files: files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
contentDigest: digest(entry.contentDigest, path),
...(entry.registryExport !== undefined ? { registryExport: entry.registryExport as string } : {}),
};
const { contentDigest, ...body } = result;
if (implementationDigest(body) !== contentDigest) fail(path, "Implementation metadata digest mismatch");
return result;
},
);
unique(
implementations.map((implementation) => implementation.id),
"implementations",
);
const components =
record.components === undefined ? undefined : array(record.components, "components").map(parseComponent);
if (components)
unique(
components.map((component) => component.id),
"components",
);
return {
schemaVersion: 1,
packageRevisionId: text(record.packageRevisionId, "packageRevisionId"),
bindingDigest: digest(record.bindingDigest, "bindingDigest"),
versions: versions as unknown as PackageArtifacts["versions"],
implementations,
...(components ? { components } : {}),
};
}
export function parsePackageArtifacts(value: unknown): PackageArtifacts {
const { sourceDigest, ...group } = object(value, "manifest");
return { ...parseArtifactGroup(group), sourceDigest: digest(sourceDigest, "sourceDigest") };
}
export function checkPackageCoverage(
expected: Pick<PlanPackage, "revision" | "modules" | "components" | "exports" | "bindingDigest" | "sourceDigest">,
manifest: PackageArtifacts,
): void {
for (const module of expected.modules ?? [])
if (!expected.components?.some((component) => component.id === module.id))
fail(module.id, `No artifact consumer for module contract ${module.contract} and its subject`);
if (expected.bindingDigest !== manifest.bindingDigest || expected.sourceDigest !== manifest.sourceDigest)
fail(expected.revision, "Substituted binding schema or source revision");
for (const component of manifest.components ?? []) {
const declaration = expected.components?.find((entry) => entry.id === component.id);
if (!declaration || declaration.subject !== component.subject || declaration.runtime !== "react")
fail(component.id, "Component differs from its declared subject/runtime contract");
}
for (const component of expected.components ?? [])
if (!manifest.components?.some((entry) => entry.id === component.id))
fail(component.id, "Missing component artifact");
for (const implementation of manifest.implementations)
for (const id of implementation.exports) {
const entry = expected.exports.find((e) => e.id === id);
if (!entry) fail(implementation.id, `Unknown export ${id}`);
if (
implementation.compatibility === "quixos-restricted-module-v1" &&
(entry!.execution.kind === "remote" || !entry!.execution.targets.includes("server"))
)
fail(implementation.id, `Export ${id} does not promise restricted server computation`);
if (
implementation.target !== "server" &&
(!entry?.execution ||
entry.execution.kind === "remote" ||
!entry.execution.targets.includes(implementation.target))
)
fail(implementation.id, `Export ${id} does not promise target ${implementation.target}`);
}
for (const entry of expected.exports) {
if (!manifest.implementations.some((i) => i.compatibility === "quixos-service-v1" && i.exports.includes(entry.id)))
fail(expected.revision, `Missing service fallback for ${entry.id}`);
if (entry.execution.kind !== "remote")
for (const target of entry.execution.targets)
if (
!manifest.implementations.some(
(i) =>
i.target === target &&
i.exports.includes(entry.id) &&
(target === "server" || i.compatibility !== "quixos-service-v1"),
)
)
fail(expected.revision, `Missing ${target} implementation for ${entry.id}`);
}
}
export type ArtifactReader = (packageRevisionId: string, path: string, maxBytes: number) => Promise<Uint8Array>;
/** Shared by standalone package checks and complete world installation. */
export async function checkArtifactFiles(manifest: ArtifactGroup, read: ArtifactReader): Promise<void> {
const files = new Map<string, ArtifactFile>();
for (const artifact of [...manifest.implementations, ...(manifest.components ?? [])])
for (const file of artifact.files) {
const existing = files.get(file.path);
if (existing && canonicalJson(existing) !== canonicalJson(file)) fail(file.path, "Conflicting file descriptors");
files.set(file.path, file);
}
for (const file of files.values()) {
const bytes = await read(manifest.packageRevisionId, file.path, file.bytes);
if (bytes.byteLength !== file.bytes || createHash("sha256").update(bytes).digest("hex") !== file.digest)
fail(file.path, "Artifact bytes do not match the checked content digest");
}
}
/** Caller supplies a reader confined to the resolved immutable package output, including symlink checks. */
export async function checkExecutionWorld(
plan: ExecutionPlan,
inputs: readonly unknown[],
read: ArtifactReader,
): Promise<CheckedWorld> {
if (canonicalJson(plan.definition.versions) !== canonicalJson(executionVersions))
fail("world", "Unsupported plan codec versions");
if ((await executionPlanDigestAsync(plan)) !== plan.digest) fail("plan", "Plan digest mismatch");
const manifests = inputs.map(parsePackageArtifacts);
unique(
manifests.map((m) => m.packageRevisionId),
"packages",
);
if (manifests.length !== plan.definition.packages.length) fail("world", "Package closure differs from plan");
const artifacts: CheckedWorld["artifacts"] = [];
for (const expected of plan.definition.packages) {
const manifest =
manifests.find((m) => m.packageRevisionId === expected.revision) ??
fail(expected.revision, "Missing package artifact");
checkPackageCoverage(expected, manifest);
if (canonicalJson(manifest.versions) !== canonicalJson(plan.definition.versions))
fail(expected.revision, "Incompatible runtime codecs");
await checkArtifactFiles(manifest, read);
for (const implementation of manifest.implementations) {
artifacts.push({
binding: JSON.stringify([expected.revision, implementation.id]),
target: implementation.target,
digest: implementation.contentDigest,
});
}
for (const component of manifest.components ?? []) {
artifacts.push({
binding: JSON.stringify([expected.revision, "component", component.id]),
target: "browser-renderer",
digest: component.contentDigest,
});
}
}
return {
codec: "camino-world-v1",
plan: plan.digest,
artifacts: artifacts.sort((a, b) =>
a.binding < b.binding ? -1 : a.binding > b.binding ? 1 : a.target < b.target ? -1 : a.target > b.target ? 1 : 0,
),
versions: { ...plan.definition.versions },
};
}
/** Node counterpart of replica-core worldDigest. The wire fixture checks cross-package agreement. */
export function checkedWorldDigest(world: CheckedWorld): string {
const normalized = {
...world,
artifacts: [...world.artifacts].sort((a, b) =>
a.binding < b.binding ? -1 : a.binding > b.binding ? 1 : a.target < b.target ? -1 : a.target > b.target ? 1 : 0,
),
};
const json = canonicalJson(normalized),
content = createHash("sha256").update(json).digest("hex");
const components = ["camino-world-v1", content].map((text) => Buffer.from(text, "utf8"));
const count = Buffer.alloc(4);
count.writeUInt32BE(components.length);
const pieces = [count];
for (const bytes of components) {
const length = Buffer.alloc(4);
length.writeUInt32BE(bytes.length);
pieces.push(length, bytes);
}
return createHash("sha256").update(Buffer.concat(pieces)).digest("hex");
}
+224
View File
@@ -0,0 +1,224 @@
import type { BindingSchema, BindingContractSchema } from "../bindings/index.js";
import type { InterfaceRevision, PackageRevision } from "../capability-model/types.js";
import { canonicalJson } from "../capability-model/evolution.js";
import { moduleContract } from "../capability-model/modules.js";
import { generatePackageComponentBindings, publicQueryContract, type PublicQueryContract } from "./client-contracts.js";
import { moduleTypeContext } from "./module-contracts.js";
import { executionVersions, hexDigest, ExecutionPlanError, type PlanPackage } from "./plan.js";
import { preparationArtifact, type ReactPreparationArtifact } from "../react-build/declarations.js";
/** Inputs to compilation, independent of source pins and installed providers.
* Declared ports select interface contracts; the React consumer projects its
* imported interfaces, public queries and module inputs. Ordinary code
* and richer language type companions remain separate, explicit Nix inputs. */
export interface PackageBuildContract {
definition: {
codec: "camino-package-build-v1";
package: Omit<PlanPackage, "source" | "sourceDigest" | "bindingDigest">;
versions: typeof executionVersions;
bindings: BindingContractSchema;
queries: PublicQueryContract[];
component?: string;
moduleInterfaces: { revisionId: string; definitionId?: string; members: string[] }[];
diagnostics: string[];
};
digest: string;
}
const normalize = <T>(value: T): T => JSON.parse(canonicalJson(value)) as T;
const indexSchema = (schema: BindingSchema) => ({
interfaces: new Map<string, InterfaceRevision>(
[...schema.interfaces, ...(schema.interfaceTemplates ?? [])].map((entry) => [entry.revisionId, entry]),
),
packages: new Map<string, PackageRevision>(schema.packages.map((entry) => [entry.revisionId, entry])),
});
export function packageBuildContexts(
schema: BindingSchema,
): Record<string, { contract: PackageBuildContract; sourceDigest: string }> {
const index = indexSchema(schema);
return Object.fromEntries(
schema.packages.map((pkg) => [
pkg.revisionId,
{
contract: projectPackageContract(schema, pkg.revisionId, index),
sourceDigest: hexDigest({ revision: pkg.revisionId, source: pkg.source }),
},
]),
);
}
export function packageBuildContract(schema: BindingSchema, revision: string): PackageBuildContract {
return projectPackageContract(schema, revision, indexSchema(schema));
}
function projectPackageContract(
schema: BindingSchema,
revision: string,
index: ReturnType<typeof indexSchema>,
): PackageBuildContract {
const declared = index.packages.get(revision);
if (!declared) throw new ExecutionPlanError("UNKNOWN_BUILD_PACKAGE", revision, "Missing package declaration");
const pkg = normalize(declared);
const interfaces = (ids: Iterable<string>) =>
[...ids].map((id) => {
const iface = index.interfaces.get(id);
if (!iface) throw new ExecutionPlanError("UNKNOWN_BUILD_INTERFACE", id, "Missing declared interface");
return normalize(iface);
});
const modules = [...(pkg.modules ?? [])]
.sort((a, b) => a.id.localeCompare(b.id))
.map((module) => {
const contract = moduleContract(interfaces([module.interfaceRevisionId]), module);
if (!contract) throw new ExecutionPlanError("UNKNOWN_MODULE_CONTRACT", module.id, "Missing module contract");
return {
...module,
contract: contract.contract,
...(contract.subject ? { subject: contract.subject } : {}),
...moduleTypeContext(index.interfaces.get(module.interfaceRevisionId)!),
};
});
const components = modules.flatMap((module) =>
["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes(
module.contract,
) && module.subject?.kind === "interface"
? [
{
id: module.id,
displayName: module.displayName,
subject: module.subject.interfaceRevisionId,
runtime: "react" as const,
},
]
: [],
);
const shape: PackageBuildContract["definition"]["package"] = {
revision,
exports: [...pkg.exports]
.sort((a, b) => a.id.localeCompare(b.id))
.map((entry) => ({ id: entry.id, execution: entry.execution ?? { kind: "remote" } })),
...(modules.length ? { modules } : {}),
...(components.length ? { components } : {}),
...(pkg.controllers?.length ? { controllers: [...pkg.controllers].sort((a, b) => a.id.localeCompare(b.id)) } : {}),
};
const portInterfaces = new Set(
pkg.exports.flatMap((entry) =>
entry.dependencyPorts.flatMap((port) =>
port.requirement.kind === "interface" ? [port.requirement.interfaceRevisionId] : [],
),
),
);
const bindings: BindingContractSchema = {
format: "quixos-bindings",
version: 1,
packages: [
{
revisionId: pkg.revisionId,
exports: [...pkg.exports].sort((a, b) => a.id.localeCompare(b.id)),
...(pkg.genericExports ? { genericExports: pkg.genericExports } : {}),
},
],
interfaces: interfaces(portInterfaces)
.map(({ source, ...contract }) => contract)
.sort((a, b) => a.revisionId.localeCompare(b.revisionId)),
};
const preparation = pkg.buildArtifacts?.[preparationArtifact] as ReactPreparationArtifact | undefined;
const diagnostics = components.flatMap((component) => {
const input = preparation?.modules.find((entry) => entry.module === component.id);
return input && pkg.queryCompositions?.some((entry) => entry.id === input.composition)
? []
: [`${component.id}: declare checked inputs in quixos.react.json`];
});
// Only interfaces whose modules are exposed by these bindings need language
// type companions. Reference types themselves do not recursively import the
// referenced object's implementation or its conformance inventory.
const used = new Set<string>(Object.values(pkg.clientInterfaces ?? {}));
for (const module of modules) used.add(module.interfaceRevisionId);
for (const component of components) used.add(component.subject);
for (const entry of preparation?.modules ?? [])
for (const child of entry.children) if (child.role) used.add(child.role.interface);
const moduleInterfaces = interfaces(used)
.flatMap((iface: InterfaceRevision) => {
const members = iface.members
.filter((member) => member.kind === "module")
.map((member) => member.id)
.sort();
return members.length
? [
{
revisionId: iface.revisionId,
...(iface.application ? { definitionId: iface.application.definitionId } : {}),
members,
},
]
: [];
})
.sort((a, b) => a.revisionId.localeCompare(b.revisionId));
for (const module of modules)
for (const argument of index.interfaces.get(module.interfaceRevisionId)?.application?.arguments ?? [])
if (argument.kind === "object" && argument.target.kind === "interface")
used.add(argument.target.interfaceRevisionId);
const providers = new Set<string>(Object.values(pkg.clientPackages ?? {}));
for (const query of pkg.queries ?? [])
for (const imported of query.importedFragments ?? []) providers.add(imported.packageRevisionId);
providers.delete(pkg.revisionId);
const componentSchema: BindingSchema = {
format: "quixos-bindings",
version: 1,
interfaces: interfaces(used),
packages: [
pkg,
...[...providers].map((id) => {
const provider = index.packages.get(id);
if (!provider) throw new ExecutionPlanError("UNKNOWN_BUILD_PROVIDER", id, "Missing declared package provider");
return normalize(provider);
}),
],
};
const definition: PackageBuildContract["definition"] = {
codec: "camino-package-build-v1",
package: shape,
versions: { ...executionVersions },
bindings,
queries: [
...new Map(
pkg.exports.flatMap((entry) =>
entry.dependencyPorts.flatMap((port) => {
if (port.requirement.kind !== "query") return [];
const requirement = port.requirement;
const provider = index.packages.get(requirement.packageRevisionId);
if (!provider)
throw new ExecutionPlanError(
"UNKNOWN_QUERY_PROVIDER",
requirement.packageRevisionId,
"Missing declared query provider",
);
return [
[
JSON.stringify([requirement.packageRevisionId, requirement.queryId]),
publicQueryContract(provider, requirement.queryId),
] as const,
];
}),
),
).entries(),
]
.sort(([a], [b]) => a.localeCompare(b))
.map(([, query]) => query),
...(components.length && !diagnostics.length
? { component: generatePackageComponentBindings(componentSchema, shape) }
: {}),
moduleInterfaces,
diagnostics,
};
const normalized = JSON.parse(canonicalJson(definition)) as typeof definition;
return { definition: normalized, digest: hexDigest(normalized) };
}
export function checkPackageBuildContract(contract: PackageBuildContract): void {
if (contract.definition.codec !== "camino-package-build-v1" || hexDigest(contract.definition) !== contract.digest)
throw new ExecutionPlanError("INVALID_BUILD_CONTRACT", contract.digest, "Package contract digest mismatch");
if (contract.definition.diagnostics.length)
throw new ExecutionPlanError(
"INCOMPLETE_BUILD_CONTRACT",
contract.definition.package.revision,
contract.definition.diagnostics.join("; "),
);
}

Some files were not shown because too many files have changed in this diff Show More