@inixiative/config 0.5.175 → 0.5.177

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,6 +58,7 @@ One version of `@inixiative/config` names one coherent ecosystem state:
58
58
  - `agentic` — the agentic lane's blessed set
59
59
  - `consumers` — private repos that follow the blessed sets but never publish, keyed by GitHub `owner/repo`: `lanes` names the lanes each consumes, `upstream` the repo it syncs from
60
60
  - `candidates` — published lane versions newer than their blessed one, waiting for every dependent to adopt them (see `train`); absent when there are none
61
+ - `holds` — lane packages kept below a version, keyed by package (see Holds below); absent when there are none
61
62
 
62
63
  ```jsonc
63
64
  "consumers": {
@@ -86,7 +87,7 @@ bunx @inixiative/config ports [project]
86
87
 
87
88
  `ports` prints each project's local ports from `ports.json` (also exported as `@inixiative/config/ports.json`). A project's port for a service is the service's usual default plus 100 times the project's block, so no two projects collide. Zealot alone sits at block 0, the defaults (Aron, 2026-10-20: it already runs there); every other project is off the defaults: Archive (block 7) serves HTTP on 4700 with Postgres on 6132; Kingdom (block 2) has web 3200, API 8200, Postgres 5632, Redis 6579. Reserved ports (macOS AirPlay on 5000 and 7000) are never assigned. Give a new project the next free block (never 0), and a new kind of service its usual default; the tests fail on any collision.
88
89
 
89
- `check` is read-only and exits non-zero on drift — run it in CI after a frozen-lockfile install. It verifies toolchain pins — declared once, at the root, pinned exactly: a workspace member declaring one is an error, and `bun.lock` must resolve each only to its pin (no second copy) — toolchain peer ranges (they must admit the pin and no older major; `sync` writes `>=<pin major>.<pin minor>.0`), `"latest"` ranges, `packageManager`/`.bun-version`, legacy `bun.lockb`, a committed (tracked, un-ignored) `bun.lock`, a gitignored `tmp/` (scratch space; `sync` adds `/tmp/*`, which keeps re-included files tracked), required scripts (`check`/`typecheck`/`lint`/`test`), lefthook (dep + `lefthook.yml` extending the shared hooks + `prepare` script; git repos only), stub `extends`, presence of this package, and for every ecosystem dependency — in the root or any workspace member, of either lane — that the declared range admits the blessed version and the lockfile actually resolves to it (the stale-lockfile class). A package with a candidate also accepts the candidate and anything between: a range admitting either passes, and so does a lock from the blessed version up to the candidate. A range that does not admit the blessed version is rewritten to `^<blessed>`, except an exact pin, which stays exact and moves to the blessed version. An npm alias (`"@inixiative/session-archive": "npm:@inixiative/archive@^0.2.1"`) is held to its target's blessed version and rewritten as an alias; `workspace:`, `file:` and `link:` ranges are a repo linking itself and are left alone.
90
+ `check` is read-only and exits non-zero on drift — run it in CI after a frozen-lockfile install. It verifies toolchain pins — declared once, at the root, pinned exactly: a workspace member declaring one is an error, and `bun.lock` must resolve each only to its pin (no second copy) — toolchain peer ranges (they must admit the pin and no older major; `sync` writes `>=<pin major>.<pin minor>.0`), `"latest"` ranges, `packageManager`/`.bun-version`, legacy `bun.lockb`, a committed (tracked, un-ignored) `bun.lock`, a gitignored `tmp/` (scratch space; `sync` adds `/tmp/*`, which keeps re-included files tracked), required scripts (`check`/`typecheck`/`lint`/`test`), lefthook (dep + `lefthook.yml` extending the shared hooks + `prepare` script; git repos only), stub `extends`, presence of this package, and for every ecosystem dependency — in the root or any workspace member, of either lane — that the declared range admits the blessed version and the lockfile actually resolves to it (the stale-lockfile class). A package with a candidate (one no hold covers) also accepts the candidate and anything between: a range admitting either passes, and so does a lock from the blessed version up to the candidate. A range that does not admit the blessed version is rewritten to `^<blessed>`, except an exact pin, which stays exact and moves to the blessed version. An npm alias (`"@inixiative/session-archive": "npm:@inixiative/archive@^0.2.1"`) is held to its target's blessed version and rewritten as an alias; `workspace:`, `file:` and `link:` ranges are a repo linking itself and are left alone.
90
91
 
91
92
  `sync` applies every fix (deleting member toolchain entries and their stale member links), re-locks via `bun install` (`--force` when a second toolchain copy survives), and runs `bun update` on stale ecosystem entries in the directory that declares them. It refuses to run on a dirty working tree without `--force`. The react preset is inferred from a `react` dependency; override with `--preset`. Missing scripts are filled with defaults; existing script bodies are never touched.
92
93
 
@@ -102,9 +103,31 @@ Blessing is two-phase, so a dependent that needs work never deadlocks a release:
102
103
  2. **Adopt.** A repo (lane checkout or consumer) whose check fails against the candidates it was moved onto doesn't stop the train. Its checkout is put back as it was, and the train opens `train/adopt-<pkg>-<version>` in a worktree (the repo's own `worktree:create <base> <branch>` when it declares one, so a template-based repo gets its slot's databases, released with its `worktree:destroy`; otherwise a temporary linked worktree; either way with the repo's ignored `.env*` files copied in where missing): the same bump and re-lock, then the repo's `train:adopt` script if its package.json declares one (Foundry's re-records cassettes with `bun run test:live`), all committed, and re-locked onto the new config release so the PR's CI judges it against the candidates. A lane checkout that adopts publishes nothing this run. A consumer that passes against candidates gets the same branch name: its default branch is still on the previous versions until the branch merges. If the adoption's commit is refused (its hooks typecheck), the branch is deleted and the summary says to adopt by hand.
103
104
  3. **Promote.** At the end of each run a candidate is blessed (moved into its lane's section) unless something holds it: its lane stopped; a repo has an adoption branch for it that is not yet on its default branch (opened this run, unmerged, or committed but never pushed); or a repo that declares it was not walked to an outcome this run — another lane's checkout, a skipped or failed one, a consumer without a checkout. A later train finds a waiting adoption branch and opens nothing new; once it merges, the repo is on the candidate and the train promotes it. A candidate every dependent already accepts is blessed in the run that published it. An adoption PR closed without merging holds its candidates until its branch is deleted.
104
105
 
105
- The summary lists what was blessed, every candidate with what holds it, and each pending adoption branch with its PR when `gh` can find one.
106
+ The summary lists what was blessed, every candidate with what holds it, every hold with its reason, and each pending adoption branch with its PR when `gh` can find one.
106
107
 
107
- A root package publishes with `npm publish`; a workspace member is packed with `bun pm pack`, which rewrites `workspace:` ranges to the versions being released, and the tarball is published with `npm publish`, so OTP prompts pass through either way. A package already on npm at its local version but neither blessed nor a candidate — published by hand, or by a train whose lane later failed — becomes a candidate on the next run. Likewise, an `@inixiative/config` version committed ahead of npm (a release whose publish failed) is published by the next run, even when it blesses nothing new.
108
+ **Holds.** A hold keeps a package's breaking line out of the ecosystem while its dependents can't take it yet (signet 0.12 waiting on Kingdom's grants cutover), without stopping the package's own releases or anyone else's:
109
+
110
+ ```jsonc
111
+ "holds": {
112
+ "@inixiative/signet": {
113
+ "below": "0.12.0",
114
+ "reason": "0.12 breaks the grants contract that prod Kingdom (0.11) speaks",
115
+ "until": "the Kingdom grants cutover", // optional
116
+ "owner": "inixiative-bf" // optional: who lifts it
117
+ }
118
+ }
119
+ ```
120
+
121
+ Every version at or above `below` is held; versions below it ride the train as usual, so a patch on the old line is still recorded and blessed. A held version:
122
+
123
+ - still publishes when the package's local version is ahead of npm, and is never recorded;
124
+ - is never rediscovered as a candidate when it is already on npm at the local version;
125
+ - is never applied or blessed when it is already recorded under `candidates`: no lane checkout or consumer is moved onto it, and it stays where it is;
126
+ - is never accepted by `check`, which holds the package to its blessed version and any candidate below the hold.
127
+
128
+ `scan` and the train's summary print each hold with its reason, and name a held candidate as not applied. Lifting a hold is deleting its entry: the next train records the held version found on npm at the local version (or applies the held candidate) and adopts it like any other.
129
+
130
+ A root package publishes with `npm publish`; a workspace member is packed with `bun pm pack`, which rewrites `workspace:` ranges to the versions being released, and the tarball is published with `npm publish`, so OTP prompts pass through either way. A package already on npm at its local version but neither blessed nor a candidate — published by hand, or by a train whose lane later failed — becomes a candidate on the next run, unless a hold covers that version. Likewise, an `@inixiative/config` version committed ahead of npm (a release whose publish failed) is published by the next run, even when it blesses nothing new.
108
131
 
109
132
  Consumers run once, after every selected lane, upstreams first, against the full blessed set: a consumer of both lanes ends coherent with both whichever lane ran. They get the same bump → re-lock → `check`, never a publish, and their changes go through review: the train commits them on a `train/ecosystem-sync-<date>` branch, in a worktree, and puts the checkout back on its default branch, clean.
110
133
 
package/dist/cli.js CHANGED
@@ -53,8 +53,23 @@ var blessedVersions = (manifest2) => ({
53
53
  ...manifest2.agentic
54
54
  });
55
55
  var laneOf = (manifest2, name) => LANES.find((lane2) => name in lanePackages(manifest2, lane2)) ?? null;
56
+ var holdOf = (manifest2, name, version) => {
57
+ const hold = manifest2.holds?.[name];
58
+ return hold && compare(version, hold.below) >= 0 ? hold : null;
59
+ };
60
+ var activeCandidates = (manifest2) => Object.fromEntries(
61
+ Object.entries(manifest2.candidates ?? {}).filter(
62
+ ([name, version]) => !holdOf(manifest2, name, version)
63
+ )
64
+ );
65
+ var holdLines = (manifest2) => Object.entries(manifest2.holds ?? {}).map(([name, hold]) => {
66
+ const terms = [hold.until && `until ${hold.until}`, hold.owner && `owner ${hold.owner}`].filter(Boolean).join("; ");
67
+ const candidate = manifest2.candidates?.[name];
68
+ const parked = candidate && holdOf(manifest2, name, candidate) ? `; candidate ${candidate} not applied` : "";
69
+ return `held: ${name} below ${hold.below} \u2014 ${hold.reason}${terms ? ` (${terms})` : ""}${parked}`;
70
+ });
56
71
  var adoptionTarget = (manifest2) => {
57
- const candidates = manifest2.candidates ?? {};
72
+ const candidates = activeCandidates(manifest2);
58
73
  const overlay = (section) => Object.fromEntries(
59
74
  Object.entries(section).map(([name, version]) => [name, candidates[name] ?? version])
60
75
  );
@@ -69,7 +84,7 @@ var promoteCandidates = (manifest2, names) => {
69
84
  for (const name of names) {
70
85
  const version = manifest2.candidates?.[name];
71
86
  const lane2 = laneOf(manifest2, name);
72
- if (!version || !lane2 || !manifest2.candidates) continue;
87
+ if (!version || !lane2 || !manifest2.candidates || holdOf(manifest2, name, version)) continue;
73
88
  laneSection(manifest2, lane2)[name] = version;
74
89
  delete manifest2.candidates[name];
75
90
  promoted.push(`${name}@${version}`);
@@ -734,7 +749,7 @@ function inspect(dir2, manifest2, presetOverride) {
734
749
  }
735
750
  }
736
751
  const blessedSet = blessedVersions(manifest2);
737
- const candidateSet = manifest2.candidates ?? {};
752
+ const candidateSet = activeCandidates(manifest2);
738
753
  const staleKeys = /* @__PURE__ */ new Set();
