@agentxm/workspace-state 0.28.4-bootstrap.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 (215) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +11 -0
  3. package/dist/src/index.d.ts +72 -0
  4. package/dist/src/index.js +88 -0
  5. package/dist/src/knowledge/discovery-config.d.ts +12 -0
  6. package/dist/src/knowledge/discovery-config.js +9 -0
  7. package/dist/src/lockfile/accepted-registry-version.d.ts +12 -0
  8. package/dist/src/lockfile/accepted-registry-version.js +13 -0
  9. package/dist/src/lockfile/entry-fields.d.ts +189 -0
  10. package/dist/src/lockfile/entry-fields.js +89 -0
  11. package/dist/src/lockfile/errors.d.ts +48 -0
  12. package/dist/src/lockfile/errors.js +27 -0
  13. package/dist/src/lockfile/index.d.ts +18 -0
  14. package/dist/src/lockfile/index.js +16 -0
  15. package/dist/src/lockfile/lockfile.d.ts +2255 -0
  16. package/dist/src/lockfile/lockfile.js +235 -0
  17. package/dist/src/lockfile/resolved-version.d.ts +4 -0
  18. package/dist/src/lockfile/resolved-version.js +7 -0
  19. package/dist/src/lockfile/schema.d.ts +3062 -0
  20. package/dist/src/lockfile/schema.js +406 -0
  21. package/dist/src/schema/index.d.ts +3 -0
  22. package/dist/src/schema/index.js +2 -0
  23. package/dist/src/schema/read-and-validate-json-file.d.ts +20 -0
  24. package/dist/src/schema/read-and-validate-json-file.js +85 -0
  25. package/dist/src/schema/types.d.ts +26 -0
  26. package/dist/src/schema/types.js +2 -0
  27. package/dist/src/settings/errors.d.ts +22 -0
  28. package/dist/src/settings/errors.js +15 -0
  29. package/dist/src/settings/format-preserving-json.d.ts +40 -0
  30. package/dist/src/settings/format-preserving-json.js +80 -0
  31. package/dist/src/settings/index.d.ts +15 -0
  32. package/dist/src/settings/index.js +14 -0
  33. package/dist/src/settings/schema.d.ts +687 -0
  34. package/dist/src/settings/schema.js +977 -0
  35. package/dist/src/settings/settings.d.ts +38 -0
  36. package/dist/src/settings/settings.js +213 -0
  37. package/dist/src/testing.d.ts +17 -0
  38. package/dist/src/testing.js +17 -0
  39. package/dist/src/utils/atomic-write.d.ts +67 -0
  40. package/dist/src/utils/atomic-write.js +71 -0
  41. package/dist/src/utils/environment.d.ts +16 -0
  42. package/dist/src/utils/environment.js +19 -0
  43. package/dist/src/utils/path-safety.d.ts +25 -0
  44. package/dist/src/utils/path-safety.js +34 -0
  45. package/dist/src/utils/resolve-parent-symlinks.d.ts +16 -0
  46. package/dist/src/utils/resolve-parent-symlinks.js +36 -0
  47. package/dist/src/workspace/accepted-canonical-ref.d.ts +58 -0
  48. package/dist/src/workspace/accepted-canonical-ref.js +229 -0
  49. package/dist/src/workspace/artifact-change.d.ts +14 -0
  50. package/dist/src/workspace/artifact-change.js +18 -0
  51. package/dist/src/workspace/canonical-observation.d.ts +39 -0
  52. package/dist/src/workspace/canonical-observation.js +307 -0
  53. package/dist/src/workspace/configured-agent-outcome.d.ts +22 -0
  54. package/dist/src/workspace/configured-agent-outcome.js +33 -0
  55. package/dist/src/workspace/configured-agent-outcomes-provider.d.ts +50 -0
  56. package/dist/src/workspace/configured-agent-outcomes-provider.js +26 -0
  57. package/dist/src/workspace/configured-agent-outcomes.d.ts +48 -0
  58. package/dist/src/workspace/configured-agent-outcomes.js +113 -0
  59. package/dist/src/workspace/configured-entry-resolution/types.d.ts +32 -0
  60. package/dist/src/workspace/configured-entry-resolution/types.js +2 -0
  61. package/dist/src/workspace/configured-entry-resolution/workspace-ref.d.ts +32 -0
  62. package/dist/src/workspace/configured-entry-resolution/workspace-ref.js +176 -0
  63. package/dist/src/workspace/constants.d.ts +6 -0
  64. package/dist/src/workspace/constants.js +6 -0
  65. package/dist/src/workspace/create-symlink.d.ts +26 -0
  66. package/dist/src/workspace/create-symlink.js +87 -0
  67. package/dist/src/workspace/desired-pack-lock.d.ts +18 -0
  68. package/dist/src/workspace/desired-pack-lock.js +132 -0
  69. package/dist/src/workspace/desired-state-enabled.d.ts +18 -0
  70. package/dist/src/workspace/desired-state-enabled.js +16 -0
  71. package/dist/src/workspace/desired-state-graph.d.ts +114 -0
  72. package/dist/src/workspace/desired-state-graph.js +384 -0
  73. package/dist/src/workspace/desired-state-problem-text.d.ts +6 -0
  74. package/dist/src/workspace/desired-state-problem-text.js +27 -0
  75. package/dist/src/workspace/errors.d.ts +184 -0
  76. package/dist/src/workspace/errors.js +79 -0
  77. package/dist/src/workspace/extension-name.d.ts +28 -0
  78. package/dist/src/workspace/extension-name.js +66 -0
  79. package/dist/src/workspace/extension-paths.d.ts +82 -0
  80. package/dist/src/workspace/extension-paths.js +172 -0
  81. package/dist/src/workspace/footprint-recorder.d.ts +30 -0
  82. package/dist/src/workspace/footprint-recorder.js +32 -0
  83. package/dist/src/workspace/layout.d.ts +43 -0
  84. package/dist/src/workspace/layout.js +164 -0
  85. package/dist/src/workspace/lock-entry-to-ref.d.ts +40 -0
  86. package/dist/src/workspace/lock-entry-to-ref.js +433 -0
  87. package/dist/src/workspace/lock-entry-to-source-params.d.ts +26 -0
  88. package/dist/src/workspace/lock-entry-to-source-params.js +81 -0
  89. package/dist/src/workspace/locked-entries.d.ts +30 -0
  90. package/dist/src/workspace/locked-entries.js +34 -0
  91. package/dist/src/workspace/materialized-file-target.d.ts +14 -0
  92. package/dist/src/workspace/materialized-file-target.js +14 -0
  93. package/dist/src/workspace/materialized-tree.d.ts +23 -0
  94. package/dist/src/workspace/materialized-tree.js +90 -0
  95. package/dist/src/workspace/mcp-entry-semantics.d.ts +31 -0
  96. package/dist/src/workspace/mcp-entry-semantics.js +60 -0
  97. package/dist/src/workspace/observed-installed.d.ts +9 -0
  98. package/dist/src/workspace/observed-installed.js +9 -0
  99. package/dist/src/workspace/pack-manifest-content-identity.d.ts +12 -0
  100. package/dist/src/workspace/pack-manifest-content-identity.js +20 -0
  101. package/dist/src/workspace/pack-paths.d.ts +24 -0
  102. package/dist/src/workspace/pack-paths.js +16 -0
  103. package/dist/src/workspace/package-hash.d.ts +25 -0
  104. package/dist/src/workspace/package-hash.js +45 -0
  105. package/dist/src/workspace/paths.d.ts +24 -0
  106. package/dist/src/workspace/paths.js +52 -0
  107. package/dist/src/workspace/read-model/__fixtures__/builder.d.ts +199 -0
  108. package/dist/src/workspace/read-model/__fixtures__/builder.js +588 -0
  109. package/dist/src/workspace/read-model/__fixtures__/decoders.d.ts +15 -0
  110. package/dist/src/workspace/read-model/__fixtures__/decoders.js +15 -0
  111. package/dist/src/workspace/read-model/__fixtures__/occurrences.d.ts +72 -0
  112. package/dist/src/workspace/read-model/__fixtures__/occurrences.js +134 -0
  113. package/dist/src/workspace/read-model/__fixtures__/scenario-harness.d.ts +99 -0
  114. package/dist/src/workspace/read-model/__fixtures__/scenario-harness.js +142 -0
  115. package/dist/src/workspace/read-model/__fixtures__/test-layer.d.ts +60 -0
  116. package/dist/src/workspace/read-model/__fixtures__/test-layer.js +57 -0
  117. package/dist/src/workspace/read-model/agent-presence.d.ts +32 -0
  118. package/dist/src/workspace/read-model/agent-presence.js +21 -0
  119. package/dist/src/workspace/read-model/agent-root-resolver.d.ts +38 -0
  120. package/dist/src/workspace/read-model/agent-root-resolver.js +35 -0
  121. package/dist/src/workspace/read-model/agents/index.d.ts +38 -0
  122. package/dist/src/workspace/read-model/agents/index.js +45 -0
  123. package/dist/src/workspace/read-model/agents/shared.d.ts +27 -0
  124. package/dist/src/workspace/read-model/agents/shared.js +63 -0
  125. package/dist/src/workspace/read-model/agents/types.d.ts +133 -0
  126. package/dist/src/workspace/read-model/agents/types.js +51 -0
  127. package/dist/src/workspace/read-model/diagnostics.d.ts +18 -0
  128. package/dist/src/workspace/read-model/diagnostics.js +14 -0
  129. package/dist/src/workspace/read-model/discovery/index.d.ts +5 -0
  130. package/dist/src/workspace/read-model/discovery/index.js +5 -0
  131. package/dist/src/workspace/read-model/discovery/plugin-manifests.d.ts +16 -0
  132. package/dist/src/workspace/read-model/discovery/plugin-manifests.js +168 -0
  133. package/dist/src/workspace/read-model/discovery/skills.d.ts +58 -0
  134. package/dist/src/workspace/read-model/discovery/skills.js +263 -0
  135. package/dist/src/workspace/read-model/discovery/subagents.d.ts +37 -0
  136. package/dist/src/workspace/read-model/discovery/subagents.js +48 -0
  137. package/dist/src/workspace/read-model/errors.d.ts +93 -0
  138. package/dist/src/workspace/read-model/errors.js +30 -0
  139. package/dist/src/workspace/read-model/extensions/actual-helpers.d.ts +4 -0
  140. package/dist/src/workspace/read-model/extensions/actual-helpers.js +4 -0
  141. package/dist/src/workspace/read-model/extensions/hook.d.ts +104 -0
  142. package/dist/src/workspace/read-model/extensions/hook.js +119 -0
  143. package/dist/src/workspace/read-model/extensions/index.d.ts +19 -0
  144. package/dist/src/workspace/read-model/extensions/index.js +43 -0
  145. package/dist/src/workspace/read-model/extensions/indexByName.d.ts +31 -0
  146. package/dist/src/workspace/read-model/extensions/indexByName.js +30 -0
  147. package/dist/src/workspace/read-model/extensions/inventory.d.ts +99 -0
  148. package/dist/src/workspace/read-model/extensions/inventory.js +111 -0
  149. package/dist/src/workspace/read-model/extensions/knowledge.d.ts +101 -0
  150. package/dist/src/workspace/read-model/extensions/knowledge.js +121 -0
  151. package/dist/src/workspace/read-model/extensions/mcp-server.d.ts +106 -0
  152. package/dist/src/workspace/read-model/extensions/mcp-server.js +122 -0
  153. package/dist/src/workspace/read-model/extensions/pack.d.ts +104 -0
  154. package/dist/src/workspace/read-model/extensions/pack.js +123 -0
  155. package/dist/src/workspace/read-model/extensions/package-root.d.ts +39 -0
  156. package/dist/src/workspace/read-model/extensions/package-root.js +48 -0
  157. package/dist/src/workspace/read-model/extensions/projection.d.ts +137 -0
  158. package/dist/src/workspace/read-model/extensions/projection.js +180 -0
  159. package/dist/src/workspace/read-model/extensions/rule.d.ts +104 -0
  160. package/dist/src/workspace/read-model/extensions/rule.js +113 -0
  161. package/dist/src/workspace/read-model/extensions/skill.d.ts +170 -0
  162. package/dist/src/workspace/read-model/extensions/skill.js +148 -0
  163. package/dist/src/workspace/read-model/extensions/subagent.d.ts +97 -0
  164. package/dist/src/workspace/read-model/extensions/subagent.js +120 -0
  165. package/dist/src/workspace/read-model/scanners/agent-dir.d.ts +49 -0
  166. package/dist/src/workspace/read-model/scanners/agent-dir.js +180 -0
  167. package/dist/src/workspace/read-model/scanners/agent-root.d.ts +73 -0
  168. package/dist/src/workspace/read-model/scanners/agent-root.js +148 -0
  169. package/dist/src/workspace/read-model/scanners/agent-settings.d.ts +43 -0
  170. package/dist/src/workspace/read-model/scanners/agent-settings.js +54 -0
  171. package/dist/src/workspace/read-model/scanners/canonical-extensions.d.ts +48 -0
  172. package/dist/src/workspace/read-model/scanners/canonical-extensions.js +237 -0
  173. package/dist/src/workspace/read-model/scanners/fs-helpers.d.ts +63 -0
  174. package/dist/src/workspace/read-model/scanners/fs-helpers.js +106 -0
  175. package/dist/src/workspace/read-model/scanners/index.d.ts +15 -0
  176. package/dist/src/workspace/read-model/scanners/index.js +34 -0
  177. package/dist/src/workspace/read-model/scanners/mcp-config.d.ts +47 -0
  178. package/dist/src/workspace/read-model/scanners/mcp-config.js +212 -0
  179. package/dist/src/workspace/read-model/scanners/types.d.ts +170 -0
  180. package/dist/src/workspace/read-model/scanners/types.js +86 -0
  181. package/dist/src/workspace/read-model/service.d.ts +103 -0
  182. package/dist/src/workspace/read-model/service.js +391 -0
  183. package/dist/src/workspace/read-model/state.d.ts +35 -0
  184. package/dist/src/workspace/read-model/state.js +97 -0
  185. package/dist/src/workspace/read-model/types.d.ts +75 -0
  186. package/dist/src/workspace/read-model/types.js +26 -0
  187. package/dist/src/workspace/read-model-record-readers.d.ts +34 -0
  188. package/dist/src/workspace/read-model-record-readers.js +467 -0
  189. package/dist/src/workspace/read-model-record-rows.d.ts +45 -0
  190. package/dist/src/workspace/read-model-record-rows.js +33 -0
  191. package/dist/src/workspace/read-model-record-types.d.ts +45 -0
  192. package/dist/src/workspace/read-model-record-types.js +13 -0
  193. package/dist/src/workspace/remove-if-exists.d.ts +13 -0
  194. package/dist/src/workspace/remove-if-exists.js +26 -0
  195. package/dist/src/workspace/rendered-files.d.ts +53 -0
  196. package/dist/src/workspace/rendered-files.js +50 -0
  197. package/dist/src/workspace/scope-refusal.d.ts +26 -0
  198. package/dist/src/workspace/scope-refusal.js +29 -0
  199. package/dist/src/workspace/service-interface.d.ts +479 -0
  200. package/dist/src/workspace/service-interface.js +27 -0
  201. package/dist/src/workspace/service.d.ts +1665 -0
  202. package/dist/src/workspace/service.js +1263 -0
  203. package/dist/src/workspace/setup-scope-support.d.ts +27 -0
  204. package/dist/src/workspace/setup-scope-support.js +299 -0
  205. package/dist/src/workspace/skill-paths.d.ts +33 -0
  206. package/dist/src/workspace/skill-paths.js +14 -0
  207. package/dist/src/workspace/source-metadata.d.ts +7 -0
  208. package/dist/src/workspace/source-metadata.js +10 -0
  209. package/dist/src/workspace/source-to-lock-entry.d.ts +32 -0
  210. package/dist/src/workspace/source-to-lock-entry.js +68 -0
  211. package/dist/src/workspace/test-stubs.d.ts +144 -0
  212. package/dist/src/workspace/test-stubs.js +412 -0
  213. package/dist/src/workspace/transaction.d.ts +218 -0
  214. package/dist/src/workspace/transaction.js +177 -0
  215. package/package.json +57 -0
