wingfoil 0.2.1

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 (339) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +334 -0
  3. package/dist/cli/error.d.ts +11 -0
  4. package/dist/cli/error.d.ts.map +1 -0
  5. package/dist/cli/error.js +27 -0
  6. package/dist/cli/error.js.map +1 -0
  7. package/dist/cli/exit.d.ts +19 -0
  8. package/dist/cli/exit.d.ts.map +1 -0
  9. package/dist/cli/exit.js +14 -0
  10. package/dist/cli/exit.js.map +1 -0
  11. package/dist/cli/index.d.ts +28 -0
  12. package/dist/cli/index.d.ts.map +1 -0
  13. package/dist/cli/index.js +36 -0
  14. package/dist/cli/index.js.map +1 -0
  15. package/dist/cli/init-command.d.ts +34 -0
  16. package/dist/cli/init-command.d.ts.map +1 -0
  17. package/dist/cli/init-command.js +100 -0
  18. package/dist/cli/init-command.js.map +1 -0
  19. package/dist/cli/mcp-command.d.ts +24 -0
  20. package/dist/cli/mcp-command.d.ts.map +1 -0
  21. package/dist/cli/mcp-command.js +42 -0
  22. package/dist/cli/mcp-command.js.map +1 -0
  23. package/dist/cli/output.d.ts +13 -0
  24. package/dist/cli/output.d.ts.map +1 -0
  25. package/dist/cli/output.js +28 -0
  26. package/dist/cli/output.js.map +1 -0
  27. package/dist/cli/program.d.ts +12 -0
  28. package/dist/cli/program.d.ts.map +1 -0
  29. package/dist/cli/program.js +279 -0
  30. package/dist/cli/program.js.map +1 -0
  31. package/dist/cli/registrar.d.ts +69 -0
  32. package/dist/cli/registrar.d.ts.map +1 -0
  33. package/dist/cli/registrar.js +82 -0
  34. package/dist/cli/registrar.js.map +1 -0
  35. package/dist/cli.d.ts +3 -0
  36. package/dist/cli.d.ts.map +1 -0
  37. package/dist/cli.js +45 -0
  38. package/dist/cli.js.map +1 -0
  39. package/dist/core/approval-authority.d.ts +83 -0
  40. package/dist/core/approval-authority.d.ts.map +1 -0
  41. package/dist/core/approval-authority.js +112 -0
  42. package/dist/core/approval-authority.js.map +1 -0
  43. package/dist/core/builtin-asset.d.ts +71 -0
  44. package/dist/core/builtin-asset.d.ts.map +1 -0
  45. package/dist/core/builtin-asset.js +137 -0
  46. package/dist/core/builtin-asset.js.map +1 -0
  47. package/dist/core/builtin-integrity.d.ts +32 -0
  48. package/dist/core/builtin-integrity.d.ts.map +1 -0
  49. package/dist/core/builtin-integrity.js +148 -0
  50. package/dist/core/builtin-integrity.js.map +1 -0
  51. package/dist/core/confinement.d.ts +57 -0
  52. package/dist/core/confinement.d.ts.map +1 -0
  53. package/dist/core/confinement.js +108 -0
  54. package/dist/core/confinement.js.map +1 -0
  55. package/dist/core/context.d.ts +187 -0
  56. package/dist/core/context.d.ts.map +1 -0
  57. package/dist/core/context.js +192 -0
  58. package/dist/core/context.js.map +1 -0
  59. package/dist/core/directive-assign.d.ts +131 -0
  60. package/dist/core/directive-assign.d.ts.map +1 -0
  61. package/dist/core/directive-assign.js +352 -0
  62. package/dist/core/directive-assign.js.map +1 -0
  63. package/dist/core/directives-list.d.ts +80 -0
  64. package/dist/core/directives-list.d.ts.map +1 -0
  65. package/dist/core/directives-list.js +148 -0
  66. package/dist/core/directives-list.js.map +1 -0
  67. package/dist/core/exit-code.d.ts +85 -0
  68. package/dist/core/exit-code.d.ts.map +1 -0
  69. package/dist/core/exit-code.js +163 -0
  70. package/dist/core/exit-code.js.map +1 -0
  71. package/dist/core/git-identity.d.ts +32 -0
  72. package/dist/core/git-identity.d.ts.map +1 -0
  73. package/dist/core/git-identity.js +72 -0
  74. package/dist/core/git-identity.js.map +1 -0
  75. package/dist/core/index.d.ts +388 -0
  76. package/dist/core/index.d.ts.map +1 -0
  77. package/dist/core/index.js +1331 -0
  78. package/dist/core/index.js.map +1 -0
  79. package/dist/core/init.d.ts +78 -0
  80. package/dist/core/init.d.ts.map +1 -0
  81. package/dist/core/init.js +167 -0
  82. package/dist/core/init.js.map +1 -0
  83. package/dist/core/loaders.d.ts +174 -0
  84. package/dist/core/loaders.d.ts.map +1 -0
  85. package/dist/core/loaders.js +364 -0
  86. package/dist/core/loaders.js.map +1 -0
  87. package/dist/core/memory-add-type.d.ts +43 -0
  88. package/dist/core/memory-add-type.d.ts.map +1 -0
  89. package/dist/core/memory-add-type.js +151 -0
  90. package/dist/core/memory-add-type.js.map +1 -0
  91. package/dist/core/memory-transition.d.ts +111 -0
  92. package/dist/core/memory-transition.d.ts.map +1 -0
  93. package/dist/core/memory-transition.js +269 -0
  94. package/dist/core/memory-transition.js.map +1 -0
  95. package/dist/core/registry.d.ts +189 -0
  96. package/dist/core/registry.d.ts.map +1 -0
  97. package/dist/core/registry.js +66 -0
  98. package/dist/core/registry.js.map +1 -0
  99. package/dist/core/relevance.d.ts +86 -0
  100. package/dist/core/relevance.d.ts.map +1 -0
  101. package/dist/core/relevance.js +258 -0
  102. package/dist/core/relevance.js.map +1 -0
  103. package/dist/core/require-reason.d.ts +27 -0
  104. package/dist/core/require-reason.d.ts.map +1 -0
  105. package/dist/core/require-reason.js +95 -0
  106. package/dist/core/require-reason.js.map +1 -0
  107. package/dist/core/types.d.ts +45 -0
  108. package/dist/core/types.d.ts.map +1 -0
  109. package/dist/core/types.js +22 -0
  110. package/dist/core/types.js.map +1 -0
  111. package/dist/core/usage-error.d.ts +20 -0
  112. package/dist/core/usage-error.d.ts.map +1 -0
  113. package/dist/core/usage-error.js +29 -0
  114. package/dist/core/usage-error.js.map +1 -0
  115. package/dist/core/write-guard.d.ts +99 -0
  116. package/dist/core/write-guard.d.ts.map +1 -0
  117. package/dist/core/write-guard.js +247 -0
  118. package/dist/core/write-guard.js.map +1 -0
  119. package/dist/directives/create.d.ts +47 -0
  120. package/dist/directives/create.d.ts.map +1 -0
  121. package/dist/directives/create.js +82 -0
  122. package/dist/directives/create.js.map +1 -0
  123. package/dist/directives/index.d.ts +20 -0
  124. package/dist/directives/index.d.ts.map +1 -0
  125. package/dist/directives/index.js +32 -0
  126. package/dist/directives/index.js.map +1 -0
  127. package/dist/directives/roles-edit.d.ts +53 -0
  128. package/dist/directives/roles-edit.d.ts.map +1 -0
  129. package/dist/directives/roles-edit.js +286 -0
  130. package/dist/directives/roles-edit.js.map +1 -0
  131. package/dist/directives/schema.d.ts +65 -0
  132. package/dist/directives/schema.d.ts.map +1 -0
  133. package/dist/directives/schema.js +68 -0
  134. package/dist/directives/schema.js.map +1 -0
  135. package/dist/dna/edit.d.ts +61 -0
  136. package/dist/dna/edit.d.ts.map +1 -0
  137. package/dist/dna/edit.js +429 -0
  138. package/dist/dna/edit.js.map +1 -0
  139. package/dist/dna/index.d.ts +24 -0
  140. package/dist/dna/index.d.ts.map +1 -0
  141. package/dist/dna/index.js +59 -0
  142. package/dist/dna/index.js.map +1 -0
  143. package/dist/dna/mutate.d.ts +74 -0
  144. package/dist/dna/mutate.d.ts.map +1 -0
  145. package/dist/dna/mutate.js +318 -0
  146. package/dist/dna/mutate.js.map +1 -0
  147. package/dist/dna/path.d.ts +175 -0
  148. package/dist/dna/path.d.ts.map +1 -0
  149. package/dist/dna/path.js +289 -0
  150. package/dist/dna/path.js.map +1 -0
  151. package/dist/dna/roles.d.ts +57 -0
  152. package/dist/dna/roles.d.ts.map +1 -0
  153. package/dist/dna/roles.js +50 -0
  154. package/dist/dna/roles.js.map +1 -0
  155. package/dist/dna/schema.d.ts +172 -0
  156. package/dist/dna/schema.d.ts.map +1 -0
  157. package/dist/dna/schema.js +182 -0
  158. package/dist/dna/schema.js.map +1 -0
  159. package/dist/dna/set.d.ts +145 -0
  160. package/dist/dna/set.d.ts.map +1 -0
  161. package/dist/dna/set.js +428 -0
  162. package/dist/dna/set.js.map +1 -0
  163. package/dist/mcp/dna-resource.d.ts +16 -0
  164. package/dist/mcp/dna-resource.d.ts.map +1 -0
  165. package/dist/mcp/dna-resource.js +56 -0
  166. package/dist/mcp/dna-resource.js.map +1 -0
  167. package/dist/mcp/index.d.ts +49 -0
  168. package/dist/mcp/index.d.ts.map +1 -0
  169. package/dist/mcp/index.js +60 -0
  170. package/dist/mcp/index.js.map +1 -0
  171. package/dist/mcp/memory-resource.d.ts +17 -0
  172. package/dist/mcp/memory-resource.d.ts.map +1 -0
  173. package/dist/mcp/memory-resource.js +133 -0
  174. package/dist/mcp/memory-resource.js.map +1 -0
  175. package/dist/mcp/prompt.d.ts +48 -0
  176. package/dist/mcp/prompt.d.ts.map +1 -0
  177. package/dist/mcp/prompt.js +176 -0
  178. package/dist/mcp/prompt.js.map +1 -0
  179. package/dist/mcp/read-only.d.ts +42 -0
  180. package/dist/mcp/read-only.d.ts.map +1 -0
  181. package/dist/mcp/read-only.js +93 -0
  182. package/dist/mcp/read-only.js.map +1 -0
  183. package/dist/mcp/registrar.d.ts +54 -0
  184. package/dist/mcp/registrar.d.ts.map +1 -0
  185. package/dist/mcp/registrar.js +72 -0
  186. package/dist/mcp/registrar.js.map +1 -0
  187. package/dist/mcp/server.d.ts +58 -0
  188. package/dist/mcp/server.d.ts.map +1 -0
  189. package/dist/mcp/server.js +64 -0
  190. package/dist/mcp/server.js.map +1 -0
  191. package/dist/mcp/workflow-resource.d.ts +17 -0
  192. package/dist/mcp/workflow-resource.d.ts.map +1 -0
  193. package/dist/mcp/workflow-resource.js +63 -0
  194. package/dist/mcp/workflow-resource.js.map +1 -0
  195. package/dist/memory/add.d.ts +50 -0
  196. package/dist/memory/add.d.ts.map +1 -0
  197. package/dist/memory/add.js +131 -0
  198. package/dist/memory/add.js.map +1 -0
  199. package/dist/memory/audit.d.ts +177 -0
  200. package/dist/memory/audit.d.ts.map +1 -0
  201. package/dist/memory/audit.js +287 -0
  202. package/dist/memory/audit.js.map +1 -0
  203. package/dist/memory/commit-message.d.ts +146 -0
  204. package/dist/memory/commit-message.d.ts.map +1 -0
  205. package/dist/memory/commit-message.js +203 -0
  206. package/dist/memory/commit-message.js.map +1 -0
  207. package/dist/memory/entry.d.ts +56 -0
  208. package/dist/memory/entry.d.ts.map +1 -0
  209. package/dist/memory/entry.js +28 -0
  210. package/dist/memory/entry.js.map +1 -0
  211. package/dist/memory/frontmatter-edit.d.ts +68 -0
  212. package/dist/memory/frontmatter-edit.d.ts.map +1 -0
  213. package/dist/memory/frontmatter-edit.js +333 -0
  214. package/dist/memory/frontmatter-edit.js.map +1 -0
  215. package/dist/memory/git-log.d.ts +32 -0
  216. package/dist/memory/git-log.d.ts.map +1 -0
  217. package/dist/memory/git-log.js +100 -0
  218. package/dist/memory/git-log.js.map +1 -0
  219. package/dist/memory/history.d.ts +142 -0
  220. package/dist/memory/history.d.ts.map +1 -0
  221. package/dist/memory/history.js +250 -0
  222. package/dist/memory/history.js.map +1 -0
  223. package/dist/memory/index.d.ts +58 -0
  224. package/dist/memory/index.d.ts.map +1 -0
  225. package/dist/memory/index.js +107 -0
  226. package/dist/memory/index.js.map +1 -0
  227. package/dist/memory/query.d.ts +150 -0
  228. package/dist/memory/query.d.ts.map +1 -0
  229. package/dist/memory/query.js +307 -0
  230. package/dist/memory/query.js.map +1 -0
  231. package/dist/memory/reject.d.ts +11 -0
  232. package/dist/memory/reject.d.ts.map +1 -0
  233. package/dist/memory/reject.js +32 -0
  234. package/dist/memory/reject.js.map +1 -0
  235. package/dist/memory/schema.d.ts +104 -0
  236. package/dist/memory/schema.d.ts.map +1 -0
  237. package/dist/memory/schema.js +161 -0
  238. package/dist/memory/schema.js.map +1 -0
  239. package/dist/memory/state-machine.d.ts +166 -0
  240. package/dist/memory/state-machine.d.ts.map +1 -0
  241. package/dist/memory/state-machine.js +431 -0
  242. package/dist/memory/state-machine.js.map +1 -0
  243. package/dist/memory/submit.d.ts +11 -0
  244. package/dist/memory/submit.d.ts.map +1 -0
  245. package/dist/memory/submit.js +44 -0
  246. package/dist/memory/submit.js.map +1 -0
  247. package/dist/storage/builtin-directives.d.ts +54 -0
  248. package/dist/storage/builtin-directives.d.ts.map +1 -0
  249. package/dist/storage/builtin-directives.js +152 -0
  250. package/dist/storage/builtin-directives.js.map +1 -0
  251. package/dist/storage/commit.d.ts +92 -0
  252. package/dist/storage/commit.d.ts.map +1 -0
  253. package/dist/storage/commit.js +201 -0
  254. package/dist/storage/commit.js.map +1 -0
  255. package/dist/storage/confinement.d.ts +93 -0
  256. package/dist/storage/confinement.d.ts.map +1 -0
  257. package/dist/storage/confinement.js +164 -0
  258. package/dist/storage/confinement.js.map +1 -0
  259. package/dist/storage/document.d.ts +27 -0
  260. package/dist/storage/document.d.ts.map +1 -0
  261. package/dist/storage/document.js +52 -0
  262. package/dist/storage/document.js.map +1 -0
  263. package/dist/storage/errors.d.ts +36 -0
  264. package/dist/storage/errors.d.ts.map +1 -0
  265. package/dist/storage/errors.js +46 -0
  266. package/dist/storage/errors.js.map +1 -0
  267. package/dist/storage/frontmatter.d.ts +27 -0
  268. package/dist/storage/frontmatter.d.ts.map +1 -0
  269. package/dist/storage/frontmatter.js +37 -0
  270. package/dist/storage/frontmatter.js.map +1 -0
  271. package/dist/storage/git-root.d.ts +17 -0
  272. package/dist/storage/git-root.d.ts.map +1 -0
  273. package/dist/storage/git-root.js +49 -0
  274. package/dist/storage/git-root.js.map +1 -0
  275. package/dist/storage/index.d.ts +29 -0
  276. package/dist/storage/index.d.ts.map +1 -0
  277. package/dist/storage/index.js +66 -0
  278. package/dist/storage/index.js.map +1 -0
  279. package/dist/storage/init-state.d.ts +9 -0
  280. package/dist/storage/init-state.d.ts.map +1 -0
  281. package/dist/storage/init-state.js +24 -0
  282. package/dist/storage/init-state.js.map +1 -0
  283. package/dist/storage/layout.d.ts +51 -0
  284. package/dist/storage/layout.d.ts.map +1 -0
  285. package/dist/storage/layout.js +88 -0
  286. package/dist/storage/layout.js.map +1 -0
  287. package/dist/storage/memory-path.d.ts +72 -0
  288. package/dist/storage/memory-path.d.ts.map +1 -0
  289. package/dist/storage/memory-path.js +127 -0
  290. package/dist/storage/memory-path.js.map +1 -0
  291. package/dist/storage/snapshot.d.ts +16 -0
  292. package/dist/storage/snapshot.d.ts.map +1 -0
  293. package/dist/storage/snapshot.js +72 -0
  294. package/dist/storage/snapshot.js.map +1 -0
  295. package/dist/storage/templates.d.ts +88 -0
  296. package/dist/storage/templates.d.ts.map +1 -0
  297. package/dist/storage/templates.js +422 -0
  298. package/dist/storage/templates.js.map +1 -0
  299. package/dist/validation/error-mapper.d.ts +16 -0
  300. package/dist/validation/error-mapper.d.ts.map +1 -0
  301. package/dist/validation/error-mapper.js +18 -0
  302. package/dist/validation/error-mapper.js.map +1 -0
  303. package/dist/validation/errors.d.ts +69 -0
  304. package/dist/validation/errors.d.ts.map +1 -0
  305. package/dist/validation/errors.js +69 -0
  306. package/dist/validation/errors.js.map +1 -0
  307. package/dist/validation/id.d.ts +16 -0
  308. package/dist/validation/id.d.ts.map +1 -0
  309. package/dist/validation/id.js +177 -0
  310. package/dist/validation/id.js.map +1 -0
  311. package/dist/validation/index.d.ts +18 -0
  312. package/dist/validation/index.d.ts.map +1 -0
  313. package/dist/validation/index.js +37 -0
  314. package/dist/validation/index.js.map +1 -0
  315. package/dist/validation/secret-scan.d.ts +129 -0
  316. package/dist/validation/secret-scan.d.ts.map +1 -0
  317. package/dist/validation/secret-scan.js +367 -0
  318. package/dist/validation/secret-scan.js.map +1 -0
  319. package/dist/validation/two-pass.d.ts +24 -0
  320. package/dist/validation/two-pass.d.ts.map +1 -0
  321. package/dist/validation/two-pass.js +24 -0
  322. package/dist/validation/two-pass.js.map +1 -0
  323. package/dist/validation/warning.d.ts +31 -0
  324. package/dist/validation/warning.d.ts.map +1 -0
  325. package/dist/validation/warning.js +81 -0
  326. package/dist/validation/warning.js.map +1 -0
  327. package/dist/validation/yaml.d.ts +9 -0
  328. package/dist/validation/yaml.d.ts.map +1 -0
  329. package/dist/validation/yaml.js +30 -0
  330. package/dist/validation/yaml.js.map +1 -0
  331. package/dist/workflow/index.d.ts +10 -0
  332. package/dist/workflow/index.d.ts.map +1 -0
  333. package/dist/workflow/index.js +16 -0
  334. package/dist/workflow/index.js.map +1 -0
  335. package/dist/workflow/schema.d.ts +89 -0
  336. package/dist/workflow/schema.d.ts.map +1 -0
  337. package/dist/workflow/schema.js +73 -0
  338. package/dist/workflow/schema.js.map +1 -0
  339. package/package.json +78 -0
