domain-knowledge-kit 0.2.15 → 0.2.16

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 (177) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +20 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/features/agent/commands/init.d.ts.map +1 -1
  5. package/dist/features/agent/commands/init.js +141 -3
  6. package/dist/features/agent/commands/init.js.map +1 -1
  7. package/dist/features/agent/commands/prime.d.ts +11 -0
  8. package/dist/features/agent/commands/prime.d.ts.map +1 -1
  9. package/dist/features/agent/commands/prime.js +104 -8
  10. package/dist/features/agent/commands/prime.js.map +1 -1
  11. package/dist/features/federation/commands/consumers.d.ts +40 -0
  12. package/dist/features/federation/commands/consumers.d.ts.map +1 -0
  13. package/dist/features/federation/commands/consumers.js +126 -0
  14. package/dist/features/federation/commands/consumers.js.map +1 -0
  15. package/dist/features/federation/commands/peers-add.d.ts +14 -0
  16. package/dist/features/federation/commands/peers-add.d.ts.map +1 -0
  17. package/dist/features/federation/commands/peers-add.js +79 -0
  18. package/dist/features/federation/commands/peers-add.js.map +1 -0
  19. package/dist/features/federation/commands/peers-list.d.ts +8 -0
  20. package/dist/features/federation/commands/peers-list.d.ts.map +1 -0
  21. package/dist/features/federation/commands/peers-list.js +51 -0
  22. package/dist/features/federation/commands/peers-list.js.map +1 -0
  23. package/dist/features/federation/commands/peers-status.d.ts +8 -0
  24. package/dist/features/federation/commands/peers-status.d.ts.map +1 -0
  25. package/dist/features/federation/commands/peers-status.js +78 -0
  26. package/dist/features/federation/commands/peers-status.js.map +1 -0
  27. package/dist/features/federation/commands/pull.d.ts +18 -0
  28. package/dist/features/federation/commands/pull.d.ts.map +1 -0
  29. package/dist/features/federation/commands/pull.js +153 -0
  30. package/dist/features/federation/commands/pull.js.map +1 -0
  31. package/dist/features/federation/git-fetcher.d.ts +45 -0
  32. package/dist/features/federation/git-fetcher.d.ts.map +1 -0
  33. package/dist/features/federation/git-fetcher.js +70 -0
  34. package/dist/features/federation/git-fetcher.js.map +1 -0
  35. package/dist/features/federation/loader.d.ts +60 -0
  36. package/dist/features/federation/loader.d.ts.map +1 -0
  37. package/dist/features/federation/loader.js +193 -0
  38. package/dist/features/federation/loader.js.map +1 -0
  39. package/dist/features/federation/lock.d.ts +12 -0
  40. package/dist/features/federation/lock.d.ts.map +1 -0
  41. package/dist/features/federation/lock.js +48 -0
  42. package/dist/features/federation/lock.js.map +1 -0
  43. package/dist/features/federation/tests/git-fetcher.test.d.ts +2 -0
  44. package/dist/features/federation/tests/git-fetcher.test.d.ts.map +1 -0
  45. package/dist/features/federation/tests/git-fetcher.test.js +167 -0
  46. package/dist/features/federation/tests/git-fetcher.test.js.map +1 -0
  47. package/dist/features/federation/tests/loader.test.d.ts +2 -0
  48. package/dist/features/federation/tests/loader.test.d.ts.map +1 -0
  49. package/dist/features/federation/tests/loader.test.js +144 -0
  50. package/dist/features/federation/tests/loader.test.js.map +1 -0
  51. package/dist/features/federation/tests/phase5.test.d.ts +2 -0
  52. package/dist/features/federation/tests/phase5.test.d.ts.map +1 -0
  53. package/dist/features/federation/tests/phase5.test.js +137 -0
  54. package/dist/features/federation/tests/phase5.test.js.map +1 -0
  55. package/dist/features/federation/tests/schema-load.test.d.ts +2 -0
  56. package/dist/features/federation/tests/schema-load.test.d.ts.map +1 -0
  57. package/dist/features/federation/tests/schema-load.test.js +97 -0
  58. package/dist/features/federation/tests/schema-load.test.js.map +1 -0
  59. package/dist/features/federation/tests/validator.test.d.ts +2 -0
  60. package/dist/features/federation/tests/validator.test.d.ts.map +1 -0
  61. package/dist/features/federation/tests/validator.test.js +319 -0
  62. package/dist/features/federation/tests/validator.test.js.map +1 -0
  63. package/dist/features/mcp/commands/serve.d.ts +10 -0
  64. package/dist/features/mcp/commands/serve.d.ts.map +1 -0
  65. package/dist/features/mcp/commands/serve.js +12 -0
  66. package/dist/features/mcp/commands/serve.js.map +1 -0
  67. package/dist/features/mcp/server.d.ts +15 -0
  68. package/dist/features/mcp/server.d.ts.map +1 -0
  69. package/dist/features/mcp/server.js +438 -0
  70. package/dist/features/mcp/server.js.map +1 -0
  71. package/dist/features/pipeline/commands/validate.d.ts.map +1 -1
  72. package/dist/features/pipeline/commands/validate.js +7 -0
  73. package/dist/features/pipeline/commands/validate.js.map +1 -1
  74. package/dist/features/pipeline/indexer.d.ts +28 -2
  75. package/dist/features/pipeline/indexer.d.ts.map +1 -1
  76. package/dist/features/pipeline/indexer.js +82 -27
  77. package/dist/features/pipeline/indexer.js.map +1 -1
  78. package/dist/features/pipeline/validator.d.ts +10 -0
  79. package/dist/features/pipeline/validator.d.ts.map +1 -1
  80. package/dist/features/pipeline/validator.js +274 -27
  81. package/dist/features/pipeline/validator.js.map +1 -1
  82. package/dist/features/query/commands/list.d.ts +10 -0
  83. package/dist/features/query/commands/list.d.ts.map +1 -1
  84. package/dist/features/query/commands/list.js +1 -1
  85. package/dist/features/query/commands/list.js.map +1 -1
  86. package/dist/features/query/commands/locate.d.ts +1 -0
  87. package/dist/features/query/commands/locate.d.ts.map +1 -1
  88. package/dist/features/query/commands/locate.js +1 -1
  89. package/dist/features/query/commands/locate.js.map +1 -1
  90. package/dist/features/query/commands/search.d.ts.map +1 -1
  91. package/dist/features/query/commands/search.js +2 -0
  92. package/dist/features/query/commands/search.js.map +1 -1
  93. package/dist/features/query/commands/show.d.ts +15 -0
  94. package/dist/features/query/commands/show.d.ts.map +1 -1
  95. package/dist/features/query/commands/show.js +116 -58
  96. package/dist/features/query/commands/show.js.map +1 -1
  97. package/dist/features/query/commands/story.d.ts +70 -0
  98. package/dist/features/query/commands/story.d.ts.map +1 -1
  99. package/dist/features/query/commands/story.js +2 -2
  100. package/dist/features/query/commands/story.js.map +1 -1
  101. package/dist/features/query/commands/summary.d.ts +3 -0
  102. package/dist/features/query/commands/summary.d.ts.map +1 -1
  103. package/dist/features/query/commands/summary.js +1 -1
  104. package/dist/features/query/commands/summary.js.map +1 -1
  105. package/dist/features/query/searcher.d.ts +18 -1
  106. package/dist/features/query/searcher.d.ts.map +1 -1
  107. package/dist/features/query/searcher.js +11 -2
  108. package/dist/features/query/searcher.js.map +1 -1
  109. package/dist/features/scaffold/commands/service-init.d.ts +12 -0
  110. package/dist/features/scaffold/commands/service-init.d.ts.map +1 -0
  111. package/dist/features/scaffold/commands/service-init.js +69 -0
  112. package/dist/features/scaffold/commands/service-init.js.map +1 -0
  113. package/dist/shared/graph.d.ts +8 -0
  114. package/dist/shared/graph.d.ts.map +1 -1
  115. package/dist/shared/graph.js +180 -112
  116. package/dist/shared/graph.js.map +1 -1
  117. package/dist/shared/index.d.ts +4 -1
  118. package/dist/shared/index.d.ts.map +1 -1
  119. package/dist/shared/index.js +6 -1
  120. package/dist/shared/index.js.map +1 -1
  121. package/dist/shared/loader.d.ts +22 -0
  122. package/dist/shared/loader.d.ts.map +1 -1
  123. package/dist/shared/loader.js +31 -1
  124. package/dist/shared/loader.js.map +1 -1
  125. package/dist/shared/paths.d.ts +59 -7
  126. package/dist/shared/paths.d.ts.map +1 -1
  127. package/dist/shared/paths.js +93 -11
  128. package/dist/shared/paths.js.map +1 -1
  129. package/dist/shared/refs.d.ts +96 -0
  130. package/dist/shared/refs.d.ts.map +1 -0
  131. package/dist/shared/refs.js +182 -0
  132. package/dist/shared/refs.js.map +1 -0
  133. package/dist/shared/service-id.d.ts +11 -0
  134. package/dist/shared/service-id.d.ts.map +1 -0
  135. package/dist/shared/service-id.js +64 -0
  136. package/dist/shared/service-id.js.map +1 -0
  137. package/dist/shared/tests/paths.test.d.ts +2 -0
  138. package/dist/shared/tests/paths.test.d.ts.map +1 -0
  139. package/dist/shared/tests/paths.test.js +111 -0
  140. package/dist/shared/tests/paths.test.js.map +1 -0
  141. package/dist/shared/tests/refs.test.d.ts +2 -0
  142. package/dist/shared/tests/refs.test.d.ts.map +1 -0
  143. package/dist/shared/tests/refs.test.js +104 -0
  144. package/dist/shared/tests/refs.test.js.map +1 -0
  145. package/dist/shared/types/domain.d.ts +14 -0
  146. package/dist/shared/types/domain.d.ts.map +1 -1
  147. package/dist/shared/types/federation.d.ts +60 -0
  148. package/dist/shared/types/federation.d.ts.map +1 -0
  149. package/dist/shared/types/federation.js +12 -0
  150. package/dist/shared/types/federation.js.map +1 -0
  151. package/package.json +6 -3
  152. package/tools/dkk/claude/agents/dkk-domain-reviewer.md +69 -0
  153. package/tools/dkk/claude/commands/dkk-adr.md +11 -0
  154. package/tools/dkk/claude/commands/dkk-impact.md +34 -0
  155. package/tools/dkk/claude/commands/dkk-implement.md +12 -0
  156. package/tools/dkk/claude/commands/dkk-prime.md +6 -0
  157. package/tools/dkk/claude/commands/dkk-review.md +12 -0
  158. package/tools/dkk/claude/commands/dkk-story.md +12 -0
  159. package/tools/dkk/claude/hooks/post-edit-validate.mjs +51 -0
  160. package/tools/dkk/claude/hooks/pre-edit-block-generated.mjs +39 -0
  161. package/tools/dkk/claude/hooks/session-start-prime.mjs +33 -0
  162. package/tools/dkk/claude/hooks/stop-validate.mjs +48 -0
  163. package/tools/dkk/claude/settings.json +62 -0
  164. package/tools/dkk/claude/skills/dkk-adr-author/SKILL.md +54 -0
  165. package/tools/dkk/claude/skills/dkk-flow-implementer/SKILL.md +51 -0
  166. package/tools/dkk/claude/skills/dkk-story-analyst/SKILL.md +108 -0
  167. package/tools/dkk/schema/actors.schema.json +1 -1
  168. package/tools/dkk/schema/adr-frontmatter.schema.json +4 -4
  169. package/tools/dkk/schema/aggregate.schema.json +1 -1
  170. package/tools/dkk/schema/command.schema.json +1 -1
  171. package/tools/dkk/schema/event.schema.json +1 -1
  172. package/tools/dkk/schema/federation.schema.json +71 -0
  173. package/tools/dkk/schema/glossary.schema.json +1 -1
  174. package/tools/dkk/schema/index.schema.json +2 -2
  175. package/tools/dkk/schema/policy.schema.json +1 -1
  176. package/tools/dkk/schema/read-model.schema.json +1 -1
  177. package/tools/dkk/schema/service.schema.json +30 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Options for {@link sparseFetch}.