@@ -0,0 +1,588 @@
1
+ /**
2
+ * Fixture builder: synthesizes minimal workspace trees from declarative specs
3
+ * against an in-memory `FileSystem` for tests.
4
+ *
5
+ * Phase 5 of the workspace read-model change. Downstream phases (scanners,
6
+ * per-extension subject modules, the live `WorkspaceReadModel` service, and
7
+ * golden-fixture scenario tests) call `buildFixture(spec)` to materialize a
8
+ * minimal directory tree; named scenario constructors below provide the
9
+ * canonical specs for the spec scenarios listed in `proposal.md`.
10
+ *
11
+ * Key design choices:
12
+ *
13
+ * - The builder operates on a self-contained in-memory `Map<string, string>`
14
+ * filesystem, exposed through `FileSystem.makeNoop`. No `node:fs` is used.
15
+ * Tests get deterministic, ordered file sets without touching disk.
16
+ * - Path-escape attempts (entries containing `..` segments or absolute paths)
17
+ * fail the builder with `PathEscapeError`. This makes the upcoming
18
+ * Phase 9 `WorkspaceRootEscape` test a separate, deliberate scenario and
19
+ * eliminates the chance of a leaky test fixture corrupting the host fs.
20
+ * - Settings and lockfile files are serialized through `JSON.stringify` and
21
+ * `YAML.stringify` respectively when their `_tag === "valid"`. For
22
+ * `byteCorrupt`, the literal bytes are written verbatim. For
23
+ * `schemaInvalid`, the JSON/YAML serialization of the spec object is
24
+ * written so the parser accepts it but the schema decoder rejects it.
25
+ * - The builder NEVER constructs the live `WorkspaceReadModel` itself. The
26
+ * sibling `test-layer.ts` exposes a `WorkspaceReadModelTest` layer that
27
+ * composes the builder output with the real (Phase 9) Live layer; in
28
+ * Phase 5 it falls back to a `Layer.fail` placeholder.
29
+ */
30
+ import * as Data from "effect/Data";
31
+ import * as Effect from "effect/Effect";
32
+ import * as FileSystem from "effect/FileSystem";
33
+ import * as Option from "effect/Option";
34
+ import * as Path from "effect/Path";
35
+ import * as PlatformError from "effect/PlatformError";
36
+ import YAML from "yaml";
37
+ import { AGENTS } from "@agentxm/extension-model/unstable/agents/registry";
38
+ import { makeAbsolutePath } from "@agentxm/extension-model/unstable/path-types";
39
+ import { resolveProjectWorkspaceLayout, resolveUserWorkspaceLayout } from "../../layout.js";
40
+ export const resolveFixtureProjectLayout = (deps, settings = {}) => resolveProjectWorkspaceLayout(makeAbsolutePath(deps.path, deps.workspaceRoot), settings).pipe(Effect.provideService(FileSystem.FileSystem, deps.fs), Effect.provideService(Path.Path, deps.path));
41
+ export const resolveFixtureUserLayout = (deps, settings = {}) => resolveUserWorkspaceLayout(makeAbsolutePath(deps.path, deps.userHome), settings).pipe(Effect.provideService(Path.Path, deps.path));
42
+ // ---------------------------------------------------------------------------
43
+ // Errors
44
+ // ---------------------------------------------------------------------------
45
+ /**
46
+ * Builder rejected an entry because the spec asked to write outside the
47
+ * workspace/user-home root. Phase 9's `WorkspaceRootEscape` is a different
48
+ * (Layer-level) error; this one is purely a fixture sanity check so tests
49
+ * cannot accidentally clobber files outside the intended tree.
50
+ */
51
+ export class PathEscapeError extends Data.TaggedError("PathEscapeError") {
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Internal helpers
55
+ // ---------------------------------------------------------------------------
56
+ const isObject = (value) => typeof value === "object" && value !== null;
57
+ const stringifyJson = (value) => typeof value === "string" ? value : JSON.stringify(value);
58
+ const stringifyYaml = (value) => typeof value === "string" ? value : YAML.stringify(value);
59
+ const validateRelativePath = (relative, owner) => Effect.suspend(() => {
60
+ if (relative.length === 0) {
61
+ return Effect.fail(new PathEscapeError({
62
+ path: relative,
63
+ reason: `${owner} entry path is empty`,
64
+ }));
65
+ }
66
+ if (relative.startsWith("/")) {
67
+ return Effect.fail(new PathEscapeError({
68
+ path: relative,
69
+ reason: `${owner} entry must be a workspace-relative path; absolute paths are rejected`,
70
+ }));
71
+ }
72
+ const segments = relative.split("/");
73
+ for (const segment of segments) {
74
+ if (segment === "..") {
75
+ return Effect.fail(new PathEscapeError({
76
+ path: relative,
77
+ reason: `${owner} entry contains a ".." segment, which would escape the synthesized root`,
78
+ }));
79
+ }
80
+ }
81
+ return Effect.void;
82
+ });
83
+ /**
84
+ * Resolve an agent's content root directory (e.g., `.claude` for `claude-code`).
85
+ *
86
+ * The agent registry exposes per-subject directories like `.claude/skills`;
87
+ * the builder writes the entire `agentDirs[agentId]` tree under the agent's
88
+ * root, which is the first segment of any of those paths. If the agent is
89
+ * not registered, the builder falls back to `.<agentId>` so callers can
90
+ * still synthesize directories for hypothetical agents.
91
+ */
92
+ const resolveAgentContentRoot = (agentId) => {
93
+ const isKnown = (id) => Object.hasOwn(AGENTS, id);
94
+ if (!isKnown(agentId))
95
+ return `.${agentId}`;
96
+ const descriptor = AGENTS[agentId];
97
+ const skillsDir = descriptor.skills?.dir;
98
+ if (skillsDir === undefined)
99
+ return descriptor.rootDir ?? `.${agentId}`;
100
+ const firstSegment = skillsDir.split("/")[0];
101
+ return firstSegment !== undefined && firstSegment.length > 0 ? firstSegment : `.${agentId}`;
102
+ };
103
+ const resolveAgentSettingsRoot = (agentId) => {
104
+ const isKnown = (id) => Object.hasOwn(AGENTS, id);
105
+ if (!isKnown(agentId))
106
+ return `.${agentId}`;
107
+ const descriptor = AGENTS[agentId];
108
+ if (typeof descriptor.rootDir === "string")
109
+ return descriptor.rootDir;
110
+ return resolveAgentContentRoot(agentId);
111
+ };
112
+ const join = (...parts) => {
113
+ const normalized = [];
114
+ for (const p of parts) {
115
+ if (p.length === 0)
116
+ continue;
117
+ if (p === "/") {
118
+ normalized.push("");
119
+ continue;
120
+ }
121
+ normalized.push(p.endsWith("/") ? p.slice(0, -1) : p);
122
+ }
123
+ return normalized.join("/");
124
+ };
125
+ // ---------------------------------------------------------------------------
126
+ // In-memory FileSystem
127
+ // ---------------------------------------------------------------------------
128
+ const notFound = (method, path) => PlatformError.systemError({
129
+ _tag: "NotFound",
130
+ module: "FileSystem",
131
+ method,
132
+ description: "No such file or directory",
133
+ pathOrDescriptor: path,
134
+ });
135
+ const makeInMemoryFs = (files) => {
136
+ const directorySet = new Set();
137
+ const encoder = new TextEncoder();
138
+ for (const filePath of files.keys()) {
139
+ const segments = filePath.split("/");
140
+ for (let i = 1; i < segments.length; i += 1) {
141
+ const dir = segments.slice(0, i).join("/");
142
+ if (dir.length > 0)
143
+ directorySet.add(dir);
144
+ }
145
+ }
146
+ return FileSystem.makeNoop({
147
+ exists: (path) => Effect.sync(() => {
148
+ if (files.has(path))
149
+ return true;
150
+ if (directorySet.has(path))
151
+ return true;
152
+ return false;
153
+ }),
154
+ readFileString: (path) => {
155
+ const value = files.get(path);
156
+ if (value === undefined) {
157
+ return Effect.fail(notFound("readFileString", path));
158
+ }
159
+ return Effect.succeed(value);
160
+ },
161
+ readFile: (path) => {
162
+ const value = files.get(path);
163
+ if (value === undefined) {
164
+ return Effect.fail(notFound("readFile", path));
165
+ }
166
+ return Effect.succeed(encoder.encode(value));
167
+ },
168
+ readDirectory: (path) => {
169
+ if (!directorySet.has(path)) {
170
+ return Effect.fail(notFound("readDirectory", path));
171
+ }
172
+ const prefix = path.endsWith("/") ? path : `${path}/`;
173
+ const entries = new Set();
174
+ for (const filePath of files.keys()) {
175
+ if (!filePath.startsWith(prefix))
176
+ continue;
177
+ const remainder = filePath.slice(prefix.length);
178
+ const head = remainder.split("/")[0];
179
+ if (head !== undefined && head.length > 0)
180
+ entries.add(head);
181
+ }
182
+ for (const dir of directorySet) {
183
+ if (!dir.startsWith(prefix))
184
+ continue;
185
+ const remainder = dir.slice(prefix.length);
186
+ const head = remainder.split("/")[0];
187
+ if (head !== undefined && head.length > 0)
188
+ entries.add(head);
189
+ }
190
+ return Effect.succeed(Array.from(entries).sort());
191
+ },
192
+ // Minimal `stat` for callers that probe entry kind. Returns `File` for a
193
+ // path tracked in the files map, `Directory` for a synthesized directory,
194
+ // and fails with NotFound otherwise. Only `type` is meaningful — the
195
+ // remaining fields keep the `FileSystem.File.Info` shape happy.
196
+ stat: (path) => {
197
+ if (files.has(path)) {
198
+ return Effect.succeed({
199
+ type: "File",
200
+ mtime: Option.none(),
201
+ atime: Option.none(),
202
+ birthtime: Option.none(),
203
+ dev: 0,
204
+ ino: Option.none(),
205
+ mode: 0,
206
+ nlink: Option.none(),
207
+ uid: Option.none(),
208
+ gid: Option.none(),
209
+ rdev: Option.none(),
210
+ size: FileSystem.Size(0),
211
+ blksize: Option.none(),
212
+ blocks: Option.none(),
213
+ });
214
+ }
215
+ if (directorySet.has(path)) {
216
+ return Effect.succeed({
217
+ type: "Directory",
218
+ mtime: Option.none(),
219
+ atime: Option.none(),
220
+ birthtime: Option.none(),
221
+ dev: 0,
222
+ ino: Option.none(),
223
+ mode: 0,
224
+ nlink: Option.none(),
225
+ uid: Option.none(),
226
+ gid: Option.none(),
227
+ rdev: Option.none(),
228
+ size: FileSystem.Size(0),
229
+ blksize: Option.none(),
230
+ blocks: Option.none(),
231
+ });
232
+ }
233
+ return Effect.fail(notFound("stat", path));
234
+ },
235
+ });
236
+ };
237
+ // ---------------------------------------------------------------------------
238
+ // Spec → file map
239
+ // ---------------------------------------------------------------------------
240
+ const writeFileSpec = (files, absolutePath, spec, format) => {
241
+ switch (spec._tag) {
242
+ case "absent": {
243
+ // No-op: the file is intentionally absent.
244
+ return;
245
+ }
246
+ case "valid": {
247
+ const serialized = format === "json" ? stringifyJson(spec.contents) : stringifyYaml(spec.contents);
248
+ files.set(absolutePath, serialized);
249
+ return;
250
+ }
251
+ case "byteCorrupt": {
252
+ files.set(absolutePath, spec.bytes);
253
+ return;
254
+ }
255
+ case "schemaInvalid": {
256
+ // Use the format's serializer so the parser succeeds but the decoder
257
+ // rejects.
258
+ const serialized = format === "json" ? stringifyJson(spec.contents) : stringifyYaml(spec.contents);
259
+ files.set(absolutePath, serialized);
260
+ return;
261
+ }
262
+ }
263
+ };
264
+ const writeTree = (files, baseAbsolute, tree, ownerLabel) => Effect.gen(function* () {
265
+ for (const [relative, value] of Object.entries(tree)) {
266
+ yield* validateRelativePath(relative, ownerLabel);
267
+ const absolute = join(baseAbsolute, relative);
268
+ if (typeof value === "string") {
269
+ files.set(absolute, value);
270
+ continue;
271
+ }
272
+ // Determine format heuristically from the file extension; default to
273
+ // JSON because tree files we've seen so far are markdown / JSON / plain
274
+ // text.
275
+ const lower = relative.toLowerCase();
276
+ const format = lower.endsWith(".yaml") || lower.endsWith(".yml") ? "yaml" : "json";
277
+ // For tree-level FileSpecs, "valid" with a string maps to literal
278
+ // bytes; with an object we fall back to JSON serialization.
279
+ writeFileSpec(files, absolute, value, format);
280
+ }
281
+ });
282
+ const writeScope = (files, scope, scopeRoot, options) => Effect.gen(function* () {
283
+ if (scope === undefined)
284
+ return;
285
+ const stateRoot = options.scope === "project" ? scopeRoot : join(scopeRoot, ".axm", "workspace");
286
+ const settingsPath = options.scope === "project" ? join(scopeRoot, "axm.json") : join(stateRoot, "axm.json");
287
+ const lockfilePath = options.includeLockfile
288
+ ? options.scope === "project"
289
+ ? join(scopeRoot, "axm-lock.yaml")
290
+ : join(stateRoot, "axm-lock.yaml")
291
+ : null;
292
+ const mcpJsonPath = join(scopeRoot, ".mcp.json");
293
+ if (scope.settings !== undefined) {
294
+ writeFileSpec(files, settingsPath, scope.settings, "json");
295
+ }
296
+ if (scope.lockfile !== undefined && lockfilePath !== null) {
297
+ writeFileSpec(files, lockfilePath, scope.lockfile, "yaml");
298
+ }
299
+ if (scope.axmExtensions !== undefined) {
300
+ yield* writeTree(files, options.scope === "project"
301
+ ? join(scopeRoot, "agent_extensions")
302
+ : join(scopeRoot, ".axm", "workspace", "agent_extensions"), scope.axmExtensions, "axmExtensions");
303
+ }
304
+ if (scope.agentDirs !== undefined) {
305
+ for (const [agentId, tree] of Object.entries(scope.agentDirs)) {
306
+ const agentRoot = resolveAgentContentRoot(agentId);
307
+ yield* writeTree(files, join(scopeRoot, agentRoot), tree, `agentDirs[${agentId}]`);
308
+ }
309
+ }
310
+ if (scope.mcpJson !== undefined) {
311
+ writeFileSpec(files, mcpJsonPath, scope.mcpJson, "json");
312
+ }
313
+ if (scope.agentSettings !== undefined) {
314
+ for (const [agentId, fileSpec] of Object.entries(scope.agentSettings)) {
315
+ const agentRoot = resolveAgentSettingsRoot(agentId);
316
+ const agentSettingsPath = join(scopeRoot, agentRoot, "settings.json");
317
+ writeFileSpec(files, agentSettingsPath, fileSpec, "json");
318
+ }
319
+ }
320
+ });
321
+ // ---------------------------------------------------------------------------
322
+ // Public builder
323
+ // ---------------------------------------------------------------------------
324
+ /**
325
+ * Build a fixture against an in-memory FileSystem.
326
+ *
327
+ * The returned `FixtureTestDeps` value is a stable, dependency-closed value:
328
+ * it carries the synthesized `FileSystem`, a `Path.Path` value, and the
329
+ * raw file map. Downstream test layers compose these with the real Live
330
+ * implementations.
331
+ *
332
+ * Failures: the builder fails with `PathEscapeError` when any tree entry
333
+ * contains a `..` segment or an absolute path. Phase 9's
334
+ * `WorkspaceRootEscape` is a different (Layer-construction) failure.
335
+ */
336
+ export const buildFixture = (spec) => Effect.gen(function* () {
337
+ const files = new Map();
338
+ yield* writeScope(files, spec.project, spec.workspaceRoot, {
339
+ scope: "project",
340
+ includeLockfile: true,
341
+ });
342
+ yield* writeScope(files, spec.user, join(spec.userHome), {
343
+ scope: "user",
344
+ includeLockfile: true,
345
+ });
346
+ const fs = makeInMemoryFs(files);
347
+ const path = yield* Path.Path;
348
+ return {
349
+ fs,
350
+ path,
351
+ files,
352
+ workspaceRoot: spec.workspaceRoot,
353
+ userHome: spec.userHome,
354
+ };
355
+ }).pipe(Effect.provide(Path.layer));
356
+ // ---------------------------------------------------------------------------
357
+ // Named scenario constructors
358
+ // ---------------------------------------------------------------------------
359
+ const validSettingsContents = {
360
+ owner: "@team",
361
+ agents: ["claude-code"],
362
+ skills: { "managed-tool": { source: "github:owner/repo", enabled: true } },
363
+ };
364
+ const validLockfileContents = {
365
+ lockfileVersion: 6,
366
+ skills: {
367
+ "managed-tool": {
368
+ type: "github",
369
+ sourceType: "github",
370
+ sourceName: "github",
371
+ endpoint: "https://github.com",
372
+ extensionType: "skill",
373
+ workspaceName: "managed-tool",
374
+ packageFormat: "agentxm",
375
+ packageOwner: "@owner",
376
+ packageName: "managed-tool",
377
+ owner: "owner",
378
+ repo: "repo",
379
+ ref: "main",
380
+ resolvedCommit: "commit-1",
381
+ resolvedTree: "tree-1",
382
+ contentIdentity: "content-1",
383
+ treeIntegrity: `sha256-tree-v1:${"0".repeat(64)}`,
384
+ },
385
+ },
386
+ };
387
+ /**
388
+ * No source files materialize. Both scopes return absent for every cell.
389
+ */
390
+ export const absentAll = (workspaceRoot, userHome) => ({
391
+ workspaceRoot,
392
+ userHome,
393
+ project: { settings: { _tag: "absent" }, lockfile: { _tag: "absent" } },
394
+ user: { settings: { _tag: "absent" } },
395
+ });
396
+ /**
397
+ * Both project sources present and decodable; user scope settings present.
398
+ */
399
+ export const validAll = (workspaceRoot, userHome) => ({
400
+ workspaceRoot,
401
+ userHome,
402
+ project: {
403
+ settings: { _tag: "valid", contents: validSettingsContents },
404
+ lockfile: { _tag: "valid", contents: validLockfileContents },
405
+ },
406
+ user: {
407
+ settings: { _tag: "valid", contents: { owner: "@user" } },
408
+ },
409
+ });
410
+ /**
411
+ * Settings is valid; the lockfile is byte-corrupt.
412
+ */
413
+ export const lockfileInvalidOnly = (workspaceRoot, userHome) => ({
414
+ workspaceRoot,
415
+ userHome,
416
+ project: {
417
+ settings: { _tag: "valid", contents: validSettingsContents },
418
+ lockfile: {
419
+ _tag: "byteCorrupt",
420
+ bytes: "skills:\n - bad\n - mismatched: indent\n bad: !!!@@@",
421
+ },
422
+ },
423
+ });
424
+ /**
425
+ * Lockfile is valid; settings is byte-corrupt.
426
+ */
427
+ export const settingsInvalidOnly = (workspaceRoot, userHome) => ({
428
+ workspaceRoot,
429
+ userHome,
430
+ project: {
431
+ settings: { _tag: "byteCorrupt", bytes: "{ this is not json" },
432
+ lockfile: { _tag: "valid", contents: validLockfileContents },
433
+ },
434
+ });
435
+ /**
436
+ * Both project sources are byte-corrupt.
437
+ */
438
+ export const bothInvalid = (workspaceRoot, userHome) => ({
439
+ workspaceRoot,
440
+ userHome,
441
+ project: {
442
+ settings: { _tag: "byteCorrupt", bytes: "{ this is not json" },
443
+ lockfile: { _tag: "byteCorrupt", bytes: "lockfile: [bogus" },
444
+ },
445
+ });
446
+ /**
447
+ * Project state present; user scope absent.
448
+ */
449
+ export const projectOnly = (workspaceRoot, userHome) => ({
450
+ workspaceRoot,
451
+ userHome,
452
+ project: {
453
+ settings: { _tag: "valid", contents: validSettingsContents },
454
+ lockfile: { _tag: "valid", contents: validLockfileContents },
455
+ },
456
+ user: { settings: { _tag: "absent" } },
457
+ });
458
+ /**
459
+ * User scope settings present; project state absent.
460
+ */
461
+ export const userOnly = (workspaceRoot, userHome) => ({
462
+ workspaceRoot,
463
+ userHome,
464
+ project: { settings: { _tag: "absent" }, lockfile: { _tag: "absent" } },
465
+ user: { settings: { _tag: "valid", contents: { owner: "@user" } } },
466
+ });
467
+ /**
468
+ * Both scopes declare a source-host with the same name. Phase 9 verifies that
469
+ * the WorkspaceReadModel exposes both scope reads independently and does not
470
+ * implicitly merge them.
471
+ */
472
+ export const projectUserShadowing = (workspaceRoot, userHome) => ({
473
+ workspaceRoot,
474
+ userHome,
475
+ project: {
476
+ settings: {
477
+ _tag: "valid",
478
+ contents: {
479
+ owner: "@team",
480
+ sources: [{ name: "shared", type: "github", url: "https://github.com/team" }],
481
+ },
482
+ },
483
+ },
484
+ user: {
485
+ settings: {
486
+ _tag: "valid",
487
+ contents: {
488
+ owner: "@user",
489
+ sources: [{ name: "shared", type: "github", url: "https://github.com/user" }],
490
+ },
491
+ },
492
+ },
493
+ });
494
+ /**
495
+ * Agent dir exists on disk but settings does not declare the agent. Phase 9
496
+ * verifies the `agents.detected` projection surfaces a mismatch.
497
+ */
498
+ export const agentPresentNoDeclaration = (workspaceRoot, userHome) => ({
499
+ workspaceRoot,
500
+ userHome,
501
+ project: {
502
+ settings: { _tag: "valid", contents: { owner: "@team" } },
503
+ agentDirs: {
504
+ "claude-code": {
505
+ "skills/some-skill/SKILL.md": "# some-skill\n",
506
+ },
507
+ },
508
+ },
509
+ });
510
+ /**
511
+ * Settings declares an agent whose directory does not exist.
512
+ */
513
+ export const agentDeclaredNotInstalled = (workspaceRoot, userHome) => ({
514
+ workspaceRoot,
515
+ userHome,
516
+ project: {
517
+ settings: {
518
+ _tag: "valid",
519
+ contents: { owner: "@team", agents: ["claude-code"] },
520
+ },
521
+ },
522
+ });
523
+ /**
524
+ * Settings disagrees with `.mcp.json`: the keys differ between the two
525
+ * surfaces. Phase 9 verifies the MCP-server projection exposes both views
526
+ * without silently reconciling them.
527
+ */
528
+ export const mcpConfigDrift = (workspaceRoot, userHome) => ({
529
+ workspaceRoot,
530
+ userHome,
531
+ project: {
532
+ settings: {
533
+ _tag: "valid",
534
+ contents: {
535
+ owner: "@team",
536
+ mcpServers: { declared: { source: "github:owner/repo" } },
537
+ },
538
+ },
539
+ mcpJson: {
540
+ _tag: "valid",
541
+ contents: { mcpServers: { drifted: { command: "echo" } } },
542
+ },
543
+ },
544
+ });
545
+ /**
546
+ * Same skill name across agent skill dirs (`.claude/skills/`, `.agents/skills/`)
547
+ * and canonical AXM storage.
548
+ */
549
+ export const sameNameAcrossOrigins = (workspaceRoot, userHome) => ({
550
+ workspaceRoot,
551
+ userHome,
552
+ project: {
553
+ agentDirs: {
554
+ "claude-code": {
555
+ "skills/some-skill/SKILL.md": "# claude-code\n",
556
+ },
557
+ codex: {
558
+ "skills/some-skill/SKILL.md": "# codex\n",
559
+ },
560
+ },
561
+ axmExtensions: {
562
+ "agentxm/@owner/skills/some-skill/skill.json": JSON.stringify({
563
+ owner: "@owner",
564
+ type: "skill",
565
+ name: "some-skill",
566
+ version: "1.0.0",
567
+ }),
568
+ "agentxm/@owner/skills/some-skill/src/SKILL.md": "---\nname: some-skill\ndescription: Canonical\n---\n# canonical\n",
569
+ },
570
+ },
571
+ });
572
+ /**
573
+ * Spec containing a `..` segment so callers can verify the builder rejects it.
574
+ * The fixture intentionally returns an unbuildable spec; consumers invoke
575
+ * `buildFixture` to assert the rejection path.
576
+ */
577
+ export const pathEscapeAttempt = (workspaceRoot, userHome) => ({
578
+ workspaceRoot,
579
+ userHome,
580
+ project: {
581
+ axmExtensions: {
582
+ "../../etc/passwd": "evil\n",
583
+ },
584
+ },
585
+ });
586
+ // Keep `isObject` available for downstream phases that need the same guard.
587
+ void isObject;
588
+ //# sourceMappingURL=builder.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Schema-decoded fixture builders for tests.
3
+ *
4
+ * Tests that need a real `Settings` or `Lockfile` value should run the input
5
+ * through the actual decoder rather than casting an arbitrary object via
6
+ * `as unknown as Settings`. These helpers wrap the canonical schema decoders
7
+ * so call sites yield the decoded value with `yield* decodedSettings({ ... })`.
8
+ */
9
+ import * as Effect from "effect/Effect";
10
+ import * as Schema from "effect/Schema";
11
+ import { type Lockfile } from "../../../lockfile/schema.js";
12
+ import { type Settings } from "../../../settings/schema.js";
13
+ export declare const decodedSettings: (input: unknown) => Effect.Effect<Settings, Schema.SchemaError>;
14
+ export declare const decodedLockfile: (input: unknown) => Effect.Effect<Lockfile, Schema.SchemaError>;
15
+ //# sourceMappingURL=decoders.d.ts.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Schema-decoded fixture builders for tests.
3
+ *
4
+ * Tests that need a real `Settings` or `Lockfile` value should run the input
5
+ * through the actual decoder rather than casting an arbitrary object via
6
+ * `as unknown as Settings`. These helpers wrap the canonical schema decoders
7
+ * so call sites yield the decoded value with `yield* decodedSettings({ ... })`.
8
+ */
9
+ import * as Effect from "effect/Effect";
10
+ import * as Schema from "effect/Schema";
11
+ import { LockfileSchema } from "../../../lockfile/schema.js";
12
+ import { SettingsSchema } from "../../../settings/schema.js";
13
+ export const decodedSettings = (input) => Schema.decodeUnknownEffect(SettingsSchema)(input);
14
+ export const decodedLockfile = (input) => Schema.decodeUnknownEffect(LockfileSchema)(input);
15
+ //# sourceMappingURL=decoders.js.map
@@ -0,0 +1,72 @@
1
+ import { type ExtensionType } from "@agentxm/extension-model/unstable/extensions/common";
2
+ import type { AgentId } from "@agentxm/extension-model/unstable/agents/types";
3
+ import type { AgentDirOccurrence, AgentDirSubjectType, AgentSettingsOccurrence, CanonicalExtensionOccurrence, CanonicalExtensionOriginKind, McpConfigOccurrence } from "../scanners/types.js";
4
+ import type { Scope } from "../types.js";
5
+ export interface MakeCanonicalOccurrenceInput {
6
+ readonly scope: Scope;
7
+ readonly type: ExtensionType;
8
+ readonly origin: CanonicalExtensionOriginKind;
9
+ readonly name: string;
10
+ readonly owner: string | null;
11
+ readonly contentLocation: string;
12
+ /**
13
+ * Override `subjectFileExists`. Defaults to `true` when the subject type has
14
+ * a canonical primary file; `false` otherwise.
15
+ */
16
+ readonly subjectFileExists?: boolean;
17
+ }
18
+ /**
19
+ * Build a `CanonicalExtensionOccurrence` with the new structural fields
20
+ * populated. `pathSegments` is computed from `contentLocation` (POSIX split).
21
+ * `subjectFile` is `Some(<dir>/<name>.md)` for skill/subagent; `None` for
22
+ * mcp-server/rule/hook/knowledge/pack. `subjectFileExists` defaults to `true` when
23
+ * the subject has a canonical primary file; pass `false` to model absence.
24
+ */
25
+ export declare const makeCanonicalOccurrence: (input: MakeCanonicalOccurrenceInput) => CanonicalExtensionOccurrence;
26
+ export interface MakeAgentDirOccurrenceInput {
27
+ readonly scope: Scope;
28
+ readonly type: AgentDirSubjectType;
29
+ readonly agentId: AgentId;
30
+ readonly name: string;
31
+ readonly contentLocation: string;
32
+ /**
33
+ * Set to `true` for single-file subagent surfaces (e.g., `.roomodes`) so
34
+ * `subjectFile` is the `contentLocation` itself.
35
+ */
36
+ readonly singleFile?: boolean;
37
+ /**
38
+ * Override `subjectFileExists`. Defaults to `true` (the occurrence exists
39
+ * because the file exists).
40
+ */
41
+ readonly subjectFileExists?: boolean;
42
+ }
43
+ /**
44
+ * Build an `AgentDirOccurrence` with the new structural fields populated.
45
+ * `pathSegments` is computed from `contentLocation` (POSIX split).
46
+ * `subjectFile` is `<contentLocation>/<canonical-file>` for directory
47
+ * occurrences and the `contentLocation` itself for single-file subagent
48
+ * surfaces. `subjectFileExists` defaults to `true`.
49
+ */
50
+ export declare const makeAgentDirOccurrence: (input: MakeAgentDirOccurrenceInput) => AgentDirOccurrence;
51
+ export interface MakeWorkspaceMcpConfigOccurrenceInput {
52
+ readonly scope: Scope;
53
+ readonly name: string;
54
+ readonly contentLocation: string;
55
+ readonly config?: Readonly<Record<string, unknown>>;
56
+ }
57
+ export interface MakeAgentMcpConfigOccurrenceInput {
58
+ readonly scope: Scope;
59
+ readonly agentId: AgentId;
60
+ readonly name: string;
61
+ readonly contentLocation: string;
62
+ readonly config?: Readonly<Record<string, unknown>>;
63
+ }
64
+ export declare const makeWorkspaceMcpConfigOccurrence: (input: MakeWorkspaceMcpConfigOccurrenceInput) => McpConfigOccurrence;
65
+ export declare const makeAgentMcpConfigOccurrence: (input: MakeAgentMcpConfigOccurrenceInput) => McpConfigOccurrence;
66
+ export interface MakeAgentSettingsOccurrenceInput {
67
+ readonly scope: Scope;
68
+ readonly agentId: AgentId;
69
+ readonly contentLocation: string;
70
+ }
71
+ export declare const makeAgentSettingsOccurrence: (input: MakeAgentSettingsOccurrenceInput) => AgentSettingsOccurrence;
72
+ //# sourceMappingURL=occurrences.d.ts.map