@@ -0,0 +1,1331 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.CORE_MODULES = exports.dnaEntryFieldOfOption = exports.dnaEntryOptionName = exports.DNA_ENTRY_OPTION_PREFIX = exports.verifyBuiltinTemplates = exports.verifyCommittedPaths = exports.undeclaredCommittedPaths = exports.requireUnmodifiedTargets = exports.requireUnmodifiedTarget = exports.requireAbsentTarget = exports.committedScopeError = exports.CONFIG_WRITE_CONTRACT = exports.resolveAddType = exports.verifyCommittedScope = exports.prepareMemoryTransition = exports.commitMemoryTransition = exports.NO_RELEVANT_MEMORY_NOTE = exports.filterRelevantMemoryDocuments = exports.DEFAULT_CONTEXT_LIMITS = exports.WINGFOIL_ALREADY_INITIALIZED = exports.initWingfoilProject = exports.initWingfoilStorage = exports.UNASSIGNED_ASSIGNMENT = exports.GLOBAL_ASSIGNMENT = exports.loadDirectiveListing = exports.buildDirectiveListing = exports.selectDirectivesById = exports.resolveRoleDirectives = exports.assembleExecutionContext = exports.loadWorkflowsYaml = exports.loadRolesYamlAtHead = exports.loadRolesYaml = exports.loadMemoryYamlAtHead = exports.loadMemoryYaml = exports.loadDnaYamlAtHead = exports.loadDnaYaml = exports.loadDirectivesAtHead = exports.loadDirectives = exports.ROLES_YAML_PATH = exports.MEMORY_YAML_PATH = exports.DNA_YAML_PATH = exports.DIRECTIVES_DIR_PATH = exports.MODULE_NAME = void 0;
18
+ /**
19
+ * `core` module — shared domain logic; single behavior behind both the CLI and MCP surfaces
20
+ * (REQ-SYS-05). Exposes one loader per Project pillar (task-004-decoupled-pillars, REQ-SYS-02):
21
+ * `loadMemoryYaml`, `loadDnaYaml`, `loadWorkflowsYaml`, `loadDirectives` — each independently reads
22
+ * and validates only its own pillar's artifact(s), so editing one pillar's config never requires
23
+ * touching another's loader.
24
+ *
25
+ * task-006-dual-interface-shared-core adds the `CoreModule` registry (spec-006-core-domain-api):
26
+ * `CORE_MODULES` below is the single array `src/cli` and `src/mcp` both derive their surfaces
27
+ * from. It is intentionally small today — see the SCOPE note on `CORE_MODULES` — the full
28
+ * memory/dna/directives/workflow domain-operation tables in spec-006 §3 (`memoryAdd`, `dnaSet`,
29
+ * `directiveCreate`, ...) are feature work for task-018..030, not this task.
30
+ */
31
+ const path_1 = require("path");
32
+ const js_yaml_1 = require("js-yaml");
33
+ const validation_1 = require("../validation");
34
+ const schema_1 = require("../dna/schema");
35
+ const edit_1 = require("../dna/edit");
36
+ const mutate_1 = require("../dna/mutate");
37
+ const path_2 = require("../dna/path");
38
+ const set_1 = require("../dna/set");
39
+ const create_1 = require("../directives/create");
40
+ const roles_edit_1 = require("../directives/roles-edit");
41
+ const storage_1 = require("../storage");
42
+ // Not re-exported by the `../storage` barrel, imported directly per that module's own convention
43
+ // (same as `src/memory/entry.ts`): `memory add` needs the CONFINED target path before it writes, to
44
+ // run task-092's absence guard on it.
45
+ const memory_path_1 = require("../storage/memory-path");
46
+ const memory_1 = require("../memory");
47
+ const loaders_1 = require("./loaders");
48
+ const directives_list_1 = require("./directives-list");
49
+ const context_1 = require("./context");
50
+ const directive_assign_1 = require("./directive-assign");
51
+ const git_identity_1 = require("./git-identity");
52
+ const builtin_asset_1 = require("./builtin-asset");
53
+ const confinement_1 = require("./confinement");
54
+ const approval_authority_1 = require("./approval-authority");
55
+ const require_reason_1 = require("./require-reason");
56
+ const memory_transition_1 = require("./memory-transition");
57
+ const memory_add_type_1 = require("./memory-add-type");
58
+ const write_guard_1 = require("./write-guard");
59
+ const usage_error_1 = require("./usage-error");
60
+ const types_1 = require("./types");
61
+ /** This module's `dna.yaml` name (`core`) — the stable identifier surfaces and tests key it by. */
62
+ exports.MODULE_NAME = 'core';
63
+ var loaders_2 = require("./loaders");
64
+ Object.defineProperty(exports, "DIRECTIVES_DIR_PATH", { enumerable: true, get: function () { return loaders_2.DIRECTIVES_DIR_PATH; } });
65
+ Object.defineProperty(exports, "DNA_YAML_PATH", { enumerable: true, get: function () { return loaders_2.DNA_YAML_PATH; } });
66
+ Object.defineProperty(exports, "MEMORY_YAML_PATH", { enumerable: true, get: function () { return loaders_2.MEMORY_YAML_PATH; } });
67
+ Object.defineProperty(exports, "ROLES_YAML_PATH", { enumerable: true, get: function () { return loaders_2.ROLES_YAML_PATH; } });
68
+ Object.defineProperty(exports, "loadDirectives", { enumerable: true, get: function () { return loaders_2.loadDirectives; } });
69
+ // The committed-baseline readers `dl-080` (B) makes the rule for any read that gates an operation.
70
+ // task-090 kept `loadDnaYamlAtHead` private while that rule was undecided; it is decided now, and a
71
+ // facility the ruling tells the next implementer to use has to be reachable (task-091, D3).
72
+ Object.defineProperty(exports, "loadDirectivesAtHead", { enumerable: true, get: function () { return loaders_2.loadDirectivesAtHead; } });
73
+ Object.defineProperty(exports, "loadDnaYaml", { enumerable: true, get: function () { return loaders_2.loadDnaYaml; } });
74
+ Object.defineProperty(exports, "loadDnaYamlAtHead", { enumerable: true, get: function () { return loaders_2.loadDnaYamlAtHead; } });
75
+ Object.defineProperty(exports, "loadMemoryYaml", { enumerable: true, get: function () { return loaders_2.loadMemoryYaml; } });
76
+ Object.defineProperty(exports, "loadMemoryYamlAtHead", { enumerable: true, get: function () { return loaders_2.loadMemoryYamlAtHead; } });
77
+ Object.defineProperty(exports, "loadRolesYaml", { enumerable: true, get: function () { return loaders_2.loadRolesYaml; } });
78
+ Object.defineProperty(exports, "loadRolesYamlAtHead", { enumerable: true, get: function () { return loaders_2.loadRolesYamlAtHead; } });
79
+ Object.defineProperty(exports, "loadWorkflowsYaml", { enumerable: true, get: function () { return loaders_2.loadWorkflowsYaml; } });
80
+ var context_2 = require("./context");
81
+ Object.defineProperty(exports, "assembleExecutionContext", { enumerable: true, get: function () { return context_2.assembleExecutionContext; } });
82
+ Object.defineProperty(exports, "resolveRoleDirectives", { enumerable: true, get: function () { return context_2.resolveRoleDirectives; } });
83
+ Object.defineProperty(exports, "selectDirectivesById", { enumerable: true, get: function () { return context_2.selectDirectivesById; } });
84
+ var directives_list_2 = require("./directives-list");
85
+ Object.defineProperty(exports, "buildDirectiveListing", { enumerable: true, get: function () { return directives_list_2.buildDirectiveListing; } });
86
+ Object.defineProperty(exports, "loadDirectiveListing", { enumerable: true, get: function () { return directives_list_2.loadDirectiveListing; } });
87
+ Object.defineProperty(exports, "GLOBAL_ASSIGNMENT", { enumerable: true, get: function () { return directives_list_2.GLOBAL_ASSIGNMENT; } });
88
+ Object.defineProperty(exports, "UNASSIGNED_ASSIGNMENT", { enumerable: true, get: function () { return directives_list_2.UNASSIGNED_ASSIGNMENT; } });
89
+ __exportStar(require("./types"), exports);
90
+ __exportStar(require("./registry"), exports);
91
+ __exportStar(require("./exit-code"), exports);
92
+ __exportStar(require("./git-identity"), exports);
93
+ __exportStar(require("./approval-authority"), exports);
94
+ __exportStar(require("./require-reason"), exports);
95
+ __exportStar(require("./builtin-asset"), exports);
96
+ __exportStar(require("./confinement"), exports);
97
+ __exportStar(require("./usage-error"), exports);
98
+ var init_1 = require("./init");
99
+ Object.defineProperty(exports, "initWingfoilStorage", { enumerable: true, get: function () { return init_1.initWingfoilStorage; } });
100
+ Object.defineProperty(exports, "initWingfoilProject", { enumerable: true, get: function () { return init_1.initWingfoilProject; } });
101
+ Object.defineProperty(exports, "WINGFOIL_ALREADY_INITIALIZED", { enumerable: true, get: function () { return init_1.WINGFOIL_ALREADY_INITIALIZED; } });
102
+ var relevance_1 = require("./relevance");
103
+ Object.defineProperty(exports, "DEFAULT_CONTEXT_LIMITS", { enumerable: true, get: function () { return relevance_1.DEFAULT_CONTEXT_LIMITS; } });
104
+ Object.defineProperty(exports, "filterRelevantMemoryDocuments", { enumerable: true, get: function () { return relevance_1.filterRelevantMemoryDocuments; } });
105
+ Object.defineProperty(exports, "NO_RELEVANT_MEMORY_NOTE", { enumerable: true, get: function () { return relevance_1.NO_RELEVANT_MEMORY_NOTE; } });
106
+ var memory_transition_2 = require("./memory-transition");
107
+ Object.defineProperty(exports, "commitMemoryTransition", { enumerable: true, get: function () { return memory_transition_2.commitMemoryTransition; } });
108
+ Object.defineProperty(exports, "prepareMemoryTransition", { enumerable: true, get: function () { return memory_transition_2.prepareMemoryTransition; } });
109
+ Object.defineProperty(exports, "verifyCommittedScope", { enumerable: true, get: function () { return memory_transition_2.verifyCommittedScope; } });
110
+ var memory_add_type_2 = require("./memory-add-type");
111
+ Object.defineProperty(exports, "resolveAddType", { enumerable: true, get: function () { return memory_add_type_2.resolveAddType; } });
112
+ var write_guard_2 = require("./write-guard");
113
+ Object.defineProperty(exports, "CONFIG_WRITE_CONTRACT", { enumerable: true, get: function () { return write_guard_2.CONFIG_WRITE_CONTRACT; } });
114
+ Object.defineProperty(exports, "committedScopeError", { enumerable: true, get: function () { return write_guard_2.committedScopeError; } });
115
+ Object.defineProperty(exports, "requireAbsentTarget", { enumerable: true, get: function () { return write_guard_2.requireAbsentTarget; } });
116
+ Object.defineProperty(exports, "requireUnmodifiedTarget", { enumerable: true, get: function () { return write_guard_2.requireUnmodifiedTarget; } });
117
+ Object.defineProperty(exports, "requireUnmodifiedTargets", { enumerable: true, get: function () { return write_guard_2.requireUnmodifiedTargets; } });
118
+ Object.defineProperty(exports, "undeclaredCommittedPaths", { enumerable: true, get: function () { return write_guard_2.undeclaredCommittedPaths; } });
119
+ Object.defineProperty(exports, "verifyCommittedPaths", { enumerable: true, get: function () { return write_guard_2.verifyCommittedPaths; } });
120
+ var builtin_integrity_1 = require("./builtin-integrity");
121
+ Object.defineProperty(exports, "verifyBuiltinTemplates", { enumerable: true, get: function () { return builtin_integrity_1.verifyBuiltinTemplates; } });
122
+ /**
123
+ * Run a synchronous, throwing pillar loader (task-004's `load*` functions) and map its expected
124
+ * failures onto a `CoreResult` (spec-006 §2): a `ValidationError` from the shared two-pass pipeline
125
+ * becomes `VALIDATION`, a missing file (Node's `ENOENT`) becomes `NOT_FOUND`; anything else
126
+ * propagates as a genuine thrown exception (a programmer bug, not a domain failure — spec-006 §2's
127
+ * "no function throws for *expected* domain failures" implies unexpected ones still may). Factored
128
+ * out of `wrapReadOnly` so both `dnaShowFn` (task-026) and `pathsFn` (task-028) reuse the exact
129
+ * same mapping for their own richer, argument-aware bodies instead of duplicating it.
130
+ */
131
+ function loadOrError(loader) {
132
+ try {
133
+ return (0, types_1.coreOk)(loader());
134
+ }
135
+ catch (error) {
136
+ if (error instanceof validation_1.ValidationError) {
137
+ return (0, types_1.coreErr)({ code: 'VALIDATION', message: error.message, details: { issues: error.issues } });
138
+ }
139
+ const errno = error;
140
+ if (errno && errno.code === 'ENOENT') {
141
+ return (0, types_1.coreErr)({ code: 'NOT_FOUND', message: errno.message });
142
+ }
143
+ throw error;
144
+ }
145
+ }
146
+ /** Adapt a synchronous, throwing pillar loader into a `CoreFn` taking just `{ root }` (spec-006 §2). */
147
+ function wrapReadOnly(loader) {
148
+ return async (params) => {
149
+ const { root } = params;
150
+ return loadOrError(() => loader(root));
151
+ };
152
+ }
153
+ /**
154
+ * `paths` `CoreOperation.fn` (P2.5, spec-002-dna-yaml-schema's `Paths` node) —
155
+ * task-028-implement-paths-category. Loads `.wingfoil/dna.yaml` via the same `loadOrError` +
156
+ * `loadDnaYaml` path `dnaShowFn` uses, then narrows to one category (the bare positional):
157
+ *
158
+ * - category given and mapped (a non-empty array) -> `coreOk({category, paths})` (spec-005 §4 shape).
159
+ * - category given but absent/empty in `paths:` -> `coreErr(NOT_FOUND, "no paths mapped for
160
+ * category '<category>'")` — the exact BDD `P2.5-paths.feature` "Error - querying an undefined
161
+ * category" wording (exit `1` via `exit-code.ts`, never a throw, never exit `2` — a read-only
162
+ * command, spec-005 §1; symmetric with `dnaShowFn`'s own unknown-section `NOT_FOUND`).
163
+ * - category omitted -> `coreOk(<whole paths node>)` (see {@link PathsParams}'s doc comment on why;
164
+ * symmetric with `dna show` returning the whole DNA when no section is given).
165
+ *
166
+ * `dna.paths` is `.passthrough()` (spec-002), so an entry beyond the five named categories is still a
167
+ * plain object property here, not necessarily an array — `Array.isArray` guards that case rather than
168
+ * assuming the shape.
169
+ */
170
+ const pathsFn = async (params) => {
171
+ const { root, positional: category } = params;
172
+ const loaded = loadOrError(() => (0, loaders_1.loadDnaYaml)(root));
173
+ if (!loaded.ok)
174
+ return loaded;
175
+ if (category === undefined)
176
+ return (0, types_1.coreOk)(loaded.value.paths);
177
+ const raw = loaded.value.paths[category];
178
+ const entries = Array.isArray(raw) ? raw : undefined;
179
+ if (!entries || entries.length === 0) {
180
+ return (0, types_1.coreErr)({ code: 'NOT_FOUND', message: `no paths mapped for category '${category}'` });
181
+ }
182
+ return (0, types_1.coreOk)({ category, paths: entries });
183
+ };
184
+ /**
185
+ * `dna show [section]` (P2.2-implement-dna-show, task-026): resolves `ParamsContext.positional`
186
+ * (`core/registry.ts` — the generic CLI-positional seam this task adds) as an optional DNA section
187
+ * name. No `positional` -> the whole parsed `DnaYaml` (unchanged behavior, what the MCP
188
+ * `wingfoil://dna/show` Resource still gets, since the MCP surface never sets this field — see
189
+ * `ParamsContext.positional`'s own doc comment). `tech_stack` resolves as a BDD-compatibility alias
190
+ * for `stacks` (spec-002-dna-yaml-schema Consequences: the schema renamed the BDD's `tech_stack`
191
+ * wording). An unknown section is a domain `NOT_FOUND` (`CoreResult.error`, exit `1` via
192
+ * `exit-code.ts` — never a thrown exception and never exit `2`, since a read-only command can only
193
+ * exit `0`/`1` per spec-005-cli-command-contract §1), with the exact P2.2 message
194
+ * `no DNA key named '<section>'`. The `tech_stack`→`stacks` alias is the shared `DNA_KEY_ALIASES`
195
+ * (`src/dna/set.ts`) — the single source of truth `dna set` uses too, so read and write stay symmetric.
196
+ */
197
+ const dnaShowFn = async (params) => {
198
+ const { root, positional: section } = params;
199
+ const loaded = loadOrError(() => (0, loaders_1.loadDnaYaml)(root));
200
+ if (!loaded.ok || section === undefined)
201
+ return loaded;
202
+ const key = set_1.DNA_KEY_ALIASES[section] ?? section;
203
+ const dna = loaded.value;
204
+ if (!(key in dna)) {
205
+ return (0, types_1.coreErr)({ code: 'NOT_FOUND', message: `no DNA key named '${section}'` });
206
+ }
207
+ return (0, types_1.coreOk)(dna[key]);
208
+ };
209
+ /**
210
+ * The shared body of every DNA mutation (`dna set`, and `dna add|remove|update` since
211
+ * task-093-dna-mutation-surface-add-remove-update) — the mutating-op template `dna set` established
212
+ * (P2.1, task-025), now with one structure-aware traversal underneath all four verbs:
213
+ *
214
+ * 1. **`requireGitIdentity` pre-flight** (REQ-SEC-01, task-014) — refuse before any read/write when
215
+ * `user.name`/`user.email` are unset, returning its `CoreResult.error` unchanged (exit 1).
216
+ * 2. **Argument validation** — a missing required argument, or a dotted path `splitDnaPath` cannot
217
+ * parse (an empty segment, the BDD's `..language`; an unterminated quote; a `"` no delimiter can
218
+ * account for), is a usage error: `throw new UsageError(...)` → exit **2**
219
+ * (mapped by `exitCodeForThrow`), per spec-005-cli-command-contract §1. A path that is well-formed
220
+ * but names nothing the schema declares is a different failure and exits **1** — see step 4.
221
+ * 3. **`requireUnmodifiedTarget` pre-flight** (`dl-080`(B) / `bug-078`, task-092) — refuse, before
222
+ * anything is read or written, while `dna.yaml` carries modifications this operation does not own.
223
+ * The plain "refuse a dirty target" rule applies here unaltered: all four verbs **edit an existing
224
+ * file in place**, so `task-092`'s two exceptions are both inapplicable — `requireAbsentTarget` is
225
+ * for a target that must be new (`memory add`), and `wingfoil init`'s exemption rests on
226
+ * `detectInitState` refusing an initialized project before a guard could run, whereas these verbs
227
+ * *require* an initialized project and load `dna.yaml` as their input. Placed before the load, so
228
+ * no refusal and no written byte can depend on a value the dirty copy contributed — which is also
229
+ * what makes `loadDnaYaml`'s working-tree read equivalent to `HEAD`'s here.
230
+ * 4. **Load** the current `.wingfoil/dna.yaml` through the same two-pass path `dnaShow` uses (a
231
+ * missing/invalid file → `NOT_FOUND`/`VALIDATION`, exit 1).
232
+ * 5. **Resolve + apply** through `src/dna`'s `applyDnaMutation`, which resolves the `<path>` against
233
+ * `DnaYaml` itself and **refuses a path that does not resolve rather than creating it**
234
+ * (`bug-084-dna-key-alias-writes-unschemad-keys`; `dl-081`'s ratification makes this a precondition
235
+ * of the surface). A refusal is a logic error (`VALIDATION` → exit 1, `spec-005` §1 as ruled on
236
+ * `bug-076`), returned not thrown, and nothing is written.
237
+ * 6. **Produce the new bytes.** The PRIMARY path is `src/dna`'s `applyDnaEditInText`: a minimal
238
+ * in-place textual edit, so every comment survives — including the inline `[SPEC]`/`[AUTHORING]`
239
+ * field-provenance annotations a whole-file re-serialization deletes
240
+ * (bug-004-dna-set-strips-yaml-comments, task-063). When no provably-minimal edit exists it returns
241
+ * `undefined` and this FALLS BACK to `dump(dna, { lineWidth: -1 })` — correct, but comment-stripping.
242
+ * Both paths are deterministic (REQ-SYS-07).
243
+ * 7. **Re-validate the written bytes** against `DnaYaml` (spec-002) BEFORE persisting — re-parsing the
244
+ * serialized form, not the in-memory object, so the check honours YAML's own scalar coercion and
245
+ * validates the exact bytes about to be written (`dna set version 2` writes `version: '2'` and
246
+ * still fails `z.number()` on read-back). It is also what enforces the cross-field rules the verbs
247
+ * do not duplicate: a member naming a role absent from `team.roles` fails here (REQ-SYS-08,
248
+ * `Team.superRefine`), which is why `dl-081` records that referential integrity needs no new check.
249
+ * 8. **Persist + commit** through the single storage primitives (task-018) — `writeDocument` then
250
+ * `commitPaths` on the ONE scoped path `.wingfoil/dna.yaml`; the returned sha rides
251
+ * `CoreResult.commit`. Writing the bytes already on disk is an idempotent no-op: no write, no
252
+ * empty commit.
253
+ * 9. **`committedScopeError` post-condition** (task-092) — assert the commit carries exactly the
254
+ * bytes this operation wrote at that one path and nothing else. An alarm rather than a rollback:
255
+ * with the step-3 guard in place no argument can reach it, but a repository-local `pre-commit`
256
+ * hook that rewrites and re-stages the file still can.
257
+ */
258
+ async function runDnaMutation(root, request, subject = dnaCommitSubject(request), scalarOnly = false) {
259
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
260
+ if (!identity.ok)
261
+ return identity;
262
+ const unmodified = (0, write_guard_1.requireUnmodifiedTarget)(root, loaders_1.DNA_YAML_PATH);
263
+ if (!unmodified.ok)
264
+ return unmodified;
265
+ const loaded = loadOrError(() => (0, loaders_1.loadDnaYaml)(root));
266
+ if (!loaded.ok)
267
+ return loaded;
268
+ if (scalarOnly) {
269
+ const resolved = (0, path_2.resolveDnaPath)(loaded.value, request.field);
270
+ if (!resolved.ok)
271
+ return (0, types_1.coreErr)({ code: 'VALIDATION', message: resolved.message });
272
+ if (resolved.target.kind !== 'scalar') {
273
+ return (0, types_1.coreErr)({
274
+ code: 'VALIDATION',
275
+ message: `'${request.field}' does not hold a single value: reach it with \`dna add|remove|update ${request.field} --value <v>\` (dl-081)`,
276
+ });
277
+ }
278
+ }
279
+ const applied = (0, mutate_1.applyDnaMutation)(loaded.value, request);
280
+ if (!applied.ok)
281
+ return (0, types_1.coreErr)({ code: 'VALIDATION', message: applied.message });
282
+ const dnaPath = (0, path_1.join)(root, loaders_1.DNA_YAML_PATH);
283
+ const current = (0, storage_1.readDocument)(dnaPath);
284
+ const serialized = (0, edit_1.applyDnaEditInText)(current, applied.edit, applied.dna) ?? (0, js_yaml_1.dump)(applied.dna, { lineWidth: -1 });
285
+ const parsed = schema_1.DnaYaml.safeParse((0, validation_1.parseYaml)(serialized, dnaPath));
286
+ if (!parsed.success) {
287
+ const validationError = (0, validation_1.toValidationError)(parsed.error, dnaPath);
288
+ return (0, types_1.coreErr)({ code: 'VALIDATION', message: validationError.message, details: { issues: validationError.issues } });
289
+ }
290
+ const outcome = { key: request.field, value: request.value };
291
+ if (current === serialized)
292
+ return (0, types_1.coreOk)(outcome);
293
+ (0, storage_1.writeDocument)(dnaPath, serialized);
294
+ const sha = (0, storage_1.commitPaths)(root, [loaders_1.DNA_YAML_PATH], subject);
295
+ const leaked = (0, write_guard_1.committedScopeError)(root, sha, loaders_1.DNA_YAML_PATH, serialized);
296
+ if (leaked)
297
+ return leaked;
298
+ return (0, types_1.coreOk)(outcome, { sha, message: subject });
299
+ }
300
+ /**
301
+ * The one-line commit subject a DNA mutation writes: `wf(dna): {verb} {field} [{value}]`, extending
302
+ * `dna set`'s established `wf(dna): set {key}` (task-025) with the payload, because for `add` and
303
+ * `remove` the value IS which entry was touched and a subject without it reads as if the whole
304
+ * collection changed. Deterministic — a pure function of the request (REQ-SYS-07).
305
+ */
306
+ function dnaCommitSubject(request) {
307
+ return `wf(dna): ${request.verb} ${request.field}${request.value === undefined ? '' : ` ${request.value}`}`;
308
+ }
309
+ /**
310
+ * The dotted path every DNA verb acts on, read from `positionals[0]` — the ONE place the four verbs
311
+ * agree on where their target comes from (`dl-082-cli-parameter-shape`: a positional carries the
312
+ * identity of the target).
313
+ *
314
+ * Three usage errors, in the order they are checked, and the order is load-bearing:
315
+ *
316
+ * 1. **Missing** — nothing to act on.
317
+ * 2. **Unparseable** — whatever `splitDnaPath` (`src/dna/set.ts`) refuses, reported with **its own**
318
+ * message rather than a flat one, because the three cases are not the same complaint: an empty
319
+ * segment (the BDD's `..language`) is malformed, an unterminated quote is a typo in the
320
+ * delimiters, and a `"` no delimiter can account for means the *name* has no spelling at all
321
+ * (`dl-083-dotted-entry-names-in-paths`, task-099). Checked BEFORE the extra-positional rule below
322
+ * so that `dna set ..language python`, which is `P2.1-dna-set.feature`'s third scenario verbatim,
323
+ * keeps reporting `invalid key path: '..language'` rather than the migration hint. A stale
324
+ * invocation that is also malformed is malformed first.
325
+ * 3. **Extra positionals** — the old `dna set <key> <value>` spelling, and any slip of the same
326
+ * shape on the three new verbs. It gets a named message rather than being ignored, because
327
+ * `dl-082` is a breaking change to a shipped command and silently dropping the second word would
328
+ * make `dna set project.license MIT` look like it worked (`--value` absent, the write refused for
329
+ * a reason that names neither the second word nor the new grammar).
330
+ *
331
+ * All three are exit `2` (`UsageError` → `exitCodeForThrow`, `spec-005-cli-command-contract` §1).
332
+ */
333
+ function dnaPathPositional(verb, positionals) {
334
+ const path = positionals?.[0];
335
+ if (path === undefined) {
336
+ throw new usage_error_1.UsageError(`missing required argument: wingfoil dna ${verb} <path> --value <value>`);
337
+ }
338
+ const split = (0, set_1.splitDnaPath)(path);
339
+ if (!split.ok) {
340
+ throw new usage_error_1.UsageError(split.message);
341
+ }
342
+ if ((positionals?.length ?? 0) > 1) {
343
+ throw new usage_error_1.UsageError(`wingfoil dna ${verb} takes one positional <path>; the value travels in --value (got ${positionals.length} positionals)`);
344
+ }
345
+ return path;
346
+ }
347
+ /**
348
+ * `dna set <path> --value <v>` `CoreOperation.fn` (P2.1) — kept, and still the scalar spelling
349
+ * (`spec-002`, `spec-005` §4). Since task-093 it is **`update` restricted to a single value**: the
350
+ * same resolver, the same write path, one spelling for the scalar case and
351
+ * `dna update <path> --value …` for everything structured (`dl-081` AC9 — `update` does not subsume
352
+ * `set`, and `set` does not grow a second mechanism).
353
+ *
354
+ * Its **second positional became `--value`** in this task (`dl-082-cli-parameter-shape`): the key was
355
+ * the target and stays positional, the value was an attribute wearing a positional's clothes. That is
356
+ * a breaking change to a shipped command, tracked as such — `P2.1-dna-set.feature` still shows the
357
+ * old spelling and is rewritten by `bug-089`, and `CHANGELOG.md` by the `user-docs` phase.
358
+ *
359
+ * A path that names a collection or a list is refused here with the verb that does reach it, rather
360
+ * than with the schema re-validation's `expected array, received string` — which is
361
+ * `bug-083-dna-set-cannot-write-array-valued-fields`'s headline symptom and says nothing about how to
362
+ * write the field.
363
+ */
364
+ const dnaSetFn = async (params) => {
365
+ const { root, positionals, options } = params;
366
+ const keyPath = dnaPathPositional('set', positionals);
367
+ const value = options?.value;
368
+ if (value === undefined) {
369
+ throw new usage_error_1.UsageError('missing required argument: --value');
370
+ }
371
+ // `wf(dna): set <key>` — the subject `dna set` has always written (task-025), kept verbatim so a
372
+ // reader (and `git log --grep`) sees the command that was run, not the verb it delegates to. The
373
+ // scalar-only restriction, `dl-080`(B)'s dirty-target guard (task-092) and the committed-tree
374
+ // post-condition all live in the shared pipeline now, on the one document it loads.
375
+ return runDnaMutation(root, { verb: 'update', field: keyPath, value }, `wf(dna): set ${keyPath}`, true);
376
+ };
377
+ /** Split a verb's invocation into the positional `<path>`, `--value` and the per-entry `--entry-<field>` values. */
378
+ function dnaMutationRequest(verb, positionals, options) {
379
+ const field = dnaPathPositional(verb, positionals);
380
+ if (verb === 'add' && options?.value === undefined)
381
+ throw new usage_error_1.UsageError('missing required argument: --value');
382
+ // Every remaining option is an entry field, carried under its `--entry-<field>` name (see
383
+ // `DNA_ENTRY_OPTION_PREFIX`); it is stripped back to the schema's own field name here, which is what
384
+ // `applyDnaMutation` matches against the entry schema. Sorted so the request — and therefore the
385
+ // batch of text edits it produces — is a deterministic function of the invocation (REQ-SYS-07).
386
+ const fields = {};
387
+ for (const name of Object.keys(options ?? {}).sort()) {
388
+ if (name === 'value')
389
+ continue;
390
+ const entryField = (0, path_2.dnaEntryFieldOfOption)(name);
391
+ // Strict, and deliberately so: the CLI can only send names this operation declares (Commander
392
+ // refuses the rest), so accepting a BARE `version` here would mean the core layer speaks a
393
+ // vocabulary the CLI layer cannot produce — which is how a test at this layer went green while
394
+ // the real command line silently did nothing. The MCP surface, whose Tool arguments are arbitrary
395
+ // JSON, is the caller that can actually reach this, and it gets a named refusal.
396
+ if (entryField === undefined) {
397
+ throw new usage_error_1.UsageError(`unknown option: --${name} (entry fields are passed as --${path_2.DNA_ENTRY_OPTION_PREFIX}<field>)`);
398
+ }
399
+ fields[entryField] = options[name];
400
+ }
401
+ return { verb, field, value: options?.value, fields };
402
+ }
403
+ /** `dna add` — create an entry, or append to a list (`dl-081` option (E); spec-006 §3, Tool `dna.add`). */
404
+ const dnaAddFn = async (params) => {
405
+ const { root, positionals, options } = params;
406
+ return runDnaMutation(root, dnaMutationRequest('add', positionals, options));
407
+ };
408
+ /** `dna remove` — drop an entry, a value from a list, or an optional field (Tool `dna.remove`). */
409
+ const dnaRemoveFn = async (params) => {
410
+ const { root, positionals, options } = params;
411
+ return runDnaMutation(root, dnaMutationRequest('remove', positionals, options));
412
+ };
413
+ /** `dna update` — change a value, a list, or the fields of one entry (Tool `dna.update`). */
414
+ const dnaUpdateFn = async (params) => {
415
+ const { root, positionals, options } = params;
416
+ return runDnaMutation(root, dnaMutationRequest('update', positionals, options));
417
+ };
418
+ /**
419
+ * The `--entry-<field>` namespace, re-exported from the DNA pillar (`src/dna/path.ts`), which owns it
420
+ * because it owns the derivation: the option set is read off `spec-002`'s entry schemas, and the
421
+ * refusal messages `src/dna/mutate.ts` writes have to spell those options the same way this module
422
+ * registers them. Re-exported rather than moved-and-forgotten so `src/core`'s public surface is
423
+ * unchanged (`spec-006` §1: the pillar is a leaf, and `src/core` is what both adapters import).
424
+ */
425
+ var path_3 = require("../dna/path");
426
+ Object.defineProperty(exports, "DNA_ENTRY_OPTION_PREFIX", { enumerable: true, get: function () { return path_3.DNA_ENTRY_OPTION_PREFIX; } });
427
+ Object.defineProperty(exports, "dnaEntryOptionName", { enumerable: true, get: function () { return path_3.dnaEntryOptionName; } });
428
+ Object.defineProperty(exports, "dnaEntryFieldOfOption", { enumerable: true, get: function () { return path_3.dnaEntryFieldOfOption; } });
429
+ /**
430
+ * The options the DNA verbs declare beside their positional `<path>`: `--value` on all four, plus —
431
+ * for `add`/`update` — one `--entry-<field>` option per field the collection entry schemas declare,
432
+ * derived from `DnaYaml` rather than hand-listed (`dnaEntryOptionNames`, `src/dna/path.ts`), so a
433
+ * collection added to `spec-002` becomes writable without editing this file.
434
+ *
435
+ * `--value`'s description carries `dl-081`'s one stated convention, because the ratification records
436
+ * it as a convention rather than something the grammar shows: it is the new entry's IDENTITY when the
437
+ * path ends at a collection and the new VALUE when it ends at a leaf (task-093 AC6). `dna set` gets
438
+ * its own, narrower description: it is the scalar verb, so `--value` there has only the second
439
+ * meaning and advertising the first would describe a case the verb refuses.
440
+ */
441
+ const DNA_VALUE_OPTION = {
442
+ name: 'value',
443
+ description: "the new entry's identity when <path> ends at a collection; the new value when it ends at a leaf (comma-separated for a list of values)",
444
+ };
445
+ const DNA_SET_VALUE_OPTION = {
446
+ name: 'value',
447
+ required: true,
448
+ description: 'the new value for <path> (scalar fields only — use `dna update` for collections and lists)',
449
+ };
450
+ const DNA_ENTRY_OPTIONS = (0, path_2.dnaEntryOptionNames)().map((field) => ({
451
+ name: (0, path_2.dnaEntryOptionName)(field),
452
+ description: `'${field}' of the entry <path> names, where that collection declares it`,
453
+ }));
454
+ /**
455
+ * `memory add` `CoreOperation.fn` (P1.3, `mutates: true` — the first Memory-document mutation and the
456
+ * pattern for all Memory CRUD; spec-006-core-domain-api §3 memory table). Follows `dna set`'s
457
+ * mutating-op template (task-025) exactly, over the Memory store instead of `dna.yaml`:
458
+ *
459
+ * 1. **`requireGitIdentity` pre-flight** (REQ-SEC-01, task-014) — refuse before any read/write when
460
+ * the git identity is unset, returning its `CoreResult.error` unchanged (exit 1).
461
+ * 2. **Argument validation** — a missing `--type`/`--title` is a usage error: `throw new UsageError`
462
+ * → exit **2** with the exact `missing required argument: --<name>` message
463
+ * (spec-008-cli-grammar §5, mapped by `exitCodeForThrow`), same classification as `dna set`'s
464
+ * missing positional.
465
+ * 3. **Resolve `--type` against the `memory.yaml` committed at `HEAD`** (spec-001-memory-yaml-schema)
466
+ * via {@link resolveAddType} — the type registry, the type's `path` and its `template` scaffold
467
+ * all resolve against the repository as committed, never against the working tree (task-095,
468
+ * `bug-085`, `dl-080` option (B)). That step takes no parsed `MemoryYaml` and this function holds
469
+ * none, so no call path can reach the decision with a working-tree registry. An unknown type is a
470
+ * domain `NOT_FOUND` (exit 1) with the exact P1.3 message
471
+ * `unknown memory type '<t>' (not defined in memory.yaml)`, returned BEFORE any write.
472
+ * 4. **Generate the id** deterministically from the type's committed `id_pattern` (task-002's
473
+ * `generateId`, REQ-SYS-07): the `{slug}` from the title, and — only for a `{n}`-token pattern — a
474
+ * sequence counter over the type's directory (`src/memory/add.ts`; no wall-clock/random). That
475
+ * counter still reads the WORKING TREE: it is `bug-087` (`release: v0.3`), a different read in
476
+ * this verb and deliberately not task-095's. What changed is only that the directory it counts in
477
+ * is now derived from the committed `path` pattern.
478
+ * 5. **Fill the committed scaffold's bytes** with only the `id`/`status: draft`/`--title`/`--tags`
479
+ * skeleton (P1.3; spec-010-memory-frontmatter-schema), then **write + commit** through task-022's
480
+ * confined `writeMemoryEntry` (REQ-SEC-06 refuse-before-write + one scoped commit
481
+ * `wf(<type>): add <id>`); the returned sha rides `CoreResult.commit`.
482
+ *
483
+ * A thrown `StorageError` (e.g. a confinement violation, an unresolved path placeholder for a
484
+ * workflow-seeded type) or a `ValidationError` (a malformed `id_pattern`) is a logic error mapped to a
485
+ * `CoreResult.error` (exit 1) so nothing escapes as an uncaught throw — the usage errors above are the
486
+ * only exit-2 path and are thrown before this try.
487
+ */
488
+ const memoryAddFn = async (params) => {
489
+ const { root, options } = params;
490
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
491
+ if (!identity.ok)
492
+ return identity;
493
+ const type = options?.type;
494
+ if (type === undefined)
495
+ throw new usage_error_1.UsageError('missing required argument: --type');
496
+ const title = options?.title;
497
+ if (title === undefined)
498
+ throw new usage_error_1.UsageError('missing required argument: --title');
499
+ const tags = (0, memory_1.parseTags)(options?.tags);
500
+ // task-095 / `bug-085`: the registry, the `path` and the scaffold all resolve at HEAD, inside a
501
+ // function that accepts no parsed `MemoryYaml` — so this verb cannot decide any of the three from
502
+ // a working-tree copy, in the same way task-091 made that true of the four transition verbs.
503
+ const resolved = (0, memory_add_type_1.resolveAddType)(root, type);
504
+ if (!resolved.ok)
505
+ return resolved;
506
+ const { pathPattern, idPattern, scaffold } = resolved.value;
507
+ try {
508
+ const sequence = (0, memory_1.hasNumericToken)(idPattern)
509
+ ? (0, memory_1.nextSequenceNumber)((0, memory_1.resolveTypeDirectory)(root, pathPattern), idPattern)
510
+ : 0;
511
+ const id = (0, validation_1.generateId)(idPattern, { slug: (0, memory_1.slugifyTitle)(title), n: sequence });
512
+ const content = (0, memory_1.renderAddDocument)(scaffold, { id, title, tags });
513
+ const message = `wf(${type}): add ${id}`;
514
+ // dl-080 (B) / bug-078, AC3: this verb CREATES, so the rule is absence rather than cleanliness.
515
+ // `nextSequenceNumber` counts the WORKING TREE, so a tree that disagrees with `HEAD` about how
516
+ // many elements exist can generate an id landing on an occupied path — and the write below is
517
+ // unconditional, which turns `wf(<type>): add <id>` into a commit that overwrites (or deletes
518
+ // lines from) an existing element. The path is resolved here rather than taken from
519
+ // `writeMemoryEntry`'s return value because the guard must run BEFORE the write; the resolution
520
+ // is pure, so doing it twice is free of side effects and keeps that throwing storage primitive's
521
+ // contract untouched. A confinement escape still throws `StorageError` from this same call.
522
+ const targetPath = (0, path_1.relative)(root, (0, memory_path_1.resolveConfinedMemoryPath)(root, pathPattern, { id })).split(path_1.sep).join('/');
523
+ const absent = (0, write_guard_1.requireAbsentTarget)(root, targetPath);
524
+ if (!absent.ok)
525
+ return absent;
526
+ const { path, sha } = (0, memory_1.writeMemoryEntry)(root, pathPattern, { id }, content, message);
527
+ const leaked = (0, write_guard_1.committedScopeError)(root, sha, targetPath, content);
528
+ if (leaked)
529
+ return leaked;
530
+ return (0, types_1.coreOk)({ id, path: (0, path_1.relative)(root, path) }, { sha, message });
531
+ }
532
+ catch (error) {
533
+ if (error instanceof storage_1.StorageError) {
534
+ return (0, types_1.coreErr)({ code: 'IO', message: error.message });
535
+ }
536
+ if (error instanceof validation_1.ValidationError) {
537
+ const reason = error.issues.length > 0 ? error.issues.map((issue) => issue.message).join('; ') : error.message;
538
+ return (0, types_1.coreErr)({ code: 'VALIDATION', message: reason });
539
+ }
540
+ throw error;
541
+ }
542
+ };
543
+ const NO_MEMORY_SEARCH_MATCHES_MESSAGE = 'no documents matched the query';
544
+ /**
545
+ * `memory search` `CoreOperation.fn` (P1.5, `mutates: false` — the FIRST Memory-document READ
546
+ * operation registered in `CORE_MODULES`; spec-006-core-domain-api §3 memory table pins its MCP
547
+ * exposure to the mechanical zero-argument Resource `wingfoil://memory/search` verbatim, so no gap
548
+ * exists to fill with a richer Resource template — see this task's Execution Notes). Wraps
549
+ * task-008/023's `src/memory/query.ts` primitives — no scan/ranking/validation logic is
550
+ * reimplemented here:
551
+ *
552
+ * 1. **Empty-query guard** ({@link validateSearchQuery}, task-023/P1.12) runs ONLY when the caller
553
+ * actually supplied a keyword positional (`positional !== undefined`): an explicit empty or
554
+ * whitespace-only string (e.g. `wingfoil memory search ""`) throws `ValidationError.semantic`
555
+ * (exit 2, message "empty search query" — `exitCodeForThrow` maps its `exitCode: 2` straight
556
+ * through, the same throw-path precedent `dnaSetFn`'s `UsageError` set). An OMITTED keyword
557
+ * (`wingfoil memory search --tag architecture`, P1.5 BDD Scenario "Filter results by metadata
558
+ * tag") is a DIFFERENT, legitimate case — a tag-only browse, per `searchMemoryDocuments`'s own doc
559
+ * comment ("browse by tag alone, no keyword" is not an empty query) — so it never reaches the
560
+ * guard at all, and resolves to `''` below.
561
+ * 2. **Load `memory.yaml`** via the same `loadOrError` + `loadMemoryYaml` path every other read op uses.
562
+ * 3. **Scan + rank** via {@link searchMemoryDocuments} (task-008: deterministic metadata-before-body
563
+ * ranking, REQ-SYS-07), narrowed by `--tag` through its own `MemorySearchOptions.tag` — the ONE
564
+ * filter the scan primitive itself understands.
565
+ * 4. **`--type`/`--status` narrow the already-ranked result** — a plain array filter, not a second
566
+ * scan pass: both are base frontmatter fields every Memory document carries
567
+ * (spec-010-memory-frontmatter-schema), and `searchMemoryDocuments` already projects both onto
568
+ * each match (task-021 added `type` alongside the pre-existing `status`), so no extra file read is
569
+ * needed.
570
+ * 5. **Zero matches is `coreOk`, never `coreErr`** (P1.5 BDD "Error - query with no matches" — the
571
+ * scenario's own title says "Error" but its assertion is exit `0`, so this is deliberately a
572
+ * successful, empty result carrying the exact message, per spec-005-cli-command-contract §1: a
573
+ * read-only command can only exit `0`/`1`).
574
+ *
575
+ * **REQ-STATE-06 (task-038; archived set widened by `dl-028-archived-states-excluded-from-context`):**
576
+ * `searchMemoryDocuments` excludes archived documents — `status: deprecated` or `superseded` — by
577
+ * default (the "default … `memory search` results" half of the Fit Criterion). This function passes
578
+ * `includeArchived: true` through to the scan ONLY when the caller's own `--status` narrow names an
579
+ * archived status (`isArchivedStatus`), so that intentional request still resolves; every other query
580
+ * (no `--status`, or a `--status` naming a live state such as `draft`) stays under the default
581
+ * exclusion.
582
+ */
583
+ const memorySearchFn = async (params) => {
584
+ const { root, positional, options } = params;
585
+ if (positional !== undefined)
586
+ (0, memory_1.validateSearchQuery)(positional);
587
+ const query = positional ?? '';
588
+ const loaded = loadOrError(() => (0, loaders_1.loadMemoryYaml)(root));
589
+ if (!loaded.ok)
590
+ return loaded;
591
+ const tag = options?.tag;
592
+ const type = options?.type;
593
+ const status = options?.status;
594
+ const scanned = (0, memory_1.searchMemoryDocuments)(root, loaded.value, query, {
595
+ ...(tag !== undefined ? { tag } : {}),
596
+ ...((0, memory_1.isArchivedStatus)(status) ? { includeArchived: true } : {}),
597
+ });
598
+ const matches = scanned
599
+ .filter((match) => (type === undefined || match.type === type) && (status === undefined || match.status === status))
600
+ .map(({ path, id, title, type: docType, status: docStatus, tags }) => ({ path, id, title, type: docType, status: docStatus, tags }));
601
+ return matches.length === 0
602
+ ? (0, types_1.coreOk)({ query, matches, message: NO_MEMORY_SEARCH_MATCHES_MESSAGE })
603
+ : (0, types_1.coreOk)({ query, matches });
604
+ };
605
+ /**
606
+ * `memory history` `CoreOperation.fn` (P1.10, `mutates: false`; spec-006-core-domain-api §3 memory
607
+ * table). Composes two existing primitives and reimplements neither — the git walk, the commit-body
608
+ * parse and the per-commit frontmatter read all belong to `src/memory`:
609
+ *
610
+ * 1. **Argument validation** — an absent (or blank) `<id>` is a usage error: `throw new UsageError`
611
+ * → exit **2** (spec-008-cli-grammar §4/§5, mapped by `exitCodeForThrow`), the same classification
612
+ * `dna set`'s missing positional gets. Blank is folded in with absent deliberately: `memory
613
+ * history ""` supplies a positional that names no document, so reporting it as "not found" would
614
+ * dress a malformed invocation up as a domain outcome (and print an id-less message).
615
+ * 2. **Load `memory.yaml`** through the same `loadOrError` + `loadMemoryYaml` path every read op uses
616
+ * — it declares the type `path` patterns that bound the id scan (spec-011: no full-repo walk).
617
+ * 3. **Resolve the id to a document** with task-009's {@link findMemoryDocumentById} — an EXACT
618
+ * frontmatter-`id` match over the deterministically-sorted document set, never a substring (that
619
+ * is `memory search`'s job). No match is a domain `NOT_FOUND` (exit 1) with the exact P1.10 message
620
+ * `document not found: <id>`, returned rather than thrown. Archived documents are deliberately NOT
621
+ * excluded: REQ-STATE-06 scopes its exclusion to default *search* results, and explicitly
622
+ * guarantees an archived element remains "present on disk and in git history" — which is precisely
623
+ * what this command reads.
624
+ * 4. **Reconstruct + project the trail** with task-015's `reconstructMemoryTransitions` (`git log
625
+ * --follow` oldest-first + a `git show` per commit for that commit's frontmatter `status`, plus the
626
+ * `Approver:`/`Reason:` body parse). This function only renames those fields into
627
+ * {@link MemoryHistoryEntryView}'s user-facing shape; it derives no state, parses no body, and
628
+ * reads no clock of its own (REQ-SYS-07 — the whole result is a pure function of the repository).
629
+ *
630
+ * A document that exists on disk but has never been committed yields `entries: []` — correctly, not
631
+ * as an error: per ADR-007 git history IS the audit trail, so an uncommitted document has none yet.
632
+ */
633
+ const memoryHistoryFn = async (params) => {
634
+ const { root, positional: id } = params;
635
+ if (id === undefined || id.trim().length === 0) {
636
+ throw new usage_error_1.UsageError('missing required argument: memory history <id>');
637
+ }
638
+ const loaded = loadOrError(() => (0, loaders_1.loadMemoryYaml)(root));
639
+ if (!loaded.ok)
640
+ return loaded;
641
+ const found = (0, memory_1.findMemoryDocumentById)(root, loaded.value, id);
642
+ if (!found) {
643
+ return (0, types_1.coreErr)({ code: 'NOT_FOUND', message: `document not found: ${id}` });
644
+ }
645
+ const entries = (0, memory_1.reconstructMemoryTransitions)(root, found.path).map((transition) => ({
646
+ sha: transition.sha,
647
+ author: `${transition.authorName} <${transition.authorEmail}>`,
648
+ timestamp: transition.date,
649
+ operation: transition.operation,
650
+ from: transition.fromState,
651
+ to: transition.toState,
652
+ approver: transition.approval
653
+ ? `${transition.approval.approverName} <${transition.approval.approverEmail}> (${transition.approval.approverRole})`
654
+ : null,
655
+ reason: transition.reason,
656
+ subject: transition.subject,
657
+ }));
658
+ return (0, types_1.coreOk)({ id, path: found.path, entries });
659
+ };
660
+ /**
661
+ * `memory submit` `CoreOperation.fn` (P1.6, `mutates: true`; spec-006-core-domain-api §3). Submit is
662
+ * "the content is written — move it forward": it commits the document as the author left it, with
663
+ * `status` advanced. Order, every refusal before the single write:
664
+ *
665
+ * 1. **`requireGitIdentity`** (REQ-SEC-01) — exit 1.
666
+ * 2. **`<id>`** absent or blank → `UsageError` (exit 2), as `memory history`.
667
+ * 3. **{@link prepareMemoryTransition}** — it resolves the `memory.yaml` committed at `HEAD` itself
668
+ * (task-091, `dl-080` (B)); no committed machine, an unreadable one, not found, unknown type,
669
+ * invalid state, or an illegal transition, each a `CoreResult.error` (exit 1); an illegal one
670
+ * carries the pinned `illegal transition <from> -> <to> for type '<type>'` (`dl-032`, P1.6 sc.2).
671
+ * 5. **Required fields** (spec-010 validation rules) — `title` and every `template.frontmatter.required`
672
+ * field must be non-empty, else `VALIDATION` `missing required field on submit: <fields>` (exit 1).
673
+ * 6. **Edit + commit** — `status` set to the target and `rejection_reason` removed (spec-010 field-write
674
+ * ownership; every other byte kept), then one commit scoped to that file, subject
675
+ * `wf(<type>): submit <id>` with no bracket and no body (spec-004 §4.3). The rendered document is
676
+ * re-parsed first (`commitMemoryTransition`'s post-condition: `status` is the target, no
677
+ * `rejection_reason`, nothing else changed); a failure is `VALIDATION` (exit 1) with nothing written. No `--reason`: spec-008 §2
678
+ * requires it only on the approval gates (`dl-027`).
679
+ */
680
+ const memorySubmitFn = async (params) => {
681
+ const { root, positional: id } = params;
682
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
683
+ if (!identity.ok)
684
+ return identity;
685
+ if (id === undefined || id.trim().length === 0) {
686
+ throw new usage_error_1.UsageError('missing required argument: memory submit <id>');
687
+ }
688
+ const prepared = (0, memory_transition_1.prepareMemoryTransition)(root, id, 'submit');
689
+ if (!prepared.ok)
690
+ return prepared;
691
+ const { type, path, frontmatter, content, from, to } = prepared.value;
692
+ const required = prepared.value.memoryYaml.types[type]?.template?.frontmatter.required ?? [];
693
+ const missing = (0, memory_1.missingRequiredFields)(frontmatter, required);
694
+ if (missing.length > 0) {
695
+ return (0, types_1.coreErr)({ code: 'VALIDATION', message: `missing required field on submit: ${missing.join(', ')}` });
696
+ }
697
+ const message = (0, memory_1.formatMemoryCommitMessage)({ type, op: 'submit', ids: [id] });
698
+ const rendered = (0, memory_1.renderSubmitDocument)(content, to);
699
+ // `carries-content`: submit is the ONE verb entitled to bring the author's body and required fields
700
+ // into its commit (spec-010's field-write ownership row), so it is not guarded against a modified
701
+ // working tree and its commit need not differ from HEAD~1 by `status` alone (task-088, bug-076 AC4).
702
+ const committed = (0, memory_transition_1.commitMemoryTransition)(root, prepared.value, rendered, message, { [memory_1.REJECTION_REASON_FIELD]: undefined }, 'carries-content');
703
+ if (!committed.ok)
704
+ return committed;
705
+ return (0, types_1.coreOk)({ id, path, from, to }, { sha: committed.value, message });
706
+ };
707
+ /**
708
+ * `memory approve` `CoreOperation.fn` (P1.7, `mutates: true`; spec-006-core-domain-api §3). Approve is
709
+ * the approval gate: it advances `status` across a `gates` edge and records **who** approved, **when**
710
+ * and **why** — identity and reason in the commit body, the ISO-8601 timestamp supplied by git itself
711
+ * (P1.2/P1.10), never written into the message. Order, every refusal before the single write, so
712
+ * "the state is unchanged" (P1.7 sc.2/sc.3) holds by construction:
713
+ *
714
+ * 1. **`requireGitIdentity`** (REQ-SEC-01) — exit 1. It is also the identity the `Approver:` line and
715
+ * the commit's own author record, so the two can never name different principals.
716
+ * 2. **`<id>`** absent or blank → `UsageError` (exit 2), as `memory submit`/`memory history`.
717
+ * 3. **`requireReason`** (REQ-SEC-04, task-041) → `UsageError` `missing required argument: --reason`
718
+ * (exit 2, P1.7 sc.2). Placed before any file is read, so an omitted reason touches nothing.
719
+ * 4. **{@link prepareMemoryTransition}** — it resolves the `memory.yaml` committed at `HEAD` itself
720
+ * (task-091, `dl-080` (B)); no committed machine, an unreadable one, not found, unknown type,
721
+ * invalid state, or an illegal transition, each a `CoreResult.error` (exit 1); an illegal one
722
+ * carries the pinned `illegal transition <from> -> <to> for type '<type>'` (`dl-032`), whose `<to>`
723
+ * is `approve`'s own next legal edge (`dl-053`).
724
+ * 6. **`requireApprovalAuthority`** (REQ-SEC-03, task-040) — exit 1 with
725
+ * `user not authorized to approve type '<type>'` (P1.7 sc.3). It runs after step 5 because its
726
+ * message interpolates the document's type, which is only knowable once the document is located,
727
+ * and `findMemoryDocumentById` is a full scan of the registered content roots — resolving the type
728
+ * twice would double the cost of every approve. Nothing is weakened by the order: the authority
729
+ * *decision* depends on nothing step 5 computes (adr-006 fixes one uniform `approver` role, keyed
730
+ * on the git identity), and step 5 writes nothing. It reads the roles from the **committed**
731
+ * `.wingfoil/dna.yaml` and resolves that file itself (task-090, `bug-079`) — which is why no
732
+ * `loadDnaYaml` call precedes it here: an uncommitted grant must not be able to reach this
733
+ * decision, and the way to guarantee that is to leave the caller nothing to pass.
734
+ * 7. **Edit + commit** — `status` set to the target and **nothing else** (spec-010 field-write
735
+ * ownership: approve changes only `status`; approver and reason live in the commit message). One
736
+ * commit scoped to that file, subject `wf(<type>): approve <id> [<from> → <to>]` with the mandatory
737
+ * `Approver:` / `Reason:` body (CLAUDE.md §5.1; `dl-054` confines the bracket to the
738
+ * approver-gated verbs, which is why `memory submit` has none). `commitMemoryTransition` re-parses
739
+ * the rendered frontmatter first and refuses (exit 1, nothing written) unless `status` is the
740
+ * target and no other field's value moved — which is exactly spec-010's "only `status`" rule, so
741
+ * no extra `expected` entry is needed here.
742
+ */
743
+ const memoryApproveFn = async (params) => {
744
+ const { root, positional: id, options } = params;
745
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
746
+ if (!identity.ok)
747
+ return identity;
748
+ if (id === undefined || id.trim().length === 0) {
749
+ throw new usage_error_1.UsageError('missing required argument: memory approve <id>');
750
+ }
751
+ const reason = (0, require_reason_1.requireReason)(options);
752
+ const prepared = (0, memory_transition_1.prepareMemoryTransition)(root, id, 'approve');
753
+ if (!prepared.ok)
754
+ return prepared;
755
+ const { type, path, content, from, to } = prepared.value;
756
+ const authorized = (0, approval_authority_1.requireApprovalAuthority)(root, type);
757
+ if (!authorized.ok)
758
+ return authorized;
759
+ const { name, email } = (0, git_identity_1.readGitIdentity)(root);
760
+ const message = (0, memory_1.formatMemoryCommitMessage)({
761
+ type,
762
+ op: 'approve',
763
+ ids: [id],
764
+ transition: { from, to },
765
+ approver: { name, email, role: approval_authority_1.APPROVER_ROLE },
766
+ reason,
767
+ });
768
+ const committed = (0, memory_transition_1.commitMemoryTransition)(root, prepared.value, (0, memory_1.setFrontmatterField)(content, 'status', to), message);
769
+ if (!committed.ok)
770
+ return committed;
771
+ return (0, types_1.coreOk)({ id, path, from, to }, { sha: committed.value, message });
772
+ };
773
+ /**
774
+ * `memory reject` `CoreOperation.fn` (P1.8, `mutates: true`; spec-006-core-domain-api §3) — the FIRST
775
+ * approver-gated verb, so the first operation that enforces REQ-SEC-03 (role authority) and REQ-SEC-04
776
+ * (mandatory reason) and the first to emit a commit with a `[from → to]` subject bracket and an
777
+ * `Approver:`/`Reason:` body (`dl-054-submit-commit-subject-bracket`, CLAUDE.md §5.1). Order, every
778
+ * refusal before the single write:
779
+ *
780
+ * 1. **`requireGitIdentity`** (REQ-SEC-01) — exit `1`.
781
+ * 2. **`<id>`** absent or blank → `UsageError` (exit `2`), as `memory submit`.
782
+ * 3. **`requireReason`** (REQ-SEC-04, task-041-mandatory-reason-on-verbs) → `UsageError`
783
+ * `missing required argument: --reason` (exit `2`, P1.8 sc.3).
784
+ * 4. **{@link prepareMemoryTransition}** with op `reject`, against the `memory.yaml` committed at
785
+ * `HEAD` (task-091, `dl-080` (B)) — no committed machine, an unreadable one, not found,
786
+ * unknown type, invalid state, or an illegal transition (the document is in no `gates`
787
+ * state), each a `CoreResult.error` at exit `1` carrying `dl-032`'s pinned contract message. The
788
+ * target is the type's `gates.<from>.reject` value, taken verbatim (`spec-001`), so it need not be
789
+ * a `sequence` member.
790
+ * 5. **{@link requireApprovalAuthority}** (REQ-SEC-03) — exit `1` with
791
+ * `user not authorized to approve type '<type>'`. It runs AFTER step 4 because that message names
792
+ * the element **type**, which is a fact of the document and is known only once the document has
793
+ * been located; nothing is written in steps 1-5, so "the state is unchanged" holds either way. The
794
+ * roles come from the **committed** `.wingfoil/dna.yaml`, which that function resolves itself
795
+ * (task-090, `bug-079`): `reject` is an approval gate and records the same `Approver:` line, so it
796
+ * takes the same baseline as `approve`, by construction rather than by remembering to.
797
+ * 6. **Edit + commit** — `status` set to the reject target and `rejection_reason` to the reason
798
+ * verbatim (spec-010 field-write ownership: reject is the one verb that writes two fields), then
799
+ * one commit scoped to that file. `commitMemoryTransition`'s post-condition re-parses the rendered
800
+ * document and refuses (`VALIDATION`, exit `1`, nothing written) unless `status` AND
801
+ * `rejection_reason` hold their expected values and no other field moved.
802
+ *
803
+ * The `Approver:` identity is the live git identity at `root` — the same one step 5 checked and the
804
+ * same one git records as the commit author, so the body line can never disagree with the attribution
805
+ * (adr-001/adr-006) — under the `APPROVER_ROLE` the authority was exercised as.
806
+ */
807
+ const memoryRejectFn = async (params) => {
808
+ const { root, positional: id, options } = params;
809
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
810
+ if (!identity.ok)
811
+ return identity;
812
+ if (id === undefined || id.trim().length === 0) {
813
+ throw new usage_error_1.UsageError('missing required argument: memory reject <id>');
814
+ }
815
+ const reason = (0, require_reason_1.requireReason)(options);
816
+ const prepared = (0, memory_transition_1.prepareMemoryTransition)(root, id, 'reject');
817
+ if (!prepared.ok)
818
+ return prepared;
819
+ const { type, path, from, to, content } = prepared.value;
820
+ const authorized = (0, approval_authority_1.requireApprovalAuthority)(root, type);
821
+ if (!authorized.ok)
822
+ return authorized;
823
+ const { name, email } = (0, git_identity_1.readGitIdentity)(root);
824
+ const message = (0, memory_1.formatMemoryCommitMessage)({
825
+ type,
826
+ op: 'reject',
827
+ ids: [id],
828
+ transition: { from, to },
829
+ approver: { name, email, role: approval_authority_1.APPROVER_ROLE },
830
+ reason,
831
+ });
832
+ const rendered = (0, memory_1.renderRejectDocument)(content, to, reason);
833
+ const committed = (0, memory_transition_1.commitMemoryTransition)(root, prepared.value, rendered, message, { [memory_1.REJECTION_REASON_FIELD]: reason });
834
+ if (!committed.ok)
835
+ return committed;
836
+ return (0, types_1.coreOk)({ id, path, from, to, reason }, { sha: committed.value, message });
837
+ };
838
+ /**
839
+ * `memory deprecate` `CoreFn` (P1.9, `mutates: true`; spec-006-core-domain-api §3) — the retire verb.
840
+ * It rides `spec-001-memory-yaml-schema`'s **implicit wildcard edge**: `deprecated` is "a built-in
841
+ * wildcard edge from *any* state to a reserved `deprecated` state, always legal", never declared in a
842
+ * type's `sequence`/`gates`/`waiting`. The target therefore comes from the engine
843
+ * (`prepareMemoryTransition` → `resolveTypeTransition`, itself resolved from `memory.yaml`), never
844
+ * from a literal here — so this one function retires a `task` sitting in a `waiting` state, an
845
+ * `accepted` `adr`, a terminal `decision-log`, and a type that falls back to `defaults.states`, with
846
+ * no per-type branch. (`adr`/`tech-spec`'s `superseded` is NOT this verb's target: it is a `waiting`
847
+ * edge fired by a later element's `supersedes:` field — see `SUPERSEDED_STATE` in
848
+ * `src/memory/state-machine.ts` and this task's Execution Notes, decision D1.)
849
+ *
850
+ * Order, every refusal before the single write:
851
+ *
852
+ * 1. **`requireGitIdentity`** (REQ-SEC-01) — exit `1`.
853
+ * 2. **`<id>`** absent or blank → `UsageError` (exit `2`), as `memory submit`/`memory reject`.
854
+ * 3. **{@link prepareMemoryTransition}** with op `deprecate`, against the `memory.yaml` committed at
855
+ * `HEAD` (task-091, `dl-080` (B)) — no committed machine, an unreadable one, not found,
856
+ * unknown type, or a `status` that is not a state of the type, each a
857
+ * `CoreResult.error` (exit `1`). There is no illegal-transition branch: the wildcard edge is legal
858
+ * from every state.
859
+ * 4. **Already-deprecated guard** (P1.9 sc.3) — `VALIDATION` `document already deprecated: <id>`
860
+ * (exit `1`), state unchanged. It lives here rather than in the state machine because the engine
861
+ * must keep the edge legal from *any* state; the condition is expressed as "the document is
862
+ * already AT the resolved target", so it never hard-codes a state name.
863
+ * 5. **Edit + commit** — `status` set to the target and **nothing else** (spec-010 field-write
864
+ * ownership: this verb is `status`-only, so `rejection_reason` and every other field survive
865
+ * untouched — enforced, not assumed, by `commitMemoryTransition`'s re-parse post-condition, which
866
+ * refuses the write if any unowned field moved). One commit scoped to that file, subject
867
+ * `wf(<type>): deprecate <id> [<from> → deprecated]` (`dl-054` / `spec-004` §4.3: the bracket
868
+ * belongs to `approve`/`reject`/`deprecate`), with a `Reason:` body line **only** when `--reason`
869
+ * was given.
870
+ *
871
+ * Deliberately absent: no `Approver:` line and no `requireApprovalAuthority` call. `deprecate` is not
872
+ * an approval gate — REQ-SEC-03's Fit Criterion is scoped to an *approve* attempt, and REQ-SEC-04's
873
+ * own Traceability line (as amended by `dl-027`) says so in as many words: "`memory deprecate` is not
874
+ * an approval gate (no `Approver:` line, no authority check)". `memory history` (P1.10) already reads
875
+ * that shape back — it parses `Reason:` independently of `Approver:` for exactly this verb.
876
+ */
877
+ const memoryDeprecateFn = async (params) => {
878
+ const { root, positional: id, options } = params;
879
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
880
+ if (!identity.ok)
881
+ return identity;
882
+ if (id === undefined || id.trim().length === 0) {
883
+ throw new usage_error_1.UsageError('missing required argument: memory deprecate <id>');
884
+ }
885
+ // `--reason` is optional here (`dl-027`), but a reason that IS given must be recordable in the
886
+ // trailer — blank or trailer-shaped is a usage error at exit 2, before anything is read or written
887
+ // (`dl-067` clauses 1 and 4; `bug-042` F2/F3, whose amendment makes THIS verb the exploitable one).
888
+ const reason = (0, require_reason_1.optionalReason)(options);
889
+ const prepared = (0, memory_transition_1.prepareMemoryTransition)(root, id, 'deprecate');
890
+ if (!prepared.ok)
891
+ return prepared;
892
+ const { type, path, from, to, content } = prepared.value;
893
+ if (from === to) {
894
+ return (0, types_1.coreErr)({ code: 'VALIDATION', message: `document already deprecated: ${id}` });
895
+ }
896
+ const message = (0, memory_1.formatMemoryCommitMessage)({ type, op: 'deprecate', ids: [id], transition: { from, to }, reason });
897
+ const rendered = (0, memory_1.setFrontmatterField)(content, 'status', to);
898
+ const committed = (0, memory_transition_1.commitMemoryTransition)(root, prepared.value, rendered, message);
899
+ if (!committed.ok)
900
+ return committed;
901
+ return (0, types_1.coreOk)({ id, path, from, to, reason }, { sha: committed.value, message });
902
+ };
903
+ /**
904
+ * `directive create` `CoreOperation.fn` (P3.1, `mutates: true` — the FIRST Directives-pillar mutation
905
+ * and the pillar's first CLI verb; spec-006-core-domain-api §3 directives table). Follows `dna set`'s
906
+ * mutating-op template (task-025) exactly, over the Directives pillar instead of `dna.yaml`:
907
+ *
908
+ * 1. **`requireGitIdentity` pre-flight** (REQ-SEC-01, task-014) — refuse before any read/write when
909
+ * the git identity is unset, returning its `CoreResult.error` unchanged (exit 1).
910
+ * 2. **Argument validation** — both failures are usage errors (`throw new UsageError` → exit **2**,
911
+ * mapped by `exitCodeForThrow`): an absent `--name` uses spec-008-cli-grammar §4's exact
912
+ * `missing required argument: --<name>` wording (same as `memoryAdd`), and a non-kebab-case name
913
+ * uses P3.1's exact `invalid directive name (use kebab-case)` message
914
+ * ({@link INVALID_DIRECTIVE_NAME_MESSAGE}). Because {@link isValidDirectiveName} runs before any
915
+ * path is built, a traversal-shaped name (`../x`, `a/b`, `/etc/passwd`) can never reach the write.
916
+ * 3. **Already-exists check** — a file at `.wingfoil/directives/custom/<name>.md` makes this a domain
917
+ * `CONFLICT` (the input is well-formed; the target is taken), returned NOT thrown, with P3.1's exact
918
+ * `directive already exists: <name>` message → exit 1 via `exitCodeForError`. Nothing is written, so
919
+ * the BDD's "no file is overwritten" holds by construction. The check is scoped to `custom/` only,
920
+ * per `X_cli-cmds.md`'s "Unique within custom directives" — `built-in/` holds package-shipped
921
+ * templates a user does not author against (spec-011's built-in/custom split).
922
+ * 4. **Render + persist + commit** — {@link renderCustomDirective} produces the whole document as a
923
+ * pure function of `name` (REQ-SYS-07: no clock, no randomness, so two runs are byte-identical),
924
+ * satisfying `DirectiveFrontmatter` (spec-013) so the new file loads through `loadDirectives`
925
+ * alongside the ten `wingfoil init` scaffolds. Then `writeDocument` + `commitPaths` on the ONE
926
+ * scoped path — exactly one commit, `wf(directive): create <name>` — and the sha rides
927
+ * `CoreResult.commit`, the same shape `dnaSet` returns for `wf(dna): set <key>`.
928
+ */
929
+ const directiveCreateFn = async (params) => {
930
+ const { root, options } = params;
931
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
932
+ if (!identity.ok)
933
+ return identity;
934
+ const name = options?.name;
935
+ if (name === undefined)
936
+ throw new usage_error_1.UsageError('missing required argument: --name');
937
+ if (!(0, create_1.isValidDirectiveName)(name))
938
+ throw new usage_error_1.UsageError(create_1.INVALID_DIRECTIVE_NAME_MESSAGE);
939
+ // One spelling of the location, reused for the existence check, the write and the commit scope, so
940
+ // the three can never drift apart. `join` normalizes the separators, so the POSIX form is also the
941
+ // correct absolute path on Windows; the root-relative form is what `commitPaths` stages.
942
+ const relativePath = `.wingfoil/directives/custom/${name}.md`;
943
+ const absolutePath = (0, path_1.join)(root, relativePath);
944
+ if ((0, storage_1.documentExists)(absolutePath)) {
945
+ return (0, types_1.coreErr)({ code: 'CONFLICT', message: `directive already exists: ${name}` });
946
+ }
947
+ // dl-080 (B) / bug-078: the existence check above only sees the WORKING TREE, so a file deleted but
948
+ // not committed leaves this path "free" while `HEAD` still carries it — and the create then commits
949
+ // a diff that REMOVES the old content under a subject saying "create". Refuse instead.
950
+ const unmodified = (0, write_guard_1.requireUnmodifiedTarget)(root, relativePath);
951
+ if (!unmodified.ok)
952
+ return unmodified;
953
+ const content = (0, create_1.renderCustomDirective)(name);
954
+ (0, storage_1.writeDocument)(absolutePath, content);
955
+ const message = `wf(directive): create ${name}`;
956
+ const sha = (0, storage_1.commitPaths)(root, [relativePath], message);
957
+ const leaked = (0, write_guard_1.committedScopeError)(root, sha, relativePath, content);
958
+ if (leaked)
959
+ return leaked;
960
+ return (0, types_1.coreOk)({ name, path: relativePath }, { sha, message });
961
+ };
962
+ /**
963
+ * `directive assign` `CoreOperation.fn` (P3.2 **and P3.7**, `mutates: true`; spec-006 §3 row
964
+ * `directiveAssign`, module `directive` per dl-041). P3.7 (US-4-06, "bind multiple directives to one
965
+ * role") registers **no operation of its own** — spec-006 §3 has three `directive` rows and none for
966
+ * it, and `spec-008` §1 / `X_cli-cmds.md` list no fourth directive verb — so it is this operation
967
+ * with a list-valued `--directive`. Same mutating-op order as {@link directiveCreateFn}:
968
+ *
969
+ * 1. `requireGitIdentity` pre-flight (REQ-SEC-01).
970
+ * 2. `--directive` then `--role` presence — `UsageError`, spec-008 §4 wording, exit 2. A `--directive`
971
+ * value that parses to **no ids** (`""`, `" "`, `","`) counts as absent, the same reading
972
+ * `parseTags` takes of an empty `--tags` (`[AUTHORING]`, task-056 D3: before P3.7 such a value
973
+ * reached step 3 and failed with an empty id in the message, `unknown directive: `).
974
+ * 3. `checkAssignable` — which resolves **both** of its baselines from the repository as committed at
975
+ * `HEAD` itself, the role catalogue (task-091, `bug-082`) and the directive inventory (task-096,
976
+ * `bug-086`), per `dl-080` (B) — returns P3.2's exact
977
+ * `unknown role '<role>' (not defined in dna.yaml)` / `unknown directive: <id>` as `NOT_FOUND`
978
+ * (exit 1), role first and then each id in argument order. It validates **every** id before
979
+ * anything is written, which is P3.7 Sc.3's "no partial assignment is persisted": one unknown id
980
+ * leaves `roles.yaml` byte-identical and commits nothing. There is no working-tree pre-load to
981
+ * pass it: an untracked directive file used to be bindable, and the committed `roles.yaml` then
982
+ * named a directive no clone would have.
983
+ * 4. `updateRoleAssignments` appends the ids (`withAssignedDirectives`: idempotent, order-preserving,
984
+ * never sorting) through the comment-preserving `roles.yaml` writer and makes ONE commit,
985
+ * `wf(directive): assign <id1>, <id2> to <role>`, staging only `.wingfoil/roles.yaml`. A request
986
+ * whose ids are all already bound is a success with no write and no commit (P3.7 Sc.2 "Binding is
987
+ * idempotent"); a partially overlapping one appends only what is missing.
988
+ */
989
+ const directiveAssignFn = async (params) => {
990
+ const { root, options } = params;
991
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
992
+ if (!identity.ok)
993
+ return identity;
994
+ const directive = options?.directive;
995
+ if (directive === undefined)
996
+ throw new usage_error_1.UsageError('missing required argument: --directive');
997
+ const directives = (0, roles_edit_1.parseDirectiveIds)(directive);
998
+ if (directives.length === 0)
999
+ throw new usage_error_1.UsageError('missing required argument: --directive');
1000
+ const role = options?.role;
1001
+ if (role === undefined)
1002
+ throw new usage_error_1.UsageError('missing required argument: --role');
1003
+ const invalid = (0, directive_assign_1.checkAssignable)(root, role, directives);
1004
+ if (invalid)
1005
+ return (0, types_1.coreErr)(invalid);
1006
+ const message = `wf(directive): assign ${directives.join(', ')} to ${role}`;
1007
+ const updated = (0, directive_assign_1.updateRoleAssignments)(root, role, (current) => (0, roles_edit_1.withAssignedDirectives)(current, directives), message);
1008
+ if (!updated.ok)
1009
+ return updated;
1010
+ return (0, types_1.coreOk)({ directives, role, assignments: updated.value.assignments }, updated.commit);
1011
+ };
1012
+ /**
1013
+ * `directive remove` `CoreOperation.fn` (P3.3, `mutates: true`; spec-006 §3 row `directiveRemove`,
1014
+ * module `directive` per dl-041 B). The first operation in the system that DELETES a file, and the
1015
+ * one that completes **REQ-SEC-07**: `task-042-immutable-builtin-assets` shipped clause (a) as the
1016
+ * `requireCustomAsset` primitive, and `dl-030-req-sec-07-referenced-asset-ownership` (`ready`,
1017
+ * option (b)) assigns clause (b)'s directive half here. Both are enforced, in this order, and both
1018
+ * strictly **before** anything on disk changes:
1019
+ *
1020
+ * 1. `requireGitIdentity` pre-flight (REQ-SEC-01).
1021
+ * 2. `<name>` presence — a missing or blank positional is a `UsageError` → exit **2**
1022
+ * (`missing required argument: directive remove <name>`, the `memory submit <id>` precedent).
1023
+ * 3. **Resolve the name to a real file**, never to a constructed path. `loadDirectives` reads every
1024
+ * installed directive and {@link selectDirectivesById} picks the winner for that id under
1025
+ * `dl-037`'s custom-wins precedence (`src/core/context.ts` — the same primitive `directives list`
1026
+ * and context assembly use, so the rule is implemented once). No match is a domain `NOT_FOUND`
1027
+ * with P3.2's wording, reused: `unknown directive: <name>`. Because the path comes from a file
1028
+ * that was just read, a traversal-shaped argument can never reach the filesystem — it simply
1029
+ * resolves to nothing.
1030
+ *
1031
+ * This read stays on the **working tree**, deliberately (task-096 AC4). It answers *which file on
1032
+ * disk am I being asked to delete* — the asset itself, not a gate on it — and step 7's write guard
1033
+ * (`requireUnmodifiedTarget`, task-092) then decides whether that file may be deleted at all. Resolving it at `HEAD` instead would
1034
+ * answer an **untracked** directive file with `unknown directive: <id>`, which is false to the
1035
+ * user's screen, where the file plainly is; the refusal they need is the write guard's, which
1036
+ * names the file and its `git status` code. Deleting a file that exists in no commit is not this
1037
+ * verb's act anyway: its contract is to produce one commit that *records* the deletion, and there
1038
+ * is nothing there to record.
1039
+ * 4. **REQ-SEC-07 clause (a)** — `requireCustomAsset('directive', <that file's path>)`. Passing the
1040
+ * RESOLVED path rather than the bare name is what makes P3.3's pinned
1041
+ * `built-in directives cannot be removed` fire instead of the primitive's generic refusal; task-042's
1042
+ * reviewer recorded that name→path gap as this task's hand-off. Since `task-057` a fresh project
1043
+ * really does carry six built-ins, so the scenario is exercised against the real scaffold.
1044
+ * 5. **REQ-SEC-06** — {@link requireConfinedTarget}, step 4's filesystem-level companion
1045
+ * (`task-102`, `bug-044`). `requireCustomAsset` judges the path's *shape* and is right to pass
1046
+ * `directives/custom/<name>.md`; only `realpath` knows that a symlinked `custom/` puts that file
1047
+ * outside the project entirely. Before this ran, the verb unlinked the outside file for real and
1048
+ * then failed on `git add` with a raw `Command failed: git …` — a destroyed file, an unmapped
1049
+ * error, and nothing recorded. The check is here, ahead of every mutation, because the order
1050
+ * *is* the property: a refusal issued after the deletion is a report, not a guard.
1051
+ * 6. **REQ-SEC-07 clause (b)** — {@link checkUnreferenced} over the `roles.yaml` committed at `HEAD`:
1052
+ * a role (or `global`) that still binds the id refuses the removal, naming the referrer. It
1053
+ * resolves that baseline itself (task-096, `bug-086`, `dl-080` (B)); there is no working-tree
1054
+ * pre-load to pass it, because an **uncommitted** deletion of the reference used to be enough to
1055
+ * destroy a file the committed `roles.yaml` still bound. A `roles.yaml` absent from `HEAD` still
1056
+ * means "no bindings yet" (task-051/053), not a failure. Note the interaction with step 4: a custom
1057
+ * file SHADOWING a built-in is removable, so removing it changes which file wins resolution — that
1058
+ * is dl-037's precedence working as specified, and the built-in is left byte-identical.
1059
+ * 7. **Delete + commit** — `removeDocument` then `commitPaths` on the ONE scoped path, producing
1060
+ * exactly one commit `wf(directive): remove <name>`; the sha rides `CoreResult.commit`, the same
1061
+ * shape every other mutating op returns. `commitPaths` stages with `git add -- <path>`, which
1062
+ * records the deletion, and commits with `git commit --only -- <path>` (bug-027), so nothing else
1063
+ * a caller had staged is swept in.
1064
+ *
1065
+ * `roles.yaml` is never written: P3.3 refuses a still-assigned directive rather than unbinding it, so
1066
+ * this operation has no interaction with the comment-preserving `roles.yaml` writer at all.
1067
+ */
1068
+ const directiveRemoveFn = async (params) => {
1069
+ const { root, positional: name } = params;
1070
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
1071
+ if (!identity.ok)
1072
+ return identity;
1073
+ if (name === undefined || name.trim().length === 0) {
1074
+ throw new usage_error_1.UsageError('missing required argument: directive remove <name>');
1075
+ }
1076
+ const directiveFiles = loadOrError(() => (0, loaders_1.loadDirectives)(root));
1077
+ if (!directiveFiles.ok)
1078
+ return directiveFiles;
1079
+ const target = (0, context_1.selectDirectivesById)(directiveFiles.value, new Set([name])).byId.get(name);
1080
+ if (target === undefined) {
1081
+ return (0, types_1.coreErr)({ code: 'NOT_FOUND', message: `unknown directive: ${name}` });
1082
+ }
1083
+ const custom = (0, builtin_asset_1.requireCustomAsset)('directive', target.path);
1084
+ if (!custom.ok)
1085
+ return custom;
1086
+ // `DirectiveFile.path` is built with the PLATFORM separator (`join('directives', …)` in
1087
+ // `./loaders.ts`), so re-spell it with `/` for the value we return and the path we stage — git
1088
+ // speaks POSIX separators, and the payload must not differ by platform (REQ-SYS-07).
1089
+ const relativePath = ['.wingfoil', ...target.path.split(/[\\/]/)].join('/');
1090
+ // REQ-SEC-06 / bug-044: `requireCustomAsset` above judged the SHAPE of that path and passed it,
1091
+ // correctly — every segment is inside the project. Only the filesystem knows that a symlinked
1092
+ // `custom/` puts the file itself outside. This is the companion clause, and it stands here, next
1093
+ // to its string-level sibling and ahead of every mutation, because the bug was order: the shipped
1094
+ // verb unlinked the outside file and reported afterwards.
1095
+ const confined = (0, confinement_1.requireConfinedTarget)(root, relativePath, 'remove');
1096
+ if (!confined.ok)
1097
+ return confined;
1098
+ const referrer = (0, directive_assign_1.checkUnreferenced)(root, name);
1099
+ if (referrer)
1100
+ return (0, types_1.coreErr)(referrer);
1101
+ // dl-080 (B) / bug-078: staging a deletion discards the working-tree blob, so an uncommitted edit
1102
+ // to the directive being removed does not ride into the commit — it is DESTROYED, reaching no
1103
+ // commit anywhere. Different harm, same rule: the target carries modifications this operation does
1104
+ // not own, so refuse and let the author decide what to do with them.
1105
+ const unmodified = (0, write_guard_1.requireUnmodifiedTarget)(root, relativePath);
1106
+ if (!unmodified.ok)
1107
+ return unmodified;
1108
+ (0, storage_1.removeDocument)((0, path_1.join)(root, relativePath));
1109
+ const message = `wf(directive): remove ${name}`;
1110
+ const sha = (0, storage_1.commitPaths)(root, [relativePath], message);
1111
+ const leaked = (0, write_guard_1.committedScopeError)(root, sha, relativePath, null);
1112
+ if (leaked)
1113
+ return leaked;
1114
+ return (0, types_1.coreOk)({ name, path: relativePath }, { sha, message });
1115
+ };
1116
+ /**
1117
+ * `directives list` `CoreOperation.fn` (P3.4, `mutates: false` — spec-006 §3 directives table). Wraps
1118
+ * `loadDirectiveListing` (`./directives-list.ts`, which carries the annotation rules and the
1119
+ * rationale for not deduplicating shadowed ids) in the same `loadOrError` mapping every read-only
1120
+ * pillar query uses, so a schema-invalid directive file or `roles.yaml` is a `VALIDATION` domain
1121
+ * failure (exit 1), never a throw.
1122
+ *
1123
+ * This replaces the bare `wrapReadOnly(loadDirectives)` registration task-006 wired in: the payload
1124
+ * keeps every field that registration returned (`path`, `frontmatter`, per entry, unchanged) and adds
1125
+ * the role annotation P3.4 requires. Since task-055 (dl-042) the value is `{ entries, warnings }`.
1126
+ */
1127
+ const directivesListFn = async (params) => {
1128
+ const { root, options } = params;
1129
+ return loadOrError(() => (0, directives_list_1.loadDirectiveListing)(root, options?.role));
1130
+ };
1131
+ /**
1132
+ * The production `CoreModule` registry (spec-006 §2, §4). `src/cli`'s command registrar and
1133
+ * `src/mcp`'s Tool/Resource registrar both import this exact array — see spec-006 §4.1: "no
1134
+ * duplicated or hand-copied operation list in either surface module".
1135
+ *
1136
+ * SCOPE (task-006-dual-interface-shared-core, see the task's Execution Notes for the full
1137
+ * rationale): only the core functions that already legitimately exist are wired in here today —
1138
+ * task-004's three read-only per-pillar loaders that have a natural spec-006 §3 counterpart
1139
+ * (`dnaShow`, `directivesList`, `workflowList` — all `mutates: false`), plus
1140
+ * task-028-implement-paths-category's `paths` (P2.5, `wingfoil paths [category]`) — the first FLAT,
1141
+ * no-verb command (`deriveVerb('paths', 'paths') === ''`, spec-008-cli-grammar §1) and the first to
1142
+ * declare a `--list` flag (`CoreOperation.flags`, `./registry.ts`); its `category` rides the same
1143
+ * generic bare-positional seam `dna show`'s `section` does (task-026's `ParamsContext.positional`),
1144
+ * so no per-operation positional metadata is needed here. `loadMemoryYaml`
1145
+ * is deliberately NOT registered as a `memory` module operation: it loads the Memory *pillar's own
1146
+ * config* (`memory.yaml`'s types/state-machines), a different concept from spec-006 §3's `memory`
1147
+ * module (which operates on Memory *documents* — `memoryAdd`, `memorySearch`, ...); registering it
1148
+ * under a `memoryXxx` name would misrepresent it as the latter. As of
1149
+ * task-025-implement-dna-set the registry has its FIRST mutating operation — `dna.dnaSet`
1150
+ * (`mutates: true`, P2.1); `memory.memoryAdd` (P1.3, task-020), `directive.directiveCreate`
1151
+ * (P3.1, task-050), `directive.directiveAssign` (P3.2, task-051) and `memory.memorySubmit` (P1.6,
1152
+ * task-045) have since joined it, and the remaining spec-006 §3 mutating functions
1153
+ * (`memoryApprove`, `directiveRemove`, `workflowStart`, ...) are still later tasks' scope. The
1154
+ * REQ-SYS-05 parity test in `test/core/parity.test.ts` runs against this exact array, so it is now a
1155
+ * live regression guard: each of those mutating ops must appear as both a CLI command and an MCP
1156
+ * Tool, or the diff fails.
1157
+ */
1158
+ exports.CORE_MODULES = [
1159
+ {
1160
+ name: 'dna',
1161
+ operations: {
1162
+ // The DNA mutation surface (task-093, `dl-081-dna-mutation-surface-shape` option (E)): three
1163
+ // verbs carrying the collection in their argument rather than in the verb name, so the verb
1164
+ // count stays constant as `spec-002`'s schema grows and spec-006 §3's one-Tool-per-function rule
1165
+ // costs three Tools (`dna.add`, `dna.remove`, `dna.update`) instead of the dozen a
1166
+ // per-collection verb set would need. The path itself is the POSITIONAL `<path>` every command
1167
+ // in this CLI uses for its target (`dl-082-cli-parameter-shape`), registered generically by
1168
+ // `src/cli/program.ts`, which is why no option declares it here.
1169
+ dnaAdd: { name: 'dnaAdd', mutates: true, options: [DNA_VALUE_OPTION, ...DNA_ENTRY_OPTIONS], fn: dnaAddFn },
1170
+ // `remove` declares no entry-field options: it takes what to drop, never what to write.
1171
+ dnaRemove: { name: 'dnaRemove', mutates: true, options: [DNA_VALUE_OPTION], fn: dnaRemoveFn },
1172
+ // The FIRST `mutates: true` operation in production (spec-006 §3 dna table) — by construction an
1173
+ // MCP Tool (`dna.set`) + CLI command (`wingfoil dna set`), and the op the REQ-SYS-05 parity test
1174
+ // now actually guards (task-025-implement-dna-set). Its second positional became `--value` in
1175
+ // task-093 (`dl-082`) — a breaking change to a shipped command, made before `minor-v0.2` ships.
1176
+ dnaSet: { name: 'dnaSet', mutates: true, options: [DNA_SET_VALUE_OPTION], fn: dnaSetFn },
1177
+ dnaShow: { name: 'dnaShow', mutates: false, fn: dnaShowFn },
1178
+ dnaUpdate: { name: 'dnaUpdate', mutates: true, options: [DNA_VALUE_OPTION, ...DNA_ENTRY_OPTIONS], fn: dnaUpdateFn },
1179
+ },
1180
+ },
1181
+ {
1182
+ name: 'memory',
1183
+ operations: {
1184
+ // The FIRST Memory-document mutation (P1.3, spec-006 §3 memory table) — `mutates: true`, so by
1185
+ // construction an MCP Tool (`memory.add`) + CLI command (`wingfoil memory add`), and the second
1186
+ // op the REQ-SYS-05 parity test now guards (alongside `dna.set`). It declares the value-bearing
1187
+ // `--type`/`--title`/`--tags` options (task-020's `CoreOperation.options` seam, reused by
1188
+ // task-021). `loadMemoryYaml` (the Memory *pillar config* loader) is deliberately NOT registered
1189
+ // here — it is a different concept from this `memory` module, which operates on Memory *documents*.
1190
+ memoryAdd: {
1191
+ name: 'memoryAdd',
1192
+ mutates: true,
1193
+ options: [
1194
+ { name: 'type', required: true },
1195
+ { name: 'title', required: true },
1196
+ { name: 'tags' },
1197
+ ],
1198
+ fn: memoryAddFn,
1199
+ },
1200
+ // P1.10 (task-049-memory-history) — `mutates: false`, so by construction an MCP Resource +
1201
+ // CLI command (`wingfoil memory history <id>`). It declares no flags and no value options: the
1202
+ // document id rides the generic bare `positional` seam (task-026), per spec-008-cli-grammar §7's
1203
+ // bare-`<id>` rule for a command whose noun already scopes the type.
1204
+ memoryHistory: { name: 'memoryHistory', mutates: false, fn: memoryHistoryFn },
1205
+ // The FIRST Memory-document READ operation (P1.5, spec-006 §3 memory table) — `mutates: false`,
1206
+ // so by construction an MCP Resource (`wingfoil://memory/search`, the mechanical zero-argument
1207
+ // form spec-006 §3 pins verbatim) + CLI command (`wingfoil memory search`). Its keyword rides the
1208
+ // generic bare `positional` seam (task-026); `--tag`/`--status`/`--type` are OPTIONAL value
1209
+ // options (task-020's seam) — none is `required`, since the AC/BDD only mandate the bare keyword
1210
+ // form and the `--tag` filter (task-021-implement-memory-search's Execution Notes).
1211
+ memorySearch: {
1212
+ name: 'memorySearch',
1213
+ mutates: false,
1214
+ options: [{ name: 'tag' }, { name: 'status' }, { name: 'type' }],
1215
+ fn: memorySearchFn,
1216
+ },
1217
+ // P1.6 (task-045-memory-submit) — `mutates: true`: CLI `wingfoil memory submit <id>` + MCP Tool
1218
+ // `memory.submit`. The id rides the bare `positional` seam (spec-008 §7); no flags, no options.
1219
+ memorySubmit: { name: 'memorySubmit', mutates: true, fn: memorySubmitFn },
1220
+ // P1.7 (task-046-memory-approve) — `mutates: true`: CLI `wingfoil memory approve <id> --reason
1221
+ // <text>` + MCP Tool `memory.approve`. The id rides the bare `positional` seam (spec-008 §7);
1222
+ // `--reason` is the one declared value option, `required` per spec-008 §2 / REQ-SEC-04 (the
1223
+ // declaration is metadata — `memoryApproveFn` does the enforcing, via `requireReason`).
1224
+ memoryApprove: {
1225
+ name: 'memoryApprove',
1226
+ mutates: true,
1227
+ options: [{ name: 'reason', required: true }],
1228
+ fn: memoryApproveFn,
1229
+ },
1230
+ // P1.8 (task-047-memory-reject) — `mutates: true`: CLI `wingfoil memory reject <id> --reason
1231
+ // <text>` + MCP Tool `memory.reject`. The FIRST approver-gated operation (REQ-SEC-03) and the
1232
+ // first with a `required` value option: `required` is declarative metadata only (Commander
1233
+ // registers every declared option the same way), so `memoryRejectFn` enforces it via
1234
+ // `requireReason` (REQ-SEC-04, task-041). The id rides the bare `positional` seam (spec-008 §7).
1235
+ memoryReject: {
1236
+ name: 'memoryReject',
1237
+ mutates: true,
1238
+ options: [{ name: 'reason', required: true }],
1239
+ fn: memoryRejectFn,
1240
+ },
1241
+ // P1.9 (task-048-memory-deprecate) — `mutates: true`: CLI `wingfoil memory deprecate <id>
1242
+ // [--reason <text>]` + MCP Tool `memory.deprecate`. `--reason` is declared WITHOUT `required`,
1243
+ // unlike `memoryReject`'s: `spec-008` §2 and `dl-027-req-sec-04-deprecate-reason-scope`
1244
+ // (option (a), already applied to REQ-SEC-04) make it optional on this verb, which is not an
1245
+ // approval gate. The id rides the bare `positional` seam (spec-008 §7).
1246
+ memoryDeprecate: {
1247
+ name: 'memoryDeprecate',
1248
+ mutates: true,
1249
+ options: [{ name: 'reason' }],
1250
+ fn: memoryDeprecateFn,
1251
+ },
1252
+ },
1253
+ },
1254
+ {
1255
+ name: 'directives',
1256
+ operations: {
1257
+ directivesList: {
1258
+ name: 'directivesList',
1259
+ mutates: false,
1260
+ options: [{ name: 'role' }],
1261
+ fn: directivesListFn,
1262
+ },
1263
+ },
1264
+ },
1265
+ {
1266
+ name: 'paths',
1267
+ operations: {
1268
+ // Self-named (operation name === module name) — the flat/no-verb `wingfoil paths [category]`
1269
+ // form (see `deriveVerb`, `./registry.ts`), not `wingfoil paths paths`. `category` rides the
1270
+ // generic bare positional (task-026's seam); only `--list` is declared here.
1271
+ paths: { name: 'paths', mutates: false, flags: ['list'], fn: pathsFn },
1272
+ },
1273
+ },
1274
+ {
1275
+ name: 'workflow',
1276
+ operations: {
1277
+ workflowList: {
1278
+ name: 'workflowList',
1279
+ mutates: false,
1280
+ fn: wrapReadOnly(loaders_1.loadWorkflowsYaml),
1281
+ },
1282
+ },
1283
+ },
1284
+ // task-050-directive-create (P3.1). A `directive` (SINGULAR) module, DISTINCT from the `directives`
1285
+ // module above, because `CoreModule.name` IS the wire-visible `wingfoil <noun>` segment
1286
+ // (`buildCliCommands`, `src/cli/registrar.ts`) and the `{module}.{verb}` MCP Tool name
1287
+ // (`deriveMcpToolName`, `src/mcp/registrar.ts`) — and the P3 pillar deliberately exposes TWO nouns:
1288
+ // `wingfoil directive create|assign|remove` (BDD P3.1/P3.2/P3.3; spec-008-cli-grammar §1 lists the
1289
+ // noun as singular `directive`) alongside `wingfoil directives list` (BDD P3.4). spec-006 §3's own
1290
+ // directives table pins both spellings on adjacent rows (`wingfoil directive create` + Tool
1291
+ // `directive.create`; `wingfoil directives list` + Resource `wingfoil://directives/list`). Filing
1292
+ // `directiveCreate` under the plural module would instead derive `wingfoil directives
1293
+ // directive-create` (`deriveVerb` kebab-cases the whole name when it does not start with the module
1294
+ // name), contradicting all of the above — so a second module is what realizes the approved contract
1295
+ // without touching `deriveVerb` or either registrar. Placed last purely to keep this task's diff off
1296
+ // the concurrently-edited `directives` block; `enumerateOperations` sorts, so position is inert.
1297
+ {
1298
+ name: 'directive',
1299
+ operations: {
1300
+ // The FIRST Directives-pillar mutation (P3.1) — `mutates: true`, so by construction an MCP Tool
1301
+ // (`directive.create`) + CLI command (`wingfoil directive create`), and the third op the
1302
+ // REQ-SYS-05 parity test guards (alongside `dna.set` and `memory.add`). `--name` is declared
1303
+ // `required` as metadata; `directiveCreateFn` itself enforces it (throwing a `UsageError` →
1304
+ // exit 2), matching `memoryAdd`'s `--type`/`--title` precedent.
1305
+ directiveCreate: {
1306
+ name: 'directiveCreate',
1307
+ mutates: true,
1308
+ options: [{ name: 'name', required: true }],
1309
+ fn: directiveCreateFn,
1310
+ },
1311
+ // P3.2 (task-051-directive-assign) — on this SINGULAR module per dl-041 B, so `deriveVerb` yields
1312
+ // `assign`: CLI `wingfoil directive assign`, Tool `directive.assign`.
1313
+ directiveAssign: {
1314
+ name: 'directiveAssign',
1315
+ mutates: true,
1316
+ options: [
1317
+ { name: 'directive', required: true },
1318
+ { name: 'role', required: true },
1319
+ ],
1320
+ fn: directiveAssignFn,
1321
+ },
1322
+ // P3.3 (task-052-directive-remove) — on this SINGULAR module per dl-041 B, so `deriveVerb`
1323
+ // yields `remove`: CLI `wingfoil directive remove <name>`, Tool `directive.remove`. It declares
1324
+ // no flags and no value options: the name rides the bare `positional` seam (spec-008 §7), like
1325
+ // `memory submit <id>`. Completes REQ-SEC-07 for the directive surface (clause (a) via
1326
+ // task-042's `requireCustomAsset`, clause (b) via `checkUnreferenced` — dl-030).
1327
+ directiveRemove: { name: 'directiveRemove', mutates: true, fn: directiveRemoveFn },
1328
+ },
1329
+ },
1330
+ ];
1331
+ //# sourceMappingURL=index.js.map