@awebai/oats 0.25.9 → 0.27.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.
Files changed (183) hide show
  1. package/README.md +8 -6
  2. package/bin/oats.mjs +648 -1755
  3. package/capabilities/oats-authoring/oats-package.json +2 -2
  4. package/capabilities/oats-authoring/oats.json +2 -2
  5. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
  6. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
  7. package/capabilities/oats-aweb/bin/oats-aweb.mjs +279 -93
  8. package/capabilities/oats-aweb/injects/aweb.md +7 -2
  9. package/capabilities/oats-aweb/lib/binding-wire.mjs +89 -13
  10. package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
  11. package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
  12. package/capabilities/oats-aweb/oats.json +8 -4
  13. package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
  14. package/capabilities/oats-jira/oats.json +2 -2
  15. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
  16. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
  17. package/capabilities/oats-linear/oats.json +2 -2
  18. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
  19. package/capabilities/oats-review/oats.json +3 -2
  20. package/docs/capabilities.md +229 -58
  21. package/docs/capability-manifest.schema.json +29 -9
  22. package/docs/configuration.md +17 -5
  23. package/docs/conventions.md +18 -28
  24. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
  25. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
  26. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
  27. package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
  28. package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
  29. package/docs/design/2026-09-20-redesign-program-board.md +2 -2
  30. package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
  31. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
  32. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +34 -1
  33. package/docs/design/2026-09-24-phase-d-plan.md +57 -0
  34. package/docs/design/2026-09-25-teams-contract.md +226 -0
  35. package/docs/design/README.md +3 -3
  36. package/docs/design/launch-configurations.md +20 -16
  37. package/docs/design/operations-contract.md +27 -10
  38. package/docs/desktop-cli-api.md +604 -271
  39. package/docs/desktop-instance-start.md +3 -3
  40. package/docs/desktop.md +7 -13
  41. package/docs/execution-targets.md +16 -18
  42. package/docs/first-team.md +15 -18
  43. package/docs/implementation.md +31 -62
  44. package/docs/integrations.md +64 -33
  45. package/docs/knowledge-capability-authoring.md +1 -1
  46. package/docs/knowledge-reference/package-craft.md +10 -8
  47. package/docs/knowledge-theory.md +1 -1
  48. package/docs/knowledge.md +10 -11
  49. package/docs/layers.md +16 -17
  50. package/docs/oats-local.schema.json +30 -1
  51. package/docs/oats-membership.schema.json +5 -3
  52. package/docs/oats-package.schema.json +2 -2
  53. package/docs/oats-workspace.schema.json +1 -1
  54. package/docs/{official-marketplace.md → official-catalog.md} +15 -16
  55. package/docs/packages.md +76 -53
  56. package/docs/release-notes/v0.22.0.md +1 -1
  57. package/docs/release-notes/v0.23.1.md +1 -1
  58. package/docs/release-notes/v0.26.0.md +670 -0
  59. package/docs/release-notes/v0.27.0.md +100 -0
  60. package/docs/schedules.md +54 -132
  61. package/docs/servers.md +4 -4
  62. package/docs/soul.schema.json +11 -4
  63. package/docs/souls-and-instances.md +60 -47
  64. package/docs/workspaces.md +80 -58
  65. package/injects/instance-boundary.md +2 -2
  66. package/injects/work-attached.md +1 -1
  67. package/injects/work-workspace.md +2 -2
  68. package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
  69. package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
  70. package/lib/capability-contract.mjs +110 -0
  71. package/lib/config-data.mjs +2 -2
  72. package/lib/core.mjs +947 -5023
  73. package/lib/deprecation.mjs +24 -0
  74. package/lib/digest.mjs +12 -0
  75. package/lib/instance-inspect.mjs +397 -0
  76. package/lib/instance-lifecycle.mjs +3 -4
  77. package/lib/instance-resolution.mjs +212 -26
  78. package/lib/instruction-composition.mjs +0 -20
  79. package/lib/materialize.mjs +6 -4
  80. package/lib/operator-dispatch.mjs +33 -13
  81. package/lib/packages.mjs +25 -190
  82. package/lib/process-group.mjs +1 -1
  83. package/lib/provider-binding.mjs +4 -2
  84. package/lib/provider-reasons.mjs +3 -68
  85. package/lib/remote.mjs +1 -1
  86. package/lib/resolve.mjs +204 -68
  87. package/lib/schedule.mjs +136 -292
  88. package/lib/servers.mjs +70 -38
  89. package/lib/{portable-shape.mjs → shape.mjs} +4 -3
  90. package/lib/tree-copy.mjs +44 -0
  91. package/lib/workspace.mjs +132 -20
  92. package/package-catalog.json +6 -6
  93. package/package.json +1 -1
  94. package/packages/record/lib/session-roots.mjs +8 -6
  95. package/skills/integration-authoring/SKILL.md +48 -40
  96. package/skills/oats-getting-started/SKILL.md +105 -110
  97. package/skills/oats-support/SKILL.md +2 -2
  98. package/skills/soul-craft/SKILL.md +13 -6
  99. package/bin/oats-pi-sdk-host.mjs +0 -17
  100. package/docs/2026-09-03-architecture-proposal.md +0 -642
  101. package/docs/artifact-approvals.schema.json +0 -7
  102. package/docs/captured-invocation-context.schema.json +0 -7
  103. package/docs/captured-resolution.schema.json +0 -7
  104. package/docs/design/package-engine-contract.md +0 -813
  105. package/docs/design/package-runtime-api.md +0 -588
  106. package/docs/desktop-succession.md +0 -57
  107. package/docs/execution-capsule.schema.json +0 -108
  108. package/docs/first-team-demo.md +0 -92
  109. package/docs/knowledge-migration.md +0 -147
  110. package/docs/migration-from-oas.md +0 -103
  111. package/docs/oats-config.schema.json +0 -172
  112. package/docs/oats-lock-v3.schema.json +0 -7
  113. package/docs/oats-lock.schema.json +0 -175
  114. package/docs/operating-team-migration.md +0 -470
  115. package/docs/portable.schema.json +0 -2512
  116. package/docs/provider-check-input.schema.json +0 -7
  117. package/docs/rebuild-to-v2.md +0 -511
  118. package/docs/workspace-adoption.md +0 -74
  119. package/injects/framework-workspace.md +0 -7
  120. package/injects/local-soul.md +0 -19
  121. package/injects/oats-portable.md +0 -20
  122. package/injects/oats.md +0 -11
  123. package/injects/portable-instance-boundary.md +0 -39
  124. package/injects/portable-work-directory.md +0 -29
  125. package/lib/artifact-approvals.mjs +0 -120
  126. package/lib/artifact-tree.mjs +0 -141
  127. package/lib/capability-artifacts.mjs +0 -179
  128. package/lib/capability-execution.mjs +0 -15
  129. package/lib/capability-inputs.mjs +0 -39
  130. package/lib/capability-provenance.mjs +0 -231
  131. package/lib/captured-action-shape.mjs +0 -21
  132. package/lib/captured-admission-shape.mjs +0 -20
  133. package/lib/captured-binding-file.mjs +0 -36
  134. package/lib/captured-dispatch.mjs +0 -66
  135. package/lib/captured-instance-index.mjs +0 -277
  136. package/lib/captured-invocation-context.mjs +0 -130
  137. package/lib/captured-launch-request.mjs +0 -66
  138. package/lib/captured-operation-process.mjs +0 -15
  139. package/lib/captured-pi-custody.mjs +0 -29
  140. package/lib/captured-pi-host.mjs +0 -167
  141. package/lib/captured-pi-outcome.mjs +0 -172
  142. package/lib/captured-resolutions.mjs +0 -275
  143. package/lib/captured-scaffold.mjs +0 -87
  144. package/lib/captured-selector.mjs +0 -28
  145. package/lib/captured-session-backend.mjs +0 -52
  146. package/lib/captured-source-receipt-file.mjs +0 -72
  147. package/lib/helper-injection-policy.mjs +0 -104
  148. package/lib/legacy-lock-codec.mjs +0 -106
  149. package/lib/manifest-settings.mjs +0 -84
  150. package/lib/package-closure.mjs +0 -48
  151. package/lib/package-materialization.mjs +0 -83
  152. package/lib/pi-sdk-host.mjs +0 -229
  153. package/lib/portable-artifacts.mjs +0 -115
  154. package/lib/portable-choices.mjs +0 -82
  155. package/lib/portable-composition.mjs +0 -136
  156. package/lib/portable-digest.mjs +0 -105
  157. package/lib/portable-identity.mjs +0 -40
  158. package/lib/portable-lock.mjs +0 -117
  159. package/lib/portable-onboarding-request.mjs +0 -49
  160. package/lib/portable-onboarding.mjs +0 -256
  161. package/lib/portable-package-preparation.mjs +0 -188
  162. package/lib/portable-policy.mjs +0 -44
  163. package/lib/portable-soul.mjs +0 -42
  164. package/lib/portable-state.mjs +0 -80
  165. package/lib/prepare-composition.mjs +0 -170
  166. package/lib/prepared-bindings.mjs +0 -92
  167. package/lib/prepared-resources.mjs +0 -127
  168. package/lib/provider-binding-broker.mjs +0 -65
  169. package/lib/provider-binding-wire.mjs +0 -116
  170. package/lib/readiness.mjs +0 -225
  171. package/lib/repository-observation.mjs +0 -226
  172. package/lib/resolution-shape.mjs +0 -393
  173. package/lib/schedule-capsule.mjs +0 -206
  174. package/lib/soul-constraints.mjs +0 -40
  175. package/lib/source-projection.mjs +0 -84
  176. package/lib/source-spec.mjs +0 -189
  177. package/lib/workspace-definition.mjs +0 -126
  178. package/lib/workspace-discovery.mjs +0 -146
  179. package/skills/oats/SKILL.md +0 -162
  180. package/skills/oats-config/SKILL.md +0 -164
  181. package/skills/oats-packages/SKILL.md +0 -184
  182. package/skills/oats-portable/SKILL.md +0 -115
  183. package/skills/oats-portable-artifacts/SKILL.md +0 -63
