@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
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/** Shared materialization mechanics. Kernel manifest/runtime validators are
|
|
2
|
-
* supplied once by the caller; this module selects no source, lock or approval. */
|
|
3
|
-
import { chmodSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { capabilityArtifactIntegrity, copyTreeSafe } from "./artifact-tree.mjs";
|
|
6
|
-
import { CAPABILITY_INSTALLATION_FILE } from "./capability-provenance.mjs";
|
|
7
|
-
import { executableSurfaceOf } from "./capability-execution.mjs";
|
|
8
|
-
import { oatsError } from "./errors.mjs";
|
|
9
|
-
|
|
10
|
-
/** Provenance format v1 is byte-stable across writer kernels and digest versions:
|
|
11
|
-
* exactly these keys/order, two-space JSON, one LF, mode0644, no writer version. */
|
|
12
|
-
function capabilityInstallationRecord(cap, pkg) {
|
|
13
|
-
return {
|
|
14
|
-
schemaVersion: 1,
|
|
15
|
-
capability: cap.id,
|
|
16
|
-
version: cap.manifest.version,
|
|
17
|
-
package: pkg.package,
|
|
18
|
-
packageVersion: pkg.version,
|
|
19
|
-
source: pkg.source,
|
|
20
|
-
commit: pkg.commit,
|
|
21
|
-
packagePath: pkg.path,
|
|
22
|
-
capabilityPath: cap.rel,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function assertMaterializationInput(root) {
|
|
26
|
-
if (!lstatSync(root).isDirectory()) throw oatsError("invalid-package-manifest", "a materialized capability root must be a real directory, not a source alias");
|
|
27
|
-
let metadata;
|
|
28
|
-
try { metadata = lstatSync(join(root, CAPABILITY_INSTALLATION_FILE)); }
|
|
29
|
-
catch (error) { if (error.code === "ENOENT") return; throw error; }
|
|
30
|
-
if (!metadata.isFile() || (metadata.mode & 0o100) !== 0 || !readdirSync(root).includes(CAPABILITY_INSTALLATION_FILE)) {
|
|
31
|
-
throw oatsError("invalid-package-manifest", "source provenance entry must not be a link, executable, directory or filesystem alias");
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function writeCapabilityInstallation(dest, cap, pkg) {
|
|
35
|
-
assertMaterializationInput(dest);
|
|
36
|
-
const temporary = mkdtempSync(join(dest, ".installation-")), candidate = join(temporary, "record.json");
|
|
37
|
-
let failed = false, primary;
|
|
38
|
-
try {
|
|
39
|
-
writeFileSync(candidate, JSON.stringify(capabilityInstallationRecord(cap, pkg), null, 2) + "\n", { flag: "wx", mode: 0o644 });
|
|
40
|
-
chmodSync(candidate, 0o644);
|
|
41
|
-
// Replace the reserved entry itself, never follow a source-controlled link
|
|
42
|
-
// or mutate another file sharing its inode. Valid v1 serialization is unchanged.
|
|
43
|
-
renameSync(candidate, join(dest, CAPABILITY_INSTALLATION_FILE));
|
|
44
|
-
} catch (error) { failed = true; primary = error; throw error; }
|
|
45
|
-
finally {
|
|
46
|
-
try { rmSync(candidate, { force: true }); rmdirSync(temporary); }
|
|
47
|
-
catch (cleanup) {
|
|
48
|
-
const error = failed ? new AggregateError([primary, cleanup], "provenance write and staging cleanup failed", { cause: primary })
|
|
49
|
-
: new Error("provenance staging cleanup failed", { cause: cleanup });
|
|
50
|
-
error.code = failed ? primary.code : cleanup.code; error.stagingPath = temporary; throw error;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function createCapabilityMaterializer({ materializeCapabilityDeps, assertCapabilitySelfContained, assertMaterializedDepsContained, assertNoNativeBinaries }) {
|
|
56
|
-
for (const validate of [materializeCapabilityDeps, assertCapabilitySelfContained, assertMaterializedDepsContained, assertNoNativeBinaries]) {
|
|
57
|
-
if (typeof validate !== "function") throw new TypeError("materializer requires the kernel's complete validation callbacks");
|
|
58
|
-
}
|
|
59
|
-
/** The returned legacy integrity remains literal compatibility evidence. New
|
|
60
|
-
* preparation computes the explicit new-format integrity over this same tree. */
|
|
61
|
-
return function materializeCapability({ cap, pkg, artifactsDir }) {
|
|
62
|
-
assertMaterializationInput(cap.dir);
|
|
63
|
-
const rep = materializeCapabilityDeps(cap.dir);
|
|
64
|
-
if (rep.error) throw oatsError("invalid-package-manifest", `runtime dependency materialization failed for capability "${cap.id}" of package "${pkg.package}": ${rep.error}`);
|
|
65
|
-
assertCapabilitySelfContained(cap.dir, cap.manifest);
|
|
66
|
-
assertMaterializedDepsContained(cap.dir);
|
|
67
|
-
assertNoNativeBinaries(cap.dir);
|
|
68
|
-
const dest = join(artifactsDir, cap.id);
|
|
69
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
70
|
-
if (cap.rel === ".") {
|
|
71
|
-
// Published flat packages need their original root for subsequent template
|
|
72
|
-
// reads; copy instead of moving, preserving literal links and real modes.
|
|
73
|
-
copyTreeSafe(cap.dir, dest);
|
|
74
|
-
} else renameSync(cap.dir, dest);
|
|
75
|
-
writeCapabilityInstallation(dest, cap, pkg);
|
|
76
|
-
return {
|
|
77
|
-
capability: cap.id, version: cap.manifest.version, package: pkg.package, path: cap.rel,
|
|
78
|
-
dir: dest, integrity: capabilityArtifactIntegrity(dest),
|
|
79
|
-
layer: cap.manifest.layer ?? null,
|
|
80
|
-
executableSurface: executableSurfaceOf(cap.manifest), manifest: cap.manifest,
|
|
81
|
-
};
|
|
82
|
-
};
|
|
83
|
-
}
|
package/lib/pi-sdk-host.mjs
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
/** Explicit, zero-plugin Pi SDK host. No auth wrapper, default resource loader,
|
|
2
|
-
* private SDK imports, model substitution or interpretation of legacy Pi argv. */
|
|
3
|
-
import { lstatSync, realpathSync } from "node:fs";
|
|
4
|
-
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import { readPortableBytes } from "./portable-files.mjs";
|
|
7
|
-
import { parseStrictJson } from "./portable-values.mjs";
|
|
8
|
-
import { oatsError } from "./errors.mjs";
|
|
9
|
-
|
|
10
|
-
export const PI_SDK_HOST_VERSION = "1";
|
|
11
|
-
export const PI_SDK_VERSION = "0.85.1";
|
|
12
|
-
export const PI_SDK_HOST = fileURLToPath(new URL("../bin/oats-pi-sdk-host.mjs", import.meta.url));
|
|
13
|
-
const fail = (code, message) => { throw oatsError(code, message); };
|
|
14
|
-
const absolute = value => typeof value === "string" && value.length > 0 && !value.includes("\0") && isAbsolute(value) && resolve(value) === value;
|
|
15
|
-
export function isPiSdkHost(recipe) {
|
|
16
|
-
return recipe?.runtime === "pi" && recipe.executable === PI_SDK_HOST;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function parsePiHostModel(value) {
|
|
20
|
-
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*\/[^\s\0*?\\]+$/.test(value)
|
|
21
|
-
|| value.includes("!") || value.includes("$") || value.includes("..")) {
|
|
22
|
-
fail("E_PI_HOST_MODEL", "captured Pi host requires an exact provider/model identifier");
|
|
23
|
-
}
|
|
24
|
-
const slash = value.indexOf("/");
|
|
25
|
-
return { provider: value.slice(0, slash), id: value.slice(slash + 1) };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Only operator-owned options; internal home/session/model/task are NEVER here. */
|
|
29
|
-
export function parsePiHostRecipeArgs(args) {
|
|
30
|
-
const keys = ["--oats-pi-host", "--mode", "--thinking", "--sdk-root", "--sdk-version"];
|
|
31
|
-
if (!Array.isArray(args) || args.length !== keys.length * 2
|
|
32
|
-
|| args.some(value => typeof value !== "string" || value.includes("\0"))
|
|
33
|
-
|| keys.some((key, i) => args[i * 2] !== key)
|
|
34
|
-
|| args[1] !== PI_SDK_HOST_VERSION || args[3] !== "print" || args[5] !== "medium"
|
|
35
|
-
|| !absolute(args[7]) || args[9] !== PI_SDK_VERSION) {
|
|
36
|
-
fail("E_PI_HOST_ARGS", "unsupported captured Pi SDK host profile or arguments");
|
|
37
|
-
}
|
|
38
|
-
return { hostVersion: args[1], mode: args[3], thinkingLevel: args[5], sdkRoot: args[7], sdkVersion: args[9] };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function validatePiHostRecipe(recipe) {
|
|
42
|
-
if (!isPiSdkHost(recipe) || recipe.executableResource || (recipe.executableResolvedFrom !== undefined && recipe.executableResolvedFrom !== "explicit-host")) {
|
|
43
|
-
fail("E_PI_HOST_SELECTION", "Pi SDK host requires its exact explicit kernel executable");
|
|
44
|
-
}
|
|
45
|
-
const options = parsePiHostRecipeArgs(recipe.args);
|
|
46
|
-
parsePiHostModel(recipe.model);
|
|
47
|
-
if (!recipe.env || typeof recipe.env !== "object" || Array.isArray(recipe.env) || Object.keys(recipe.env).length
|
|
48
|
-
|| recipe.yolo !== false) fail("E_PI_HOST_SELECTION", "Pi host-v1 has no launch environment contributions or yolo policy");
|
|
49
|
-
if (recipe.hooks && (Object.keys(recipe.hooks.env ?? {}).length || Object.keys(recipe.hooks.launch ?? {}).length || (recipe.hooks.contributions ?? []).length)) {
|
|
50
|
-
fail("E_PI_HOST_SELECTION", "Pi host-v1 does not accept launch-hook contributions");
|
|
51
|
-
}
|
|
52
|
-
return options;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function piHostArgv(recipe, { home, sessionDir, taskFile = join(home, "TASK.md") }) {
|
|
56
|
-
validatePiHostRecipe(recipe);
|
|
57
|
-
if (![home, sessionDir, taskFile].every(absolute) || taskFile !== join(home, "TASK.md")) fail("E_PI_HOST_ARGS", "invalid kernel Pi host paths");
|
|
58
|
-
return ["--home", home, "--session-dir", sessionDir, "--model", recipe.model, "--task-file", taskFile, ...recipe.args];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function parsePiHostArgv(argv) {
|
|
62
|
-
const keys = ["--home", "--session-dir", "--model", "--task-file"];
|
|
63
|
-
if (!Array.isArray(argv) || argv.length !== 18 || keys.some((key, i) => argv[i * 2] !== key)) {
|
|
64
|
-
fail("E_PI_HOST_ARGS", "Pi host accepts only the kernel-rendered argv prefix and closed host profile");
|
|
65
|
-
}
|
|
66
|
-
const [home, sessionDir, model, taskFile] = keys.map((_, i) => argv[i * 2 + 1]);
|
|
67
|
-
if (![home, sessionDir, taskFile].every(absolute) || taskFile !== join(home, "TASK.md")) fail("E_PI_HOST_ARGS", "invalid kernel Pi host paths");
|
|
68
|
-
const modelSelection = parsePiHostModel(model);
|
|
69
|
-
return { home, sessionDir, model, modelSelection, taskFile, ...parsePiHostRecipeArgs(argv.slice(8)) };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Package metadata only; no SDK import, auth inspection, refresh or model call. */
|
|
73
|
-
export function resolvePiSdkEntry({ sdkRoot, sdkVersion }) {
|
|
74
|
-
if (!absolute(sdkRoot) || sdkVersion !== PI_SDK_VERSION || !lstatSync(sdkRoot).isDirectory() || realpathSync(sdkRoot) !== sdkRoot) {
|
|
75
|
-
fail("E_PI_HOST_SDK", "Pi SDK root must be the exact physical selected installation");
|
|
76
|
-
}
|
|
77
|
-
// OATS-managed payloads must use their own retained-resource approval surface.
|
|
78
|
-
if (sdkRoot.split(sep).includes(".agents")) fail("E_PI_HOST_SDK", "managed OATS resources cannot be used as an external SDK installation");
|
|
79
|
-
const pkg = parseStrictJson(readPortableBytes(join(sdkRoot, "package.json")));
|
|
80
|
-
const entry = pkg.exports?.["."]?.import;
|
|
81
|
-
if (pkg.name !== "@earendil-works/pi-coding-agent" || pkg.version !== sdkVersion || typeof entry !== "string" || !entry.startsWith("./")) {
|
|
82
|
-
fail("E_PI_HOST_SDK", "selected SDK package/name/version/public export does not match the host profile");
|
|
83
|
-
}
|
|
84
|
-
const file = resolve(sdkRoot, entry);
|
|
85
|
-
if (!file.startsWith(sdkRoot + sep) || realpathSync(file) !== file || !lstatSync(file).isFile()) fail("E_PI_HOST_SDK", "Pi public SDK export escapes its selected installation");
|
|
86
|
-
return pathToFileURL(file).href;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function readPiHostTask(taskFile) {
|
|
90
|
-
const bytes = readPortableBytes(taskFile);
|
|
91
|
-
if (bytes.length > 512 * 1024) fail("E_PI_HOST_TASK", "captured Pi task exceeds the launch bound");
|
|
92
|
-
let task;
|
|
93
|
-
try { task = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
|
|
94
|
-
catch { fail("E_PI_HOST_TASK", "captured Pi task is not UTF-8"); }
|
|
95
|
-
if (!task.trim() || task.includes("\0")) fail("E_PI_HOST_TASK", "captured Pi print requires a nonempty task");
|
|
96
|
-
return { bytes, task };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Public ResourceLoader interface. verify() reopens retained authority on EVERY
|
|
100
|
-
* reload, before loadSkills reads anything. No SDK discovery/loader is copied. */
|
|
101
|
-
export function createPiResourceLoader(sdk, { cwd, agentDir, verify }) {
|
|
102
|
-
let agents, skills, extensions;
|
|
103
|
-
const ready = () => { if (!agents) fail("E_PI_HOST_CURRICULUM", "Pi curriculum has not been verified"); };
|
|
104
|
-
return {
|
|
105
|
-
getExtensions() { ready(); return extensions; },
|
|
106
|
-
getSkills() { ready(); return skills; },
|
|
107
|
-
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
108
|
-
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
109
|
-
getAgentsFiles() { ready(); return { agentsFiles: [agents] }; },
|
|
110
|
-
getSystemPrompt: () => undefined,
|
|
111
|
-
getSystemPromptSource: () => undefined,
|
|
112
|
-
getAppendSystemPrompt: () => [],
|
|
113
|
-
getAppendSystemPromptSources: () => [],
|
|
114
|
-
extendResources(paths) {
|
|
115
|
-
if (!paths || typeof paths !== "object" || Array.isArray(paths)
|
|
116
|
-
|| Object.keys(paths).some(key => !["skillPaths", "promptPaths", "themePaths"].includes(key))
|
|
117
|
-
|| Object.values(paths).some(value => !Array.isArray(value) || value.length)) {
|
|
118
|
-
fail("E_PI_HOST_CURRICULUM", "unselected Pi resource additions are not supported");
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
async reload() {
|
|
122
|
-
agents = skills = extensions = undefined; // failed reload cannot expose a stale verified view
|
|
123
|
-
const selected = await verify();
|
|
124
|
-
if (selected.cwd !== cwd || typeof selected.text !== "string" || !Array.isArray(selected.skills)) fail("E_PI_HOST_CURRICULUM", "Pi retained curriculum context changed");
|
|
125
|
-
const result = sdk.loadSkills({ cwd, agentDir, skillPaths: selected.skills.map(skill => skill.path), includeDefaults: false });
|
|
126
|
-
if (result.diagnostics.length || result.skills.length !== selected.skills.length) fail("E_PI_HOST_CURRICULUM", "Pi skill loader did not consume the exact retained skill set");
|
|
127
|
-
for (const wanted of selected.skills) {
|
|
128
|
-
const found = result.skills.filter(skill => skill.name === wanted.name);
|
|
129
|
-
if (found.length !== 1 || realpathSync(found[0].filePath) !== realpathSync(wanted.path)) fail("E_PI_HOST_CURRICULUM", "Pi skill identity differs from its retained selection");
|
|
130
|
-
}
|
|
131
|
-
await verify(); // no selected-file drift during SDK parsing
|
|
132
|
-
agents = { path: selected.agentsPath, content: selected.text };
|
|
133
|
-
skills = result;
|
|
134
|
-
extensions = { extensions: [], errors: [], runtime: sdk.createExtensionRuntime() };
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Print exposes no history UI/import/resume/branch operations. The proxy is a
|
|
140
|
-
* before-delegation boundary around the real public SDK runtime, not an engine
|
|
141
|
-
* replacement. It never calls a late factory after opening an unowned target. */
|
|
142
|
-
export function guardPiPrintRuntime(runtime) {
|
|
143
|
-
const unsupported = new Set(["switchSession", "importFromJsonl", "fork", "newSession"]);
|
|
144
|
-
return new Proxy(runtime, {
|
|
145
|
-
get(target, key) {
|
|
146
|
-
if (unsupported.has(key)) return () => fail("E_PI_HOST_HISTORY", "session replacement/import is not supported by this explicit print profile");
|
|
147
|
-
const value = Reflect.get(target, key, target);
|
|
148
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Non-authorizing metadata from actual PUBLIC SDK/session objects. No prompt,
|
|
154
|
-
* content, raw error, credential/profile or request-as-result material is copied. */
|
|
155
|
-
export function observePiSdkSession(session, sdkVersion) {
|
|
156
|
-
const text = value => typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0") ? value : null;
|
|
157
|
-
const manager = session.sessionManager, header = manager.getHeader();
|
|
158
|
-
const messages = session.messages, last = messages.at(-1);
|
|
159
|
-
const entry = manager.getBranch().findLast(value => value.type === "message");
|
|
160
|
-
const native = entry?.message;
|
|
161
|
-
const matchesLast = last?.role === "assistant" && native?.role === "assistant"
|
|
162
|
-
&& native.timestamp === last.timestamp && native.provider === last.provider
|
|
163
|
-
&& native.model === last.model && native.stopReason === last.stopReason;
|
|
164
|
-
const finalAssistant = matchesLast && text(entry.id) && text(native.provider) && text(native.model)
|
|
165
|
-
&& Number.isFinite(native.timestamp) && native.timestamp >= 0
|
|
166
|
-
&& ["stop", "length", "toolUse", "error", "aborted"].includes(native.stopReason)
|
|
167
|
-
? { entryId: entry.id, provider: native.provider, model: native.model,
|
|
168
|
-
responseModel: text(native.responseModel), stopReason: native.stopReason, timestamp: native.timestamp } : null;
|
|
169
|
-
return {
|
|
170
|
-
sdkVersion: text(sdkVersion),
|
|
171
|
-
header: header?.type === "session" && Number.isSafeInteger(header.version) && text(header.id) && text(header.cwd) && text(header.timestamp)
|
|
172
|
-
? { type: "session", version: header.version, id: header.id, cwd: header.cwd, timestamp: header.timestamp } : null,
|
|
173
|
-
sessionId: text(manager.getSessionId()), sessionFile: text(manager.getSessionFile()),
|
|
174
|
-
model: text(session.model?.provider) && text(session.model?.id) ? { provider: session.model.provider, id: session.model.id } : null,
|
|
175
|
-
finalAssistant,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Production uses native services in the user's UNCHANGED profile. No options
|
|
180
|
-
* are passed to ModelRuntime to override stores/auth/config/refresh behavior.
|
|
181
|
-
* Dependencies are injectable for UNIT orchestration checks, not readiness. */
|
|
182
|
-
export async function runPiSdkHost(argv, { verifyContext, onOutcome, importSdk = url => import(url), cwd = process.cwd() } = {}) {
|
|
183
|
-
const options = parsePiHostArgv(argv);
|
|
184
|
-
if (typeof verifyContext !== "function" || realpathSync(cwd) !== options.home) fail("E_PI_HOST_CUSTODY", "Pi host requires its admitted captured home context");
|
|
185
|
-
const verify = () => verifyContext(options);
|
|
186
|
-
await verify();
|
|
187
|
-
const { task } = readPiHostTask(options.taskFile);
|
|
188
|
-
const entry = resolvePiSdkEntry(options);
|
|
189
|
-
const sdk = await importSdk(entry); // the verified PUBLIC root export only
|
|
190
|
-
if (sdk.VERSION !== options.sdkVersion) fail("E_PI_HOST_SDK", "loaded Pi SDK version differs from selection");
|
|
191
|
-
await verify();
|
|
192
|
-
const agentDir = sdk.getAgentDir(); // native profile resolution, never sessionDir
|
|
193
|
-
const modelRuntime = await sdk.ModelRuntime.create();
|
|
194
|
-
const model = modelRuntime.getModel(options.modelSelection.provider, options.modelSelection.id);
|
|
195
|
-
if (!model || model.provider !== options.modelSelection.provider || model.id !== options.modelSelection.id) fail("E_PI_HOST_MODEL", "selected native Pi model is unavailable; no default was selected");
|
|
196
|
-
const factory = async ({ cwd: target, agentDir: profile, sessionManager, sessionStartEvent }) => {
|
|
197
|
-
await verify();
|
|
198
|
-
if (target !== options.home || profile !== agentDir || sessionManager.getCwd() !== target || sessionManager.getSessionDir() !== options.sessionDir) fail("E_PI_HOST_HISTORY", "Pi runtime context differs from its owned history");
|
|
199
|
-
const settingsManager = sdk.SettingsManager.create(target, agentDir);
|
|
200
|
-
const resourceLoader = createPiResourceLoader(sdk, { cwd: target, agentDir, verify });
|
|
201
|
-
await resourceLoader.reload();
|
|
202
|
-
const services = { cwd: target, agentDir, modelRuntime, settingsManager, resourceLoader, diagnostics: [] };
|
|
203
|
-
const built = await sdk.createAgentSessionFromServices({ services, sessionManager, sessionStartEvent, model, thinkingLevel: options.thinkingLevel });
|
|
204
|
-
if (built.session.model?.id !== model.id || built.session.model?.provider !== model.provider || built.modelFallbackMessage) fail("E_PI_HOST_MODEL", "Pi substituted the captured model");
|
|
205
|
-
return { ...built, services, diagnostics: [] };
|
|
206
|
-
};
|
|
207
|
-
await verify();
|
|
208
|
-
const sessionManager = sdk.SessionManager.create(options.home, options.sessionDir);
|
|
209
|
-
const runtime = await sdk.createAgentSessionRuntime(factory, { cwd: options.home, agentDir, sessionManager });
|
|
210
|
-
let handedToPrint = false, observation = null;
|
|
211
|
-
try {
|
|
212
|
-
if (onOutcome) runtime.setBeforeSessionInvalidate(() => {
|
|
213
|
-
// Public synchronous hook: capture real session data BEFORE disposal, never
|
|
214
|
-
// synthesize a header/model/final message from the requested launch values.
|
|
215
|
-
try { observation = observePiSdkSession(runtime.session, sdk.VERSION); }
|
|
216
|
-
catch { observation = null; } // unavailable evidence cannot qualify success
|
|
217
|
-
});
|
|
218
|
-
await verify();
|
|
219
|
-
if (readPiHostTask(options.taskFile).task !== task) fail("E_PI_HOST_TASK", "captured task changed during Pi startup");
|
|
220
|
-
handedToPrint = true; // native runPrintMode owns normal disposal/signals/errors
|
|
221
|
-
const exitCode = await sdk.runPrintMode(guardPiPrintRuntime(runtime), { mode: "text", initialMessage: task });
|
|
222
|
-
const verified = await verify(); // no successful host outcome after observed custody drift
|
|
223
|
-
if (!Number.isInteger(exitCode) || exitCode < 0 || exitCode > 255) fail("E_PI_HOST_OUTCOME", "native print mode returned no valid status");
|
|
224
|
-
if (onOutcome) await onOutcome(options, { exitCode, observation }, verified);
|
|
225
|
-
return exitCode;
|
|
226
|
-
} finally {
|
|
227
|
-
if (!handedToPrint) await runtime.dispose();
|
|
228
|
-
}
|
|
229
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/** New-format immutable tree storage. Selection, source identity observation,
|
|
2
|
-
* provenance/requirements, approval and dispatch remain separate upper layers.
|
|
3
|
-
* No ambient lock/config/network lookup and no migration of legacy evidence. */
|
|
4
|
-
import { lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
5
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
-
import { assertRetainableTree, ensureStoreIgnore } from "./capability-artifacts.mjs";
|
|
7
|
-
import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
|
|
8
|
-
import { publishArtifactTree } from "./artifact-tree.mjs";
|
|
9
|
-
import { canonicalJson } from "./portable-values.mjs";
|
|
10
|
-
import { TREE_FORMAT, jsonIntegrity, treeIntegrity, validateIntegrity } from "./portable-digest.mjs";
|
|
11
|
-
import { oatsError } from "./errors.mjs";
|
|
12
|
-
|
|
13
|
-
const exists = (path) => {
|
|
14
|
-
try { return lstatSync(path); }
|
|
15
|
-
catch (error) { if (error.code === "ENOENT") return null; throw error; }
|
|
16
|
-
};
|
|
17
|
-
const outside = (root, target) => {
|
|
18
|
-
const path = relative(root, target);
|
|
19
|
-
return path === ".." || path.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(path);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function referenceParts(reference) {
|
|
23
|
-
canonicalJson(reference, { maxBytes: 64 * 1024, maxDepth: 16, maxEntries: 1024 });
|
|
24
|
-
if (!reference || typeof reference !== "object" || Array.isArray(reference)) {
|
|
25
|
-
throw oatsError("invalid-artifact-reference", "expected a portable artifact reference");
|
|
26
|
-
}
|
|
27
|
-
validateIntegrity(reference.integrity, [TREE_FORMAT]);
|
|
28
|
-
const keys = Object.keys(reference).sort().join(",");
|
|
29
|
-
let root, extra = [];
|
|
30
|
-
if (reference.kind === "capability" && keys === "capability,integrity,kind" && isMaterializedCapabilityId(reference.capability)) {
|
|
31
|
-
root = [".agents", "capabilities", "artifacts"]; extra = [reference.capability];
|
|
32
|
-
} else if (reference.kind === "soul" && keys === "identity,integrity,kind"
|
|
33
|
-
&& reference.identity && typeof reference.identity === "object" && !Array.isArray(reference.identity)
|
|
34
|
-
&& Object.keys(reference.identity).length > 0) {
|
|
35
|
-
// The source/record codec qualifies the identity and witness. Storage only
|
|
36
|
-
// derives an opaque safe key from that exact identity; aliases are not keys.
|
|
37
|
-
root = [".agents", "soul-artifacts"]; extra = [jsonIntegrity(reference.identity).value];
|
|
38
|
-
} else if (reference.kind === "resource" && keys === "integrity,kind") {
|
|
39
|
-
root = [".agents", "resource-artifacts"];
|
|
40
|
-
} else throw oatsError("invalid-artifact-reference", "invalid portable artifact reference kind/fields");
|
|
41
|
-
return { root, tail: [...extra, "tree-exec-v1", reference.integrity.value] };
|
|
42
|
-
}
|
|
43
|
-
function explicitScope(scope) {
|
|
44
|
-
if (typeof scope !== "string" || !isAbsolute(scope)) {
|
|
45
|
-
throw oatsError("invalid-artifact-store", "artifact deployment scope must be explicit and absolute");
|
|
46
|
-
}
|
|
47
|
-
try {
|
|
48
|
-
const actual = realpathSync(scope);
|
|
49
|
-
if (!lstatSync(actual).isDirectory()) throw oatsError("invalid-artifact-store", "artifact scope is not a directory");
|
|
50
|
-
return actual;
|
|
51
|
-
} catch (error) {
|
|
52
|
-
if (error.code === "ENOENT") throw oatsError("artifact-not-found", "artifact deployment scope is absent");
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Lexical path only. This does not verify the store, bytes, provenance or trust. */
|
|
58
|
-
export function portableArtifactDir(scope, reference) {
|
|
59
|
-
if (typeof scope !== "string" || !isAbsolute(scope)) {
|
|
60
|
-
throw oatsError("invalid-artifact-store", "artifact deployment scope must be explicit and absolute");
|
|
61
|
-
}
|
|
62
|
-
const { root, tail } = referenceParts(reference);
|
|
63
|
-
return join(resolve(scope), ...root, ...tail);
|
|
64
|
-
}
|
|
65
|
-
function storeDirectory(scope, reference, create) {
|
|
66
|
-
const parts = referenceParts(reference), prefix = [...parts.root, ...parts.tail.slice(0, -1)];
|
|
67
|
-
let path = explicitScope(scope);
|
|
68
|
-
for (let index = 0; index < prefix.length; index++) {
|
|
69
|
-
path = join(path, prefix[index]);
|
|
70
|
-
if (create) {
|
|
71
|
-
try { mkdirSync(path, { mode: 0o700 }); }
|
|
72
|
-
catch (error) { if (error.code !== "EEXIST") throw error; }
|
|
73
|
-
}
|
|
74
|
-
const stat = exists(path);
|
|
75
|
-
if (!stat) throw oatsError("artifact-not-found", "portable artifact store is absent");
|
|
76
|
-
if (!stat.isDirectory()) throw oatsError("invalid-artifact-store", "managed artifact store component is not a real directory");
|
|
77
|
-
if (create && index === parts.root.length - 1) ensureStoreIgnore(path);
|
|
78
|
-
}
|
|
79
|
-
return path;
|
|
80
|
-
}
|
|
81
|
-
function verifyTree(path, reference) {
|
|
82
|
-
const stat = exists(path);
|
|
83
|
-
if (!stat) throw oatsError("artifact-not-found", "portable artifact tree is absent");
|
|
84
|
-
if (!stat.isDirectory()) throw oatsError("invalid-artifact", "portable artifact root must be a real directory");
|
|
85
|
-
// Digest traversal is bounded and does not follow links. Then qualify link
|
|
86
|
-
// containment; neither check uses the original source or an ambient package row.
|
|
87
|
-
const actual = treeIntegrity(path);
|
|
88
|
-
if (actual.value !== reference.integrity.value) throw oatsError("integrity-drift", "portable artifact digest does not match its reference");
|
|
89
|
-
assertRetainableTree(path);
|
|
90
|
-
return path;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Byte/containment verification only, not a complete captured-resolution proof. */
|
|
94
|
-
export function verifyPortableArtifact(scope, reference) {
|
|
95
|
-
const root = storeDirectory(scope, reference, false);
|
|
96
|
-
const dir = join(root, reference.integrity.value);
|
|
97
|
-
verifyTree(dir, reference);
|
|
98
|
-
return { reference, dir };
|
|
99
|
-
}
|
|
100
|
-
export function retainPortableArtifact(scope, sourceDir, reference) {
|
|
101
|
-
referenceParts(reference);
|
|
102
|
-
const deployment = explicitScope(scope);
|
|
103
|
-
if (typeof sourceDir !== "string" || !isAbsolute(sourceDir)) {
|
|
104
|
-
throw oatsError("invalid-artifact", "artifact input must be an explicit materialized directory");
|
|
105
|
-
}
|
|
106
|
-
verifyTree(sourceDir, reference);
|
|
107
|
-
const candidate = portableArtifactDir(deployment, reference), source = realpathSync(sourceDir);
|
|
108
|
-
if (!outside(source, candidate) && source !== candidate) {
|
|
109
|
-
throw oatsError("invalid-artifact", "artifact source contains its destination store");
|
|
110
|
-
}
|
|
111
|
-
const parent = storeDirectory(deployment, reference, true), dir = join(parent, reference.integrity.value);
|
|
112
|
-
if (exists(dir)) { verifyTree(dir, reference); return { reference, dir, status: "kept" }; }
|
|
113
|
-
const status = publishArtifactTree(sourceDir, dir, (tree) => verifyTree(tree, reference));
|
|
114
|
-
return { reference, dir, status };
|
|
115
|
-
}
|
package/lib/portable-choices.mjs
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/** The single bounded choice resolver: equality/presence constraints plus
|
|
2
|
-
* fallback values. No expressions, source fetching, provider internals or trust.
|
|
3
|
-
* Field codecs represent a disabled/unbound selection as null; false/[] may be
|
|
4
|
-
* legitimate DATA values. This engine does not guess meaning from strings. */
|
|
5
|
-
import { canonicalJson, compareUtf8 } from "./portable-values.mjs";
|
|
6
|
-
import { invalidShape, objectAt, stringAt } from "./portable-shape.mjs";
|
|
7
|
-
|
|
8
|
-
const PRIORITY = Object.freeze({ "manifest-default": -1, "workspace-default": 0, "soul-default": 1, "import-adoption": 2, operator: 3 });
|
|
9
|
-
export const CHOICE_KINDS = Object.freeze(Object.keys(PRIORITY));
|
|
10
|
-
const equal = (a, b) => canonicalJson(a) === canonicalJson(b);
|
|
11
|
-
const originOrder = (a, b) => compareUtf8(canonicalJson(a.origin), canonicalJson(b.origin))
|
|
12
|
-
|| compareUtf8(canonicalJson(a), canonicalJson(b));
|
|
13
|
-
|
|
14
|
-
export function resolveChoices({ requirements = [], candidates = [] } = {}) {
|
|
15
|
-
canonicalJson({ requirements, candidates });
|
|
16
|
-
if (!Array.isArray(requirements) || !Array.isArray(candidates)) invalidShape("", "choice inputs must be arrays");
|
|
17
|
-
const groups = new Map(), choices = Object.create(null), problems = [];
|
|
18
|
-
const group = (key) => {
|
|
19
|
-
stringAt(key, "/key");
|
|
20
|
-
if (!key.startsWith("/")) invalidShape("/key", "choice keys are absolute JSON pointers");
|
|
21
|
-
if (!groups.has(key)) groups.set(key, { requirements: [], candidates: [] });
|
|
22
|
-
return groups.get(key);
|
|
23
|
-
};
|
|
24
|
-
for (const [index, requirement] of requirements.entries()) {
|
|
25
|
-
const at = `/requirements/${index}`;
|
|
26
|
-
objectAt(requirement, ["key", "kind", "value", "origin"], ["key", "kind", "origin"], at);
|
|
27
|
-
if (!["equals", "required"].includes(requirement.kind)) invalidShape(`${at}/kind`, "unsupported constraint kind");
|
|
28
|
-
if ((requirement.kind === "equals") !== Object.hasOwn(requirement, "value")) invalidShape(at, "constraint value does not match its kind");
|
|
29
|
-
objectAt(requirement.origin, null, [], `${at}/origin`);
|
|
30
|
-
group(requirement.key).requirements.push(requirement);
|
|
31
|
-
}
|
|
32
|
-
for (const [index, candidate] of candidates.entries()) {
|
|
33
|
-
const at = `/candidates/${index}`;
|
|
34
|
-
objectAt(candidate, ["key", "kind", "value", "origin"], ["key", "kind", "value", "origin"], at);
|
|
35
|
-
if (!Object.hasOwn(PRIORITY, candidate.kind)) invalidShape(`${at}/kind`, "unsupported choice origin kind");
|
|
36
|
-
objectAt(candidate.origin, null, [], `${at}/origin`);
|
|
37
|
-
group(candidate.key).candidates.push(candidate);
|
|
38
|
-
}
|
|
39
|
-
const conflict = (key, a, b, message) => {
|
|
40
|
-
problems.push({ code: "requirement-conflict", key, message, origins: [a.origin, b.origin] });
|
|
41
|
-
};
|
|
42
|
-
for (const key of [...groups.keys()].sort(compareUtf8)) {
|
|
43
|
-
const input = groups.get(key);
|
|
44
|
-
input.requirements.sort(originOrder);
|
|
45
|
-
input.candidates.sort((a, b) => PRIORITY[a.kind] - PRIORITY[b.kind] || originOrder(a, b));
|
|
46
|
-
const fixed = input.requirements.filter((item) => item.kind === "equals");
|
|
47
|
-
const presence = input.requirements.find((item) => item.kind === "required");
|
|
48
|
-
const seed = fixed[0];
|
|
49
|
-
let value = seed ? seed.value : null, selectedBy = seed ? seed.origin : null;
|
|
50
|
-
for (const other of fixed.slice(1)) if (!equal(seed.value, other.value)) conflict(key, seed, other, "hard requirements disagree");
|
|
51
|
-
if (seed && presence && seed.value === null) conflict(key, presence, seed, "required value is explicitly absent");
|
|
52
|
-
const considered = [];
|
|
53
|
-
let previous;
|
|
54
|
-
for (const candidate of input.candidates) {
|
|
55
|
-
if (previous && PRIORITY[candidate.kind] === PRIORITY[previous.kind] && !equal(candidate.value, previous.value)) {
|
|
56
|
-
conflict(key, previous, candidate, "equal-authority choices disagree");
|
|
57
|
-
}
|
|
58
|
-
previous = candidate;
|
|
59
|
-
const incompatible = (seed && !equal(candidate.value, seed.value)) || (presence && candidate.value === null);
|
|
60
|
-
if (incompatible) {
|
|
61
|
-
if (PRIORITY[candidate.kind] >= PRIORITY["import-adoption"]) {
|
|
62
|
-
conflict(key, seed ?? presence, candidate, "explicit choice cannot erase a hard requirement");
|
|
63
|
-
}
|
|
64
|
-
considered.push({ kind: candidate.kind, value: candidate.value, origin: candidate.origin, disposition: "overridden" });
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
if (!seed || PRIORITY[candidate.kind] >= PRIORITY["import-adoption"]) {
|
|
68
|
-
value = candidate.value; selectedBy = candidate.origin;
|
|
69
|
-
}
|
|
70
|
-
considered.push({ kind: candidate.kind, value: candidate.value, origin: candidate.origin, disposition: "overridden" });
|
|
71
|
-
}
|
|
72
|
-
for (const entry of considered) {
|
|
73
|
-
if (selectedBy && equal(entry.origin, selectedBy) && equal(entry.value, value)) entry.disposition = "selected";
|
|
74
|
-
}
|
|
75
|
-
if (presence && value === null && !problems.some((item) => item.key === key)) {
|
|
76
|
-
problems.push({ code: "needs-configuration", key, message: "required value has no concrete binding", origins: [presence.origin] });
|
|
77
|
-
}
|
|
78
|
-
choices[key] = { value, selectedBy, constraints: input.requirements.map(({ key: _, ...constraint }) => constraint), considered };
|
|
79
|
-
}
|
|
80
|
-
return { contractVersion: 1, status: problems.some((item) => item.code === "requirement-conflict")
|
|
81
|
-
? "conflict" : problems.length ? "needs-configuration" : "resolved", choices, problems };
|
|
82
|
-
}
|