739
754
  for (const target of rangeTargets) {
740
755
  for (const field of DEP_FIELDS) {
@@ -1181,9 +1196,10 @@ if (command === "scan") {
1181
1196
  const missing = [...plan.lanes.flatMap((lane2) => lane2.missing), ...plan.missingConsumers];
1182
1197
  const ambiguous = [...plan.lanes.flatMap((lane2) => lane2.ambiguous), ...plan.ambiguousConsumers];
1183
1198
  const drifted = [];
1184
- for (const [name, version] of Object.entries(manifest.candidates ?? {})) {
1199
+ for (const [name, version] of Object.entries(activeCandidates(manifest))) {
1185
1200
  console.log(`candidate: ${name}@${version} (blessed ${blessedVersions(manifest)[name]})`);
1186
1201
  }
1202
+ for (const line of holdLines(manifest)) console.log(line);
1187
1203
  for (const checkout of checkouts) {
1188
1204
  console.log(`
1189
1205
  ${checkout.name} \u2014 ${checkout.dir} (${laneLabel(checkout)})`);
@@ -1387,7 +1403,7 @@ if (command === "train") {
1387
1403
  spawnSync2("bun", ["install", "--frozen-lockfile"], { cwd: checkout.dir, stdio: "inherit" });
1388
1404
  });
1389
1405
  const movesOf = (checkout) => {
1390
- const candidates2 = manifest.candidates ?? {};
1406
+ const candidates2 = activeCandidates(manifest);
1391
1407
  const names = inspect(checkout.dir, target()).findings.flatMap(
1392
1408
  (finding) => (finding.kind === "ecosystem-range" || finding.kind === "stale-lock") && finding.target && finding.target in candidates2 ? [finding.target] : []
1393
1409
  );
@@ -1643,15 +1659,23 @@ if (command === "train") {
1643
1659
  walked.add(checkout.dir);
1644
1660
  followers.push({ checkout, onBranch: false });
1645
1661
  for (const { entry, local, remote, ahead: ahead2 } of releases) {
1646
- if (ahead2) {
1647
- publish(checkout, entry, local, remote);
1648
- } else if (section[entry.name] === local) {
1662
+ if (ahead2) publish(checkout, entry, local, remote);
1663
+ const hold = holdOf(manifest, entry.name, local);
1664
+ if (hold) {
1665
+ console.log(
1666
+ `\u23F8 ${entry.name}@${local} ${ahead2 ? "published" : "on npm"}, held below ${hold.below} (${hold.reason}) \u2014 not recorded`
1667
+ );
1668
+ continue;
1669
+ }
1670
+ if (!ahead2 && section[entry.name] === local) {
1649
1671
  console.log(`\u2713 ${entry.name}@${local} already on npm and blessed`);
1650
1672
  continue;
1651
- } else if (manifest.candidates?.[entry.name] === local) {
1673
+ }
1674
+ if (!ahead2 && manifest.candidates?.[entry.name] === local) {
1652
1675
  console.log(`\u2713 ${entry.name}@${local} already on npm and a candidate`);
1653
1676
  continue;
1654
- } else {
1677
+ }
1678
+ if (!ahead2) {
1655
1679
  console.log(`\u2713 ${entry.name}@${local} already on npm \u2014 recording it as a candidate`);
1656
1680
  }
1657
1681
  manifest.candidates = { ...manifest.candidates, [entry.name]: local };
@@ -1715,7 +1739,7 @@ if (command === "train") {
1715
1739
  console.error(`\u2717 ${error.message} \u2014 its checkout is back on its default branch`);
1716
1740
  }
1717
1741
  }
1718
- const candidateNames = Object.keys(manifest.candidates ?? {});
1742
+ const candidateNames = Object.keys(activeCandidates(manifest));
1719
1743
  if (candidateNames.length > 0) {
1720
1744
  const everything = trainPlan(dir, manifest, LANES);
1721
1745
  for (const checkout of [
@@ -1764,7 +1788,7 @@ if (command === "train") {
1764
1788
  };
1765
1789
  console.log(`
1766
1790
  blessed: ${promoted.length === 0 ? "nothing" : promoted.join(", ")}`);
1767
- const candidates = Object.entries(manifest.candidates ?? {});
1791
+ const candidates = Object.entries(activeCandidates(manifest));
1768
1792
  if (candidates.length > 0) {
1769
1793
  console.log("candidates (check accepts them; not yet blessed):");
1770
1794
  for (const [name, version] of candidates) {
@@ -1772,6 +1796,7 @@ blessed: ${promoted.length === 0 ? "nothing" : promoted.join(", ")}`);
1772
1796
  console.log(` ${name}@${version}${on.length === 0 ? "" : ` \u2014 waiting on ${on.join(", ")}`}`);
1773
1797
  }
1774
1798
  }
1799
+ for (const line of holdLines(manifest)) console.log(line);
1775
1800
  if (waiting.length > 0) {
1776
1801
  console.log("pending (merge each adoption PR, then run the train again to bless):");
1777
1802
  for (const entry of waiting) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/config",
3
- "version": "0.5.175",
3
+ "version": "0.5.177",
4
4
  "description": "Shared toolchain for the inixiative ecosystem: tsconfig/biome/tsup presets, a version manifest (BOM), and a sync/check CLI",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/versions.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "@types/bun"
14
14
  ],
15
15
  "ecosystem": {
16
- "@inixiative/config": "0.5.175",
16
+ "@inixiative/config": "0.5.177",
17
17
  "@inixiative/json-rules": "3.16.0",
18
18
  "@inixiative/permissions": "0.18.0",
19
19
  "@inixiative/transitions": "0.6.0",
@@ -50,9 +50,17 @@
50
50
  }
51
51
  },
52
52
  "candidates": {
53
- "@inixiative/agent-session": "0.8.0",
54
- "@inixiative/archive": "0.31.0",
53
+ "@inixiative/agent-session": "0.9.0",
54
+ "@inixiative/archive": "0.32.0",
55
55
  "@inixiative/atlas": "0.5.0",
56
- "@inixiative/signet": "0.12.0"
56
+ "@inixiative/signet": "0.11.1"
57
+ },
58
+ "holds": {
59
+ "@inixiative/signet": {
60
+ "below": "0.12.0",
61
+ "reason": "0.12 breaks the grants contract that prod Kingdom (0.11) speaks",
62
+ "until": "the Kingdom grants cutover",
63
+ "owner": "inixiative-bf"
64
+ }
57
65
  }
58
66
  }