# package.osy

> The manifest that makes a git repository a publishable Osy# package. It names the package, says which of the repository's files actually ship, declares the platform floor its source needs, and lists the packages it builds on.

<!-- id: project-package · area: project · stability: preview · html: https://osysharp.com/reference/project/package/ -->

## Summary        {#summary}
`app.osy` describes an **application**: everything a compile consumes, tests included. `package.osy` describes an
**artifact**: what goes into the archive somebody else fetches. A repository carries tests, CI, docs and
`node_modules`; none of those are the package, and the manifest's globs are where that line is drawn.

A package's identity is `Owner.Name`, and the owner is what makes the name yours — it maps to the GitHub account or
organisation that publishes it, so nobody can publish under a name they do not control. A name may go deeper —
`Owner.Repo.Folder` — for a package that lives in a [folder of a family repository](#family), beside its siblings.

> **Preview.** Writing, publishing, fetching and resolving all work today — `osy publish` creates the release and
> `osy lock` pins what it resolved. What is still moving is the platform's own kits: they are becoming packages one at
> a time, so some of them are still bundled with the binary.

## Signature      {#signature}
```osy syntax
package <Owner>.<Name> {
  version     "1.0.0";        // required — what a `use` constraint resolves against
  minPlatform "0.9.0";        // the platform floor this package's SOURCE needs
  contract    "1.1";          // the control ABI generation, a separate axis
  summary     "One line.";
  tests       "tests";        // your own test project — `osy publish` runs it, and it does NOT ship
  personal    Content;        // a personal-data level its fields carry — one line per level (a bare word)
  role        vertical;       // hub · horizontal (the default) · vertical — what it is to the kits around it
  appPage     "/account";     // a page it sends people to that the APP serves — one line per route
  completes   <Owner>.<A>@0, <Owner>.<B>@0;   // a BRIDGE: it arrives in an app that uses both
  deprecated  "<Control>(<former parameters>)" { since "0.16.1"; use "<Control>(<new parameters>)"; }   // a retired signature

  model    "model/**/*.osy";  // the Osy# a consumer's compiler reads
  controls "controls/**";     // built control bundles and their chunks
  fonts    "fonts/**";
  native   "lib/Acme.Grid.dll";  // assemblies implementing its native classes — no default

  use <Owner>.<Other>@1;      // a package this one builds on
  use Osysharp.Host { program "git"; folder "temp:/acme"; }   // what it would do on the computer — a request the app answers
}
```

## Description    {#description}

### What a package says about itself   {#settings}

| Setting | Literal | What it decides |
|---|---|---|
| `version` | string | **Required.** The version a `use Owner.Name@1;` constraint resolves against, and the git tag publishing creates. A package without one cannot be depended on. |
| `minPlatform` | string | The oldest platform this package's source will run on — a **floor**: a platform at or above it may lock the release, however much newer. Whether it then compiles there is decided by compiling it. Read **first**, before anything else in the manifest — see [below](#min-platform). |
| `contract` | string | The control ABI generation the package's controls are written against — `"1.0"` or `"1.1"` today. Independent of `version`: a package can ship many versions against one ABI. A generation this platform cannot host is refused, naming the ones it can. |
| `summary` | string | One line, shown when somebody is deciding whether to depend on you. |
| `tests` | string | The directory, relative to the package root, holding your own [test project](#testing). `osy publish` runs it and refuses to publish a package whose tests fail. **Nothing it names ships** — it is a setting, not a role. A path that leaves the package is refused. |

A setting is declared once. A **role** may repeat, and every glob applies — that difference is the whole reason the
two are separate vocabularies.

**`personal <Level>;`** announces a personal-data level the package classifies its own fields with — `Contact`,
`Identity`, `Financial`, `Content` or `Special`, a bare word, one line per level. An app reads it to know what personal
data the package brings, and may map a level onto its own classification (`Includes = [Owner.Name.Personal.Content]`);
a level it maps to nothing is read through each row's own security ([[security-personal-data#kit-levels]]). A field
classified with a level the manifest does not announce is refused (`PERSONAL_LEVEL_UNDECLARED`), and a word outside the
five is refused naming them (`PACKAGE_PERSONAL_LEVEL`).

**`role <word>;`** says what the kit is to the kits around it: `hub` (it holds nouns other kits meet at),
`horizontal` (the default — something kits build on freely) or `vertical` (a product other kits meet only through a
bridge). `osy kits check` refuses a kit that uses a vertical it neither completes nor extends ([Checking a kit composes — osy kits check](https://osysharp.com/reference/local/checking-a-kit/)).

**`completes <A>@<major>, <B>@<major>;`** makes the package a **bridge**: an app that uses every package it names gets
it without a `use` line, and `osy lock` says so. Each one must be a package the bridge `use`s, at that major
([Bridges — a package that arrives when an app uses the two it completes](https://osysharp.com/reference/project/bridges/)).

**`appPage "<route>";`** names a page the package's code sends someone to and the **app** serves — a shop kit sends a
signed-in customer to `/account`, a look's frame links to `/bag`. The page is the app's own (its layout, its words), so
the package names the route and the app declares a `[Route("/account")]` there. One line per route, written exactly as
the `[Route]` that serves it; a path that does not start with `/` is refused (`PACKAGE_APP_PAGE`). It is also what lets
the package be [checked on its own](#validating): the check declares a page at each listed route, and a route the
package navigates to without listing it is refused there with the line to add.

**`deprecated "<Control>(<former parameters>)" { since "…"; use "…"; }`** retires a former signature of one of the
package's controls. New source that calls it the old way is refused naming `use`; an app already deployed with the old
call keeps rebuilding until `refusedFrom` (by default the minor after `since`), with a warning on its version; and
`osy migrate code` rewrites the call. Settings: `since` and `use` (required), `why`, `refusedFrom`; a setting outside
those, or a value that is not a control's name and parameter list, is refused (`PACKAGE_DEPRECATED_SHAPE`). One entry
per retired form ([[project-deprecations#kit-deprecations]]).

### Which of the repository’s files ship   {#roles}

Each role is a glob, and the globs are what decides what ships. Declare a role and your globs replace its default.

| Role | Default | What it carries |
|---|---|---|
| `model` | `model/**/*.osy` | The Osy# a consumer's compiler reads. A package that ships none declares nothing. |
| `controls` | `controls/**` | Built control bundles and their chunks. |
| `fonts` | `**/fonts/*.woff2` and the other three web font formats | Font files, pinned by hash. |
| `icons` | `**/icons/*.svg` | Build-time glyphs, exactly as in an app. |
| `svg` | `**/art/*.svg` | Illustrations rendered inline, keeping their own fills. |
| `textures` | `**/textures/*.png` and the other raster formats | Raster assets. |
| `sounds` | `**/sounds/*.mp3` and the other audio formats | Audio assets. |
| `native` | none — declared or absent | .NET assemblies implementing the package's [native classes](https://osysharp.com/reference/class/native/). `.dll` files only, and a declared glob must match at least one. |

There is no `tests` role, and that is deliberate: a package's tests are its repository's business and run before an
archive exists. They are not part of what somebody downloads.

Nothing assumes your sources live under `src/`. A flat repository is a normal package — say so and it works:

```osy preview
package Osysharp.Charts {
  version "1.0.0";
  summary "Composable charts, in Osy#.";

  model "*.osy";
  model "vocabularies/*.osy";
}
```

### A control and its bundle travel together   {#controls}

A control is two halves: a `control` declaration in your Osy#, and the JavaScript bundle it names. They go into one
archive under one hash, so a consumer cannot end up with one half from one build and the other from another. That is
not a rule anybody has to remember — the halves are not two artifacts.

```osy preview
package Someone.Editor {
  version     "2.0.0";
  minPlatform "1.0.0";
  contract    "1.1";

  model    "model/**/*.osy";
  controls "controls/**";
  fonts    "fonts/**";
}
```

### Depending on another package   {#use}

`use` inside a package manifest means what it means inside `app.osy`: this is a dependency, at this version. Ask for
a package and its own dependencies come with it.

```osy preview
package Someone.Dashboard {
  version "1.4.0";
  model   "model/**/*.osy";

  use Osysharp.Charts@1;
  use Someone.Editor@2;
}
```

**A dependency is transitive; an import is not.** Fetching `Someone.Dashboard` fetches `Osysharp.Charts` too, because
the dashboard needs it to compile. It does **not** put the charts in *your* app's scope. If a page of yours names a
chart directly, your own `app.osy` says `use Osysharp.Charts@1;`. That is C#'s rule — a `using` never re-exports — and
it is what stops an app quietly depending on something it never declared, then breaking the day a package drops it.

**And it reads the same way in reverse: a package reaches only what its own manifest declares.** Not what the app
composing it declared, and not what another package alongside it declared. A package that writes no `use` line calls
no `Http.*`, however many of its neighbours ask for `Osysharp.Http` — the refusal names the package and its own
manifest:

```text
`Http.Get` requires `use Osysharp.Http;` in `Acme.Quiet`'s OWN `package { }` manifest — a package reaches only what
IT declares, never what the app composing it declared.
```

That is what makes a manifest worth reading before you depend on something: it is the whole of what that code can
reach, decided by the person who wrote it and by nobody standing next to it.

### Asking for authority — `egress`, `secret` and `theme`   {#grants}

Osy# has no ambient authority: no filesystem, no process, no shell. **Outbound HTTP is the one exception**, and it
stops being harmless the moment the code making the call is somebody else's. So a package **declares** what it needs
and the app that depends on it **grants** it — or the compile fails. (The computer a local platform runs on is asked
for and granted the same way, with two differences — see [the computer](#host-ask).)

```osy title="a package declares the hosts and secrets it needs" preview
package Acme.Snow {
  version "1.0.0";
  model   "model/**/*.osy";

  egress  "*.snow.example";      // hosts it needs to reach
  secret  "SnowKey";             // a secret it expects you to hand it
}
```

Both lines repeat, like a source role: a package that needs two hosts writes two lines.

The app decides, in the `use` line's body:

```osy title="the app grants them where it depends on the package" preview
app Ledger {
  model "model/**/*.osy";

  use Acme.Snow@1 {
    egress "*.snow.example";
    secret "SnowKey";
  }
}
```

**A bare `use Acme.Snow;` grants nothing**, and that is the default on purpose — a package must not acquire authority
by being depended on. Depend on one that needs something you have not granted and the compile stops, naming the line
to write:

> `Acme.Snow` declares it needs to reach `*.snow.example`, and this app has not granted it. Write
> `use Acme.Snow@1 { egress "*.snow.example"; }` — a package gets no authority from being depended on, so the grant
> is where you decide it is allowed to.

The reverse is a **warning**: a grant the package does not declare is authority handed over for nothing. It is not an
error, because a package that dropped a need in a newer version would otherwise break every consumer for becoming
safer.

**The package is held to its own declaration too.** Inside the package's own repository, an `Http.*` call to a
literal host its manifest does not list is a compile error there — so what a consumer reads in the manifest is what
the package can reach, rather than a claim nobody checks.

**And the grant is enforced when the call happens**, which is the case no static check can decide: a url built at run
time is exactly what a package would use to go somewhere its manifest does not name. A package reaching an ungranted
host fails there, naming the package, the host, and what it *is* allowed to reach. The same applies to
`Secret.<Name>` — a package reads only the credentials you handed it, and that is the half that matters most: a host
is a place, a credential is spending power.

**Your own code is not on a leash by default.** `Http.*` stays open for the app you wrote — the platform blocks only
internal addresses — until an administrator sets egress rules for the app ([Where your app may connect — the .egress file](https://osysharp.com/reference/project/egress/): `osy egress allow` / `osy egress deny`), or its
organization does. The package leash exists regardless, because a package is somebody else's code running inside your
app.

**The operator's rules bind a package's grant too.** A grant is the app's word about that package, so the app's own
allow-list does not narrow it — but a **deny**, the app's or the organization's, refuses the host whatever was granted,
and an organization's allow-list is a ceiling no grant can reach past. `osy egress list` shows every package's granted
hosts beside the rules, and whether each is in force.

A wildcard covers subdomains and **not** the apex: `*.snow.example` matches `api.snow.example` and does not match
`snow.example`. That is the rule browsers and TLS certificates use, and the direction a security check should be
wrong in is the narrow one.

### Asking for the computer — `use Osysharp.Host { … }`   {#host-ask}

A package that starts a program or keeps files on the user's computer says so in its manifest, in the body of its
own `use Osysharp.Host` — the same three words an app declares for itself ([Host — the computer a local app runs on](https://osysharp.com/reference/stdlib/host/)):

```osy title="a package asks for a program and a folder" syntax
package Acme.GitTools {
  version "1.0.0";
  model   "model/**/*.osy";

  use Osysharp.Host {
    program "git";
    folder  "temp:/acme-git";
  }
}
```

It is a **request**. The app that depends on the package answers each line in the body of its `use` of the package:
the line itself grants it, a folder inside the one asked for grants less, and `without program "git";` declines it.

```osy title="the app answers every line the package asks" syntax
use Acme.GitTools@1 { program "git"; folder "temp:/acme-git/work"; }
```

Two things differ from `egress` and `secret`:

- **A grant is never wider than the request, and that is an error, not a warning.** A program the package does not
  ask for, or a folder outside the one it names, stops the compile. So what you read in a package's manifest is the
  most it can do on a computer, whatever an app writes — and ask for what you need by name: there is no line that
  means every program or every folder.
- **A line can be declined.** `without <line>` uses the package without it; the package's call is then refused when
  it runs, so write the code that uses the computer to say so on the page rather than assume the grant.

An unanswered line stops the compile, naming your package, what it asks and the line to write. A package cannot
grant another package the computer — `use Acme.GitTools { program "git"; }` in a package's manifest is refused — and
an app must name every package that uses the computer itself, including one that arrives through yours. Raise your
`version` when you ask for more: `osy update` does not pin a release that asks for something the app has not
answered. The whole rule, from the app's side, is [Host — the computer a local app runs on](https://osysharp.com/reference/stdlib/host/) `#packages`.

### A host the app chooses — `egress consumer;` — and any host — `egress "*";`   {#egress-any}

A package whose endpoint its **consumer** configures — a self-hosted model, an S3-compatible store, a customer's own
gateway — cannot name the host in its manifest. It says so with the bare word, and the app grants the host it set up:

```osy title="the package takes a host its consumer names" syntax
package Acme.Store {
  version "1.0.0";
  egress consumer;
}

app Ledger {
  use Acme.Store@1 { egress "files.ledger.example"; }
}
```

`egress consumer;` hands over nothing: the app's grant is still the only thing that names a host. Some packages reach
hosts that nobody knows when the app is compiled — an outbound-webhooks kit posts to whatever endpoint an
administrator subscribes while the app runs. For those, the app grants **any public host**:

```osy title="any public host, for a package whose hosts are chosen at run time" syntax
app Desk {
  use Osysharp.Webhooks@0 { egress "*"; }
}
```

- `"*"` is accepted only for a package that declares `egress consumer;`. Granted to one whose manifest names its hosts,
  it is a compile error listing the hosts to grant instead.
- A package never asks for it: `egress "*";` in a package's own manifest is refused — the package says the host is its
  consumer's to choose, and the consumer decides it may be any.
- It is never a browser grant: a page cannot be opened to every host for one control.
- It is no exemption from anything else: the platform still refuses internal addresses (loopback, the cloud metadata
  address), a `deny` — the app's or the organization's — still refuses its hosts, an organization's allow-list is still
  the ceiling, and a secret still goes only where a `secretIn` line takes it.

**A secret in a URL or a body is declared on the host line.** A secret may end in a request header wherever it goes;
an API that wants it in the query string (`?apikey=`) or in a body field (`client_secret=`) takes a body on the egress
line — `egress "api2.postnord.com" { secretIn query "apikey"; }` — and the app's grant of that host accepts it. An
ungranted one is refused saying a secret travels there. See [where a secret may be sent — secretIn](https://osysharp.com/reference/project/secret-sinks/).

### Hosts a control reaches from the browser — `egress browser`   {#browser-egress}

A control is browser code, and the app's pages carry a Content-Security-Policy that lets browser code reach only the
app's own origin. A control that genuinely needs another host — a model file served from a CDN, a map's tile server, a
realtime service — asks for it the same way a server host is asked for, with one word more:

```osy title="a package with a control declares what its browser code reaches" syntax
package Acme.Viz {
  version  "1.0.0";
  controls "viz.js";

  egress browser "models.example.com";          // data: fetch, WebSocket, fonts, media
  egress browser script "cdn.example.com";      // CODE: scripts and workers from that host
}
```

```osy title="the app grants them in the same use body" syntax
app Ledger {
  use Acme.Viz@1 {
    egress browser "models.example.com";
    egress browser script "cdn.example.com";
  }
}
```

The rules are the ones above: an ungranted need stops the compile naming the line to write, an unneeded grant is a
warning, and the operator's denies and organization ceiling decide whether a grant is in force. What differs is where
it is enforced — **the page's Content-Security-Policy**, widened for this app only:

| grant | widens |
|---|---|
| `egress browser "h"` | `connect-src` (`https://h`, `wss://h`), `font-src`, `media-src` |
| `egress browser script "h"` | all of the above, plus `script-src` and `worker-src` |

Three things follow from a CSP being **page-wide** — it cannot say "this control may reach X", only "this page may":

- **A server `egress` grant never widens the page.** `api.klarna.com` granted to a payment kit's Osy# code is not a host
  every script on your page should be able to call, so a browser need is its own grant.
- **A browser need belongs only to a package that ships a control.** One declared in a package with no `controls` entry
  is refused where the manifest is read — there is no browser code of its own for it to serve.
- **`egress browser script` is the largest grant a package can ask for**: somebody else's code running on your page with
  your user's session. Vendoring the library is still the better answer wherever it is possible.

**A host the app chooses — `egress browser consumer;`.** A control that talks to a server its *consumer* configures —
a self-hosted analytics server, the app's own tile server — cannot name it in its manifest. It says so with the bare
word, and the app grants whichever host (and kind) it configured:

```osy title="the control reaches a host the app names" syntax
package Acme.Stats {
  version  "1.0.0";
  controls "stats.js";
  egress browser consumer;
}

app Ledger {
  use Acme.Stats@1 { egress browser "stats.ledger.example"; }
}
```

Like `egress consumer;` for a server host, the word hands over nothing: the page's policy is built from the app's grants
alone, so a package that says it and is granted nothing reaches nothing. What it changes is that the app's grant is no
longer reported as unused. It belongs only to a package that ships a control.

A **wildcard** is admitted to the page only as a whole. A CSP source cannot carry an exception, so if any deny touches
a host inside `*.example.com`, the wildcard is left out entirely — the control loses the grant rather than the deny being
lost.

### Repainting the app — `theme`   {#theme-grant}

A theme's token names are **one flat namespace**: an app has one `Colors.Primary`, not one per theme. That is what
makes theming composable — a control paints with `Colors.Primary` and the app decides what it is — and it is also why
a package declaring one is not a local decision. Every bundled control paints with the platform's leaves whether your
app names them or not, so a package that re-declares `Colors.Primary` repaints your whole app.

So it is the same negotiation, with the token's `Group.Leaf` path as the value:

```osy title="a palette package declares the leaves it supplies" preview
package Acme.Palette {
  version "1.0.0";
  model   "model/**/*.osy";

  theme   "Colors.Primary";      // a PLATFORM leaf it re-declares
  theme   "Colors.Surface";
}
```

```osy title="the app grants them, and its whole UI changes" preview
app Ledger {
  model "model/**/*.osy";

  use Acme.Palette@1 {
    theme "Colors.Primary";
    theme "Colors.Surface";
  }
}
```

Adopting someone's palette is a perfectly good thing to want — that is what a theme package is for. What must not
happen is it arriving unannounced, which is what a package that re-declares a platform leaf **without** declaring it
gets refused for:

> `Acme.Palette` declares the theme token `Colors.Primary`, which the platform declares too — and token leaf names are
> ONE FLAT NAMESPACE, so this package would supply the value every bundled control paints with, across the whole app.
> Grant it if you mean it: `use Acme.Palette@1 { theme "Colors.Primary"; }`. Or declare `Colors.Primary` in your OWN
> `theme`, which owns the leaf outright — then neither the platform's value nor the package's applies.

Two things follow, and both are deliberate:

**Your own `theme` needs no grant, and settles the question.** A leaf the app declares belongs to the app — the
package's value never applies to it, so there is nothing to ask about. That is the existing local-wins rule, not a
new one.

**Two packages sharing a leaf the platform does not declare is not this rule.** If two packages both ship an `Accent`
your app never names, nothing is overridden and nothing is refused; it becomes an error only where your app *writes*
`Accent`, and then it names both owners. This grant governs exactly the overlap that check cannot see — the one where
the other owner is the platform, whose leaves are named by its own controls.

### minPlatform is read first, and that is not an implementation detail   {#min-platform}

A package published against a **newer** platform carries fields an older one has never heard of. If the older
platform checked the vocabulary first, it would complain about one of those — which reads like a broken package and
is actually an out-of-date reader.

So `minPlatform` is understood before anything else in the manifest is interpreted. An old platform meeting a new
package says the one useful thing:

```console
package 'Someone.Future' needs platform >= 2.0.0; this platform is 1.4.0.
Upgrade the platform, or use a version of the package published for this one.
```

Once the floor is met, the unknown entries are the story again and each is named.

### Which platforms a release runs on   {#platform-range}

**A release's `minPlatform` is a floor.** A platform at or above it may lock the release, however much newer it is:
`minPlatform "0.9.0"` is eligible on 0.9.4, on 0.12.0 and on 2.0.0, and refused on 0.8.9. A kit does not stop working
because the platform released — whether it runs here is decided by **compiling it** in your project, never by the
version number alone.

**`osy lock` takes the newest release your constraint allows whose floor this platform meets.** For a range like
`use Acme.Shop@0;` it passes over a release whose floor is above this platform for the newest one that runs here. When
none does — or you pinned an exact version — it refuses in one line and names a release that runs here:

```console
✗ Acme.Shop 1.0.0 needs platform 0.10.0 or newer; this platform is 0.9.0. 0.2.0 runs here — write `use Acme.Shop@0.2.0;`.
```

A release published for an older platform is locked, and `osy lock` says so beside it —
`published for platform 0.9.0 — osy validate compiles it here`.

**`osy validate` is the verdict.** It compiles every locked release in your project. If one published for an older
platform no longer compiles here, the first error you see says so in one sentence, followed by the kit's own errors:

```console
✗ PACKAGE_DOES_NOT_COMPILE_HERE  Acme.Shop 1.0.0 was published for platform 0.9.0 and does not compile on platform 0.12.3: unknown type 'NoSuchKind' — take a newer release if there is one (raise `use Acme.Shop@…;` in app.osy and run `osy lock`); otherwise tell its author (github.com/acme/shop) that it no longer compiles on platform 0.12.3. Its own errors follow.
```

A lock already written is never refused because the platform moved — a build that worked yesterday keeps building.
`osy search` and `osy kits find` warn only about a package whose newest release needs a **newer** platform than yours;
one published for an older platform is shown as information (`published for platform 0.9.0`), not as a warning.

**`osy publish` checks the field.** It validates the package and runs its tests on the platform doing the publishing,
so the floor must name that platform's line: a release that declares no `minPlatform`, or a floor from an earlier
line, is refused before anything is created, naming the line to write — `minPlatform "0.9.4";` on platform 0.9.4. The
floor says the oldest line the release was actually run on; consumers on every later platform may still take it.

### A family of packages — a repository and a folder   {#family}

Related packages can share one repository. A name with more than two segments is a **folder** of the repository its
first two name — each further segment one folder, lower-cased, the same rule the two-segment name always used:

| Package | Repository | Folder | Released by the tag |
|---|---|---|---|
| `Osysharp.Payments` | `github.com/osysharp/payments` | the root | `v0.3.0` |
| `Osysharp.Payments.Stripe` | `github.com/osysharp/payments` | `stripe/` | `stripe/v0.2.0` |
| `Osysharp.Payments.Klarna` | `github.com/osysharp/payments` | `klarna/` | `klarna/v0.9.0` |
| `Acme.Tools.Ui.Grid` | `github.com/acme/tools` | `ui/grid/` | `ui/grid/v1.0.0` |

Each folder holds its own `package.osy`, its own `model/` and its own tests:

```osy title="payments/stripe/package.osy — a member of the Payments family" syntax
package Osysharp.Payments.Stripe {
  version "0.2.0";
  model   "model/*.osy";
  use Osysharp.Payments@0;     // its sibling at the repository root, depended on like any package
}
```

**The folder-prefixed tag is what lets a family version its members independently.** `stripe/v0.2.0` releases
Stripe and nothing else; Klarna stays at whatever it last released. Resolving `use Osysharp.Payments.Stripe@0;` reads
only the `stripe/v…` tags, and the root package reads only the tags with no folder in them — so a family repository's
root never mistakes a member's release for its own.

`osyrin.lock` records all three — repository, folder and tag — in the entry's `source`:

```console
"name": "Osysharp.Payments.Stripe", "version": "0.2.0", "source": "github:osysharp/payments/stripe@stripe/v0.2.0"
```

Three rules keep the members apart:

- **A member's folder is not part of the package above it.** A root `model "**/*.osy";` reaches into `stripe/`, and
  the root's archive still leaves it out — a folder holding its own `package.osy` is another package.
- **The root may not declare into a member's name.** A child namespace is normally a package's own
  (`Osysharp.Payments.Internal`), but once `stripe/` publishes `Osysharp.Payments.Stripe`, a root file writing
  `namespace Osysharp.Payments.Stripe;` is refused when it is packed.
- **Depending on the parent does not import a member.** `use Osysharp.Payments;` gives you the Payments namespace and
  the child namespaces Payments itself declares — not Stripe, which is its own package with its own `use`:

```console
model/checkout.osy:1:1  ERROR  USING_WITHOUT_USE  `using Osysharp.Payments.Stripe;` imports a capability the app
does not depend on. Add `use Osysharp.Payments.Stripe;` inside the `app { }` manifest block … `use Osysharp.Payments;`
does not provide it: `Osysharp.Payments` declares no namespace `Osysharp.Payments.Stripe`, and a nested name like
this is a package of its own (a folder of the same repository), with its own `use`.
```

A two-segment name is unchanged by any of this: it lives at the root of its own repository and releases at `v…`,
exactly as before.

### Publishing is a git tag   {#publishing}

There is no registry, no account with us, and nothing to upload to. `osy publish` validates, packs and creates the
GitHub Release on the package's **own** repository — `Osysharp.Charts` is `github.com/osysharp/charts`, derived from
the name with no exceptions, which is what makes namespace ownership a fact rather than a rule somebody enforces.

```console
$ osy publish
Osysharp.Charts 1.0.0 → github.com/osysharp/charts
  archive   10 file(s), 58 206 bytes
  address   sha256:603a64ddb81c43423d5c2948ae44caf15adc0982f7e02bee2440394e2cf3e477
  commit    0a058a26 → tag v1.0.0
✓ Published Osysharp.Charts 1.0.0 — fetched back and verified, 58 206 bytes
```

Every check happens before anything is created, and `--dry-run` stops exactly there: the archive builds, the package's
own sources compile, its own [tests](#testing) pass when it declares where they are, the working tree is clean and pushed, `origin` IS the repository the package claims to be and the package sits in the folder its name says (the root, for a two-segment name), GitHub
has the commit the tag will name, and the version is not already published. That last one is a refusal rather than an
overwrite — a consumer's lock pins the hash of the archive **that** version published, so replacing it would not
update anybody, it would make their build refuse the bytes.

A [family member](#family) publishes from its folder, and the release is tagged with the folder:

```console
$ cd payments/stripe && osy publish
Osysharp.Payments.Stripe 0.2.0 → github.com/osysharp/payments/stripe
  archive   6 file(s), 21 904 bytes
  commit    4be1c2d0 → tag stripe/v0.2.0
```

Run from anywhere else in the repository, it refuses and names the folder the name means — and, when you are in some
other folder, the name that folder would give the package.

**The owner is fixed here, and asked for if it is not one yet.** A kit made beside an app with `osy kits new` is
named for that app until it goes public (`app Crux` → `Crux.Passes`) — nobody has to pick a publishing name for a kit
that may never leave the app. Publishing is the moment the name becomes permanent, so `osy publish` refuses a package
whose owner is still the name of the app it sits beside, or is not the owner its repository's `origin` publishes as,
before it validates or tests anything:

```console
$ osy publish kits/passes
✗ `Crux.Passes` is still named after the app it was made beside (`app Crux`) — `osy kits new` names a kit for its app
  until it is published, and this is that moment. Name the owner it will be published under — the GitHub account or
  organisation whose repository it goes to: rename `package Crux.Passes` to `package <Owner>.Passes` in package.osy,
  its `namespace` lines, and the app's `use Crux.Passes` line. A published name is permanent — every consumer's `use`
  spells it.
```

A publish needs a token with write access to the repository's contents: `--token`, `$OSY_GITHUB_TOKEN`,
`$GITHUB_TOKEN`, or a `gh auth login` it can read. The output says which one it used. **`--no-gh`** takes the last one
away — the token is then one you passed, or there is none and the publish is refused — and `OSY_NO_GH=1` does the same
for every `osy` verb in that environment. That is how an organisation publishes as its own bot from a machine where a
person is signed in to `gh`: a missing bot token becomes a refusal, never a publish under the person's name.

**Recreating a release — `--republish <sha256>`.** A version is immutable, so the only way to put it back on a
repository (one recreated under a new owner, say) is with exactly the bytes it had. `osy publish --republish
sha256:…` requires the archive this tree packs to to BE that hash, refuses anything else, and does not re-run validate
or the package's tests: they ran when those bytes were first released, and a newer compiler refusing source an older
one accepted must not stop the same bytes being restored for the locks that already pin them. Any change at all is a
new version.

Last, and never fatally, it adds the `osy-package` topic to the repository so [`osy search`](#search) finds it. That step is not
part of the publication — the release is — so a token that may not write topics leaves a package that resolves
perfectly and says plainly that it will not be found.

### Can a package live in a private repository?   {#private}

A package's repository may be **private**, and nothing else about it changes: the same `use Acme.Internal@1;`, the
same `osy lock`, the same content address in the lock file. Sharing a package between an organisation's own apps does
not need an organisation model, an account with us, or a mirror — it needs a repository your team can read.

The only difference is a **token**, resolved exactly as `osy publish` resolves it: `--token`, then
`$OSY_GITHUB_TOKEN`, then `$GITHUB_TOKEN`, then a `gh auth login` it can read. A fine-grained token needs
`Contents: read` on the repository; a classic one needs `repo`.

```console
$ osy lock
  github.com/acme/internal-kit → 2.1.0
✓ Acme.InternalKit 2.1.0 pinned — sha256:9f2c…
```

Two things are worth knowing, because both are visible when something goes wrong.

**A private repository answers 404, not 403.** GitHub does that on purpose — a 403 would let anyone enumerate private
repository names — so "this package does not exist" and "you are not signed in for it" are the *same answer*. A fetch
with no token says so rather than sending you to look for a typo:

```console
$ osy package fetch Acme.InternalKit@2.1.0
✗ no release asset at https://github.com/acme/internal-kit/releases/download/v2.1.0/package.zip.
  Three things look identical from here: acme/internal-kit has published no v2.1.0; its release carries
  no `package.zip`; or the repository is PRIVATE, which answers 404 to an anonymous request so that
  private names cannot be enumerated. If it is private, supply a GitHub token — `--token`,
  `$OSY_GITHUB_TOKEN`, `$GITHUB_TOKEN`, or `gh auth login`.
```

**The public download is still tried first, always.** A locked build of a *public* package makes no API call at all
and that is unchanged whether or not you have a token — the private door is reached only after the free one answers
404, and it costs two API calls. Your token is sent to `api.github.com` and nowhere else: not to the download URL,
and not to the storage host that serves the bytes.

⚠ **The token is a developer's, not an app's.** It is never written to `osyrin.lock`, never put in `.secrets`, and
never sent to a server — the CLI fetches and the server never does, so a deployed app carries the package's *bytes*
and no credential that could fetch them again.

### Do I need a GitHub account to use packages?   {#anonymous}
No. Without a token, `osy lock` resolves each `use Owner.Name@<major>;` from the **published index** — a public file
listing every published package and its versions — and downloads the archive from the release itself; neither counts
against GitHub's API limit, so a first lock over many packages, and `osy kits find`, cost nothing there. A token
(`gh auth login`, or `$OSY_GITHUB_TOKEN`) makes `osy lock` ask GitHub's API instead, which is fresher by however long
the index takes to be regenerated after a release, and is the only way to read a private package. A package the index
does not list yet is asked of the API either way. If GitHub's anonymous API limit (60 requests an hour per machine) is
ever spent, the refusal says so in one line and names those two ways past it.

### A project on a computer that has never fetched its packages   {#pinned-fetch}
**The lock travels with the project; the package cache does not.** Clone a project, copy it to a second computer, or
clear `~/.osy/packages`, and its `osyrin.lock` still says exactly which release of each package it uses and the
content address of its bytes. So every verb that reads the project — `validate`, `test`, `compile`, `launch` — first
fetches any pinned release this computer does not hold, checks it against the pinned address, and stores it. Nothing
is resolved again and the lock is not touched: you get the versions the author locked, not newer ones. Bytes that do
not match the pin are refused and never used.

With no network the fetch fails and the verb says `PACKAGE_NOT_IN_CACHE` for each missing pin, with the reason;
running the same command again once github.com is reachable is the whole remedy.

### The kits your `osy` carries are not in the lock   {#carried-kits}
**A kit your `osy` carries is never fetched and never pinned.** `Osysharp.Ui`, `Osysharp.Workflow`, `Osysharp.Llm` and
the rest of the platform's own kits are in the binary. Every verb takes them from the `osy` that is running —
`validate`, `test`, `compile`, `launch`, a deploy — and `osyrin.lock` holds no entry for them: it pins the releases
your project fetched, and nothing else. `osy lock` lists them all the same, marked *carried by this osy — not
pinned*, because they are part of what your project compiles.

So upgrading `osy` needs no step in your projects: the next command uses the kits the new `osy` carries. A lock
written before this rule may still list a carried kit (`"source": "prewarmed:osysharp/ui"`). It is read as though it
did not, and the next `osy lock` or `osy update` (or a compile, which ships) writes the file without it.

**What that means for reproducing a build.** A project builds with the carried kits of the `osy` that builds it; the
lock's `platform.version` says which `osy` wrote the file, and every *fetched* package is pinned to its exact bytes as
before. Two things hold a build still beyond that:

- **A deployed version keeps its own copy.** A compile request sends every package the project uses — the carried
  kits included, exactly as your `osy` has them — so the server compiles the kits you validated against, not the
  ones it carries, and the version stores them. Rebuilding that version later uses those same bytes.
- **Ask for a release exactly to stay on it.** `use Osysharp.Ui@2.21.1;` is not answered by a carried `2.22.0`: that
  release is fetched and pinned like any other package, and stays until you change the line.

`KIT_PIN_STALE` is left with one meaning: the lock pins a **release** of a kit your `osy` now carries, at a version
your `use` line accepts (a lock from before `osy` carried that kit, typically). The pinned release is what compiles
until you run **`osy update`**, which takes the kit from your `osy` and drops the pin — signed out and offline. A
release you asked for exactly is never reported.

### Finding one — `osy search`   {#search}

There is no registry to browse, so discovery is a repository **topic**: a published package carries `osy-package`, and
`osy search` asks GitHub which repositories have it. `osy publish` applies the topic, so an author never has to
remember it.

```console
$ osy search chart
Osysharp.Charts v1.0.0
  The Osysharp.Charts kit for Osy# — an Osy# package.
  use Osysharp.Charts@1;   github.com/osysharp/charts

Add a line to your `app { }` manifest, then `osy lock` to pin the version and its content address.
```

With no term it lists everything published. `--limit` widens the page, `--json` is the machine-readable form, and a
token is **optional** — search over public repositories works without one, at a lower rate limit (the refusal says so,
and names the remedy).

What each hit prints is the **line you paste**: the `use Owner.Name@<major>;` its latest release supports. A [family
repository](#family) is listed once per package it has released — its root and each member folder, each with the
`use` line of its own newest tag. A repository
carrying the topic that has published no release says so instead — it is discoverable and not installable, and a `use`
line that cannot resolve would be worse than none.

### A package in your own repository   {#workspace}

A repository that holds a package's source is working **on** it, and resolving the published copy instead would mean
an edit did not reach the app in the next directory until somebody cut a release. `osy.workspace` at the repository
root lists the package directories:

```console
# osy.workspace
kits/*
packages/ui-extras
```

A `use` naming one of those resolves by packing that directory — same packer as publishing, so the archive is what a
remote consumer would fetch. The constraint is still checked against the local version: `use X@2;` against a local
`1.0.0` is refused, not quietly satisfied.

**A workspace package is read from its source and never pinned.** A project inside the workspace (and a project's own
`kits/`) takes those packages from the directory as it is now, in every verb — `validate`, `lint`, `test`, `compile`,
a deploy — and its `osyrin.lock` pins only what comes from outside the workspace: the releases it fetched. So editing a
package, or releasing a new version of it, changes the package and nothing else: no lock in the repository moves. What
a deploy sends still carries every package's bytes, so the version it makes can be rebuilt without the workspace.

A lock written before this rule may still pin a workspace package (`"source": "workspace:kits/chart"`). Every verb
reads the source instead of that pin, and `osy lock`, `osy compile` or a deploy removes it from the file. A lock
carried OUT of the workspace keeps its pins, since nothing there could resolve them again.

### Testing the package you are writing   {#testing}

**A package's tests are not part of the package, and `osy publish` runs them anyway.** Those are two separate facts
and both matter: your test project never ships — no `tests` ROLE exists and none is coming, because a role names files
that ship and handing your test app to every consumer's compile would be absurd — but publishing is the one
irreversible act in the whole story, so it is where a package that does not *work* gets stopped.

You test a package the way you test anything — **an ordinary app beside it**, depending on it by name, with
`osy.workspace` pointing at the package directory:

```console
acme-widget/
  osy.workspace         # → kits/*
  kits/widget/
    package.osy         # package Acme.Widget { version "1.0.0"; model "model/**/*.osy"; tests "tests"; }
    model/widget.osy
    tests/
      app.osy           # app WidgetTests { model "model/**/*.osy"; use Acme.Widget; }
      model/widget.test.osy
```

```osy title="the test project is an ordinary app that depends on the package" syntax
app WidgetTests {
  model "model/**/*.osy";
  use Acme.Widget;             // resolved from the WORKING TREE, because osy.workspace names it
}
```

Then `osy test` in that directory runs them — and so does `osy test` or `osy check` in the **package's own
directory**, when the manifest names the tests app (`tests "tests";`, below): the test leg runs that app and says so
in one line.

```console
$ osy check                      # in kits/widget/
✓ validate
✓ lint
✓ test — 18 passed
  ⓘ `Acme.Widget` is a package — running its own tests app, `tests/`.
```

The point of the workspace line is that your tests read **the tree, not
the last release** — without it every run would test the version you published yesterday, and pass.

⭐ **Name the directory in the manifest and publishing runs it for you.** `tests "tests";` is a SETTING, so nothing it
names is packed — the archive is the union of the [role globs](#roles) and a setting is not one:

```console
$ osy publish
Acme.Widget 1.0.1 → github.com/acme/widget
  archive   4 file(s), 12 480 bytes        # the test project is not among them
  tests     tests
  ✓ 18 passed
  commit    0a058a26 → tag v1.0.1
```

It is declared rather than conventional on purpose. A hardcoded `tests/` would decide something the manifest's globs
are supposed to decide completely, and it would turn a stray directory into a publish gate for an author who never
wrote a test. A package that declares nothing here publishes exactly as it did before.

⭐ **`osy lint` in the test project judges the package too.** In an app, a package's code is read *through* — a
call into it is understood — and never judged: it is not the app author's to change. In the package's own test
project it is yours, so its source gets the rules that say a shape multiplies cost — a read with no bound, a query in
a loop, the same read twice, a child query where the collection exists — reported under the package's own files:

```console
$ cd tests && osy lint
  • Widget (in EveryWidget) — 'EveryWidget' materializes 'Widget' with no `Take` …
    ../model/widgets.osy:9:38
    cost-unbounded-read · fix: bound it — `.Take(50)` — or page it …
```

Only those rules. A package leaves its roles, its policies and who may open its pages to the app that installs it,
so the rules about an app's completeness have nothing true to say about it. They are advice, as in an app: `osy lint
--strict` fails on them, `osy check` and `osy publish` do not. A finding you have weighed and accept is silenced where
it fires, with `[SuppressWarning("…")]` in the package's source — and that decision travels with the package.

⚠ A `tests` path that leaves the package — rooted, or climbing with `..` — is refused. It is what publishing *runs*,
so a path pointing outside would name a project that exists on your machine and on nobody else's.

### Validating a package on its own   {#validating}

**`osy validate` in a package directory answers the question a consumer's compile will ask: used by an app, does this
package compile?** `osy publish` asks it before anything else. It does not need your test project, this repository,
or anything but the package directory and the `osy` CLI:

```console
$ cd kits/record-timeline && osy validate
Validating 3 files: model/Activity.osy, model/ui/Timeline.osy, package.osy

✓ Validated successfully (2 entities, 2 classes, 1 enums, 4 functions, 1 workflows, 5 components wire-checked (1 yours, 4 from the kit))
```

With no lock yet, the first line says what it resolved and how to record it:

```console
↻ this package has no osyrin.lock yet — its dependencies were resolved for this run (Osysharp.Identity,
  Osysharp.Ui, Osysharp.Workflow). Run `osy lock` in the package directory to record them.
```

What it does, in order:

1. **Packs the package** — the same archive a release carries, so a file your role globs miss is missing here too.
2. **Resolves its dependencies the way an app's are resolved.** A package listed in an `osy.workspace` above you comes
   from its directory; otherwise the package's **own `osyrin.lock`** decides — the pinned archive from your cache, or
   that exact version fetched and refused if its bytes are not the ones the lock names (`PACKAGE_PIN_MISMATCH`); then
   the packages the platform ships; then the catalog (GitHub releases). Nothing is written: `osy lock` in the package
   directory records the pins, and committing that lock is what makes the check reproducible for everyone else.
3. **Composes the package into a small check app** that uses it as a consumer would — so the package compiles in its
   own pass, exactly as it will in an app — and answers, neutrally, everything the package asks an app for:

   | the package says | the check app supplies |
   |---|---|
   | `policy ManagesFields;` (or a `virtual policy` whose default names the app's `IsAdmin`) | a definition nobody satisfies |
   | `role.Authenticator` — the app's `[Role]` enum | a role enum with every member the packages name, unless one of them declares the app's role enum itself |
   | `egress`, `secret`, `clientCert`, `serverCa`, `theme` in its manifest | the grant, on its `use` line |
   | `appPage "/account";` | a page at that route |
   | a component taking `Textures` or `Sounds` | one neutral asset of that kind |

   Each of those is already declared once — in the package's source or its manifest — so there is nothing to repeat
   for the check.
4. **Checks every component the package ships** for the browser, not only the ones a page reaches, and reports each
   finding at the file on disk.

A finding the check app causes is about what the package asks of an app, and says so: a route the package navigates
to that its manifest does not list is reported with the `appPage` line to add. A dependency that cannot be resolved at
all stops the check before anything compiles (`PACKAGE_UNCHECKABLE`, naming it).

### A package with settings joins the app's settings area   {#settings-area}

**If staff configure something your package ships, it announces a section in the app's settings area.** An app is
made of many packages, and its settings must still read as one place: the people it manages, how long it keeps
personal data, how reviews are moderated, the chart of accounts. So a package does not ship a configuration page of
its own somewhere in its routes.

⭐ **A row of settings is DECLARED, not built.** Mark the row `[Settings("Title", Authorize = …)]` and each field on it
`[Setting("Label")]`: the settings area lists the section to whoever `Authorize` admits and draws its page — each field
by its type, Save and Discard, who changed each group, and the organisation's lock — in whichever app uses the
package, with no page and no provider written (see [[Settings] and [Setting]: a settings section declared on its row, each field drawn by its type](https://osysharp.com/reference/ui/settings-declared/)). The reviews and bookkeeping kits are
built this way.

```osy title="a package's settings row, declared — the section and its page come with it" test app=project-package-settings
[Principal] entity Member { [MaxLength(100)] string Name; }
policy KeepsTheLibrary => IsAuthenticated;

/// The library's one settings row: how long a loan runs.
[Settings("Loans", Authorize = KeepsTheLibrary, Group = "Library", Icon = Icons.Clock, Order = 40,
          Hint = "How long a loan runs, and whether it renews online.", Saved = "Loans saved.")]
entity LoanRules {
  /// An organisation may bound it for every library it runs.
  [Setting("Days", Hint = "How long a loan runs."), Range(1, 90)] [OrgPolicy(OrgPolicyMode.Range)] int LoanDays = 21;
  [Setting("Renew online")] bool RenewOnline = true;
  security { allow read, create, update when KeepsTheLibrary; }
}

string Announced() { return string.Join(", ", AnnouncedSettings().Select(l => l.Group + "/" + l.Label + " " + l.To)); }
```

```osy title="the section is announced to whoever keeps the library, and to nobody else" run app=project-package-settings
[TestFixture]
void Library() { new Member { Name = "Ada" }; }

principal Ada => Member.Single(m => m.Name == "Ada");

[Test(Library)]
[runas(Ada)]
void The_loans_section_is_announced() { Assert.Equal("Library/Loans /settings/loans", Announced()); }

[Test]
void A_visitor_is_shown_no_section() { Assert.Equal("", Announced()); }
```

**For records and lists** — saved replies, queues with their members, mailboxes to connect — a row of fields is not
enough, and the escape hatch is a page of your own: implement `ISettingsProvider` and serve the page its section opens
under `[Layout(SettingsFrame)]`, behind its own `[Authorize]` (see [ISettingsProvider and [OrgPolicy]: kits announce their settings, and a setting says whether an organisation may govern it](https://osysharp.com/reference/ui/settings-registry/), which has the worked
example). `SettingsArea()` finds it with no wiring, exactly as it finds a declared section.

⭐ **The compiler holds every package to it.** On a package's own compile (`osy validate` in its directory), a
configuration surface with neither a `[Settings]` row nor an `ISettingsProvider` anywhere in the package is a warning, `SETTINGS_NOT_ANNOUNCED`, at
the declaration. A configuration surface is any of:

- a page in the settings vocabulary — routed under `/settings`, in `[Layout(SettingsFrame)]`, or rendering a `SettingsPage`;
- an entity with an `[OrgPolicy]` field;
- an entity its author named as settings (`…Settings`, `…Setting`, `…Config`, `…Configuration`) that someone may update;
- a settings row — an entity whose every stored field is a value with its default (or nullable), none a reference to
  another entity, that someone may update across every row (an `update` with no `where`).

```console
model/Reviews.osy:28  WARNING  SETTINGS_NOT_ANNOUNCED  `ReviewSettings` is an entity named as settings that staff
update: configuration staff edit, and package `Osysharp.Shop.Reviews` announces no settings section, so it is missing
from the app's settings area. When it is a row of switches, choices, numbers, durations and short text, DECLARE the
section on it — `[Settings("Title", Authorize = <policy>)]` on the entity and `[Setting("Label")]` on each field …
```

It is said only on the package's own compile — an app that uses the package cannot fix it, so it never sees it — and
it asks only whether the package announces anything: which section links to which page is the provider's to get right.
A row that matches and is not configuration takes `[SuppressWarning("SETTINGS_NOT_ANNOUNCED")]` on its declaration
(see [[SuppressWarning]](https://osysharp.com/reference/diagnostics/suppress-warning/)). Measured on the platform's own 76 kits when it was written, the check named
exactly the kits with staff-edited configuration and no section, and nothing else.

### Where the file goes   {#location}

`package.osy` sits at the root of the package's repository — or, for a [nested name](#family), in the folder that
name maps to — and it is the only file a `package` block belongs in.
Writing one inside an app's `model/` is refused, because nothing in an application reads it:

```console
a `package` declaration belongs in `package.osy` at the root of the package's repository, not in `m.osy`.
```

## See also       {#see-also}
- [app.osy](https://osysharp.com/reference/project/manifest/) — `app.osy`, the same idea for an application, and where a `use` line goes
- [project layout](https://osysharp.com/reference/project/layout/) — where files go in a project
- [use](https://osysharp.com/reference/types/use/) — `use` and version pins
- [native class — a provider implemented in .NET](https://osysharp.com/reference/class/native/) — a class implemented by one of the package's assemblies
- [[Settings] and [Setting]: a settings section declared on its row, each field drawn by its type](https://osysharp.com/reference/ui/settings-declared/) — `[Settings]` and `[Setting]`, a package's settings row declared and drawn with no page code
- [ISettingsProvider and [OrgPolicy]: kits announce their settings, and a setting says whether an organisation may govern it](https://osysharp.com/reference/ui/settings-registry/) — `ISettingsProvider` and `[OrgPolicy]`, how a package's settings join the app's settings area
