dna-sdk 0.1.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 (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/adapters/embedding/onnx.d.ts +32 -0
  4. package/dist/adapters/embedding/onnx.js +51 -0
  5. package/dist/adapters/filesystem/cache.d.ts +16 -0
  6. package/dist/adapters/filesystem/cache.js +113 -0
  7. package/dist/adapters/filesystem/index.d.ts +2 -0
  8. package/dist/adapters/filesystem/index.js +2 -0
  9. package/dist/adapters/filesystem/source.d.ts +82 -0
  10. package/dist/adapters/filesystem/source.js +357 -0
  11. package/dist/adapters/postgres/index.d.ts +2 -0
  12. package/dist/adapters/postgres/index.js +2 -0
  13. package/dist/adapters/postgres/migrations.d.ts +14 -0
  14. package/dist/adapters/postgres/migrations.js +88 -0
  15. package/dist/adapters/postgres/source.d.ts +172 -0
  16. package/dist/adapters/postgres/source.js +569 -0
  17. package/dist/adapters/resolvers/github.d.ts +12 -0
  18. package/dist/adapters/resolvers/github.js +47 -0
  19. package/dist/adapters/resolvers/http.d.ts +22 -0
  20. package/dist/adapters/resolvers/http.js +145 -0
  21. package/dist/adapters/resolvers/index.d.ts +3 -0
  22. package/dist/adapters/resolvers/index.js +3 -0
  23. package/dist/adapters/resolvers/local.d.ts +24 -0
  24. package/dist/adapters/resolvers/local.js +132 -0
  25. package/dist/adapters/search/driver.d.ts +45 -0
  26. package/dist/adapters/search/driver.js +138 -0
  27. package/dist/adapters/search/migrations.d.ts +26 -0
  28. package/dist/adapters/search/migrations.js +58 -0
  29. package/dist/adapters/search/rrf.d.ts +23 -0
  30. package/dist/adapters/search/rrf.js +42 -0
  31. package/dist/adapters/search/sqlite-vec.d.ts +66 -0
  32. package/dist/adapters/search/sqlite-vec.js +294 -0
  33. package/dist/bootstrap.d.ts +45 -0
  34. package/dist/bootstrap.js +134 -0
  35. package/dist/extensions/agentskills/DOCS.md +18 -0
  36. package/dist/extensions/agentskills.d.ts +28 -0
  37. package/dist/extensions/agentskills.js +252 -0
  38. package/dist/extensions/agentsmd/DOCS.md +18 -0
  39. package/dist/extensions/agentsmd.d.ts +22 -0
  40. package/dist/extensions/agentsmd.js +170 -0
  41. package/dist/extensions/audit/kinds/audit-log.kind.yaml +148 -0
  42. package/dist/extensions/audit.d.ts +25 -0
  43. package/dist/extensions/audit.js +62 -0
  44. package/dist/extensions/automation/kinds/automation.kind.yaml +296 -0
  45. package/dist/extensions/automation/query.d.ts +49 -0
  46. package/dist/extensions/automation/query.js +77 -0
  47. package/dist/extensions/automation/write-guards.d.ts +59 -0
  48. package/dist/extensions/automation/write-guards.js +153 -0
  49. package/dist/extensions/automation.d.ts +41 -0
  50. package/dist/extensions/automation.js +54 -0
  51. package/dist/extensions/collab.d.ts +16 -0
  52. package/dist/extensions/collab.js +79 -0
  53. package/dist/extensions/doc/kinds/doc.kind.yaml +118 -0
  54. package/dist/extensions/doc.d.ts +25 -0
  55. package/dist/extensions/doc.js +10 -0
  56. package/dist/extensions/eval/kinds/eval-baseline.kind.yaml +65 -0
  57. package/dist/extensions/eval/kinds/eval-case.kind.yaml +140 -0
  58. package/dist/extensions/eval/kinds/eval-run.kind.yaml +134 -0
  59. package/dist/extensions/eval/kinds/eval-suite.kind.yaml +87 -0
  60. package/dist/extensions/eval.d.ts +24 -0
  61. package/dist/extensions/eval.js +31 -0
  62. package/dist/extensions/evidence/kinds/evidence.kind.yaml +92 -0
  63. package/dist/extensions/evidence.d.ts +17 -0
  64. package/dist/extensions/evidence.js +94 -0
  65. package/dist/extensions/federation.d.ts +22 -0
  66. package/dist/extensions/federation.js +173 -0
  67. package/dist/extensions/guardrails/DOCS.md +20 -0
  68. package/dist/extensions/guardrails.d.ts +31 -0
  69. package/dist/extensions/guardrails.js +228 -0
  70. package/dist/extensions/helix/DOCS-Actor.md +20 -0
  71. package/dist/extensions/helix/DOCS-Agent.md +20 -0
  72. package/dist/extensions/helix/DOCS-UseCase.md +20 -0
  73. package/dist/extensions/helix/layers.d.ts +12 -0
  74. package/dist/extensions/helix/layers.js +18 -0
  75. package/dist/extensions/helix/write-guards.d.ts +70 -0
  76. package/dist/extensions/helix/write-guards.js +172 -0
  77. package/dist/extensions/helix.d.ts +29 -0
  78. package/dist/extensions/helix.js +963 -0
  79. package/dist/extensions/helix_extras.d.ts +124 -0
  80. package/dist/extensions/helix_extras.js +328 -0
  81. package/dist/extensions/hooks.d.ts +31 -0
  82. package/dist/extensions/hooks.js +247 -0
  83. package/dist/extensions/kinddef/DOCS.md +24 -0
  84. package/dist/extensions/kinddef.d.ts +40 -0
  85. package/dist/extensions/kinddef.js +171 -0
  86. package/dist/extensions/lesson.d.ts +6 -0
  87. package/dist/extensions/lesson.js +194 -0
  88. package/dist/extensions/modelreg/kinds/model-profile.kind.yaml +155 -0
  89. package/dist/extensions/modelreg.d.ts +30 -0
  90. package/dist/extensions/modelreg.js +38 -0
  91. package/dist/extensions/recognizer.d.ts +34 -0
  92. package/dist/extensions/recognizer.js +272 -0
  93. package/dist/extensions/research.d.ts +32 -0
  94. package/dist/extensions/research.js +292 -0
  95. package/dist/extensions/safety.d.ts +33 -0
  96. package/dist/extensions/safety.js +230 -0
  97. package/dist/extensions/sdlc/kinds/adr.kind.yaml +136 -0
  98. package/dist/extensions/sdlc/kinds/archive-proposal.kind.yaml +107 -0
  99. package/dist/extensions/sdlc/kinds/changelog.kind.yaml +108 -0
  100. package/dist/extensions/sdlc/kinds/cognitive-policy.kind.yaml +470 -0
  101. package/dist/extensions/sdlc/kinds/forecast.kind.yaml +125 -0
  102. package/dist/extensions/sdlc/kinds/insight.kind.yaml +87 -0
  103. package/dist/extensions/sdlc/kinds/kaizen.kind.yaml +106 -0
  104. package/dist/extensions/sdlc/kinds/lesson-learned.kind.yaml +270 -0
  105. package/dist/extensions/sdlc/kinds/narrative.kind.yaml +189 -0
  106. package/dist/extensions/sdlc/kinds/pattern-insight.kind.yaml +204 -0
  107. package/dist/extensions/sdlc/kinds/postmortem.kind.yaml +168 -0
  108. package/dist/extensions/sdlc/kinds/pre-mortem.kind.yaml +145 -0
  109. package/dist/extensions/sdlc/kinds/prompt-template.kind.yaml +109 -0
  110. package/dist/extensions/sdlc/kinds/retrospective.kind.yaml +186 -0
  111. package/dist/extensions/sdlc/kinds/risk-register.kind.yaml +178 -0
  112. package/dist/extensions/sdlc/kinds/saved-view.kind.yaml +127 -0
  113. package/dist/extensions/sdlc/kinds/status-report.kind.yaml +129 -0
  114. package/dist/extensions/sdlc/kinds/synthesis-run.kind.yaml +279 -0
  115. package/dist/extensions/sdlc/kinds/synthesizer-state.kind.yaml +196 -0
  116. package/dist/extensions/sdlc/kinds/workflow-event.kind.yaml +195 -0
  117. package/dist/extensions/sdlc.d.ts +32 -0
  118. package/dist/extensions/sdlc.js +1318 -0
  119. package/dist/extensions/soulspec/DOCS.md +19 -0
  120. package/dist/extensions/soulspec.d.ts +22 -0
  121. package/dist/extensions/soulspec.js +287 -0
  122. package/dist/extensions/tenant.d.ts +6 -0
  123. package/dist/extensions/tenant.js +291 -0
  124. package/dist/extensions/testkit.d.ts +24 -0
  125. package/dist/extensions/testkit.js +211 -0
  126. package/dist/index.d.ts +80 -0
  127. package/dist/index.js +102 -0
  128. package/dist/kernel/_text.d.ts +28 -0
  129. package/dist/kernel/_text.js +42 -0
  130. package/dist/kernel/bundle-handle.d.ts +83 -0
  131. package/dist/kernel/bundle-handle.js +184 -0
  132. package/dist/kernel/capabilities.d.ts +157 -0
  133. package/dist/kernel/capabilities.js +109 -0
  134. package/dist/kernel/collaborator-ports.d.ts +91 -0
  135. package/dist/kernel/collaborator-ports.js +1 -0
  136. package/dist/kernel/composition-resolver.d.ts +215 -0
  137. package/dist/kernel/composition-resolver.js +558 -0
  138. package/dist/kernel/composition.d.ts +10 -0
  139. package/dist/kernel/composition.js +1 -0
  140. package/dist/kernel/descriptor-loader.d.ts +13 -0
  141. package/dist/kernel/descriptor-loader.js +48 -0
  142. package/dist/kernel/document.d.ts +43 -0
  143. package/dist/kernel/document.js +71 -0
  144. package/dist/kernel/embedding.d.ts +28 -0
  145. package/dist/kernel/embedding.js +78 -0
  146. package/dist/kernel/errors.d.ts +62 -0
  147. package/dist/kernel/errors.js +82 -0
  148. package/dist/kernel/events.d.ts +6 -0
  149. package/dist/kernel/events.js +21 -0
  150. package/dist/kernel/evidence-capture.d.ts +69 -0
  151. package/dist/kernel/evidence-capture.js +138 -0
  152. package/dist/kernel/fs.d.ts +20 -0
  153. package/dist/kernel/fs.js +128 -0
  154. package/dist/kernel/generic-rw.d.ts +33 -0
  155. package/dist/kernel/generic-rw.js +235 -0
  156. package/dist/kernel/hooks.d.ts +172 -0
  157. package/dist/kernel/hooks.js +256 -0
  158. package/dist/kernel/index.d.ts +624 -0
  159. package/dist/kernel/index.js +1638 -0
  160. package/dist/kernel/instance.d.ts +211 -0
  161. package/dist/kernel/instance.js +487 -0
  162. package/dist/kernel/kind-registry.d.ts +183 -0
  163. package/dist/kernel/kind-registry.js +710 -0
  164. package/dist/kernel/kind_base.d.ts +106 -0
  165. package/dist/kernel/kind_base.js +244 -0
  166. package/dist/kernel/layer-resolver.d.ts +48 -0
  167. package/dist/kernel/layer-resolver.js +284 -0
  168. package/dist/kernel/lock-manager.d.ts +21 -0
  169. package/dist/kernel/lock-manager.js +43 -0
  170. package/dist/kernel/lock.d.ts +38 -0
  171. package/dist/kernel/lock.js +115 -0
  172. package/dist/kernel/meta.d.ts +135 -0
  173. package/dist/kernel/meta.js +817 -0
  174. package/dist/kernel/models.d.ts +3089 -0
  175. package/dist/kernel/models.js +764 -0
  176. package/dist/kernel/navigator.d.ts +36 -0
  177. package/dist/kernel/navigator.js +132 -0
  178. package/dist/kernel/port-surface.d.ts +33 -0
  179. package/dist/kernel/port-surface.js +180 -0
  180. package/dist/kernel/preview.d.ts +60 -0
  181. package/dist/kernel/preview.js +54 -0
  182. package/dist/kernel/prompt-budget.d.ts +39 -0
  183. package/dist/kernel/prompt-budget.js +43 -0
  184. package/dist/kernel/prompt-builder.d.ts +25 -0
  185. package/dist/kernel/prompt-builder.js +237 -0
  186. package/dist/kernel/protocols.d.ts +751 -0
  187. package/dist/kernel/protocols.js +493 -0
  188. package/dist/kernel/reports.d.ts +26 -0
  189. package/dist/kernel/reports.js +215 -0
  190. package/dist/kernel/resolver.d.ts +146 -0
  191. package/dist/kernel/resolver.js +232 -0
  192. package/dist/kernel/resource.d.ts +57 -0
  193. package/dist/kernel/resource.js +97 -0
  194. package/dist/kernel/runtime.d.ts +16 -0
  195. package/dist/kernel/runtime.js +18 -0
  196. package/dist/kernel/safety-scanner.d.ts +38 -0
  197. package/dist/kernel/safety-scanner.js +228 -0
  198. package/dist/kernel/serialize-to-files.d.ts +29 -0
  199. package/dist/kernel/serialize-to-files.js +23 -0
  200. package/dist/kernel/spec-access.d.ts +23 -0
  201. package/dist/kernel/spec-access.js +40 -0
  202. package/dist/kernel/studio_ui.d.ts +88 -0
  203. package/dist/kernel/studio_ui.js +135 -0
  204. package/dist/kernel/templates.d.ts +77 -0
  205. package/dist/kernel/templates.js +116 -0
  206. package/dist/kernel/tool-registry.d.ts +52 -0
  207. package/dist/kernel/tool-registry.js +89 -0
  208. package/dist/kernel/write-pipeline.d.ts +65 -0
  209. package/dist/kernel/write-pipeline.js +271 -0
  210. package/dist/kernel/writer-helpers.d.ts +24 -0
  211. package/dist/kernel/writer-helpers.js +42 -0
  212. package/dist/memory/decay.d.ts +31 -0
  213. package/dist/memory/decay.js +97 -0
  214. package/dist/memory/ecphory.d.ts +25 -0
  215. package/dist/memory/ecphory.js +178 -0
  216. package/dist/memory/encodingContext.d.ts +21 -0
  217. package/dist/memory/encodingContext.js +62 -0
  218. package/dist/memory/index.d.ts +23 -0
  219. package/dist/memory/index.js +23 -0
  220. package/dist/memory/memoryType.d.ts +7 -0
  221. package/dist/memory/memoryType.js +24 -0
  222. package/dist/memory/policy.d.ts +35 -0
  223. package/dist/memory/policy.js +35 -0
  224. package/dist/memory/retrieval.d.ts +21 -0
  225. package/dist/memory/retrieval.js +145 -0
  226. package/dist/testing/index.d.ts +8 -0
  227. package/dist/testing/index.js +8 -0
  228. package/dist/testing/recordSearchConformance.d.ts +43 -0
  229. package/dist/testing/recordSearchConformance.js +147 -0
  230. package/dist/viz/ascii.d.ts +7 -0
  231. package/dist/viz/ascii.js +60 -0
  232. package/dist/viz/health.d.ts +8 -0
  233. package/dist/viz/health.js +161 -0
  234. package/dist/viz/index.d.ts +10 -0
  235. package/dist/viz/index.js +10 -0
  236. package/dist/viz/matrix.d.ts +8 -0
  237. package/dist/viz/matrix.js +84 -0
  238. package/dist/viz/mermaid.d.ts +38 -0
  239. package/dist/viz/mermaid.js +706 -0
  240. package/package.json +82 -0
@@ -0,0 +1,19 @@
1
+ # Soul
2
+
3
+ A Soul defines an agent's personality, voice, and guiding principles as prose
4
+ โ€” not code. It is stored as a bundle on disk: `SOUL.md` (the canonical body)
5
+ plus optional `STYLE.md` and `soul.json` metadata. Canonical spec:
6
+ https://soulspec.org.
7
+
8
+ When an agent references a Soul via `dep_filters.soul`, the kernel flattens
9
+ the Soul content directly into the agent's system prompt every turn
10
+ (`flatten_in_context=true`). Souls are great when you want several agents to
11
+ share a voice: the Soul is the ethos, the agents are the tools.
12
+
13
+ **Key fields:** `soul_content` (the prose body), `style` overrides, optional
14
+ labels for multi-persona sets.
15
+
16
+ **Divergences from canonical soulspec.org:** the helix implementation does
17
+ not ship `IDENTITY.md` or `HEARTBEAT.md` auxiliary files. Everything lives in
18
+ the single bundle directory and helix treats the Soul as pure prompt content
19
+ rather than an evolving runtime artifact.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * SoulSpecExtension โ€” Soul kind + SoulReader.
3
+ *
4
+ * 1:1 parity with Python dna.v3.extensions.soulspec.
5
+ */
6
+ import type { BundleHandle } from "../kernel/bundle-handle.js";
7
+ import type { FSLike } from "../kernel/fs.js";
8
+ import type { ExtensionHost, Extension, SerializedFile, WriterPort } from "../kernel/protocols.js";
9
+ export declare class SoulWriter implements WriterPort {
10
+ private fs;
11
+ constructor(fs?: FSLike);
12
+ canWrite(raw: Record<string, unknown>): boolean;
13
+ write(bundle: BundleHandle, raw: Record<string, unknown>): void;
14
+ serialize(raw: Record<string, unknown>): SerializedFile[];
15
+ }
16
+ export declare class SoulSpecExtension implements Extension {
17
+ private fs;
18
+ readonly name = "soulspec";
19
+ readonly version = "1.0.0";
20
+ constructor(fs?: FSLike);
21
+ register(kernel: ExtensionHost): void;
22
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * SoulSpecExtension โ€” Soul kind + SoulReader.
3
+ *
4
+ * 1:1 parity with Python dna.v3.extensions.soulspec.
5
+ */
6
+ import yaml from "js-yaml";
7
+ import { deriveFirstLine } from "../kernel/_text.js";
8
+ import { nodeFS } from "../kernel/fs.js";
9
+ import { KindBase } from "../kernel/kind_base.js";
10
+ import { SoulSchema, SoulSpecSchema, zodSpecToJsonSchema } from "../kernel/models.js";
11
+ import { SD } from "../kernel/protocols.js";
12
+ // ---------------------------------------------------------------------------
13
+ // Frontmatter helpers
14
+ // ---------------------------------------------------------------------------
15
+ function parseSoulFrontmatter(text) {
16
+ const match = text.match(/^---\n([\s\S]*?)---\n?([\s\S]*)$/);
17
+ if (!match)
18
+ return { metadata: {}, body: text };
19
+ try {
20
+ const parsed = yaml.load(match[1]);
21
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
22
+ return {
23
+ metadata: parsed,
24
+ body: match[2].replace(/^\n+/, ""),
25
+ };
26
+ }
27
+ }
28
+ catch { }
29
+ return { metadata: {}, body: text };
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // SoulKind
33
+ // ---------------------------------------------------------------------------
34
+ class SoulKind extends KindBase {
35
+ apiVersion = "soulspec.org/v1";
36
+ kind = "Soul";
37
+ alias = "soulspec-soul";
38
+ isSchemaAffecting = true;
39
+ origin = "soulspec.org";
40
+ isPromptTarget = true;
41
+ promptTargetPriority = 1;
42
+ flattenInContext = true;
43
+ descriptionFallbackField = "soul_content";
44
+ storage = SD.bundle("souls", "SOUL.md");
45
+ graphStyle = { fill: "#8B5CF6", stroke: "#7C3AED", textColor: "#fff" };
46
+ asciiIcon = "๐Ÿง ";
47
+ displayLabel = "Souls";
48
+ _sourceUrl = import.meta.url;
49
+ docs = "A Soul defines an agent's personality, voice, and guiding principles " +
50
+ "as prose โ€” not code. Stored as a bundle: SOUL.md plus optional " +
51
+ "STYLE.md and soul.json. When an agent references a Soul, the kernel " +
52
+ "flattens it directly into the system prompt (flatten_in_context=true).";
53
+ uiSchema = {
54
+ soul_content: {
55
+ widget: "markdown-toc",
56
+ label: "SOUL.md",
57
+ help: "Main prose describing the agent's personality, voice, and principles.",
58
+ height: 480,
59
+ order: 10,
60
+ },
61
+ style_content: {
62
+ widget: "markdown",
63
+ label: "STYLE.md",
64
+ help: "Communication style, formatting conventions, tone.",
65
+ height: 260,
66
+ order: 20,
67
+ },
68
+ soul_json: {
69
+ widget: "code",
70
+ language: "json",
71
+ label: "soul.json",
72
+ help: "Structured soulspec.org metadata (specVersion, tags, etc.).",
73
+ height: 220,
74
+ order: 30,
75
+ },
76
+ agents_content: {
77
+ widget: "markdown",
78
+ label: "AGENTS.md (companion)",
79
+ help: "Optional agents.md-style workflow description.",
80
+ height: 220,
81
+ order: 40,
82
+ },
83
+ identity_content: {
84
+ widget: "markdown",
85
+ label: "IDENTITY.md",
86
+ help: "Who the agent is โ€” role, background, expertise.",
87
+ height: 220,
88
+ order: 50,
89
+ },
90
+ heartbeat_content: {
91
+ widget: "markdown",
92
+ label: "HEARTBEAT.md",
93
+ help: "Autonomous scheduled tasks โ€” cron for the agent, in plain language.",
94
+ height: 220,
95
+ order: 60,
96
+ },
97
+ };
98
+ schema() { return zodSpecToJsonSchema(SoulSpecSchema); }
99
+ parse(raw) {
100
+ return SoulSchema.parse(raw);
101
+ }
102
+ summary() { return null; }
103
+ promptTemplate() {
104
+ return "{{{soul_content}}}";
105
+ }
106
+ preview(doc) {
107
+ const spec = (doc.spec ?? {});
108
+ const blocks = [];
109
+ if (typeof spec.soul_content === "string" && spec.soul_content) {
110
+ blocks.push({ kind: "markdown", title: "SOUL.md", body: spec.soul_content });
111
+ }
112
+ if (typeof spec.style_content === "string" && spec.style_content) {
113
+ blocks.push({ kind: "markdown", title: "STYLE.md", body: spec.style_content });
114
+ }
115
+ if (spec.soul_json && typeof spec.soul_json === "object") {
116
+ blocks.push({
117
+ kind: "code",
118
+ title: "soul.json",
119
+ body: JSON.stringify(spec.soul_json, null, 2),
120
+ language: "json",
121
+ });
122
+ }
123
+ if (typeof spec.agents_content === "string" && spec.agents_content) {
124
+ blocks.push({
125
+ kind: "markdown",
126
+ title: "AGENTS.md (companion)",
127
+ body: spec.agents_content,
128
+ });
129
+ }
130
+ if (typeof spec.identity_content === "string" && spec.identity_content) {
131
+ blocks.push({ kind: "markdown", title: "IDENTITY.md", body: spec.identity_content });
132
+ }
133
+ if (typeof spec.heartbeat_content === "string" && spec.heartbeat_content) {
134
+ blocks.push({ kind: "markdown", title: "HEARTBEAT.md", body: spec.heartbeat_content });
135
+ }
136
+ if (blocks.length === 0) {
137
+ return [{ kind: "empty", title: "Soul (empty)" }];
138
+ }
139
+ return blocks;
140
+ }
141
+ }
142
+ // ---------------------------------------------------------------------------
143
+ // SoulReader
144
+ // ---------------------------------------------------------------------------
145
+ class SoulReader {
146
+ fs;
147
+ constructor(fs) {
148
+ this.fs = fs;
149
+ }
150
+ detect(bundle) {
151
+ const path = bundle.path ?? "";
152
+ return this.fs.exists(`${path}/SOUL.md`) || this.fs.exists(`${path}/soul.json`);
153
+ }
154
+ read(bundle) {
155
+ const path = bundle.path ?? "";
156
+ const name = path.split("/").pop() || "";
157
+ const spec = {};
158
+ const metadata = {};
159
+ // Read SOUL.md โ€” parse frontmatter if present
160
+ if (this.fs.exists(`${path}/SOUL.md`)) {
161
+ const text = this.fs.readFile(`${path}/SOUL.md`);
162
+ const { metadata: fm, body } = parseSoulFrontmatter(text);
163
+ if (Object.keys(fm).length > 0) {
164
+ Object.assign(metadata, fm);
165
+ spec.soul_content = body;
166
+ }
167
+ else {
168
+ spec.soul_content = text;
169
+ }
170
+ }
171
+ // Read soul.json
172
+ if (this.fs.exists(`${path}/soul.json`)) {
173
+ const parsed = JSON.parse(this.fs.readFile(`${path}/soul.json`));
174
+ spec.soul_json = parsed;
175
+ if (!spec.soul_content) {
176
+ spec.soul_content = JSON.stringify(parsed, null, 2);
177
+ }
178
+ }
179
+ // Companion files โ€” soulspec.org standard
180
+ const companions = [
181
+ ["style_content", "STYLE.md"],
182
+ ["agents_content", "AGENTS.md"],
183
+ ["identity_content", "IDENTITY.md"],
184
+ ["heartbeat_content", "HEARTBEAT.md"],
185
+ ];
186
+ for (const [field, fname] of companions) {
187
+ if (this.fs.exists(`${path}/${fname}`)) {
188
+ spec[field] = this.fs.readFile(`${path}/${fname}`);
189
+ }
190
+ }
191
+ // name fallback โ€” always ensure metadata has a name
192
+ if (metadata.name == null)
193
+ metadata.name = name;
194
+ return {
195
+ apiVersion: "soulspec.org/v1",
196
+ kind: "Soul",
197
+ metadata,
198
+ spec,
199
+ };
200
+ }
201
+ }
202
+ // ---------------------------------------------------------------------------
203
+ // SoulWriter
204
+ // ---------------------------------------------------------------------------
205
+ export class SoulWriter {
206
+ fs;
207
+ constructor(fs = nodeFS) {
208
+ this.fs = fs;
209
+ }
210
+ canWrite(raw) {
211
+ return raw.kind === "Soul";
212
+ }
213
+ write(bundle, raw) {
214
+ const path = bundle.path ?? "";
215
+ this.fs.mkdir(path);
216
+ for (const f of this.serialize(raw)) {
217
+ this.fs.writeFile(`${path}/${f.relativePath}`, f.content ?? "");
218
+ }
219
+ }
220
+ serialize(raw) {
221
+ const files = [];
222
+ const spec = (raw.spec ?? {});
223
+ const meta = (raw.metadata ?? {});
224
+ // Build frontmatter preserving insertion order. Skip null/undefined.
225
+ const fm = {};
226
+ for (const [k, v] of Object.entries(meta)) {
227
+ if (v == null)
228
+ continue;
229
+ fm[k] = v;
230
+ }
231
+ const soulBody = spec.soul_content ?? "";
232
+ // F3 market fidelity: metadata.description may have been ENRICHED at
233
+ // parse time (deriveFirstLine of the body). Persisting it would emit
234
+ // frontmatter the source bundle never had โ€” elide when derivable.
235
+ if (typeof fm.description === "string" && fm.description === deriveFirstLine(soulBody)) {
236
+ delete fm.description;
237
+ }
238
+ const bodyHasFm = soulBody.trimStart().startsWith("---");
239
+ // Byte-compat for the simple "only name" case โ€” previous writer emitted
240
+ // just the body. Keeps existing fixture SOUL.md files from diffing.
241
+ const fmKeys = Object.keys(fm);
242
+ const onlyName = fmKeys.length === 0 || (fmKeys.length === 1 && fmKeys[0] === "name");
243
+ const needsFm = fmKeys.length > 0 && !bodyHasFm && !onlyName;
244
+ if (needsFm) {
245
+ const fmBody = yaml.dump(fm, { flowLevel: -1, sortKeys: false, lineWidth: -1 }).replace(/\n+$/, "");
246
+ files.push({ relativePath: "SOUL.md", content: `---\n${fmBody}\n---\n${soulBody}` });
247
+ }
248
+ else {
249
+ files.push({ relativePath: "SOUL.md", content: soulBody });
250
+ }
251
+ // soul.json
252
+ const soulJson = spec.soul_json;
253
+ if (soulJson) {
254
+ files.push({ relativePath: "soul.json", content: JSON.stringify(soulJson, null, 2) });
255
+ }
256
+ // Companion files (soulspec.org standard)
257
+ const companions = [
258
+ ["style_content", "STYLE.md"],
259
+ ["agents_content", "AGENTS.md"],
260
+ ["identity_content", "IDENTITY.md"],
261
+ ["heartbeat_content", "HEARTBEAT.md"],
262
+ ];
263
+ for (const [specKey, filename] of companions) {
264
+ const content = spec[specKey];
265
+ if (content) {
266
+ files.push({ relativePath: filename, content });
267
+ }
268
+ }
269
+ return files;
270
+ }
271
+ }
272
+ // ---------------------------------------------------------------------------
273
+ // Extension
274
+ // ---------------------------------------------------------------------------
275
+ export class SoulSpecExtension {
276
+ fs;
277
+ name = "soulspec";
278
+ version = "1.0.0";
279
+ constructor(fs = nodeFS) {
280
+ this.fs = fs;
281
+ }
282
+ register(kernel) {
283
+ kernel.kind(new SoulKind());
284
+ kernel.reader(new SoulReader(this.fs));
285
+ kernel.writer(new SoulWriter(this.fs));
286
+ }
287
+ }
@@ -0,0 +1,6 @@
1
+ import type { ExtensionHost, Extension } from "../kernel/protocols.js";
2
+ export declare class TenantExtension implements Extension {
3
+ name: string;
4
+ version: string;
5
+ register(kernel: ExtensionHost): void;
6
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * TenantExtension โ€” multi-tenant identity Kinds (TS twin).
3
+ *
4
+ * 1:1 parity with python/dna/extensions/tenant/__init__.py. Two GLOBAL
5
+ * bundle Kinds:
6
+ * - Tenant (tenant-tenant) โ€” org/team identity (TENANT.md).
7
+ * - TenantMembership (tenant-membership) โ€” userโ†”tenant link (MEMBERSHIP.md).
8
+ * Both strict-detect on apiVersion; envelope (fm.spec) reader with flat fallback.
9
+ */
10
+ import yaml from "js-yaml";
11
+ import { KindBase } from "../kernel/kind_base.js";
12
+ import { SD, TenantScope } from "../kernel/protocols.js";
13
+ import { popSourceFilesAsEntries, writeEntriesToHandle } from "../kernel/writer-helpers.js";
14
+ const API_VERSION = "github.com/ruinosus/dna/tenant/v1";
15
+ function parseFrontmatter(text) {
16
+ const m = text.match(/^---\n([\s\S]*?)---\n?([\s\S]*)$/);
17
+ if (!m)
18
+ return { fm: {}, body: text };
19
+ try {
20
+ const parsed = yaml.load(m[1]);
21
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
22
+ return { fm: parsed, body: (m[2] ?? "").replace(/^\n+/, "") };
23
+ }
24
+ }
25
+ catch {
26
+ // fall through
27
+ }
28
+ return { fm: {}, body: text };
29
+ }
30
+ function cleanSpecMeta(raw) {
31
+ const spec = (raw["spec"] ?? {});
32
+ const meta = (raw["metadata"] ?? {});
33
+ const cleanSpec = {};
34
+ for (const [k, v] of Object.entries(spec)) {
35
+ if (v === null || v === undefined || v === "")
36
+ continue;
37
+ if (Array.isArray(v) && v.length === 0)
38
+ continue;
39
+ if (typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0)
40
+ continue;
41
+ cleanSpec[k] = v;
42
+ }
43
+ const cleanMeta = {};
44
+ for (const [k, v] of Object.entries(meta)) {
45
+ if (v !== null && v !== undefined)
46
+ cleanMeta[k] = v;
47
+ }
48
+ return { cleanSpec, cleanMeta };
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // Tenant
52
+ // ---------------------------------------------------------------------------
53
+ class TenantKind extends KindBase {
54
+ apiVersion = API_VERSION;
55
+ kind = "Tenant";
56
+ alias = "tenant-tenant";
57
+ origin = "github.com/ruinosus/dna/tenant";
58
+ // GLOBAL โ€” the Tenant kind is itself global, no tenant overlay.
59
+ scope = TenantScope.GLOBAL;
60
+ storage = SD.bundle("tenants", "TENANT.md");
61
+ graphStyle = { fill: "#0EA5E9", stroke: "#0369A1", textColor: "#fff" };
62
+ asciiIcon = "๐Ÿข";
63
+ displayLabel = "Tenants";
64
+ isPromptTarget = false;
65
+ flattenInContext = false;
66
+ docs = "A Tenant is the identity of an organization/team/individual that owns scopes " +
67
+ "and the documents within them. Stored as bundle (TENANT.md frontmatter = spec) " +
68
+ "under the special `_lib` scope. Slug rules match the runtime tenant claim " +
69
+ "format ([a-z0-9-]{1,253}). Created by platform admins via POST /tenants. " +
70
+ "Suspended via PATCH; soft-deleted via DELETE (status=deleted, 30d grace period " +
71
+ "before physical purge by background cron). Member management lives in Phase B " +
72
+ "(separate TenantMembership kind).";
73
+ dependencies() { return null; }
74
+ schema() {
75
+ return {
76
+ type: "object",
77
+ required: ["slug", "display_name", "owner_email", "status"],
78
+ additionalProperties: true,
79
+ properties: {
80
+ slug: { type: "string", pattern: "^[a-z0-9-]{1,253}$", description: "Tenant identity. Used as the value of dna_documents.tenant for every doc owned by this tenant. Must match the runtime tenant claim format." },
81
+ display_name: { type: "string", minLength: 1, maxLength: 200, description: "Human-readable name shown in Studio." },
82
+ owner_email: { type: "string", format: "email", description: "Email of the human that provisioned this tenant. First member of the tenant by default." },
83
+ status: { type: "string", enum: ["active", "suspended", "deleted"], default: "active", description: "Lifecycle state. `deleted` is soft โ€” docs stay in PG until the purge cron runs (~30d later)." },
84
+ plan: { type: "string", enum: ["free", "pro", "enterprise"], default: "free", description: "Billing/feature tier." },
85
+ created_at: { type: "string", format: "date-time", description: "ISO timestamp when the Tenant was provisioned." },
86
+ suspended_at: { type: "string", format: "date-time" },
87
+ deleted_at: { type: "string", format: "date-time", description: "Set on soft-delete. Cron purges ~30d later." },
88
+ member_count_cached: { type: "integer", minimum: 0, default: 0, description: "Denormalized count. Refreshed by membership mutations (Phase B). Eventually-consistent." },
89
+ metadata: { type: "object", additionalProperties: true, description: "Free-form metadata (region, lgpd_consent, billing_account_id, etc). Forward-compatible." },
90
+ },
91
+ };
92
+ }
93
+ describe(doc) {
94
+ const spec = (doc.spec ?? {});
95
+ const name = spec.display_name || spec.slug || "?";
96
+ return `${name} [${spec.status ?? "?"} ยท ${spec.plan ?? "?"}]`;
97
+ }
98
+ summary(doc) {
99
+ const spec = (doc.spec ?? {});
100
+ return {
101
+ slug: spec.slug ?? "",
102
+ display_name: spec.display_name ?? "",
103
+ status: spec.status ?? "active",
104
+ plan: spec.plan ?? "free",
105
+ member_count: spec.member_count_cached ?? 0,
106
+ };
107
+ }
108
+ }
109
+ class TenantReader {
110
+ async detect(bundle) {
111
+ if (!(await bundle.exists("TENANT.md")))
112
+ return false;
113
+ try {
114
+ const { fm } = parseFrontmatter(await bundle.readText("TENANT.md"));
115
+ return !!fm && fm["apiVersion"] === API_VERSION;
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ async read(bundle) {
122
+ const { fm } = parseFrontmatter(await bundle.readText("TENANT.md"));
123
+ if ("spec" in fm) {
124
+ const metadata = fm["metadata"] ?? {};
125
+ if (!("name" in metadata))
126
+ metadata["name"] = bundle.name;
127
+ return {
128
+ apiVersion: fm["apiVersion"] ?? API_VERSION,
129
+ kind: fm["kind"] ?? "Tenant",
130
+ metadata,
131
+ spec: fm["spec"] ?? {},
132
+ };
133
+ }
134
+ const name = fm["name"] ?? bundle.name;
135
+ delete fm["name"];
136
+ return { apiVersion: API_VERSION, kind: "Tenant", metadata: { name }, spec: fm };
137
+ }
138
+ }
139
+ class TenantWriter {
140
+ canWrite(raw) {
141
+ return raw["kind"] === "Tenant";
142
+ }
143
+ serialize(raw) {
144
+ const spec = (raw["spec"] ?? {});
145
+ const extraEntries = popSourceFilesAsEntries(spec, "Tenant");
146
+ const { cleanSpec, cleanMeta } = cleanSpecMeta(raw);
147
+ const envelope = {
148
+ apiVersion: raw["apiVersion"] ?? API_VERSION,
149
+ kind: raw["kind"] ?? "Tenant",
150
+ metadata: cleanMeta,
151
+ spec: cleanSpec,
152
+ };
153
+ const fmYaml = yaml.dump(envelope, { lineWidth: 100, noRefs: true, sortKeys: false }).trimEnd();
154
+ const slug = cleanSpec["slug"] ?? "?";
155
+ const display = cleanSpec["display_name"] ?? "?";
156
+ const status = cleanSpec["status"] ?? "active";
157
+ const plan = cleanSpec["plan"] ?? "free";
158
+ const body = `# Tenant โ€” ${display} (\`${slug}\`)\n\n` +
159
+ `**Status:** ${status} ยท **Plan:** ${plan}\n\n` +
160
+ `Owner: \`${cleanSpec["owner_email"] ?? "?"}\` ยท ` +
161
+ `Created: \`${cleanSpec["created_at"] ?? "?"}\`\n\n` +
162
+ `All DNA documents that carry \`dna_documents.tenant = '${slug}'\` belong to ` +
163
+ `this Tenant. Lifecycle managed by platform admins via \`POST /tenants\`, ` +
164
+ `\`PATCH /tenants/{slug}\`, \`DELETE /tenants/{slug}\`.`;
165
+ return [{ relativePath: "TENANT.md", content: `---\n${fmYaml}\n---\n\n${body}` }, ...extraEntries];
166
+ }
167
+ async write(bundle, raw) {
168
+ await writeEntriesToHandle(bundle, this.serialize(raw));
169
+ }
170
+ }
171
+ // ---------------------------------------------------------------------------
172
+ // TenantMembership
173
+ // ---------------------------------------------------------------------------
174
+ class TenantMembershipKind extends KindBase {
175
+ apiVersion = API_VERSION;
176
+ kind = "TenantMembership";
177
+ alias = "tenant-membership";
178
+ origin = "github.com/ruinosus/dna/tenant";
179
+ scope = TenantScope.GLOBAL;
180
+ storage = SD.bundle("tenant-memberships", "MEMBERSHIP.md");
181
+ graphStyle = { fill: "#8B5CF6", stroke: "#5B21B6", textColor: "#fff" };
182
+ asciiIcon = "๐Ÿ‘ฅ";
183
+ displayLabel = "Tenant Memberships";
184
+ isPromptTarget = false;
185
+ flattenInContext = false;
186
+ docs = "Links a user to a Tenant with a role. One row per (tenant, user) pair. " +
187
+ "Created when an admin invites a member via POST /tenants/{slug}/members. " +
188
+ "Deleted by DELETE on same path. Tenant.spec.member_count_cached is updated " +
189
+ "by the route handler on each mutation (eventually-consistent).";
190
+ dependencies() { return null; }
191
+ schema() {
192
+ return {
193
+ type: "object",
194
+ required: ["tenant_slug", "user_email", "role", "joined_at"],
195
+ additionalProperties: true,
196
+ properties: {
197
+ tenant_slug: { type: "string", pattern: "^[a-z0-9-]{1,253}$", description: "Slug of the Tenant this user belongs to." },
198
+ user_email: { type: "string", format: "email", description: "Email identity of the user." },
199
+ user_id: { type: "string", description: "Stable user identifier from the IdP (Clerk sub, OIDC sub, etc). May be absent for invites pending first login โ€” in that case user_email is the key." },
200
+ role: { type: "string", enum: ["owner", "admin", "member", "viewer"], default: "member", description: "Per-tenant role. `owner` is the user who provisioned the tenant (set by POST /tenants). `admin` can manage members + tenant settings. `member` can read/write scope docs. `viewer` is read-only." },
201
+ joined_at: { type: "string", format: "date-time", description: "ISO timestamp when the membership was created." },
202
+ invited_by: { type: "string", format: "email", description: "Email of the admin who invited this member." },
203
+ status: { type: "string", enum: ["active", "pending", "revoked"], default: "active", description: "`pending` for invites awaiting first login; `active` after first login (route handler transitions); `revoked` after admin removes." },
204
+ view_preset: { type: "string", description: "Optional override of Studio's auto-detected view. When set, the UI renders the curated menu/mode-tab subset matching this preset instead of deriving from the user's roles. Lets a power-user temporarily 'see as' a consumer/educator. Auto-detect from roles is the default when this is null. Values follow the same vocabulary as Role (consumer, maker, qa, po, pm, architect, tech-lead, compliance, power-user, tenant-admin, tenant-owner, platform-admin) โ€” pick the single most-relevant intent." },
205
+ },
206
+ };
207
+ }
208
+ describe(doc) {
209
+ const spec = (doc.spec ?? {});
210
+ return `${spec.user_email ?? "?"} @ ${spec.tenant_slug ?? "?"} [${spec.role ?? "?"}]`;
211
+ }
212
+ summary(doc) {
213
+ const spec = (doc.spec ?? {});
214
+ return {
215
+ tenant_slug: spec.tenant_slug ?? "",
216
+ user_email: spec.user_email ?? "",
217
+ role: spec.role ?? "member",
218
+ status: spec.status ?? "active",
219
+ };
220
+ }
221
+ }
222
+ class TenantMembershipReader {
223
+ async detect(bundle) {
224
+ if (!(await bundle.exists("MEMBERSHIP.md")))
225
+ return false;
226
+ try {
227
+ const { fm } = parseFrontmatter(await bundle.readText("MEMBERSHIP.md"));
228
+ return !!fm && fm["apiVersion"] === API_VERSION && fm["kind"] === "TenantMembership";
229
+ }
230
+ catch {
231
+ return false;
232
+ }
233
+ }
234
+ async read(bundle) {
235
+ const { fm } = parseFrontmatter(await bundle.readText("MEMBERSHIP.md"));
236
+ if ("spec" in fm) {
237
+ const metadata = fm["metadata"] ?? {};
238
+ if (!("name" in metadata))
239
+ metadata["name"] = bundle.name;
240
+ return {
241
+ apiVersion: fm["apiVersion"] ?? API_VERSION,
242
+ kind: fm["kind"] ?? "TenantMembership",
243
+ metadata,
244
+ spec: fm["spec"] ?? {},
245
+ };
246
+ }
247
+ const name = fm["name"] ?? bundle.name;
248
+ delete fm["name"];
249
+ return { apiVersion: API_VERSION, kind: "TenantMembership", metadata: { name }, spec: fm };
250
+ }
251
+ }
252
+ class TenantMembershipWriter {
253
+ canWrite(raw) {
254
+ return raw["kind"] === "TenantMembership";
255
+ }
256
+ serialize(raw) {
257
+ const spec = (raw["spec"] ?? {});
258
+ const extraEntries = popSourceFilesAsEntries(spec, "TenantMembership");
259
+ const { cleanSpec, cleanMeta } = cleanSpecMeta(raw);
260
+ const envelope = {
261
+ apiVersion: raw["apiVersion"] ?? API_VERSION,
262
+ kind: raw["kind"] ?? "TenantMembership",
263
+ metadata: cleanMeta,
264
+ spec: cleanSpec,
265
+ };
266
+ const fmYaml = yaml.dump(envelope, { lineWidth: 100, noRefs: true, sortKeys: false }).trimEnd();
267
+ const userEmail = cleanSpec["user_email"] ?? "?";
268
+ const tenantSlug = cleanSpec["tenant_slug"] ?? "?";
269
+ const role = cleanSpec["role"] ?? "member";
270
+ const body = `# Membership โ€” \`${userEmail}\` @ \`${tenantSlug}\`\n\n` +
271
+ `**Role:** ${role} ยท **Status:** ${cleanSpec["status"] ?? "active"}\n\n` +
272
+ `Joined: \`${cleanSpec["joined_at"] ?? "?"}\` ยท ` +
273
+ `Invited by: \`${cleanSpec["invited_by"] ?? "(self-provisioned)"}\``;
274
+ return [{ relativePath: "MEMBERSHIP.md", content: `---\n${fmYaml}\n---\n\n${body}` }, ...extraEntries];
275
+ }
276
+ async write(bundle, raw) {
277
+ await writeEntriesToHandle(bundle, this.serialize(raw));
278
+ }
279
+ }
280
+ export class TenantExtension {
281
+ name = "tenant";
282
+ version = "1.0.0";
283
+ register(kernel) {
284
+ kernel.kind(new TenantKind());
285
+ kernel.reader(new TenantReader());
286
+ kernel.writer(new TenantWriter());
287
+ kernel.kind(new TenantMembershipKind());
288
+ kernel.reader(new TenantMembershipReader());
289
+ kernel.writer(new TenantMembershipWriter());
290
+ }
291
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * TestkitExtension โ€” first-class TEST artifacts for the SDLC (TS twin).
3
+ *
4
+ * 1:1 parity with python/dna/extensions/testkit. Registers TWO artifact
5
+ * KindPorts (a test script is a document, like a Spec/HtmlArtifact โ€” produced BY
6
+ * a work item and verifying it, not a work-item itself):
7
+ *
8
+ * - TestGuide (testkit-test-guide) โ€” a declarative test SCRIPT: ordered `steps`
9
+ * (action โ†’ expected) that validate work items, linked via `verifies`.
10
+ * - TestRun (testkit-test-run) โ€” an EXECUTION record: `outcome`, who ran it,
11
+ * per-step results + evidence. A passing run whose `verifies` points at a
12
+ * Story drives the derived journey's `verify` phase.
13
+ *
14
+ * Both GLOBAL, stored under `<scope>/.dna/<scope>/test-guides|test-runs/`.
15
+ * Auto-exposed in Studio via `ui` metadata (generic docs surface; schema forms).
16
+ */
17
+ import type { ExtensionHost, Extension, KindPort } from "../kernel/protocols.js";
18
+ export declare class TestkitExtension implements Extension {
19
+ readonly name = "testkit";
20
+ readonly version = "1.0.0";
21
+ register(kernel: ExtensionHost): void;
22
+ /** Parity with Python's `TestkitExtension.kinds()`. */
23
+ kinds(): KindPort[];
24
+ }