@@ -1,40 +0,0 @@
1
- /** Source-derived hard field constraints, shared by preparation and verification.
2
- * No defaults are resolved here and no source/artifact is acquired. */
3
- import { invalidShape, pointerKey } from "./portable-shape.mjs";
4
- import { validateRepoAnchor } from "./source-spec.mjs";
5
- import { FUNDAMENTAL_SLOTS } from "./portable-policy.mjs";
6
-
7
- export const capabilityChoiceKey = (id) => `/capabilities/${pointerKey(id)}`;
8
- export const settingChoiceKey = (id, name) => `/settings/${pointerKey(id)}/${pointerKey(name)}`;
9
- export const layerChoiceKey = (slot) => `/layers/${slot}`;
10
-
11
- /** A repo: relation belongs to the declaring source, not the consumer's cwd or
12
- * another authority's identically spelled relative path. Operator inputs need
13
- * an explicit source context because their origin is not a repository document. */
14
- export function sourceSelectionValue(id, parsed, pointer, sourceContext) {
15
- const source = parsed.sources[`${pointer}/source`];
16
- if (!source) invalidShape(`${pointer}/source`, "missing normalized selection source");
17
- if (source.kind !== "repo") return { capability: id, source };
18
- const anchor = sourceContext ?? parsed.origins[`${pointer}/source`]?.document;
19
- if (!anchor || typeof anchor.source !== "string") invalidShape(`${pointer}/source`, "repo source needs an explicit declaring repository snapshot", "needs-configuration");
20
- const snapshot = validateRepoAnchor({ source: anchor.source, revision: anchor.revision });
21
- return { capability: id, source: { ...source, anchor: snapshot } };
22
- }
23
-
24
- export function soulConstraints(parsed) {
25
- const result = [], source = parsed.declaration.requires ?? {};
26
- const origin = (pointer) => ({ ...parsed.origins[pointer], kind: "soul-requirement" });
27
- const selection = (id, item, pointer, key) => {
28
- result.push({ key, kind: "equals", value: sourceSelectionValue(id, parsed, pointer), origin: origin(pointer) });
29
- for (const [name, value] of Object.entries(item.settings ?? {})) {
30
- result.push({ key: settingChoiceKey(id, name), kind: "equals", value, origin: origin(`${pointer}/settings/${pointerKey(name)}`) });
31
- }
32
- };
33
- for (const [id, item] of Object.entries(source.capabilities ?? {})) selection(id, item, `/requires/capabilities/${pointerKey(id)}`, capabilityChoiceKey(id));
34
- for (const slot of FUNDAMENTAL_SLOTS) {
35
- if (!Object.hasOwn(source, slot)) continue;
36
- if (source[slot] === "any") result.push({ key: layerChoiceKey(slot), kind: "required", origin: origin(`/requires/${slot}`) });
37
- else selection(source[slot].capability, source[slot], `/requires/${slot}`, layerChoiceKey(slot));
38
- }
39
- return result;
40
- }
@@ -1,84 +0,0 @@
1
- /** One guarded filesystem writer for Git and explicit local source projections.
2
- * Source paths cannot introduce write-parent or leaf aliases before rejection. */
3
- import { chmodSync, constants, copyFileSync, lstatSync, mkdirSync, readdirSync, realpathSync, symlinkSync, writeFileSync } from "node:fs";
4
- import { isAbsolute, join } from "node:path";
5
- import { decodeUtf8 } from "./portable-values.mjs";
6
- import { portablePath } from "./source-spec.mjs";
7
- import { assertRetainableTree } from "./capability-artifacts.mjs";
8
- import { oatsError } from "./errors.mjs";
9
-
10
- export function validateProjectionPath(path, platform = process.platform) {
11
- portablePath(path);
12
- if (platform === "win32" && path.split("/").some((part) => /[:<>"|?*\x00-\x1f]/.test(part) || /[ .]$/.test(part)
13
- || /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(part))) throw oatsError("source-incomplete", "source path is not an ordinary filename on this host");
14
- return path;
15
- }
16
-
17
- export function createSourceProjection(destination) {
18
- if (typeof destination !== "string" || !isAbsolute(destination)) throw oatsError("invalid-source", "source projection destination must be explicit and absolute");
19
- mkdirSync(destination, { mode: 0o700 });
20
- const boundary = realpathSync(destination), directories = new Set([""]), expected = new Map(), directoryModes = new Map();
21
- let finished = false;
22
- const active = () => { if (finished) throw oatsError("source-incomplete", "source projection is already finalized"); };
23
- const ensureDirectory = (parts) => {
24
- let current = boundary, relative = "";
25
- for (const part of parts) {
26
- relative = relative ? `${relative}/${part}` : part; current = join(current, part);
27
- let existed = false;
28
- try { mkdirSync(current, { mode: 0o700 }); }
29
- catch (error) { if (error.code !== "EEXIST") throw error; existed = true; }
30
- if (!lstatSync(current).isDirectory() || (existed && !directories.has(relative))) throw oatsError("artifact-not-contained", "projection parent is a link or aliases another source path");
31
- directories.add(relative);
32
- }
33
- return current;
34
- };
35
- const target = (path, kind) => {
36
- active(); validateProjectionPath(path);
37
- if (expected.has(path) || directories.has(path)) throw oatsError("invalid-source", "source projection repeats a path");
38
- const parts = path.split("/"), parent = ensureDirectory(parts.slice(0, -1));
39
- expected.set(path, kind);
40
- return join(parent, parts.at(-1));
41
- };
42
- const modeOf = (path, mode) => {
43
- chmodSync(path, mode);
44
- if ((lstatSync(path).mode & 0o100) !== (mode & 0o100)) throw oatsError("source-incomplete", "host filesystem cannot retain the source owner-execute flag");
45
- };
46
- return {
47
- directory(path, mode) {
48
- active(); validateProjectionPath(path);
49
- if (expected.has(path)) throw oatsError("invalid-source", "source projection path changes kind");
50
- const directory = ensureDirectory(path.split("/"));
51
- if (mode !== undefined) directoryModes.set(directory, mode);
52
- },
53
- file(path, bytes, mode) {
54
- const file = target(path, "file"); writeFileSync(file, bytes, { flag: "wx", mode }); modeOf(file, mode);
55
- },
56
- copyFile(path, source, mode) {
57
- const file = target(path, "file"); copyFileSync(source, file, constants.COPYFILE_EXCL); modeOf(file, mode);
58
- },
59
- link(path, value) { symlinkSync(value, target(path, "link")); },
60
- finish() {
61
- active();
62
- const seenDirectories = new Set([""]);
63
- const inspect = (directory, prefix = "") => {
64
- for (const raw of readdirSync(directory, { encoding: "buffer" })) {
65
- const name = decodeUtf8(raw), path = prefix ? `${prefix}/${name}` : name, target = join(directory, name), stat = lstatSync(target);
66
- if (stat.isDirectory()) {
67
- if (!directories.has(path)) throw oatsError("source-incomplete", "host projection directory spelling differs from source");
68
- seenDirectories.add(path); inspect(target, path);
69
- } else {
70
- const kind = expected.get(path);
71
- if (!kind || (kind === "link" ? !stat.isSymbolicLink() : !stat.isFile())) throw oatsError("source-incomplete", "host projection paths differ from source");
72
- expected.delete(path);
73
- }
74
- }
75
- };
76
- inspect(boundary);
77
- if (expected.size || seenDirectories.size !== directories.size) throw oatsError("source-incomplete", "host filesystem did not retain every source path");
78
- assertRetainableTree(boundary);
79
- for (const [path, mode] of [...directoryModes].sort((a, b) => b[0].length - a[0].length)) chmodSync(path, mode);
80
- finished = true;
81
- return boundary;
82
- },
83
- };
84
- }
@@ -1,189 +0,0 @@
1
- /** Portable source grammar. Pure normalization only: no cwd/HOME inference,
2
- * filesystem access, repository identity claims or executable approval.
3
- * Legacy lock readers retain their original interpretation in capability-provenance.mjs. */
4
- import { isAbsolute, resolve } from "node:path";
5
- import { oatsError } from "./errors.mjs";
6
- import { canonicalJson, scalarString } from "./portable-values.mjs";
7
-
8
- const refuse = (message) => { throw oatsError("invalid-source", message); };
9
- const text = (value) => {
10
- scalarString(value, "source");
11
- if (!value || value.length > 8192 || value.trim() !== value || /[\x00-\x1f\x7f]/.test(value)) refuse("invalid source text");
12
- return value;
13
- };
14
- export function portablePath(value, { allowRoot = false } = {}) {
15
- scalarString(value, "source path");
16
- if (allowRoot && value === ".") return value;
17
- if (!value || value.includes("\\") || value.includes("\0") || /^[A-Za-z]:/.test(value)
18
- || value.split("/").some((part) => !part || part === "." || part === "..")) {
19
- throw oatsError("path-escape", "expected a contained canonical repository-relative path");
20
- }
21
- return value;
22
- }
23
- export function revisionSelector(value) {
24
- text(value);
25
- if (value === "@" || value.startsWith("-") || value.endsWith(".") || value.includes("..")
26
- || value.includes("@{") || /[\s~^:?*\[\\]/.test(value)
27
- || value.split("/").some((part) => !part || part.startsWith(".") || part.endsWith(".lock"))) {
28
- refuse("invalid Git revision selector");
29
- }
30
- return value;
31
- }
32
-
33
- function splitGit(body) {
34
- // Locate the repository-path start before looking for the selector delimiter.
35
- // SSH's user@host is authority; refs/heads/topic is one selector, not a URL.
36
- let start;
37
- const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(body);
38
- if (scheme) {
39
- start = body.indexOf("/", scheme[0].length);
40
- if (start < 0) refuse("Git repository source needs a path");
41
- } else if (/^[^/@:]+@[^/:]+:/.test(body)) start = body.indexOf(":") + 1;
42
- else {
43
- start = body.indexOf("/");
44
- if (start < 0) refuse("Git source needs host and repository path");
45
- }
46
- const positions = [];
47
- for (let i = start; i < body.length; i++) if (body[i] === "@") positions.push(i);
48
- if (positions.length > 1) refuse("ambiguous Git selector delimiter; use an unambiguous repository locator");
49
- if (!positions.length) return { repository: body, selector: undefined };
50
- const at = positions[0];
51
- return { repository: body.slice(0, at), selector: revisionSelector(body.slice(at + 1)) };
52
- }
53
- function remoteUrl(repository, { allowLocalGit = false } = {}) {
54
- let spelling = repository, shorthand = false, scp = null;
55
- if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(spelling)) {
56
- scp = /^([^/@:]+)@([^/:]+):(.+)$/.exec(spelling);
57
- shorthand = !scp;
58
- spelling = scp ? `ssh://${scp[1]}@${scp[2]}${scp[3].startsWith("/") ? "" : "/"}${scp[3]}` : `https://${spelling}`;
59
- }
60
- if (/\s|\\/.test(spelling)) refuse("invalid Git repository URL");
61
- // Reject hidden path rewriting rather than letting URL dot normalization pick
62
- // another repository. Percent-encoded separators are equally ambiguous.
63
- const pathStart = spelling.indexOf("/", spelling.indexOf("://") + 3);
64
- const rawPath = spelling.slice(pathStart);
65
- if (pathStart < 0 || rawPath.split("/").slice(1).some((part) => {
66
- let decoded;
67
- try { decoded = decodeURIComponent(part); } catch { return true; }
68
- return !part || decoded === "." || decoded === ".." || /[/\\\x00-\x1f\x7f]/.test(decoded);
69
- })) refuse("Git repository path is not canonical");
70
- let url;
71
- try { url = new URL(spelling); } catch { refuse("invalid Git repository URL"); }
72
- if (!["https:", "http:", "ssh:", "git:", ...(allowLocalGit ? ["file:"] : [])].includes(url.protocol)) {
73
- refuse("unsupported Git transport");
74
- }
75
- if (url.search || url.hash || url.password || (url.username && url.protocol !== "ssh:")) {
76
- refuse("Git repository locator must not contain credentials, query or fragment");
77
- }
78
- if (url.protocol !== "file:" && !url.hostname) refuse("Git repository needs a host");
79
- if (url.hostname === "github.com" && url.pathname.split("/").filter(Boolean).length !== 2) {
80
- refuse("GitHub repository locator needs owner and repository");
81
- }
82
- // SCP's host:path is home-relative; ssh://host/path is absolute. Keep the SCP
83
- // spelling rather than changing the path that Git sends to git-upload-pack.
84
- if (scp) return `${scp[1]}@${url.hostname}:${scp[3]}`;
85
- if (shorthand && !url.pathname.endsWith(".git")) url.pathname += ".git";
86
- return url.href;
87
- }
88
- function gitParts(spec, options) {
89
- text(spec);
90
- if (!spec.startsWith("git:")) refuse("expected an explicit git: source");
91
- const body = spec.slice(4), hash = body.indexOf("#");
92
- if (hash >= 0 && body.indexOf("#", hash + 1) >= 0) refuse("source has multiple package fragments");
93
- const fragment = hash < 0 ? undefined : body.slice(hash + 1);
94
- const split = splitGit(hash < 0 ? body : body.slice(0, hash));
95
- return { ...split, url: remoteUrl(split.repository, options), fragment };
96
- }
97
-
98
- /** Repository/import/knowledge locators never acquire a package-root default. */
99
- export function parseRepositorySource(spec, options) {
100
- const parsed = gitParts(spec, options);
101
- if (parsed.selector !== undefined || parsed.fragment !== undefined) {
102
- refuse("repository locator keeps revision and exported path in separate fields");
103
- }
104
- return { kind: "git", url: parsed.url, normalized: `git:${parsed.url}` };
105
- }
106
-
107
- export function parsePortableSource(spec, { localBase, allowLocalPaths = false } = {}) {
108
- text(spec);
109
- if (spec.startsWith("repo:")) return { kind: "repo", path: portablePath(spec.slice(5), { allowRoot: true }) };
110
- if (spec.startsWith("path:")) {
111
- const source = spec.slice(5);
112
- if (!source || source.startsWith("~") || source.includes("#")) refuse("local source needs an explicit path, not HOME or a fragment");
113
- if (!allowLocalPaths) refuse("local inputs require explicit local adoption authorization");
114
- if (localBase !== undefined && !isAbsolute(localBase)) refuse("local source base must be absolute");
115
- if (!isAbsolute(source) && localBase === undefined) refuse("relative path: source needs an explicit local base");
116
- const path = isAbsolute(source) ? resolve(source) : resolve(localBase, source);
117
- return { kind: "path", source: `path:${path}`, path: ".", localPath: path, portable: false };
118
- }
119
- const parsed = gitParts(spec);
120
- if (parsed.selector === undefined) refuse("portable Git capability source requires a revision selector");
121
- const path = parsed.fragment === undefined ? "oats-package"
122
- : parsed.fragment === "" ? "." : portablePath(parsed.fragment, { allowRoot: true });
123
- return { kind: "git", source: `git:${parsed.url}@${parsed.selector}`, url: parsed.url,
124
- selector: parsed.selector, path, portable: true };
125
- }
126
-
127
- /** Package dependency inputs retain existing CLI conveniences, but never infer
128
- * an ambient local base or allow an unpinned Git dependency. Soul requirements
129
- * continue to use parsePortableSource and cannot borrow catalog nicknames. */
130
- export function parsePackageDependency3(spec, { localBase, allowLocalPaths = false } = {}) {
131
- text(spec);
132
- if (spec.startsWith("repo:") || spec.startsWith("path:")) return parsePortableSource(spec, { localBase, allowLocalPaths });
133
- if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~")) return parsePortableSource(`path:${spec}`, { localBase, allowLocalPaths });
134
- if (spec.startsWith("git:") && !spec.startsWith("git://")) return parsePortableSource(spec);
135
- if (/^(https?:\/\/|ssh:\/\/|git:\/\/|git@)/.test(spec)) return parsePortableSource(`git:${spec}`);
136
- const catalog = /^(?:catalog:)?([a-z0-9][a-z0-9._-]*)(?:@([^\s#]+))?$/.exec(spec);
137
- if (!catalog) refuse("invalid package dependency source");
138
- const source = `catalog:${catalog[1]}${catalog[2] ? `@${catalog[2]}` : ""}`;
139
- parseLockedSource3(source, "oats-package");
140
- return { kind: "catalog", source, id: catalog[1], ...(catalog[2] ? { selector: catalog[2] } : {}) };
141
- }
142
-
143
- /** Repository-relative selections carry the exact declaring observation. */
144
- export function validateRepoAnchor(anchor) {
145
- canonicalJson(anchor);
146
- if (!anchor || Object.keys(anchor).sort().join(",") !== "revision,source") refuse("invalid repository snapshot anchor");
147
- if (typeof anchor.source === "string" && anchor.source.startsWith("path:")) {
148
- parseLockedSource3(anchor.source, ".");
149
- if (anchor.revision !== "local") refuse("local source anchor requires a local observation");
150
- } else if (parseRepositorySource(anchor.source).normalized !== anchor.source || typeof anchor.revision !== "string" || !/^[a-f0-9]{40}$/.test(anchor.revision)) {
151
- refuse("repository source anchor requires its exact observation");
152
- }
153
- return anchor;
154
- }
155
-
156
- /** Normalized choice values cannot disagree with their own source/path/url fields.
157
- * This is still data validation, not acquisition or local-path authorization. */
158
- export function validateSelectionSource(value) {
159
- const encoded = canonicalJson(value);
160
- let expected;
161
- if (value?.kind === "git") {
162
- portablePath(value.path, { allowRoot: true });
163
- expected = parsePortableSource(`${value.source}#${value.path}`);
164
- } else if (value?.kind === "path") expected = parsePortableSource(value.source, { allowLocalPaths: true });
165
- else if (value?.kind === "repo") expected = { ...parsePortableSource(`repo:${value.path}`), anchor: validateRepoAnchor(value.anchor) };
166
- else refuse("invalid normalized selection source");
167
- if (canonicalJson(expected) !== encoded) refuse("normalized selection source fields disagree");
168
- return value;
169
- }
170
-
171
- /** New-format locked sources already carry a canonical URL and a separate path.
172
- * Reading must not repair noncanonical persisted values or inherit cwd defaults. */
173
- export function parseLockedSource3(source, path) {
174
- text(source); portablePath(path, { allowRoot: true });
175
- if (source.startsWith("path:")) {
176
- const local = source.slice(5);
177
- if (!isAbsolute(local) || local.includes("#") || resolve(local) !== local || path !== ".") refuse("invalid canonical local lock source");
178
- return { kind: "path", source, path, localPath: local, portable: false };
179
- }
180
- if (source.startsWith("catalog:")) {
181
- const entry = /^catalog:([a-z0-9][a-z0-9._-]*)(?:@([^\s#]+))?$/.exec(source);
182
- if (!entry) refuse("invalid canonical catalog lock source");
183
- return { kind: "catalog", source, path, id: entry[1], selector: entry[2] };
184
- }
185
- const parsed = gitParts(source, { allowLocalGit: true });
186
- if (parsed.fragment !== undefined || parsed.selector === undefined
187
- || source !== `git:${parsed.url}@${parsed.selector}`) refuse("invalid canonical Git lock source");
188
- return { kind: "git", source, path, url: parsed.url, selector: parsed.selector, portable: true };
189
- }
@@ -1,126 +0,0 @@
1
- /** Portable workspace/member/import declarations. Data validation only;
2
- * repository identity, reciprocal admission and fetching belong to discovery. */
3
- import { parseConfigData } from "./config-data.mjs";
4
- import { canonicalJson } from "./portable-values.mjs";
5
- import { parseRepositorySource, portablePath, revisionSelector } from "./source-spec.mjs";
6
- import { invalidShape, objectAt, pointerKey, stringAt, versionAt } from "./portable-shape.mjs";
7
- import { FUNDAMENTAL_SLOTS, validatePolicyChoices, validateProviderDeclaration } from "./portable-policy.mjs";
8
- import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
9
- import { oatsError } from "./errors.mjs";
10
-
11
- const ALIAS = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
12
- const SOUL_ALIAS = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
13
- function arrayAt(value, pointer) { if (!Array.isArray(value)) invalidShape(pointer, "expected array"); return value; }
14
- function repositoryAt(value, pointer, { revisionRequired = false, path = false } = {}) {
15
- objectAt(value, ["source", "revision", ...(path ? ["path"] : [])], ["source", ...(revisionRequired ? ["revision"] : [])], pointer);
16
- const source = parseRepositorySource(value.source);
17
- if (Object.hasOwn(value, "revision")) revisionSelector(value.revision);
18
- if (path && Object.hasOwn(value, "path")) portablePath(value.path);
19
- return { ...value, source: source.normalized };
20
- }
21
- function duplicate(label, first, current, origins) {
22
- throw oatsError("requirement-conflict", `duplicate ${label}`, [origins[first] ?? { pointer: first }, origins[current] ?? { pointer: current }]);
23
- }
24
-
25
- /** Identical four-field source reference for standalone and workspace imports.
26
- * Normalization is not verified upstream identity or an enrollment operation. */
27
- export function parseSoulImport(input, { pointer = "", origins = {}, origin = null, localBase, allowLocalPaths = false } = {}) {
28
- canonicalJson(input, { maxBytes: 256 * 1024, maxDepth: 32, maxEntries: 10_000 });
29
- const value = objectAt(input, ["source", "soul", "revision", "alias", "adoption"], ["source", "soul", "revision", "alias"], pointer);
30
- const source = parseRepositorySource(value.source);
31
- portablePath(value.soul); revisionSelector(value.revision);
32
- stringAt(value.alias, `${pointer}/alias`, { pattern: SOUL_ALIAS });
33
- const sources = Object.create(null);
34
- if (Object.hasOwn(value, "adoption")) {
35
- const adoption = objectAt(value.adoption, ["teamAliases", "providers", "bindings"], [], `${pointer}/adoption`);
36
- if (Object.hasOwn(adoption, "teamAliases")) {
37
- objectAt(adoption.teamAliases, null, [], `${pointer}/adoption/teamAliases`);
38
- for (const [name, target] of Object.entries(adoption.teamAliases)) {
39
- stringAt(name, `${pointer}/adoption/teamAliases`, { pattern: ALIAS });
40
- stringAt(target, `${pointer}/adoption/teamAliases/${pointerKey(name)}`, { pattern: ALIAS });
41
- }
42
- }
43
- if (Object.hasOwn(adoption, "providers")) {
44
- objectAt(adoption.providers, FUNDAMENTAL_SLOTS, [], `${pointer}/adoption/providers`);
45
- Object.assign(sources, validatePolicyChoices(adoption.providers, {
46
- pointer: `${pointer}/adoption/providers`, origins, origin, localBase, allowLocalPaths,
47
- }));
48
- }
49
- if (Object.hasOwn(adoption, "bindings")) objectAt(adoption.bindings, null, [], `${pointer}/adoption/bindings`);
50
- }
51
- return { reference: { ...value, source: source.normalized }, sources };
52
- }
53
-
54
- export function parseWorkspaceDefinition(input, { origin = null, localBase, allowLocalPaths = false, limits } = {}) {
55
- const parsed = parseConfigData(input, { origin, limits });
56
- const value = objectAt(parsed.value, ["schemaVersion", "name", "members", "defaults", "knowledge", "teams", "catalogs", "imports"], ["schemaVersion", "name"]);
57
- versionAt(value.schemaVersion); stringAt(value.name, "/name");
58
- const sources = Object.create(null), members = [], imports = [], catalogs = [];
59
- if (Object.hasOwn(value, "defaults")) Object.assign(sources, validatePolicyChoices(value.defaults, {
60
- pointer: "/defaults", origins: parsed.origins, origin, localBase, allowLocalPaths,
61
- }));
62
- if (Object.hasOwn(value, "members")) {
63
- const seen = new Map();
64
- arrayAt(value.members, "/members").forEach((entry, index) => {
65
- const at = `/members/${index}`, member = repositoryAt(entry, at);
66
- if (seen.has(member.source)) duplicate("member repository", seen.get(member.source), at, parsed.origins);
67
- seen.set(member.source, at); members.push(member);
68
- });
69
- }
70
- if (Object.hasOwn(value, "knowledge")) {
71
- objectAt(value.knowledge, ["stores"], ["stores"], "/knowledge");
72
- arrayAt(value.knowledge.stores, "/knowledge/stores").forEach((store, index) => validateProviderDeclaration(store, `/knowledge/stores/${index}`));
73
- }
74
- if (Object.hasOwn(value, "teams")) {
75
- objectAt(value.teams, null, [], "/teams");
76
- for (const [alias, team] of Object.entries(value.teams)) {
77
- const at = `/teams/${pointerKey(alias)}`;
78
- stringAt(alias, at, { pattern: ALIAS });
79
- if (alias === "private") { if (team !== "per-human") invalidShape(at, "private team floor must be per-human"); continue; }
80
- objectAt(team, ["provider", "id"], ["provider", "id"], at);
81
- if (!isMaterializedCapabilityId(team.provider)) invalidShape(`${at}/provider`, "invalid messaging provider identity");
82
- stringAt(team.id, `${at}/id`);
83
- }
84
- }
85
- if (Object.hasOwn(value, "catalogs")) {
86
- arrayAt(value.catalogs, "/catalogs").forEach((entry, index) => catalogs.push(repositoryAt(entry, `/catalogs/${index}`, { path: true })));
87
- }
88
- if (Object.hasOwn(value, "imports")) {
89
- const seen = new Map();
90
- arrayAt(value.imports, "/imports").forEach((entry, index) => {
91
- const at = `/imports/${index}`;
92
- const imported = parseSoulImport(entry, { pointer: at, origins: parsed.origins, origin, localBase, allowLocalPaths });
93
- if (seen.has(imported.reference.alias)) duplicate("import alias", seen.get(imported.reference.alias), at, parsed.origins);
94
- seen.set(imported.reference.alias, at);
95
- Object.assign(sources, imported.sources); imports.push(imported.reference);
96
- });
97
- }
98
- return { declaration: value, sources, members, imports, catalogs, origins: parsed.origins, integrity: parsed.integrity };
99
- }
100
-
101
- export function parseMemberExports(input, { origin = null, limits } = {}) {
102
- const parsed = parseConfigData(input, { origin, limits });
103
- const value = objectAt(parsed.value, ["schemaVersion", "workspace", "exports"], ["schemaVersion", "exports"]);
104
- versionAt(value.schemaVersion);
105
- const workspace = Object.hasOwn(value, "workspace") ? repositoryAt(value.workspace, "/workspace") : null;
106
- const exports = objectAt(value.exports, ["souls", "packages", "knowledge"], [], "/exports");
107
- for (const kind of ["souls", "packages"]) {
108
- if (!Object.hasOwn(exports, kind)) continue;
109
- const seen = new Map();
110
- arrayAt(exports[kind], `/exports/${kind}`).forEach((entry, index) => {
111
- const at = `/exports/${kind}/${index}`;
112
- objectAt(entry, kind === "souls" ? ["path", "definition", "description"] : ["path", "description"],
113
- kind === "souls" ? ["path", "definition"] : ["path"], at);
114
- portablePath(entry.path, { allowRoot: kind === "packages" });
115
- if (seen.has(entry.path)) duplicate(`${kind} export path`, seen.get(entry.path), at, parsed.origins);
116
- seen.set(entry.path, at);
117
- if (kind === "souls") {
118
- portablePath(entry.definition);
119
- if (!entry.definition.startsWith(`${entry.path}/`)) invalidShape(`${at}/definition`, "definition must be inside its exported soul path");
120
- }
121
- if (Object.hasOwn(entry, "description")) stringAt(entry.description, `${at}/description`, { empty: true });
122
- });
123
- }
124
- if (Object.hasOwn(exports, "knowledge")) arrayAt(exports.knowledge, "/exports/knowledge").forEach((store, index) => validateProviderDeclaration(store, `/exports/knowledge/${index}`));
125
- return { declaration: value, workspace, exports, origins: parsed.origins, integrity: parsed.integrity };
126
- }
@@ -1,146 +0,0 @@
1
- /** Reciprocal workspace discovery and by-reference soul imports through one
2
- * repository transaction. Membership is observed eligibility, not enrollment. */
3
- import { canonicalJson, compareUtf8, freezeJson } from "./portable-values.mjs";
4
- import { objectAt, pointerKey } from "./portable-shape.mjs";
5
- import { sameIdentity } from "./portable-identity.mjs";
6
- import { validateOrigin } from "./resolution-shape.mjs";
7
- import { parseMemberExports, parseSoulImport, parseWorkspaceDefinition } from "./workspace-definition.mjs";
8
- import { parsePortableSoul } from "./portable-soul.mjs";
9
- import { oatsError } from "./errors.mjs";
10
-
11
- function declaredOrigin(parsed, pointer, kind) {
12
- const result = { ...parsed.origins[pointer], kind };
13
- validateOrigin(result); return result;
14
- }
15
- function minimumRoots(paths) {
16
- const roots = new Set(paths);
17
- if (roots.has(".")) return ["."];
18
- return [...roots].filter((path) => {
19
- for (let at = path.indexOf("/"); at >= 0; at = path.indexOf("/", at + 1)) if (roots.has(path.slice(0, at))) return false;
20
- return true;
21
- }).sort(compareUtf8);
22
- }
23
-
24
- export function createWorkspaceDiscovery(transaction) {
25
- const workspaces = new WeakMap();
26
- return {
27
- readWorkspace(request) {
28
- canonicalJson(request);
29
- objectAt(request, ["source", "revision", "origin", "expectedIdentity"], ["source", "origin"]);
30
- const observation = transaction.observe(request.source, request);
31
- const document = transaction.readFile(observation, "oats-workspace.yaml");
32
- const parsed = parseWorkspaceDefinition(document.bytes, { origin: document.origin });
33
- const identity = { repository: observation.source.identity, path: "oats-workspace.yaml" };
34
- const view = freezeJson({ schemaVersion: 1, identity, source: observation.source, parsed, accessContextKey: observation.accessContextKey });
35
- workspaces.set(view, { observation, parsed, identity, document: document.origin });
36
- return view;
37
- },
38
-
39
- checkMember(view, request) {
40
- const workspace = workspaces.get(view);
41
- if (!workspace) throw oatsError("invalid-source", "workspace view was not issued by this discovery transaction");
42
- canonicalJson(request);
43
- objectAt(request, ["source", "origin", "expectedIdentity"], ["source", "origin"]);
44
- validateOrigin(request.origin);
45
- const evidence = [], problems = [];
46
- let member = null;
47
- const result = (status) => freezeJson({ schemaVersion: 1, status, workspace: { identity: workspace.identity,
48
- revision: workspace.observation.source.commit, document: workspace.document }, member, evidence, problems });
49
- const problem = (code, message, origins) => problems.push({ code, message, origins });
50
- const unavailable = (error, origins) => {
51
- problem(error.code ?? "source-unavailable", error.message, origins);
52
- return result(["source-identity-unresolved", "source-identity-change"].includes(error.code) ? "identity-unresolved" : "unavailable");
53
- };
54
- let candidate;
55
- try { candidate = transaction.identify(request.source, request); }
56
- catch (error) { return unavailable(error, [request.origin]); }
57
- if (candidate.accessContextKey !== view.accessContextKey) {
58
- problem("source-access-context-change", "membership observations use different access contexts", [request.origin]);
59
- return result("unavailable");
60
- }
61
- const entries = workspace.parsed.members;
62
- let index = entries.findIndex((entry) => entry.source === candidate.source);
63
- if (index < 0) {
64
- const unresolved = [];
65
- for (const [at, entry] of entries.entries()) {
66
- try {
67
- const listed = transaction.identify(entry.source);
68
- if (listed.accessContextKey !== view.accessContextKey) throw oatsError("source-access-context-change", "allowlist identity has a different access context");
69
- if (sameIdentity(listed.identity, candidate.identity)) { index = at; break; }
70
- } catch (error) { unresolved.push({ error, origin: declaredOrigin(workspace.parsed, `/members/${at}`, "workspace-admission") }); }
71
- }
72
- if (index < 0 && unresolved.length) return unavailable(unresolved[0].error, [unresolved[0].origin]);
73
- }
74
- if (index < 0) {
75
- problem("workspace-disallows-member", "repository is not admitted by this workspace", [request.origin]);
76
- return result("not-member");
77
- }
78
- const entry = entries[index], admission = declaredOrigin(workspace.parsed, `/members/${index}`, "workspace-admission");
79
- evidence.push(admission);
80
- try {
81
- // The workspace's declared selector governs admission evidence, not the
82
- // caller's work-tree branch or an arbitrary fork's copied descriptor.
83
- const observation = transaction.observe(entry.source, { ...(entry.revision === undefined ? {} : { revision: entry.revision }),
84
- origin: admission, expectedIdentity: candidate.identity });
85
- if (observation.accessContextKey !== view.accessContextKey) throw oatsError("source-access-context-change", "member observation has a different access context");
86
- if (!sameIdentity(observation.source.identity, candidate.identity)) throw oatsError("source-identity-change", "admission observed a different member identity");
87
- const document = transaction.readFile(observation, "oats.yaml", { optional: true });
88
- member = { identity: observation.source.identity, revision: observation.source.commit, document: document?.origin ?? null };
89
- if (!document) {
90
- problem("member-backlink-missing", "admitted repository has no member descriptor", [admission]);
91
- return result("not-member");
92
- }
93
- const parsed = parseMemberExports(document.bytes, { origin: document.origin });
94
- if (!parsed.workspace) {
95
- problem("member-backlink-missing", "repository exports do not declare workspace membership", [admission]);
96
- return result("not-member");
97
- }
98
- const backlink = declaredOrigin(parsed, "/workspace", "member-backlink"); evidence.push(backlink);
99
- const target = transaction.identify(parsed.workspace.source);
100
- if (target.accessContextKey !== view.accessContextKey) throw oatsError("source-access-context-change", "backlink identity has a different access context");
101
- if (!sameIdentity(target.identity, workspace.identity.repository)) {
102
- problem("member-backlink-mismatch", "member points to a different workspace identity", [admission, backlink]);
103
- return result("not-member");
104
- }
105
- const linkedWorkspace = transaction.observe(parsed.workspace.source, { ...(parsed.workspace.revision === undefined ? {} : { revision: parsed.workspace.revision }),
106
- origin: backlink, expectedIdentity: workspace.identity.repository });
107
- if (linkedWorkspace.accessContextKey !== view.accessContextKey) throw oatsError("source-access-context-change", "backlink observation has a different access context");
108
- if (!sameIdentity(linkedWorkspace.source.identity, workspace.identity.repository)) throw oatsError("source-identity-change", "backlink observed a different workspace identity");
109
- if (linkedWorkspace.source.commit !== workspace.observation.source.commit) {
110
- problem("workspace-observation-mismatch", "backlink resolves a different workspace revision; choose matching observations", [admission, backlink]);
111
- return result("stale");
112
- }
113
- return result("eligible");
114
- } catch (error) { return unavailable(error, evidence); }
115
- },
116
-
117
- importSoul(reference, { origin, expectedIdentity } = {}) {
118
- validateOrigin(origin);
119
- const parsedImport = parseSoulImport(reference, { pointer: origin.pointer, origin: origin.document });
120
- const imported = parsedImport.reference;
121
- const importOrigins = Object.create(null);
122
- const mark = (value, pointer) => {
123
- importOrigins[pointer] = { document: origin.document, pointer };
124
- if (value && typeof value === "object") for (const [key, child] of Object.entries(value)) mark(child, `${pointer}/${pointerKey(key)}`);
125
- };
126
- mark(reference, origin.pointer);
127
- const observation = transaction.observe(imported.source, { revision: imported.revision, origin, ...(expectedIdentity === undefined ? {} : { expectedIdentity }) });
128
- const index = transaction.readFile(observation, "oats.yaml", { optional: true });
129
- if (!index) throw oatsError("export-not-found", "source repository has no advertised soul export index");
130
- const exports = parseMemberExports(index.bytes, { origin: index.origin });
131
- const at = (exports.exports.souls ?? []).findIndex((entry) => entry.path === imported.soul);
132
- if (at < 0) throw oatsError("export-not-found", "requested soul path is not advertised by this source revision");
133
- const exported = exports.exports.souls[at], definition = transaction.readFile(observation, exported.definition);
134
- const soul = parsePortableSoul(definition.bytes, { origin: definition.origin });
135
- const roots = [imported.soul, ...(soul.declaration.resources ?? [])];
136
- for (const [pointer, source] of Object.entries(soul.sources)) if (pointer.startsWith("/requires/") && source.kind === "repo") roots.push(source.path);
137
- // The publisher's backlink is parsed as data, never followed as an adopter
138
- // workspace or membership requirement. No adopter-owned soul is written.
139
- const identity = { kind: "git-soul", repository: observation.source.identity, exportPath: imported.soul };
140
- return freezeJson({ schemaVersion: 1, identity,
141
- reference: imported, observation, definition: exported.definition, soul, roots: minimumRoots(roots),
142
- adoption: imported.adoption ? { identity, parsed: parsedImport, origins: importOrigins, pointer: origin.pointer } : null,
143
- provenance: [declaredOrigin(exports, `/exports/souls/${at}`, "source-export")] });
144
- },
145
- };
146
- }