@awebai/oats 0.25.8 → 0.26.0
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 +8 -6
- package/bin/oats.mjs +576 -1714
- package/capabilities/oats-authoring/oats-package.json +2 -2
- package/capabilities/oats-authoring/oats.json +2 -2
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +334 -72
- package/capabilities/oats-aweb/injects/aweb.md +7 -2
- package/capabilities/oats-aweb/lib/binding-wire.mjs +107 -5
- package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
- package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
- package/capabilities/oats-aweb/oats.json +14 -4
- package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
- package/capabilities/oats-jira/oats.json +2 -2
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
- package/capabilities/oats-linear/oats.json +2 -2
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +1 -1
- package/capabilities/oats-okf/lib/binding-wire.mjs +1 -1
- package/capabilities/oats-okf/lib/migration.mjs +2 -2
- package/capabilities/oats-okf/lib/sources.mjs +5 -4
- package/capabilities/oats-okf/lib/stores.mjs +40 -9
- package/capabilities/oats-okf/lib/worker.mjs +3 -3
- package/capabilities/oats-okf/oats.json +1 -1
- package/capabilities/oats-review/oats.json +3 -2
- package/docs/capabilities.md +218 -47
- package/docs/capability-manifest.schema.json +13 -4
- package/docs/configuration.md +17 -5
- package/docs/conventions.md +16 -26
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
- package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
- package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
- package/docs/design/2026-09-20-redesign-program-board.md +2 -2
- package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +54 -10
- package/docs/design/2026-09-24-phase-d-plan.md +77 -0
- package/docs/design/2026-09-25-teams-contract.md +226 -0
- package/docs/design/README.md +3 -3
- package/docs/design/launch-configurations.md +20 -16
- package/docs/design/operations-contract.md +27 -10
- package/docs/desktop-cli-api.md +546 -264
- package/docs/desktop-instance-start.md +1 -1
- package/docs/desktop.md +7 -13
- package/docs/execution-targets.md +16 -18
- package/docs/first-team.md +14 -17
- package/docs/implementation.md +28 -59
- package/docs/integrations.md +88 -32
- package/docs/knowledge-capability-authoring.md +1 -1
- package/docs/knowledge-reference/package-craft.md +10 -8
- package/docs/knowledge-theory.md +1 -1
- package/docs/knowledge.md +10 -11
- package/docs/layers.md +16 -17
- package/docs/oats-local.schema.json +29 -1
- package/docs/oats-membership.schema.json +5 -3
- package/docs/oats-package.schema.json +2 -2
- package/docs/oats-workspace.schema.json +1 -1
- package/docs/{official-marketplace.md → official-catalog.md} +15 -16
- package/docs/packages.md +75 -52
- package/docs/release-notes/v0.22.0.md +1 -1
- package/docs/release-notes/v0.23.1.md +1 -1
- package/docs/release-notes/v0.25.9.md +23 -0
- package/docs/release-notes/v0.26.0.md +670 -0
- package/docs/schedules.md +48 -126
- package/docs/soul.schema.json +11 -4
- package/docs/souls-and-instances.md +56 -43
- package/docs/workspaces.md +80 -58
- package/injects/instance-boundary.md +1 -1
- package/injects/work-attached.md +1 -1
- package/injects/work-workspace.md +2 -2
- package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
- package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
- package/lib/capability-contract.mjs +110 -0
- package/lib/config-data.mjs +2 -2
- package/lib/core.mjs +700 -4824
- package/lib/digest.mjs +12 -0
- package/lib/instance-inspect.mjs +396 -0
- package/lib/instance-lifecycle.mjs +3 -4
- package/lib/instance-resolution.mjs +212 -26
- package/lib/instruction-composition.mjs +0 -20
- package/lib/materialize.mjs +6 -4
- package/lib/operator-dispatch.mjs +33 -13
- package/lib/packages.mjs +25 -190
- package/lib/provider-binding.mjs +4 -2
- package/lib/provider-reasons.mjs +3 -68
- package/lib/resolve.mjs +204 -68
- package/lib/schedule.mjs +97 -272
- package/lib/servers.mjs +13 -13
- package/lib/{portable-shape.mjs → shape.mjs} +4 -3
- package/lib/tree-copy.mjs +44 -0
- package/lib/workspace.mjs +125 -20
- package/package-catalog.json +7 -7
- package/package.json +1 -1
- package/skills/integration-authoring/SKILL.md +48 -40
- package/skills/oats-getting-started/SKILL.md +105 -110
- package/skills/oats-support/SKILL.md +2 -2
- package/skills/soul-craft/SKILL.md +13 -6
- package/bin/oats-pi-sdk-host.mjs +0 -17
- package/docs/2026-09-03-architecture-proposal.md +0 -642
- package/docs/artifact-approvals.schema.json +0 -7
- package/docs/captured-invocation-context.schema.json +0 -7
- package/docs/captured-resolution.schema.json +0 -7
- package/docs/design/package-engine-contract.md +0 -813
- package/docs/design/package-runtime-api.md +0 -588
- package/docs/desktop-succession.md +0 -57
- package/docs/execution-capsule.schema.json +0 -108
- package/docs/first-team-demo.md +0 -92
- package/docs/knowledge-migration.md +0 -147
- package/docs/migration-from-oas.md +0 -103
- package/docs/oats-config.schema.json +0 -172
- package/docs/oats-lock-v3.schema.json +0 -7
- package/docs/oats-lock.schema.json +0 -175
- package/docs/operating-team-migration.md +0 -470
- package/docs/portable.schema.json +0 -2512
- package/docs/provider-check-input.schema.json +0 -7
- package/docs/rebuild-to-v2.md +0 -511
- package/docs/workspace-adoption.md +0 -74
- package/injects/framework-workspace.md +0 -7
- package/injects/local-soul.md +0 -19
- package/injects/oats-portable.md +0 -20
- package/injects/oats.md +0 -11
- package/injects/portable-instance-boundary.md +0 -39
- package/injects/portable-work-directory.md +0 -29
- package/lib/artifact-approvals.mjs +0 -120
- package/lib/artifact-tree.mjs +0 -141
- package/lib/capability-artifacts.mjs +0 -179
- package/lib/capability-execution.mjs +0 -15
- package/lib/capability-inputs.mjs +0 -39
- package/lib/capability-provenance.mjs +0 -231
- package/lib/captured-action-shape.mjs +0 -21
- package/lib/captured-admission-shape.mjs +0 -20
- package/lib/captured-binding-file.mjs +0 -36
- package/lib/captured-dispatch.mjs +0 -66
- package/lib/captured-instance-index.mjs +0 -277
- package/lib/captured-invocation-context.mjs +0 -130
- package/lib/captured-launch-request.mjs +0 -66
- package/lib/captured-operation-process.mjs +0 -15
- package/lib/captured-pi-custody.mjs +0 -29
- package/lib/captured-pi-host.mjs +0 -167
- package/lib/captured-pi-outcome.mjs +0 -172
- package/lib/captured-resolutions.mjs +0 -275
- package/lib/captured-scaffold.mjs +0 -87
- package/lib/captured-selector.mjs +0 -28
- package/lib/captured-session-backend.mjs +0 -52
- package/lib/captured-source-receipt-file.mjs +0 -72
- package/lib/helper-injection-policy.mjs +0 -104
- package/lib/legacy-lock-codec.mjs +0 -106
- package/lib/manifest-settings.mjs +0 -84
- package/lib/package-closure.mjs +0 -48
- package/lib/package-materialization.mjs +0 -83
- package/lib/pi-sdk-host.mjs +0 -229
- package/lib/portable-artifacts.mjs +0 -115
- package/lib/portable-choices.mjs +0 -82
- package/lib/portable-composition.mjs +0 -136
- package/lib/portable-digest.mjs +0 -105
- package/lib/portable-identity.mjs +0 -40
- package/lib/portable-lock.mjs +0 -117
- package/lib/portable-onboarding-request.mjs +0 -49
- package/lib/portable-onboarding.mjs +0 -256
- package/lib/portable-package-preparation.mjs +0 -188
- package/lib/portable-policy.mjs +0 -44
- package/lib/portable-soul.mjs +0 -42
- package/lib/portable-state.mjs +0 -80
- package/lib/prepare-composition.mjs +0 -170
- package/lib/prepared-bindings.mjs +0 -92
- package/lib/prepared-resources.mjs +0 -127
- package/lib/provider-binding-broker.mjs +0 -65
- package/lib/provider-binding-wire.mjs +0 -116
- package/lib/readiness.mjs +0 -225
- package/lib/repository-observation.mjs +0 -226
- package/lib/resolution-shape.mjs +0 -393
- package/lib/schedule-capsule.mjs +0 -206
- package/lib/soul-constraints.mjs +0 -40
- package/lib/source-projection.mjs +0 -84
- package/lib/source-spec.mjs +0 -189
- package/lib/workspace-definition.mjs +0 -126
- package/lib/workspace-discovery.mjs +0 -146
- package/skills/oats/SKILL.md +0 -162
- package/skills/oats-config/SKILL.md +0 -164
- package/skills/oats-packages/SKILL.md +0 -184
- package/skills/oats-portable/SKILL.md +0 -115
- package/skills/oats-portable-artifacts/SKILL.md +0 -63
package/lib/resolve.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `resolveSoul(discovery, soulEntry, options)` turns a discovered soul into the
|
|
8
8
|
* exact set of modules an instance will be built from: which capability comes
|
|
9
9
|
* from where (a confirmed member at its latest commit, or a locked package at
|
|
10
|
-
* its pinned commit), which module fills each
|
|
10
|
+
* its pinned commit), which module fills each core-capability slot, the merged
|
|
11
11
|
* provider payload per capability, the composed skill set, the injects, and a
|
|
12
12
|
* `revision` that changes whenever any of that changes.
|
|
13
13
|
*
|
|
@@ -18,14 +18,16 @@
|
|
|
18
18
|
* is usable only from its own repo (E_CAPABILITY_PRIVATE). It NEVER looks
|
|
19
19
|
* inside the repo's oats-package/ (non-collapse rule): a name that exists only
|
|
20
20
|
* there fails with details.hint "provided by package <id>; use from: package".
|
|
21
|
-
* - `from: package` → the lock's packageProviding(name) (E_PACKAGE_MISSING)
|
|
22
|
-
*
|
|
21
|
+
* - `from: package` → the lock's packageProviding(name) (E_PACKAGE_MISSING). There is no approval
|
|
22
|
+
* gate: declaring the package in `packages:` is the trust decision (human
|
|
23
|
+
* decision 2026-09-24). It NEVER looks at member capabilities, even when the
|
|
23
24
|
* package's repo is a member.
|
|
24
25
|
*
|
|
25
26
|
* Composition order (soul wins; `off` removes):
|
|
26
27
|
* workspace.defaults.{knowledge,messaging,tasks} (slot defaults; a soul `none` drops them)
|
|
27
|
-
* ⊕ workspace.defaults.capabilities ⊕ workspace.defaults.byTeam[
|
|
28
|
-
* ⊕ soul.capabilities
|
|
28
|
+
* ⊕ workspace.defaults.capabilities ⊕ workspace.defaults.byTeam[<label>].capabilities for each of the
|
|
29
|
+
* soul's team labels, in order ⊕ soul.capabilities. Two labels that give one capability different
|
|
30
|
+
* entries → E_TEAM_CONFLICT naming both (teams contract 2026-09-25, decision 2).
|
|
29
31
|
*
|
|
30
32
|
* Slot `none` (contract §3, post-0.25.0 rule): a soul's `<slot>: none` EMPTIES the slot — it drops the
|
|
31
33
|
* workspace's `defaults.<slot>` AND any capability of that layer the workspace defaults contributed
|
|
@@ -33,10 +35,9 @@
|
|
|
33
35
|
* next to `none` is contradictory and stays E_SLOT_CONFLICT { reason: "none" } (spell `<cap>: off` to
|
|
34
36
|
* remove a default explicitly; drop the soul's own line to fill the slot).
|
|
35
37
|
*
|
|
36
|
-
* Package modules are
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* { reason: "digest-mismatch", approved, executables }. An edited lock never runs unapproved hooks.
|
|
38
|
+
* Package modules are read at the locked commit, and the lock must describe that tree: the capability
|
|
39
|
+
* list `<path>/oats-package.json` declares there must equal the entry's `capabilities`, else
|
|
40
|
+
* E_PACKAGE_INTEGRITY { why: "capabilities", listed, locked }.
|
|
40
41
|
*
|
|
41
42
|
* Revision (decision 14 + preview): `declRevision` fingerprints the declarations (soul identity, modules
|
|
42
43
|
* with their commits/versions/manifests, slots, skills, injects); `payloadRevision` fingerprints the merged
|
|
@@ -44,7 +45,8 @@
|
|
|
44
45
|
* everything, while a preview can say WHAT changed since the previous instance (declarations | payload | both).
|
|
45
46
|
*
|
|
46
47
|
* Payloads (decision 14), later wins on scalars/arrays, objects deep-merge:
|
|
47
|
-
* workspace.messaging (messaging slot only
|
|
48
|
+
* workspace.messaging (messaging slot only; its base, `byTeam` stripped and never merged — teams amendment K)
|
|
49
|
+
* ⊕ soul.<slot> ⊕ local.settings[cap] ⊕ spawn.providers[cap]
|
|
48
50
|
*
|
|
49
51
|
* This module shells out to nothing. Remote access is injected (`remote`, default
|
|
50
52
|
* lib/remote.mjs; `remoteOptions` threaded into every call). `resolveSoul` is
|
|
@@ -52,10 +54,12 @@
|
|
|
52
54
|
* over the remote; everything else is pure and exported for direct testing.
|
|
53
55
|
*/
|
|
54
56
|
import { createHash } from "node:crypto";
|
|
57
|
+
import { readFileSync } from "node:fs";
|
|
55
58
|
import { posix } from "node:path";
|
|
56
59
|
import { oatsError as baseOatsError } from "./errors.mjs";
|
|
57
60
|
import * as defaultRemote from "./remote.mjs";
|
|
58
|
-
import { bindRemote,
|
|
61
|
+
import { bindRemote, packageProviding, readPackageManifests, validateLock } from "./packages.mjs";
|
|
62
|
+
import { settingValueProblems } from "./capability-contract.mjs";
|
|
59
63
|
|
|
60
64
|
export const RESOLUTION_API = 1;
|
|
61
65
|
export const SLOTS = Object.freeze(["knowledge", "messaging", "tasks"]);
|
|
@@ -110,6 +114,42 @@ function mergeInto(base, over) {
|
|
|
110
114
|
}
|
|
111
115
|
const clone = (v) => (v === undefined ? undefined : JSON.parse(JSON.stringify(v)));
|
|
112
116
|
|
|
117
|
+
/** A manifest's declared setting defaults (`settings.<key>.default`) as a payload layer: the
|
|
118
|
+
* intrinsic field fallbacks, merged BELOW every workspace, soul, host and spawn layer. */
|
|
119
|
+
export function manifestDefaultsPayload(manifest) {
|
|
120
|
+
const out = {};
|
|
121
|
+
if (!isObject(manifest?.settings)) return out;
|
|
122
|
+
for (const [key, declaration] of Object.entries(manifest.settings)) {
|
|
123
|
+
if (isObject(declaration) && Object.hasOwn(declaration, "default") && declaration.default !== undefined) out[key] = clone(declaration.default);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Where each leaf of a merged payload came from: JSON pointer → the origin of the LAST layer that
|
|
129
|
+
* set it, under mergePayload's rules (objects merge recursively; arrays and scalars replace, taking
|
|
130
|
+
* every leaf below them with them). `layers` is [{ payload, origin }] in merge order. */
|
|
131
|
+
export function payloadOrigins(layers) {
|
|
132
|
+
const origins = {};
|
|
133
|
+
const escape = (k) => String(k).replace(/~/g, "~0").replace(/\//g, "~1");
|
|
134
|
+
const dropBelow = (pointer) => { for (const p of Object.keys(origins)) if (p === pointer || p.startsWith(`${pointer}/`)) delete origins[p]; };
|
|
135
|
+
const walk = (value, pointer, origin, mergedBefore) => {
|
|
136
|
+
for (const [k, v] of Object.entries(value)) {
|
|
137
|
+
if (v === undefined) continue;
|
|
138
|
+
const at = `${pointer}/${escape(k)}`;
|
|
139
|
+
if (isObject(v) && isObject(mergedBefore?.[k])) { delete origins[at]; walk(v, at, origin, mergedBefore[k]); }
|
|
140
|
+
else if (isObject(v) && Object.keys(v).length) { dropBelow(at); walk(v, at, origin, null); }
|
|
141
|
+
else { dropBelow(at); origins[at] = origin; }
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
let merged = {};
|
|
145
|
+
for (const { payload, origin } of layers) {
|
|
146
|
+
if (!isObject(payload)) continue;
|
|
147
|
+
walk(payload, "", origin, merged);
|
|
148
|
+
merged = mergeInto(merged, payload);
|
|
149
|
+
}
|
|
150
|
+
return origins;
|
|
151
|
+
}
|
|
152
|
+
|
|
113
153
|
/** `byTeam` is RESERVED (decision 23): it addresses a per-team payload and is legal only at the top level of
|
|
114
154
|
* workspace.messaging, where the resolver merges base ⊕ byTeam[soul.team] and strips it. In any other payload
|
|
115
155
|
* layer — a soul's slot payload, local.settings[cap], spawn.providers[cap] — it would reach the provider
|
|
@@ -265,6 +305,22 @@ export function satisfiesRange(version, range) {
|
|
|
265
305
|
return false;
|
|
266
306
|
}
|
|
267
307
|
|
|
308
|
+
/** The running kernel's version: a capability's `compatibility.oats` range is checked against it. */
|
|
309
|
+
export const KERNEL_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
310
|
+
|
|
311
|
+
/** A capability manifest's own kernel range (`compatibility.oats`) against `kernel`:
|
|
312
|
+
* `{ ok, range, kernel }`; `range: null` (always ok) when the manifest declares none. Throws
|
|
313
|
+
* E_CAPABILITY_INCOMPATIBLE { why: "range" } for a range that is not a version range. */
|
|
314
|
+
export function kernelCompatibility(manifest, kernel = KERNEL_VERSION) {
|
|
315
|
+
const range = isObject(manifest?.compatibility) && Object.hasOwn(manifest.compatibility, "oats") ? manifest.compatibility.oats : null;
|
|
316
|
+
if (range === null) return { ok: true, range: null, kernel };
|
|
317
|
+
try { return { ok: satisfiesRange(kernel, range), range, kernel }; }
|
|
318
|
+
catch (e) {
|
|
319
|
+
if (e?.code !== "E_COMPATIBILITY") throw e;
|
|
320
|
+
throw fail("E_CAPABILITY_INCOMPATIBLE", `${manifest?.capability ?? "capability"}: compatibility.oats ${show(range)} is not a version range`, { capability: manifest?.capability ?? null, range, kernel, why: "range" });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
268
324
|
/* ───────────────────────────── composition ────────────────────────────── */
|
|
269
325
|
|
|
270
326
|
const choiceOf = (value, path, via) => {
|
|
@@ -277,13 +333,43 @@ const choiceOf = (value, path, via) => {
|
|
|
277
333
|
throw fail("E_WORKSPACE_SCHEMA", `${path} must be { from: <location> } or "off", got ${show(value)}`, { path, value });
|
|
278
334
|
};
|
|
279
335
|
|
|
336
|
+
/** A soul's team labels, primary first: `team` is a label or a list of distinct labels (teams
|
|
337
|
+
* contract 2026-09-25, decision 1). A discovery SoulEntry carries them as `labels`; an entry
|
|
338
|
+
* without them (a hand-built one) has its `team` as the only label. */
|
|
339
|
+
export function teamLabelsOf(soulEntry) {
|
|
340
|
+
if (Array.isArray(soulEntry?.labels)) return soulEntry.labels.filter((l) => typeof l === "string");
|
|
341
|
+
return typeof soulEntry?.team === "string" ? [soulEntry.team] : [];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The eligible teams of a soul (teams contract, decision 3): one entry per label, in soul order,
|
|
346
|
+
* `{ label, team, mapped, payload }`. `payload` is workspace.messaging's base ⊕ byTeam[label] when the
|
|
347
|
+
* workspace maps the label, else the base alone with `mapped: false`; `team` is the payload's team id
|
|
348
|
+
* when mapped (null otherwise). Kernel-owned and delivered BESIDE a provider's settings, never inside
|
|
349
|
+
* them; joining any of them is the messaging provider's explicit act. No label → [] ("personal only").
|
|
350
|
+
* Pure: resolveSoul validates every carried label's entry (reserved byTeam, hostOnly keys) first.
|
|
351
|
+
*/
|
|
352
|
+
export function teamsOf(workspace, labels) {
|
|
353
|
+
const messaging = isObject(workspace?.messaging) ? workspace.messaging : {};
|
|
354
|
+
const { byTeam, ...base } = messaging;
|
|
355
|
+
return (labels || []).map((label) => {
|
|
356
|
+
const mapped = isObject(byTeam) && Object.hasOwn(byTeam, label) && isObject(byTeam[label]);
|
|
357
|
+
const payload = mapped ? mergePayload(base, byTeam[label]) : mergePayload(base);
|
|
358
|
+
return { label, team: mapped && typeof payload.team === "string" ? payload.team : null, mapped, payload };
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
280
362
|
/**
|
|
281
363
|
* The ordered capability map of a soul BEFORE any lookup:
|
|
282
|
-
* slot defaults (dropped where the soul says `none`) ⊕ defaults.capabilities ⊕ defaults.byTeam[
|
|
364
|
+
* slot defaults (dropped where the soul says `none`) ⊕ defaults.capabilities ⊕ defaults.byTeam[label] for
|
|
365
|
+
* each label in order ⊕ soul.capabilities
|
|
283
366
|
* → [{ name, from, via }] sorted by name; `off` removes the entry from every lower layer.
|
|
284
|
-
* `via` is one of "defaults.<slot>" | "defaults.capabilities" | "defaults.byTeam.<
|
|
367
|
+
* `via` is one of "defaults.<slot>" | "defaults.capabilities" | "defaults.byTeam.<label>" | "soul".
|
|
368
|
+
* Two labels that give one capability different entries (`off` vs a location, or two locations) →
|
|
369
|
+
* E_TEAM_CONFLICT { capability, labels: [a, b] }; identical entries are not a conflict, and a capability
|
|
370
|
+
* the soul names itself is not one either (the soul's entry wins over both).
|
|
285
371
|
*/
|
|
286
|
-
export function composeCapabilities(workspace, soulDefinition, { team = null } = {}) {
|
|
372
|
+
export function composeCapabilities(workspace, soulDefinition, { team = null, labels = team === null ? [] : [team] } = {}) {
|
|
287
373
|
const map = new Map();
|
|
288
374
|
const apply = (entries, via, path) => {
|
|
289
375
|
for (const [name, value] of Object.entries(entries || {})) {
|
|
@@ -301,8 +387,21 @@ export function composeCapabilities(workspace, soulDefinition, { team = null } =
|
|
|
301
387
|
apply(d, `defaults.${slot}`, `/defaults/${slot}`);
|
|
302
388
|
}
|
|
303
389
|
apply(defaults.capabilities, "defaults.capabilities", "/defaults/capabilities");
|
|
304
|
-
|
|
305
|
-
|
|
390
|
+
const byLabel = new Map(); // capability -> { label, choice } of the first label that set it
|
|
391
|
+
// The soul's own entry wins over every label, so a capability the soul names settles the conflict.
|
|
392
|
+
const soulNames = new Set(Object.keys(isObject(soulDefinition?.capabilities) ? soulDefinition.capabilities : {}));
|
|
393
|
+
for (const label of labels) {
|
|
394
|
+
if (!isObject(defaults.byTeam) || !Object.hasOwn(defaults.byTeam, label) || !isObject(defaults.byTeam[label])) continue;
|
|
395
|
+
const entries = defaults.byTeam[label].capabilities, path = `/defaults/byTeam/${label}/capabilities`;
|
|
396
|
+
for (const [name, value] of Object.entries(entries || {})) {
|
|
397
|
+
const choice = choiceOf(value, `${path}/${name}`, `defaults.byTeam.${label}`);
|
|
398
|
+
const seen = byLabel.get(name);
|
|
399
|
+
if (seen && !soulNames.has(name) && canonicalJson(seen.choice) !== canonicalJson(choice)) {
|
|
400
|
+
throw fail("E_TEAM_CONFLICT", `${name}: team labels ${show(seen.label)} and ${show(label)} give it different entries (${canonicalJson(seen.choice)} vs ${canonicalJson(choice)}) in defaults.byTeam — make them agree, or name ${name} in the soul`, { capability: name, labels: [seen.label, label], entries: [seen.choice, choice], paths: [`/defaults/byTeam/${seen.label}/capabilities/${name}`, `${path}/${name}`] });
|
|
401
|
+
}
|
|
402
|
+
if (!seen) byLabel.set(name, { label, choice });
|
|
403
|
+
}
|
|
404
|
+
apply(entries, `defaults.byTeam.${label}`, path);
|
|
306
405
|
}
|
|
307
406
|
apply(soulDefinition?.capabilities, "soul", "/capabilities");
|
|
308
407
|
return [...map.values()].sort((a, b) => byCodepoint(a.name, b.name));
|
|
@@ -315,7 +414,7 @@ function memberRow(discovery, repoKey) {
|
|
|
315
414
|
}
|
|
316
415
|
|
|
317
416
|
/** The ref the workspace lists a member under (keeps the operator's spelling: ssh vs https); else from the key. */
|
|
318
|
-
function memberRef(discovery, remote, repoKey) {
|
|
417
|
+
export function memberRef(discovery, remote, repoKey) {
|
|
319
418
|
for (const ref of discovery?.workspace?.members || []) {
|
|
320
419
|
try { if (remote.parseRepoRef(ref).key === repoKey) return ref; } catch { /* validated upstream */ }
|
|
321
420
|
}
|
|
@@ -338,6 +437,9 @@ function lookupMember(discovery, soul, name, from, via, lock) {
|
|
|
338
437
|
const cap = (row.capabilities || []).find((c) => c.name === name) || null;
|
|
339
438
|
if (!cap) {
|
|
340
439
|
const details = { ...where, commit: row.commit };
|
|
440
|
+
// Discovery found the manifest but refused it (lib/capability-contract.mjs): say that, not "missing".
|
|
441
|
+
const refused = (discovery?.problems || []).filter((p) => p.repoKey === repoKey && typeof p.path === "string" && p.path.startsWith(`capabilities/${name}/oats.json#`));
|
|
442
|
+
if (refused.length) throw fail("E_WORKSPACE_SCHEMA", `${name}: ${repoKey}@${short(row.commit)} declares it, but its manifest is refused: ${refused.map((p) => `${p.path}: ${p.message}`).join("; ")}`, { ...details, problems: refused, reason: "manifest-contract" });
|
|
341
443
|
// The name may live in a package tier: say so, but never resolve it from here (decision 19).
|
|
342
444
|
const providing = safePackageProviding(lock, name);
|
|
343
445
|
let hint = null;
|
|
@@ -355,44 +457,21 @@ function safePackageProviding(lock, name) {
|
|
|
355
457
|
try { return packageProviding(lock, name); } catch { return null; }
|
|
356
458
|
}
|
|
357
459
|
|
|
358
|
-
/** Resolve `from: package` through the lock only — never through member capabilities.
|
|
359
|
-
|
|
460
|
+
/** Resolve `from: package` through the lock only — never through member capabilities.
|
|
461
|
+
* Declaring a package in the workspace's packages: is the trust decision (human
|
|
462
|
+
* decision 2026-09-24), so a workspace view admits only a locked package the
|
|
463
|
+
* workspace STILL declares: a package removed from packages: but left in a stale
|
|
464
|
+
* lock (no `oats sync` since) is refused, never materialized. `declared` is null
|
|
465
|
+
* for a standalone view — its lock holds only what a standalone sync wrote. */
|
|
466
|
+
function lookupPackage(lock, name, via, soul, declared) {
|
|
360
467
|
const where = { capability: name, from: "package", soul: soul.name, via };
|
|
361
468
|
if (!isObject(lock) || !isObject(lock.packages)) throw fail("E_PACKAGE_MISSING", `${name}: from: package needs the workspace lock (oats-lock.json v3) — run \`oats sync\``, { ...where, reason: "no-lock" });
|
|
362
469
|
const providing = packageProviding(lock, name);
|
|
363
470
|
if (!providing) throw fail("E_PACKAGE_MISSING", `${name}: no locked package provides it — add the package to packages: and run \`oats sync\``, { ...where, locked: Object.keys(lock.packages).sort() });
|
|
364
|
-
if (!providing.
|
|
365
|
-
throw fail("E_PACKAGE_UNAPPROVED", `${name}: package ${providing.id} v${providing.entry.version} (${short(providing.entry.commit)}) is not approved — review its executables and approve once per version`, { ...where, id: providing.id, version: providing.entry.version, commit: providing.entry.commit, reason: "unapproved" });
|
|
366
|
-
}
|
|
471
|
+
if (declared && !Object.hasOwn(declared, providing.id)) throw fail("E_PACKAGE_MISSING", `${name}: the lock's package ${providing.id} is no longer declared in the workspace's packages: — run \`oats sync\` (or declare it again)`, { ...where, id: providing.id, reason: "undeclared" });
|
|
367
472
|
return providing;
|
|
368
473
|
}
|
|
369
474
|
|
|
370
|
-
/**
|
|
371
|
-
* The approval must describe THIS tree (decision 8: the lock carries the approval next to the commit it
|
|
372
|
-
* approved). Recompute the executables digest over the package tree at entry.commit — the very computation
|
|
373
|
-
* `oats sync` approved — and require equality with approved.executables. A lock edited to another commit
|
|
374
|
-
* (same id/version, approval copied along) fails here: E_PACKAGE_UNAPPROVED { reason: "digest-mismatch" }
|
|
375
|
-
* naming both digests and the executables that would have run. Cached per (remote, repo, commit, path).
|
|
376
|
-
*/
|
|
377
|
-
async function assertApprovalDescribesTree({ remote, remoteOptions, ref, id, entry, name, via, soul }) {
|
|
378
|
-
const where = { capability: name, from: "package", soul: soul.name, via, id, version: entry.version, commit: entry.commit, path: entry.path };
|
|
379
|
-
let computed;
|
|
380
|
-
try { computed = await executablesDigestAt(remote, ref, entry.commit, entry.path, Array.isArray(entry.capabilities) ? entry.capabilities : null, { remoteOptions }); }
|
|
381
|
-
catch (e) {
|
|
382
|
-
if (e?.code === "E_PACKAGE_INTEGRITY" && (e.details ?? e.provenance)?.why === "capabilities") {
|
|
383
|
-
const d = e.details ?? e.provenance;
|
|
384
|
-
throw fail("E_PACKAGE_INTEGRITY", `${name}: the lock says package ${id} v${entry.version} provides [${d.locked.join(", ")}], but ${entry.path}/oats-package.json at ${short(entry.commit)} declares [${d.listed.join(", ")}]`, { ...where, why: "capabilities", listed: d.listed, locked: d.locked });
|
|
385
|
-
}
|
|
386
|
-
throw e;
|
|
387
|
-
}
|
|
388
|
-
if (computed.digest !== entry.approved.executables) {
|
|
389
|
-
throw fail("E_PACKAGE_UNAPPROVED",
|
|
390
|
-
`${name}: package ${id} v${entry.version} @ ${short(entry.commit)}: the recorded approval ${entry.approved.executables} does not describe this tree's executables (${computed.digest}) — the lock was edited or the approval copied from another commit; run \`oats sync\` and approve what it shows`,
|
|
391
|
-
{ ...where, reason: "digest-mismatch", approved: entry.approved.executables, executables: computed.digest, targets: computed.executables.map((x) => `${x.capability}: ${x.kind} ${x.name} → ${x.target}`) });
|
|
392
|
-
}
|
|
393
|
-
return computed;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
475
|
/** The repo ref a locked package is read from: the lock's recorded url, else the catalog's url for catalog ids, else the key for git refs. */
|
|
397
476
|
export function packageRef(id, entry, catalog, remote) {
|
|
398
477
|
const cat = isObject(catalog) ? (isObject(catalog.packages) && !("url" in catalog.packages) ? catalog.packages : catalog) : {};
|
|
@@ -470,28 +549,28 @@ async function enumerateSkills({ remote, ref, commit, dir, manifest, listing, mo
|
|
|
470
549
|
* options: { local, lock, spawn = { providers? }, catalog, remote, remoteOptions }
|
|
471
550
|
* `catalog` (package-catalog.json shape) tells which repo a `catalog:<id>` lock entry is read from.
|
|
472
551
|
*
|
|
473
|
-
* → Resolution { resolutionApi: 1, soul, modules[], slots, payloads, skills[], injects[], revision } — deep-frozen.
|
|
552
|
+
* → Resolution { resolutionApi: 1, soul, modules[], slots, payloads, payloadOrigins, skills[], injects[], revision } — deep-frozen.
|
|
474
553
|
* Extra fields beyond the contract (recorded for materialize): module.dir (capability dir, repo-relative)
|
|
475
554
|
* and from.repoKey on package modules.
|
|
476
555
|
*/
|
|
477
|
-
export async function resolveSoul(discovery, soulEntry, { local = null, lock = null, spawn = {}, catalog = null, remote: injected, remoteOptions } = {}) {
|
|
556
|
+
export async function resolveSoul(discovery, soulEntry, { local = null, lock = null, spawn = {}, catalog = null, remote: injected, remoteOptions, kernel = KERNEL_VERSION } = {}) {
|
|
478
557
|
if (!isObject(soulEntry) || typeof soulEntry.name !== "string" || typeof soulEntry.repoKey !== "string") {
|
|
479
558
|
throw new TypeError("resolveSoul: soulEntry must be a discovery SoulEntry { name, path, repoKey, commit, team, private, definition }");
|
|
480
559
|
}
|
|
481
560
|
if (!isObject(spawn)) throw new TypeError("resolveSoul: spawn must be an object");
|
|
482
561
|
if (lock !== null && lock !== undefined) validateLock(lock); // E_LOCK_SCHEMA: a lock passed in memory meets the same bar as one read from disk
|
|
483
|
-
const rawRemote = injected ?? defaultRemote; // identity for the per-process digest cache (bound copies are per call)
|
|
484
562
|
const remote = remoteOf({ remote: injected, remoteOptions });
|
|
485
563
|
const workspace = isObject(discovery?.workspace) ? discovery.workspace : null;
|
|
486
564
|
assertSoulDiscovered(discovery, soulEntry);
|
|
487
565
|
const definition = isObject(soulEntry.definition) ? soulEntry.definition : {};
|
|
488
|
-
const
|
|
566
|
+
const labels = teamLabelsOf(soulEntry);
|
|
567
|
+
const team = labels[0] ?? null; // the PRIMARY label: the merged messaging payload and OATS_TEAM_LABEL follow it
|
|
489
568
|
const soul = { name: soulEntry.name, repoKey: soulEntry.repoKey, commit: soulEntry.commit ?? null, team, path: soulEntry.path ?? null };
|
|
490
569
|
|
|
491
570
|
// Standalone: the soul's own repo only, workspace defaults unknown (decision 10).
|
|
492
571
|
const declared = discovery?.standalone === true
|
|
493
|
-
? composeCapabilities(null, { ...definition, capabilities: soulEntry.capabilities ?? definition.capabilities }, {
|
|
494
|
-
: composeCapabilities(workspace, definition, {
|
|
572
|
+
? composeCapabilities(null, { ...definition, capabilities: soulEntry.capabilities ?? definition.capabilities }, { labels })
|
|
573
|
+
: composeCapabilities(workspace, definition, { labels });
|
|
495
574
|
|
|
496
575
|
const modules = [];
|
|
497
576
|
const skills = [];
|
|
@@ -504,12 +583,15 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
504
583
|
for (const { name, from, via } of declared) {
|
|
505
584
|
let module;
|
|
506
585
|
if (from === "package") {
|
|
507
|
-
const { id, entry } = lookupPackage(lock, name, via, soul);
|
|
586
|
+
const { id, entry } = lookupPackage(lock, name, via, soul, discovery?.standalone === true ? null : (isObject(workspace?.packages) ? workspace.packages : {}));
|
|
508
587
|
const ref = packageRef(id, entry, catalog, remote);
|
|
509
588
|
const details = { capability: name, id, version: entry.version, commit: entry.commit, path: entry.path };
|
|
510
|
-
// M3: the approval must describe the tree at entry.commit (the digest `oats sync` approved), else refuse.
|
|
511
|
-
await assertApprovalDescribesTree({ remote: rawRemote, remoteOptions, ref, id, entry, name, via, soul });
|
|
512
589
|
const { capabilities } = await readPackageManifests(remote, ref, entry.commit, entry.path, details);
|
|
590
|
+
// The lock must describe the tree it names: its capability list is what the package declares there.
|
|
591
|
+
const listed = capabilities.map((c) => c.name).sort(), locked = [...entry.capabilities].sort(); // validateLock guarantees the array
|
|
592
|
+
if (listed.length !== locked.length || listed.some((c, i) => c !== locked[i])) {
|
|
593
|
+
throw fail("E_PACKAGE_INTEGRITY", `${name}: the lock says package ${id} v${entry.version} provides [${locked.join(", ")}], but ${entry.path}/oats-package.json at ${short(entry.commit)} declares [${listed.join(", ")}]`, { ...details, why: "capabilities", listed, locked });
|
|
594
|
+
}
|
|
513
595
|
const cap = capabilities.find((c) => c.name === name);
|
|
514
596
|
if (!cap) throw fail("E_PACKAGE_INTEGRITY", `${name}: the lock says package ${id} v${entry.version} provides it, but ${entry.path}/oats-package.json at ${short(entry.commit)} does not`, { ...details, listed: capabilities.map((c) => c.name) });
|
|
515
597
|
const layer = layerOf(cap.manifest);
|
|
@@ -545,6 +627,7 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
545
627
|
// the soul's own contradiction → E_SLOT_CONFLICT { reason: "none" } — judged FIRST, so it is never reported
|
|
546
628
|
// as a two-module clash.
|
|
547
629
|
const slots = { knowledge: null, messaging: null, tasks: null };
|
|
630
|
+
const slotsFrom = { knowledge: null, messaging: null, tasks: null };
|
|
548
631
|
const viaOf = new Map(declared.map((d) => [d.name, d.via]));
|
|
549
632
|
for (const m of modules) {
|
|
550
633
|
const via = viaOf.get(m.name);
|
|
@@ -557,6 +640,7 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
557
640
|
if (emptied.has(m.layer)) throw fail("E_SLOT_CONFLICT", `slot ${m.layer}: the soul says ${m.layer}: none but itself names ${m.name}, which declares layer ${m.layer} — drop one of the two (a workspace default of that layer would have been dropped by none; this one is the soul's own)`, { slot: m.layer, modules: [m.name], soul: soul.name, reason: "none", via });
|
|
558
641
|
if (slots[m.layer]) throw fail("E_SLOT_CONFLICT", `slot ${m.layer}: both ${slots[m.layer]} and ${m.name} declare layer ${m.layer}; a soul fills each slot with at most one capability`, { slot: m.layer, modules: [slots[m.layer], m.name], soul: soul.name });
|
|
559
642
|
slots[m.layer] = m.name;
|
|
643
|
+
slotsFrom[m.layer] = fromOfVia(via);
|
|
560
644
|
}
|
|
561
645
|
|
|
562
646
|
// Duplicate skill names within the composed set (decision 16).
|
|
@@ -569,7 +653,8 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
569
653
|
seenSkills.set(s.name, s);
|
|
570
654
|
}
|
|
571
655
|
|
|
572
|
-
// Payloads (decision 14): workspace.messaging (messaging slot) ⊕ soul.<slot>
|
|
656
|
+
// Payloads (decision 14; addendum 5): manifest defaults ⊕ workspace.messaging (messaging slot) ⊕ soul.<slot>
|
|
657
|
+
// ⊕ local.settings[cap] ⊕ spawn.providers[cap], each leaf's origin kept in payloadOrigins.
|
|
573
658
|
const settings = isObject(local?.settings) ? local.settings : {};
|
|
574
659
|
const providers = isObject(spawn.providers) ? spawn.providers : {};
|
|
575
660
|
const moduleNames = new Set(modules.map((m) => m.name));
|
|
@@ -580,28 +665,63 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
580
665
|
assertNoReservedKey(value, `/spawn/providers/${cap}`);
|
|
581
666
|
if (!moduleNames.has(cap)) throw fail("E_CAPABILITY_MISSING", `--provider ${cap}: the soul ${soul.name} does not resolve a capability named ${show(cap)} (modules: ${[...moduleNames].sort().join(", ") || "none"})`, { capability: cap, soul: soul.name, hint: "a provider payload targets one of the soul's resolved capabilities", modules: [...moduleNames].sort() });
|
|
582
667
|
}
|
|
583
|
-
const payloads = {};
|
|
668
|
+
const payloads = {}, origins = {};
|
|
584
669
|
// The soul's slot payloads are checked whether or not the slot resolves to a module: a reserved key is a
|
|
585
670
|
// schema fault of the soul, not of the spawn that happened to fill the slot.
|
|
586
671
|
for (const slot of SLOTS) if (isObject(definition[slot])) assertNoReservedKey(definition[slot], `/${slot}`);
|
|
587
672
|
for (const m of modules) {
|
|
588
|
-
|
|
673
|
+
// Addendum 5: the manifest's declared defaults are the LOWEST layer, so the preview shows (and the
|
|
674
|
+
// provider receives) e.g. identity.mode = local with its origin, not an absent key.
|
|
675
|
+
const layers = [], sourced = [];
|
|
676
|
+
const add = (payload, origin) => { layers.push(payload); sourced.push({ payload, origin }); };
|
|
677
|
+
for (const [key, value] of Object.entries(manifestDefaultsPayload(m.manifest))) {
|
|
678
|
+
add({ [key]: value }, { kind: "manifest-default", at: `oats.json#/settings/${key.replace(/~/g, "~0").replace(/\//g, "~1")}/default` });
|
|
679
|
+
}
|
|
589
680
|
// Decision 27 (K1″): a manifest may mark a settings key `hostOnly` — a host fact (a custody
|
|
590
681
|
// path, a state directory) that only the deployment's own oats-local.yaml may supply. Every
|
|
591
682
|
// committed or per-spawn layer is refused with that key present, BEFORE the merge, because the
|
|
592
683
|
// provider receives one merged payload without provenance and cannot enforce this itself.
|
|
593
684
|
const hostOnly = hostOnlyKeys(m.manifest);
|
|
594
|
-
const committed = (payload, path) => { assertNoHostOnlyKey(payload, path, hostOnly, m.name);
|
|
685
|
+
const committed = (payload, path, origin) => { assertNoHostOnlyKey(payload, path, hostOnly, m.name); add(payload, origin); };
|
|
595
686
|
if (m.layer === "messaging" && isObject(workspace?.messaging)) {
|
|
596
|
-
// Decision 23
|
|
687
|
+
// Decision 23 as amended by K (teams contract, co-lead ruling): the provider's settings are
|
|
688
|
+
// base ⊕ soul ⊕ host ⊕ spawn. No byTeam[<label>] is merged, the primary's included: each
|
|
689
|
+
// label's base ⊕ byTeam[label] is delivered only in its `teams` entry (teamsOf → OATS_TEAMS),
|
|
690
|
+
// so settings.team is the personal team a host, soul or spawn set. `byTeam` never reaches the
|
|
691
|
+
// provider, and the primary team's own payload may still not nest one.
|
|
597
692
|
const { byTeam, ...base } = workspace.messaging;
|
|
598
|
-
committed(base, "/messaging");
|
|
599
|
-
|
|
693
|
+
committed(base, "/messaging", { kind: "workspace", at: "oats-workspace.yaml#/messaging" });
|
|
694
|
+
// Every label's entry still reaches the provider, in OATS_TEAMS (teamsOf), so each one the soul
|
|
695
|
+
// carries — the primary's and every other — may nest no byTeam and carry no hostOnly key.
|
|
696
|
+
for (const label of labels) {
|
|
697
|
+
if (!isObject(byTeam) || !Object.hasOwn(byTeam, label) || !isObject(byTeam[label])) continue;
|
|
698
|
+
assertNoReservedKey(byTeam[label], `/messaging/byTeam/${label}`);
|
|
699
|
+
assertNoHostOnlyKey(byTeam[label], `/messaging/byTeam/${label}`, hostOnly, m.name);
|
|
700
|
+
}
|
|
600
701
|
}
|
|
601
|
-
if (m.layer && isObject(definition[m.layer])) { assertNoReservedKey(definition[m.layer], `/${m.layer}`); committed(definition[m.layer], `/${m.layer}`); }
|
|
602
|
-
if (Object.hasOwn(settings, m.name) && isObject(settings[m.name])) { assertNoReservedKey(settings[m.name], `/settings/${m.name}`);
|
|
603
|
-
if (Object.hasOwn(providers, m.name) && isObject(providers[m.name])) committed(providers[m.name], `/spawn/providers/${m.name}`);
|
|
702
|
+
if (m.layer && isObject(definition[m.layer])) { assertNoReservedKey(definition[m.layer], `/${m.layer}`); committed(definition[m.layer], `/${m.layer}`, { kind: "soul", at: `soul.yaml#/${m.layer}` }); }
|
|
703
|
+
if (Object.hasOwn(settings, m.name) && isObject(settings[m.name])) { assertNoReservedKey(settings[m.name], `/settings/${m.name}`); add(settings[m.name], { kind: "host", at: `oats-local.yaml#/settings/${m.name}` }); } // the host layer: hostOnly keys are legal here
|
|
704
|
+
if (Object.hasOwn(providers, m.name) && isObject(providers[m.name])) committed(providers[m.name], `/spawn/providers/${m.name}`, { kind: "spawn", at: `--provider ${m.name}` });
|
|
604
705
|
payloads[m.name] = mergePayload(...layers);
|
|
706
|
+
origins[m.name] = payloadOrigins(sourced);
|
|
707
|
+
// A value outside the manifest's `settings.<key>.values` is refused where it was set: a
|
|
708
|
+
// conditional `requires` row reads it, so a typo would silently skip every such row.
|
|
709
|
+
for (const { key, value, values } of settingValueProblems(m.manifest, payloads[m.name])) {
|
|
710
|
+
const at = origins[m.name][`/${key.replace(/~/g, "~0").replace(/\//g, "~1")}`]?.at ?? null;
|
|
711
|
+
throw fail("E_WORKSPACE_SCHEMA", `${m.name}: setting ${show(key)} is ${show(value)}, not one of ${values.map(show).join(", ")}${at ? ` (set at ${at})` : ""}`, { capability: m.name, key, value, values, at, reason: "setting-value" });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Each capability's own kernel range (its manifest's compatibility.oats) must admit the running
|
|
716
|
+
// kernel: a package or member capability written for another kernel is refused before anything
|
|
717
|
+
// is composed from it, never discovered at its first hook.
|
|
718
|
+
for (const m of modules) {
|
|
719
|
+
const c = kernelCompatibility({ ...m.manifest, capability: m.name }, kernel);
|
|
720
|
+
if (c.ok) continue;
|
|
721
|
+
const remedy = m.from.kind === "package"
|
|
722
|
+
? `pin a release of ${m.from.package} whose range admits ${kernel} in the workspace's packages:, or run a kernel the range admits`
|
|
723
|
+
: `update ${m.name} in ${m.from.repoKey}, or run a kernel the range admits`;
|
|
724
|
+
throw fail("E_CAPABILITY_INCOMPATIBLE", `${m.name} requires oats ${c.range}; this kernel is ${kernel} — ${remedy}`, { capability: m.name, range: c.range, kernel, from: m.from });
|
|
605
725
|
}
|
|
606
726
|
|
|
607
727
|
// Compatibility floors (soul.compatibility) are constraints on PACKAGE versions.
|
|
@@ -630,7 +750,23 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
630
750
|
const declRevision = revisionOf(decl);
|
|
631
751
|
const payloadRevision = revisionOf(payloads);
|
|
632
752
|
const revision = revisionOf({ declRevision, payloadRevision });
|
|
633
|
-
|
|
753
|
+
// payloadOrigins is provenance only: it is derived from the same inputs the two revisions already
|
|
754
|
+
// bind, so it does not enter either fingerprint. `teams` (the eligible teams, teams contract
|
|
755
|
+
// decision 3) is live messaging state, not composition: it stays out of both fingerprints too, so a
|
|
756
|
+
// single-label soul's revision is what it was.
|
|
757
|
+
// `slotsFrom` (where each filled slot's capability came from) is provenance, like payloadOrigins: outside
|
|
758
|
+
// both fingerprints, so the same capability reached another way is not a composition change.
|
|
759
|
+
const teams = teamsOf(discovery?.standalone === true ? null : workspace, labels);
|
|
760
|
+
return deepFreeze({ ...decl, payloads, payloadOrigins: origins, teams, slotsFrom, declRevision, payloadRevision, revision });
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** Where a composed capability came from, as the Desktop names it (`layers.<layer>.from`): the soul's own
|
|
764
|
+
* entry → "soul"; defaults.<slot> or defaults.capabilities → "workspace"; defaults.byTeam.<label> → "team:<label>". */
|
|
765
|
+
export function fromOfVia(via) {
|
|
766
|
+
if (via === "soul") return "soul";
|
|
767
|
+
if (typeof via === "string" && via.startsWith("defaults.byTeam.")) return `team:${via.slice("defaults.byTeam.".length)}`;
|
|
768
|
+
if (typeof via === "string" && via.startsWith("defaults.")) return "workspace";
|
|
769
|
+
return null;
|
|
634
770
|
}
|
|
635
771
|
|
|
636
772
|
function layerOf(manifest) {
|