3
+ */
4
+ export interface SparseFetchOptions {
5
+ /** Git URL (https / ssh). */
6
+ url: string;
7
+ /** Branch to track. */
8
+ branch: string;
9
+ /**
10
+ * Sub-path inside the peer repo where the service lives (i.e. the
11
+ * directory whose child is `.dkk/`). Empty string for repos that
12
+ * have `.dkk/` at the root.
13
+ */
14
+ subpath: string;
15
+ /**
16
+ * Absolute destination directory. Will be wiped if it exists.
17
+ * After fetch, the peer's repo content lives at `<dest>` and the
18
+ * service's `.dkk/` lives at `<dest>/<subpath>/.dkk` (or
19
+ * `<dest>/.dkk` when subpath is empty).
20
+ */
21
+ dest: string;
22
+ }
23
+ /** Result of a successful sparse fetch. */
24
+ export interface SparseFetchResult {
25
+ /** Resolved commit SHA at HEAD of the fetched branch. */
26
+ sha: string;
27
+ }
28
+ /**
29
+ * Clone a peer repository into `dest` using a sparse-checkout of only
30
+ * the `.dkk/` subtree. The clone is depth-1 + blobless to keep it fast
31
+ * and small. Returns the resolved commit SHA.
32
+ *
33
+ * Steps:
34
+ * 1. Wipe `dest` if it exists (a stale partial clone is worse than no clone).
35
+ * 2. `git clone --filter=blob:none --depth 1 --no-checkout --branch <b> <url> <dest>`.
36
+ * 3. `git sparse-checkout init --no-cone` inside `dest`.
37
+ * 4. `git sparse-checkout set <subpath>/.dkk` (or `.dkk` when subpath empty).
38
+ * 5. `git checkout <branch>` to materialise the sparse set on disk.
39
+ * 6. `git rev-parse HEAD` to capture the SHA.
40
+ *
41
+ * Errors from `git` propagate as thrown exceptions with their stderr
42
+ * attached, so the caller can surface a meaningful message to the user.
43
+ */
44
+ export declare function sparseFetch(opts: SparseFetchOptions): SparseFetchResult;
45
+ //# sourceMappingURL=git-fetcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-fetcher.d.ts","sourceRoot":"","sources":["../../../src/features/federation/git-fetcher.ts"],"names":[],"mappings":"AAcA;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,uBAAuB;IACvB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AAED,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IAChC,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC;CACb;AAcD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,iBAAiB,CAoCvE"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Sparse-checkout fetcher for git-source peers.
3
+ *
4
+ * Clones a peer repository into a local cache, pulling only the
5
+ * subdirectory containing `.dkk/` to keep disk cost minimal — a
6
+ * typical peer is well under a megabyte.
7
+ *
8
+ * The user's existing git credential helper / SSH agent is what
9
+ * authenticates the clone. DKK never handles tokens directly.
10
+ */
11
+ import { execFileSync } from "node:child_process";
12
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
13
+ import { dirname } from "node:path";
14
+ /**
15
+ * Run `git` with the given args inside `cwd`. Throws on non-zero exit.
16
+ * Captures stdout for callers that need it (e.g. `rev-parse`).
17
+ */
18
+ function git(args, cwd) {
19
+ return execFileSync("git", args, {
20
+ cwd,
21
+ stdio: ["ignore", "pipe", "pipe"],
22
+ encoding: "utf-8",
23
+ }).trim();
24
+ }
25
+ /**
26
+ * Clone a peer repository into `dest` using a sparse-checkout of only
27
+ * the `.dkk/` subtree. The clone is depth-1 + blobless to keep it fast
28
+ * and small. Returns the resolved commit SHA.
29
+ *
30
+ * Steps:
31
+ * 1. Wipe `dest` if it exists (a stale partial clone is worse than no clone).
32
+ * 2. `git clone --filter=blob:none --depth 1 --no-checkout --branch <b> <url> <dest>`.
33
+ * 3. `git sparse-checkout init --no-cone` inside `dest`.
34
+ * 4. `git sparse-checkout set <subpath>/.dkk` (or `.dkk` when subpath empty).
35
+ * 5. `git checkout <branch>` to materialise the sparse set on disk.
36
+ * 6. `git rev-parse HEAD` to capture the SHA.
37
+ *
38
+ * Errors from `git` propagate as thrown exceptions with their stderr
39
+ * attached, so the caller can surface a meaningful message to the user.
40
+ */
41
+ export function sparseFetch(opts) {
42
+ if (existsSync(opts.dest)) {
43
+ rmSync(opts.dest, { recursive: true, force: true });
44
+ }
45
+ mkdirSync(dirname(opts.dest), { recursive: true });
46
+ // Step 2: blobless, depth-1, no checkout yet.
47
+ git([
48
+ "clone",
49
+ "--filter=blob:none",
50
+ "--depth",
51
+ "1",
52
+ "--no-checkout",
53
+ "--branch",
54
+ opts.branch,
55
+ opts.url,
56
+ opts.dest,
57
+ ], process.cwd());
58
+ // Steps 3-4: enable sparse-checkout, restrict to the .dkk/ subtree.
59
+ git(["sparse-checkout", "init", "--no-cone"], opts.dest);
60
+ const sparsePattern = opts.subpath
61
+ ? `${opts.subpath.replace(/\/$/, "")}/.dkk/*`
62
+ : ".dkk/*";
63
+ git(["sparse-checkout", "set", sparsePattern], opts.dest);
64
+ // Step 5: materialise.
65
+ git(["checkout", opts.branch], opts.dest);
66
+ // Step 6: capture HEAD SHA.
67
+ const sha = git(["rev-parse", "HEAD"], opts.dest);
68
+ return { sha };
69
+ }
70
+ //# sourceMappingURL=git-fetcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-fetcher.js","sourceRoot":"","sources":["../../../src/features/federation/git-fetcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+BpC;;;GAGG;AACH,SAAS,GAAG,CAAC,IAAc,EAAE,GAAW;IACtC,OAAO,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE;QAC/B,GAAG;QACH,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;QACjC,QAAQ,EAAE,OAAO;KAClB,CAAC,CAAC,IAAI,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,WAAW,CAAC,IAAwB;IAClD,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEnD,8CAA8C;IAC9C,GAAG,CACD;QACE,OAAO;QACP,oBAAoB;QACpB,SAAS;QACT,GAAG;QACH,eAAe;QACf,UAAU;QACV,IAAI,CAAC,MAAM;QACX,IAAI,CAAC,GAAG;QACR,IAAI,CAAC,IAAI;KACV,EACD,OAAO,CAAC,GAAG,EAAE,CACd,CAAC;IAEF,oEAAoE;IACpE,GAAG,CAAC,CAAC,iBAAiB,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACzD,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO;QAChC,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS;QAC7C,CAAC,CAAC,QAAQ,CAAC;IACb,GAAG,CAAC,CAAC,iBAAiB,EAAE,KAAK,EAAE,aAAa,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAE1D,uBAAuB;IACvB,GAAG,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAE1C,4BAA4B;IAC5B,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAElD,OAAO,EAAE,GAAG,EAAE,CAAC;AACjB,CAAC"}
@@ -0,0 +1,60 @@
1
+ import type { DomainModel } from "../../shared/types/domain.js";
2
+ import type { FederationManifest, PeerSpec } from "../../shared/types/federation.js";
3
+ /** Per-peer resolution outcome surfaced to the loader/caller. */
4
+ export interface PeerResolution {
5
+ /** The peer's repo root on disk (where its `.dkk/` lives), if reachable. */
6
+ peerRoot: string | null;
7
+ /** True when the peer's `.dkk/` directory exists on disk. */
8
+ reachable: boolean;
9
+ /** A short reason string used for warnings (e.g. "git cache empty"). */
10
+ reason?: string;
11
+ }
12
+ /**
13
+ * Read `.dkk/federation.yml` (if present), validate it against
14
+ * `federation.schema.json`, and return the parsed manifest. Returns
15
+ * `null` for unfederated repos.
16
+ *
17
+ * Throws when the file exists but does not conform to the schema —
18
+ * the message includes Ajv's error list so the user can fix the
19
+ * manifest directly.
20
+ */
21
+ export declare function loadFederation(root?: string): FederationManifest | null;
22
+ /**
23
+ * Resolve a single peer spec into an absolute repo-root path on disk.
24
+ *
25
+ * - `local` sources resolve relative to the local repo root (so
26
+ * `../order-svc` in `billing-svc/.dkk/federation.yml` points at the
27
+ * sibling directory regardless of `cwd`). Env-var override
28
+ * `DKK_PEER_<SERVICE_NAME_UPPER>` (uppercase, hyphens → underscores)
29
+ * takes precedence over the manifest's `source.path`.
30
+ * - `git` sources resolve to the cache directory
31
+ * `.dkk/imports/<service>/` populated by `dkk pull`.
32
+ *
33
+ * The returned `peerRoot` points at the peer's repository root (so
34
+ * `<peerRoot>/.dkk/` is where the peer's domain lives).
35
+ */
36
+ export declare function resolvePeerRoot(spec: PeerSpec, localRepoRoot: string): PeerResolution;
37
+ /**
38
+ * Load a peer's domain model in "peer mode": one level deep (peer's
39
+ * own `federation.yml` is skipped) and resilient to minor schema
40
+ * drift. The peer's model is structurally identical to a local model
41
+ * so the same `loadDomainModel` is reused; the federation pass is
42
+ * suppressed via the `followPeers: false` option.
43
+ */
44
+ export declare function loadPeerModel(peerRoot: string): DomainModel;
45
+ /**
46
+ * Resolve and load every peer declared in the manifest. Unreachable
47
+ * peers are reported via `warnings` (each line is one peer) but never
48
+ * abort the load — the caller (typically the main loader) attaches
49
+ * the resulting map to `model.peers`.
50
+ */
51
+ export declare function loadAllPeers(localRepoRoot: string, manifest: FederationManifest): {
52
+ peers: Map<string, DomainModel>;
53
+ warnings: string[];
54
+ };
55
+ /**
56
+ * Build the env-var key used to override a peer's source path.
57
+ * Exposed for tests and for the `peers status` command.
58
+ */
59
+ export declare function peerEnvKey(serviceName: string): string;
60
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/features/federation/loader.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EACV,kBAAkB,EAClB,QAAQ,EACT,MAAM,kCAAkC,CAAC;AAuB1C,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,6DAA6D;IAC7D,SAAS,EAAE,OAAO,CAAC;IACnB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAkBvE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,GAAG,cAAc,CAoDrF;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAE3D;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,kBAAkB,GAC3B;IAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAoBzD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEtD"}
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Federation loader — resolves peer services declared in
3
+ * `.dkk/federation.yml` and loads each peer's `.dkk/` as a read-only
4
+ * sub-model attached at `model.peers.get(serviceName)`.
5
+ *
6
+ * Peer loading is exactly one level deep: a peer's own `federation.yml`
7
+ * is intentionally NOT followed. Peers are loaded in lenient mode so
8
+ * that DKK schema drift across services (peer YAML using fields the
9
+ * local DKK version doesn't understand) degrades to a warning rather
10
+ * than failing the consumer's load.
11
+ *
12
+ * Two source types are supported:
13
+ * - `local`: a filesystem path (absolute or relative to repo root).
14
+ * - `git`: resolved against the cache at `.dkk/imports/<service>/`
15
+ * populated by `dkk pull` (Phase 3).
16
+ */
17
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
18
+ import { isAbsolute, resolve, join } from "node:path";
19
+ import { createRequire } from "node:module";
20
+ import { parseYaml } from "../../shared/yaml.js";
21
+ import { federationFile, importedServiceDir, schemaDir, repoRoot } from "../../shared/paths.js";
22
+ import { loadDomainModel, setFederationHook } from "../../shared/loader.js";
23
+ // ajv is a CJS package — use createRequire for ESM interop.
24
+ const require = createRequire(import.meta.url);
25
+ const Ajv = require("ajv").default;
26
+ const addFormats = require("ajv-formats").default;
27
+ /** Cached Ajv instance for federation.yml validation. */
28
+ let cachedAjv = null;
29
+ function getAjv() {
30
+ if (cachedAjv)
31
+ return cachedAjv;
32
+ const ajv = new Ajv({ allErrors: true, strict: true });
33
+ addFormats(ajv);
34
+ const dir = schemaDir();
35
+ for (const f of readdirSync(dir).filter((n) => n.endsWith(".schema.json"))) {
36
+ const schema = JSON.parse(readFileSync(join(dir, f), "utf-8"));
37
+ ajv.addSchema(schema, schema.$id);
38
+ }
39
+ cachedAjv = ajv;
40
+ return ajv;
41
+ }
42
+ /**
43
+ * Read `.dkk/federation.yml` (if present), validate it against
44
+ * `federation.schema.json`, and return the parsed manifest. Returns
45
+ * `null` for unfederated repos.
46
+ *
47
+ * Throws when the file exists but does not conform to the schema —
48
+ * the message includes Ajv's error list so the user can fix the
49
+ * manifest directly.
50
+ */
51
+ export function loadFederation(root) {
52
+ const path = federationFile(root);
53
+ if (!existsSync(path))
54
+ return null;
55
+ const text = readFileSync(path, "utf-8");
56
+ const parsed = parseYaml(text);
57
+ const ajv = getAjv();
58
+ const valid = ajv.validate("federation.schema.json", parsed);
59
+ if (!valid) {
60
+ const details = (ajv.errors ?? [])
61
+ .map((e) => ` - ${e.instancePath || "/"}: ${e.message ?? "invalid"}`)
62
+ .join("\n");
63
+ throw new Error(`Invalid ${path}:\n${details}\n\nExpected shape: { peers: [{ name, source: { type: "local" | "git", ... } }] }`);
64
+ }
65
+ return parsed;
66
+ }
67
+ /**
68
+ * Resolve a single peer spec into an absolute repo-root path on disk.
69
+ *
70
+ * - `local` sources resolve relative to the local repo root (so
71
+ * `../order-svc` in `billing-svc/.dkk/federation.yml` points at the
72
+ * sibling directory regardless of `cwd`). Env-var override
73
+ * `DKK_PEER_<SERVICE_NAME_UPPER>` (uppercase, hyphens → underscores)
74
+ * takes precedence over the manifest's `source.path`.
75
+ * - `git` sources resolve to the cache directory
76
+ * `.dkk/imports/<service>/` populated by `dkk pull`.
77
+ *
78
+ * The returned `peerRoot` points at the peer's repository root (so
79
+ * `<peerRoot>/.dkk/` is where the peer's domain lives).
80
+ */
81
+ export function resolvePeerRoot(spec, localRepoRoot) {
82
+ const source = spec.source;
83
+ // Env-var override: applies to any source type for the convenience of
84
+ // developers who want to point at a local checkout regardless of
85
+ // what the committed manifest says.
86
+ const envKey = `DKK_PEER_${spec.name.toUpperCase().replace(/-/g, "_")}`;
87
+ const envOverride = process.env[envKey];
88
+ if (envOverride && envOverride.length > 0) {
89
+ const peerRoot = isAbsolute(envOverride)
90
+ ? envOverride
91
+ : resolve(localRepoRoot, envOverride);
92
+ const reachable = existsSync(peerRoot + "/.dkk");
93
+ return {
94
+ peerRoot,
95
+ reachable,
96
+ reason: reachable ? undefined : `env override ${envKey} points at ${peerRoot} but it has no .dkk/`,
97
+ };
98
+ }
99
+ if (source.type === "local") {
100
+ const peerRoot = isAbsolute(source.path)
101
+ ? source.path
102
+ : resolve(localRepoRoot, source.path);
103
+ const reachable = existsSync(peerRoot + "/.dkk");
104
+ return {
105
+ peerRoot,
106
+ reachable,
107
+ reason: reachable ? undefined : `local path ${peerRoot} has no .dkk/`,
108
+ };
109
+ }
110
+ if (source.type === "git") {
111
+ const cacheRoot = importedServiceDir(spec.name, localRepoRoot);
112
+ // When the peer's `.dkk/` lives in a sub-directory of its repo
113
+ // (monorepo case), the manifest's `source.path` names that
114
+ // sub-directory; the sparse-checkout pulls it into the cache at
115
+ // the same relative location.
116
+ const peerRoot = source.path
117
+ ? `${cacheRoot}/${source.path.replace(/\/$/, "")}`
118
+ : cacheRoot;
119
+ const reachable = existsSync(peerRoot + "/.dkk");
120
+ return {
121
+ peerRoot,
122
+ reachable,
123
+ reason: reachable ? undefined : `git cache empty for "${spec.name}" — run \`dkk pull ${spec.name}\``,
124
+ };
125
+ }
126
+ // Exhaustiveness check — future source types should be added here.
127
+ const exhaustive = source;
128
+ return { peerRoot: null, reachable: false, reason: `unknown source type: ${JSON.stringify(exhaustive)}` };
129
+ }
130
+ /**
131
+ * Load a peer's domain model in "peer mode": one level deep (peer's
132
+ * own `federation.yml` is skipped) and resilient to minor schema
133
+ * drift. The peer's model is structurally identical to a local model
134
+ * so the same `loadDomainModel` is reused; the federation pass is
135
+ * suppressed via the `followPeers: false` option.
136
+ */
137
+ export function loadPeerModel(peerRoot) {
138
+ return loadDomainModel({ root: peerRoot, followPeers: false });
139
+ }
140
+ /**
141
+ * Resolve and load every peer declared in the manifest. Unreachable
142
+ * peers are reported via `warnings` (each line is one peer) but never
143
+ * abort the load — the caller (typically the main loader) attaches
144
+ * the resulting map to `model.peers`.
145
+ */
146
+ export function loadAllPeers(localRepoRoot, manifest) {
147
+ const peers = new Map();
148
+ const warnings = [];
149
+ for (const spec of manifest.peers ?? []) {
150
+ const resolution = resolvePeerRoot(spec, localRepoRoot);
151
+ if (!resolution.reachable || !resolution.peerRoot) {
152
+ warnings.push(`peer "${spec.name}" unreachable: ${resolution.reason ?? "unknown"}`);
153
+ continue;
154
+ }
155
+ try {
156
+ const model = loadPeerModel(resolution.peerRoot);
157
+ peers.set(spec.name, model);
158
+ }
159
+ catch (err) {
160
+ const msg = err instanceof Error ? err.message : String(err);
161
+ warnings.push(`peer "${spec.name}" failed to load: ${msg}`);
162
+ }
163
+ }
164
+ return { peers, warnings };
165
+ }
166
+ /**
167
+ * Build the env-var key used to override a peer's source path.
168
+ * Exposed for tests and for the `peers status` command.
169
+ */
170
+ export function peerEnvKey(serviceName) {
171
+ return `DKK_PEER_${serviceName.toUpperCase().replace(/-/g, "_")}`;
172
+ }
173
+ // ── Hook registration ────────────────────────────────────────────────
174
+ //
175
+ // Register the peer-hydration hook with the shared loader at module
176
+ // initialisation. Any CLI command that imports this slice (directly or
177
+ // transitively via the federation commands wired in cli.ts) will cause
178
+ // `loadDomainModel` to start populating `model.peers`. Scripts that
179
+ // import the shared loader without the federation slice get plain
180
+ // unfederated behaviour — no surprises, no cycles.
181
+ setFederationHook((root, model) => {
182
+ const manifest = loadFederation(root);
183
+ if (!manifest)
184
+ return;
185
+ const resolvedRoot = repoRoot(root);
186
+ const { peers, warnings } = loadAllPeers(resolvedRoot, manifest);
187
+ if (peers.size > 0)
188
+ model.peers = peers;
189
+ for (const w of warnings) {
190
+ console.warn(`dkk: ${w}`);
191
+ }
192
+ });
193
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/features/federation/loader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { FederationLock, LockEntry } from "../../shared/types/federation.js";
2
+ /**
3
+ * Read the lockfile from disk. Returns an empty record when the file
4
+ * is absent. Malformed JSON throws (the file is committed, so a parse
5
+ * failure should be loud).
6
+ */
7
+ export declare function readLock(root?: string): FederationLock;
8
+ /** Write the lockfile to disk with stable formatting. */
9
+ export declare function writeLock(lock: FederationLock, root?: string): void;
10
+ /** Build a fresh lock entry from a fetch result. */
11
+ export declare function makeEntry(source: LockEntry["source"], sha: string | undefined): LockEntry;
12
+ //# sourceMappingURL=lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../../src/features/federation/lock.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElF;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,cAAc,CAMtD;AAED,yDAAyD;AACzD,wBAAgB,SAAS,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CASnE;AAED,oDAAoD;AACpD,wBAAgB,SAAS,CACvB,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,EAC3B,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,SAAS,CAKX"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Federation lockfile (.dkk/federation.lock.json).
3
+ *
4
+ * Records the resolved commit SHA each git-source peer was fetched at,
5
+ * along with a snapshot of the source spec used. Committing the
6
+ * lockfile means two developers running `dkk pull` see the same peer
7
+ * state until someone explicitly re-pulls with `--refresh`.
8
+ *
9
+ * Local-source peers have no lockfile entry — they're always live from
10
+ * disk.
11
+ */
12
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
13
+ import { dirname } from "node:path";
14
+ import { federationLockFile } from "../../shared/paths.js";
15
+ /**
16
+ * Read the lockfile from disk. Returns an empty record when the file
17
+ * is absent. Malformed JSON throws (the file is committed, so a parse
18
+ * failure should be loud).
19
+ */
20
+ export function readLock(root) {
21
+ const path = federationLockFile(root);
22
+ if (!existsSync(path))
23
+ return {};
24
+ const text = readFileSync(path, "utf-8");
25
+ if (text.trim().length === 0)
26
+ return {};
27
+ return JSON.parse(text);
28
+ }
29
+ /** Write the lockfile to disk with stable formatting. */
30
+ export function writeLock(lock, root) {
31
+ const path = federationLockFile(root);
32
+ mkdirSync(dirname(path), { recursive: true });
33
+ // Stable key ordering (sorted by service name) keeps diffs minimal.
34
+ const sorted = {};
35
+ for (const key of Object.keys(lock).sort()) {
36
+ sorted[key] = lock[key];
37
+ }
38
+ writeFileSync(path, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
39
+ }
40
+ /** Build a fresh lock entry from a fetch result. */
41
+ export function makeEntry(source, sha) {
42
+ const entry = { source };
43
+ if (sha)
44
+ entry.sha = sha;
45
+ entry.fetchedAt = new Date().toISOString();
46
+ return entry;
47
+ }
48
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../../src/features/federation/lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAG3D;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAa;IACpC,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACzC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACxC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAmB,CAAC;AAC5C,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,SAAS,CAAC,IAAoB,EAAE,IAAa;IAC3D,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACtC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,oEAAoE;IACpE,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IACD,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;AACvE,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,SAAS,CACvB,MAA2B,EAC3B,GAAuB;IAEvB,MAAM,KAAK,GAAc,EAAE,MAAM,EAAE,CAAC;IACpC,IAAI,GAAG;QAAE,KAAK,CAAC,GAAG,GAAG,GAAG,CAAC;IACzB,KAAK,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=git-fetcher.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-fetcher.test.d.ts","sourceRoot":"","sources":["../../../../src/features/federation/tests/git-fetcher.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Tests for the sparse-checkout git fetcher.
3
+ *
4
+ * Uses a local bare repository as the "remote" so the test runs
5
+ * fully offline. Verifies that:
6
+ * - sparseFetch clones with a `.dkk/` sparse pattern.
7
+ * - Only `.dkk/` content is materialised; other top-level files are
8
+ * excluded by the sparse rule.
9
+ * - The returned SHA matches `HEAD` of the configured branch.
10
+ * - `dkk pull` populates the cache and writes a lockfile entry.
11
+ * - A subsequent `dkk pull` is a no-op (the SHA in the lock matches).
12
+ * - `dkk pull --refresh` re-fetches.
13
+ *
14
+ * If `git` is not available on PATH, the test is skipped (we don't
15
+ * fail CI on environments without git).
16
+ */
17
+ import { execFileSync, spawnSync } from "node:child_process";
18
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, realpathSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { tmpdir } from "node:os";
21
+ import { sparseFetch } from "../git-fetcher.js";
22
+ let passed = 0;
23
+ let failed = 0;
24
+ let skipped = false;
25
+ function assert(label, condition, detail) {
26
+ if (condition) {
27
+ console.log(` OK: ${label}`);
28
+ passed++;
29
+ }
30
+ else {
31
+ console.error(`FAIL: ${label}${detail ? ` — ${detail}` : ""}`);
32
+ failed++;
33
+ }
34
+ }
35
+ function gitAvailable() {
36
+ try {
37
+ execFileSync("git", ["--version"], { stdio: "ignore" });
38
+ return true;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ if (!gitAvailable()) {
45
+ console.log("git not available on PATH — skipping git-fetcher tests");
46
+ skipped = true;
47
+ }
48
+ if (!skipped) {
49
+ const RAW_TMP = join(tmpdir(), `dkk-git-${Date.now()}`);
50
+ mkdirSync(RAW_TMP, { recursive: true });
51
+ const TMP = realpathSync(RAW_TMP);
52
+ // Layout:
53
+ // <TMP>/remote/ — a bare git repo (the "remote")
54
+ // <TMP>/source/ — a regular repo we'll push from
55
+ // .dkk/ — the only content sparseFetch should pull
56
+ // service.yml
57
+ // domain/.../OrderPlaced.yml
58
+ // README.md — should NOT appear in the cache
59
+ // <TMP>/local/.dkk/imports/ordering/ — destination cache
60
+ const REMOTE = join(TMP, "remote");
61
+ const SOURCE = join(TMP, "source");
62
+ const LOCAL = join(TMP, "local");
63
+ const CACHE_DIR = join(LOCAL, ".dkk", "imports", "ordering");
64
+ function run(args, cwd) {
65
+ const result = spawnSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" });
66
+ if (result.status !== 0) {
67
+ throw new Error(`git ${args.join(" ")} failed in ${cwd}\nstderr: ${result.stderr}`);
68
+ }
69
+ return result.stdout.trim();
70
+ }
71
+ try {
72
+ // Create the bare remote.
73
+ mkdirSync(REMOTE, { recursive: true });
74
+ run(["init", "--bare", "--initial-branch=main"], REMOTE);
75
+ // Create the source repo with .dkk/ content + a noise file.
76
+ mkdirSync(join(SOURCE, ".dkk", "domain", "contexts", "ordering", "events"), {
77
+ recursive: true,
78
+ });
79
+ writeFileSync(join(SOURCE, ".dkk", "service.yml"), "name: ordering\nexports:\n - ordering\n");
80
+ writeFileSync(join(SOURCE, ".dkk", "domain", "index.yml"), "contexts:\n - name: ordering\n description: Ordering context\nflows: []\n");
81
+ writeFileSync(join(SOURCE, ".dkk", "domain", "actors.yml"), "actors: []\n");
82
+ writeFileSync(join(SOURCE, ".dkk", "domain", "contexts", "ordering", "context.yml"), "name: ordering\ndescription: Ordering bounded context\n");
83
+ writeFileSync(join(SOURCE, ".dkk", "domain", "contexts", "ordering", "events", "OrderPlaced.yml"), "name: OrderPlaced\ndescription: Raised when an order is placed.\n");
84
+ writeFileSync(join(SOURCE, "README.md"), "# Should NOT be in the sparse checkout\n");
85
+ run(["init", "--initial-branch=main"], SOURCE);
86
+ run(["config", "user.email", "test@example.com"], SOURCE);
87
+ run(["config", "user.name", "Test"], SOURCE);
88
+ run(["config", "commit.gpgsign", "false"], SOURCE);
89
+ run(["add", "."], SOURCE);
90
+ run(["commit", "-m", "initial"], SOURCE);
91
+ run(["remote", "add", "origin", REMOTE], SOURCE);
92
+ run(["push", "-u", "origin", "main"], SOURCE);
93
+ const sourceHead = run(["rev-parse", "HEAD"], SOURCE);
94
+ // ── sparseFetch pulls only .dkk/ ──────────────────────────────────
95
+ console.log("\n=== sparseFetch ===");
96
+ const result = sparseFetch({
97
+ url: REMOTE,
98
+ branch: "main",
99
+ subpath: "",
100
+ dest: CACHE_DIR,
101
+ });
102
+ assert("returned SHA matches source HEAD", result.sha === sourceHead);
103
+ assert(".dkk/ exists in cache", existsSync(join(CACHE_DIR, ".dkk")));
104
+ assert("service.yml present", existsSync(join(CACHE_DIR, ".dkk", "service.yml")));
105
+ assert("OrderPlaced.yml present", existsSync(join(CACHE_DIR, ".dkk", "domain", "contexts", "ordering", "events", "OrderPlaced.yml")));
106
+ assert("README.md NOT in cache (sparse rule excluded it)", !existsSync(join(CACHE_DIR, "README.md")));
107
+ // ── dkk pull integrates the fetcher + lockfile ────────────────────
108
+ console.log("\n=== dkk pull (CLI integration) ===");
109
+ // Set up a `local` repo with a federation manifest pointing at the
110
+ // bare remote. Wipe the cache from the previous step so `pull` has
111
+ // to re-fetch.
112
+ rmSync(join(LOCAL, ".dkk"), { recursive: true, force: true });
113
+ mkdirSync(join(LOCAL, ".dkk", "domain"), { recursive: true });
114
+ writeFileSync(join(LOCAL, ".dkk", "service.yml"), "name: billing\nexports:\n - billing\n");
115
+ writeFileSync(join(LOCAL, ".dkk", "federation.yml"), [
116
+ "peers:",
117
+ " - name: ordering",
118
+ " source:",
119
+ " type: git",
120
+ ` url: ${REMOTE}`,
121
+ " branch: main",
122
+ ].join("\n") + "\n");
123
+ const cliEntry = join(process.cwd(), "src", "cli.ts");
124
+ const pullResult = spawnSync("npx", ["tsx", cliEntry, "pull", "--root", LOCAL, "--json"], { encoding: "utf-8" });
125
+ if (pullResult.status !== 0) {
126
+ throw new Error(`dkk pull failed: ${pullResult.stderr}\n${pullResult.stdout}`);
127
+ }
128
+ const pullReport = JSON.parse(pullResult.stdout);
129
+ assert("pull report has one peer", pullReport.peers.length === 1);
130
+ assert("ordering peer fetched", pullReport.peers[0].name === "ordering" && pullReport.peers[0].outcome === "fetched");
131
+ assert("lockfile sha matches source HEAD", pullReport.peers[0].sha === sourceHead);
132
+ assert("lockfile written to disk", existsSync(join(LOCAL, ".dkk", "federation.lock.json")));
133
+ // Subsequent pull is a no-op.
134
+ const pullAgain = spawnSync("npx", ["tsx", cliEntry, "pull", "--root", LOCAL, "--json"], { encoding: "utf-8" });
135
+ const pullAgainReport = JSON.parse(pullAgain.stdout);
136
+ assert("second pull is cached (no-op)", pullAgainReport.peers[0].outcome === "cached");
137
+ // --refresh forces re-fetch even when cached.
138
+ const pullRefresh = spawnSync("npx", ["tsx", cliEntry, "pull", "--root", LOCAL, "--refresh", "--json"], { encoding: "utf-8" });
139
+ const pullRefreshReport = JSON.parse(pullRefresh.stdout);
140
+ assert("--refresh re-fetches even when cached", pullRefreshReport.peers[0].outcome === "fetched");
141
+ // --offline with cache present is a no-op.
142
+ const pullOffline = spawnSync("npx", ["tsx", cliEntry, "pull", "--root", LOCAL, "--offline", "--json"], { encoding: "utf-8" });
143
+ const pullOfflineReport = JSON.parse(pullOffline.stdout);
144
+ assert("--offline with cache returns skipped-offline-cached", pullOfflineReport.peers[0].outcome === "skipped-offline-cached");
145
+ // Verify lockfile contents.
146
+ const lock = JSON.parse(readFileSync(join(LOCAL, ".dkk", "federation.lock.json"), "utf-8"));
147
+ assert("lockfile has ordering entry", lock.ordering !== undefined);
148
+ assert("lockfile entry has git source", lock.ordering.source.type === "git");
149
+ assert("lockfile entry has sha", typeof lock.ordering.sha === "string" && lock.ordering.sha.length > 0);
150
+ // ── Federation loader sees the cached peer ────────────────────────
151
+ console.log("\n=== loader sees cached peer ===");
152
+ const showResult = spawnSync("npx", ["tsx", cliEntry, "show", "ordering:OrderPlaced", "--root", LOCAL, "--json"], { encoding: "utf-8" });
153
+ if (showResult.status !== 0) {
154
+ throw new Error(`dkk show failed: ${showResult.stderr}\n${showResult.stdout}`);
155
+ }
156
+ const shown = JSON.parse(showResult.stdout);
157
+ assert("peer item resolved via cache", shown.data?.name === "OrderPlaced");
158
+ assert("peer label includes [peer: ordering]", shown.label.includes("peer: ordering"));
159
+ }
160
+ finally {
161
+ rmSync(RAW_TMP, { recursive: true, force: true });
162
+ }
163
+ }
164
+ console.log(`\n${passed} passed, ${failed} failed${skipped ? " (test suite skipped)" : ""}`);
165
+ if (failed > 0)
166
+ process.exit(1);
167
+ //# sourceMappingURL=git-fetcher.test.js.map