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.
- package/LICENSE +21 -0
- package/README.md +334 -0
- package/dist/cli/error.d.ts +11 -0
- package/dist/cli/error.d.ts.map +1 -0
- package/dist/cli/error.js +27 -0
- package/dist/cli/error.js.map +1 -0
- package/dist/cli/exit.d.ts +19 -0
- package/dist/cli/exit.d.ts.map +1 -0
- package/dist/cli/exit.js +14 -0
- package/dist/cli/exit.js.map +1 -0
- package/dist/cli/index.d.ts +28 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +36 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-command.d.ts +34 -0
- package/dist/cli/init-command.d.ts.map +1 -0
- package/dist/cli/init-command.js +100 -0
- package/dist/cli/init-command.js.map +1 -0
- package/dist/cli/mcp-command.d.ts +24 -0
- package/dist/cli/mcp-command.d.ts.map +1 -0
- package/dist/cli/mcp-command.js +42 -0
- package/dist/cli/mcp-command.js.map +1 -0
- package/dist/cli/output.d.ts +13 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +28 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/program.d.ts +12 -0
- package/dist/cli/program.d.ts.map +1 -0
- package/dist/cli/program.js +279 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/registrar.d.ts +69 -0
- package/dist/cli/registrar.d.ts.map +1 -0
- package/dist/cli/registrar.js +82 -0
- package/dist/cli/registrar.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +45 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/approval-authority.d.ts +83 -0
- package/dist/core/approval-authority.d.ts.map +1 -0
- package/dist/core/approval-authority.js +112 -0
- package/dist/core/approval-authority.js.map +1 -0
- package/dist/core/builtin-asset.d.ts +71 -0
- package/dist/core/builtin-asset.d.ts.map +1 -0
- package/dist/core/builtin-asset.js +137 -0
- package/dist/core/builtin-asset.js.map +1 -0
- package/dist/core/builtin-integrity.d.ts +32 -0
- package/dist/core/builtin-integrity.d.ts.map +1 -0
- package/dist/core/builtin-integrity.js +148 -0
- package/dist/core/builtin-integrity.js.map +1 -0
- package/dist/core/confinement.d.ts +57 -0
- package/dist/core/confinement.d.ts.map +1 -0
- package/dist/core/confinement.js +108 -0
- package/dist/core/confinement.js.map +1 -0
- package/dist/core/context.d.ts +187 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +192 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/directive-assign.d.ts +131 -0
- package/dist/core/directive-assign.d.ts.map +1 -0
- package/dist/core/directive-assign.js +352 -0
- package/dist/core/directive-assign.js.map +1 -0
- package/dist/core/directives-list.d.ts +80 -0
- package/dist/core/directives-list.d.ts.map +1 -0
- package/dist/core/directives-list.js +148 -0
- package/dist/core/directives-list.js.map +1 -0
- package/dist/core/exit-code.d.ts +85 -0
- package/dist/core/exit-code.d.ts.map +1 -0
- package/dist/core/exit-code.js +163 -0
- package/dist/core/exit-code.js.map +1 -0
- package/dist/core/git-identity.d.ts +32 -0
- package/dist/core/git-identity.d.ts.map +1 -0
- package/dist/core/git-identity.js +72 -0
- package/dist/core/git-identity.js.map +1 -0
- package/dist/core/index.d.ts +388 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +1331 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/init.d.ts +78 -0
- package/dist/core/init.d.ts.map +1 -0
- package/dist/core/init.js +167 -0
- package/dist/core/init.js.map +1 -0
- package/dist/core/loaders.d.ts +174 -0
- package/dist/core/loaders.d.ts.map +1 -0
- package/dist/core/loaders.js +364 -0
- package/dist/core/loaders.js.map +1 -0
- package/dist/core/memory-add-type.d.ts +43 -0
- package/dist/core/memory-add-type.d.ts.map +1 -0
- package/dist/core/memory-add-type.js +151 -0
- package/dist/core/memory-add-type.js.map +1 -0
- package/dist/core/memory-transition.d.ts +111 -0
- package/dist/core/memory-transition.d.ts.map +1 -0
- package/dist/core/memory-transition.js +269 -0
- package/dist/core/memory-transition.js.map +1 -0
- package/dist/core/registry.d.ts +189 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +66 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/relevance.d.ts +86 -0
- package/dist/core/relevance.d.ts.map +1 -0
- package/dist/core/relevance.js +258 -0
- package/dist/core/relevance.js.map +1 -0
- package/dist/core/require-reason.d.ts +27 -0
- package/dist/core/require-reason.d.ts.map +1 -0
- package/dist/core/require-reason.js +95 -0
- package/dist/core/require-reason.js.map +1 -0
- package/dist/core/types.d.ts +45 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +22 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/usage-error.d.ts +20 -0
- package/dist/core/usage-error.d.ts.map +1 -0
- package/dist/core/usage-error.js +29 -0
- package/dist/core/usage-error.js.map +1 -0
- package/dist/core/write-guard.d.ts +99 -0
- package/dist/core/write-guard.d.ts.map +1 -0
- package/dist/core/write-guard.js +247 -0
- package/dist/core/write-guard.js.map +1 -0
- package/dist/directives/create.d.ts +47 -0
- package/dist/directives/create.d.ts.map +1 -0
- package/dist/directives/create.js +82 -0
- package/dist/directives/create.js.map +1 -0
- package/dist/directives/index.d.ts +20 -0
- package/dist/directives/index.d.ts.map +1 -0
- package/dist/directives/index.js +32 -0
- package/dist/directives/index.js.map +1 -0
- package/dist/directives/roles-edit.d.ts +53 -0
- package/dist/directives/roles-edit.d.ts.map +1 -0
- package/dist/directives/roles-edit.js +286 -0
- package/dist/directives/roles-edit.js.map +1 -0
- package/dist/directives/schema.d.ts +65 -0
- package/dist/directives/schema.d.ts.map +1 -0
- package/dist/directives/schema.js +68 -0
- package/dist/directives/schema.js.map +1 -0
- package/dist/dna/edit.d.ts +61 -0
- package/dist/dna/edit.d.ts.map +1 -0
- package/dist/dna/edit.js +429 -0
- package/dist/dna/edit.js.map +1 -0
- package/dist/dna/index.d.ts +24 -0
- package/dist/dna/index.d.ts.map +1 -0
- package/dist/dna/index.js +59 -0
- package/dist/dna/index.js.map +1 -0
- package/dist/dna/mutate.d.ts +74 -0
- package/dist/dna/mutate.d.ts.map +1 -0
- package/dist/dna/mutate.js +318 -0
- package/dist/dna/mutate.js.map +1 -0
- package/dist/dna/path.d.ts +175 -0
- package/dist/dna/path.d.ts.map +1 -0
- package/dist/dna/path.js +289 -0
- package/dist/dna/path.js.map +1 -0
- package/dist/dna/roles.d.ts +57 -0
- package/dist/dna/roles.d.ts.map +1 -0
- package/dist/dna/roles.js +50 -0
- package/dist/dna/roles.js.map +1 -0
- package/dist/dna/schema.d.ts +172 -0
- package/dist/dna/schema.d.ts.map +1 -0
- package/dist/dna/schema.js +182 -0
- package/dist/dna/schema.js.map +1 -0
- package/dist/dna/set.d.ts +145 -0
- package/dist/dna/set.d.ts.map +1 -0
- package/dist/dna/set.js +428 -0
- package/dist/dna/set.js.map +1 -0
- package/dist/mcp/dna-resource.d.ts +16 -0
- package/dist/mcp/dna-resource.d.ts.map +1 -0
- package/dist/mcp/dna-resource.js +56 -0
- package/dist/mcp/dna-resource.js.map +1 -0
- package/dist/mcp/index.d.ts +49 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +60 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/memory-resource.d.ts +17 -0
- package/dist/mcp/memory-resource.d.ts.map +1 -0
- package/dist/mcp/memory-resource.js +133 -0
- package/dist/mcp/memory-resource.js.map +1 -0
- package/dist/mcp/prompt.d.ts +48 -0
- package/dist/mcp/prompt.d.ts.map +1 -0
- package/dist/mcp/prompt.js +176 -0
- package/dist/mcp/prompt.js.map +1 -0
- package/dist/mcp/read-only.d.ts +42 -0
- package/dist/mcp/read-only.d.ts.map +1 -0
- package/dist/mcp/read-only.js +93 -0
- package/dist/mcp/read-only.js.map +1 -0
- package/dist/mcp/registrar.d.ts +54 -0
- package/dist/mcp/registrar.d.ts.map +1 -0
- package/dist/mcp/registrar.js +72 -0
- package/dist/mcp/registrar.js.map +1 -0
- package/dist/mcp/server.d.ts +58 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +64 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/workflow-resource.d.ts +17 -0
- package/dist/mcp/workflow-resource.d.ts.map +1 -0
- package/dist/mcp/workflow-resource.js +63 -0
- package/dist/mcp/workflow-resource.js.map +1 -0
- package/dist/memory/add.d.ts +50 -0
- package/dist/memory/add.d.ts.map +1 -0
- package/dist/memory/add.js +131 -0
- package/dist/memory/add.js.map +1 -0
- package/dist/memory/audit.d.ts +177 -0
- package/dist/memory/audit.d.ts.map +1 -0
- package/dist/memory/audit.js +287 -0
- package/dist/memory/audit.js.map +1 -0
- package/dist/memory/commit-message.d.ts +146 -0
- package/dist/memory/commit-message.d.ts.map +1 -0
- package/dist/memory/commit-message.js +203 -0
- package/dist/memory/commit-message.js.map +1 -0
- package/dist/memory/entry.d.ts +56 -0
- package/dist/memory/entry.d.ts.map +1 -0
- package/dist/memory/entry.js +28 -0
- package/dist/memory/entry.js.map +1 -0
- package/dist/memory/frontmatter-edit.d.ts +68 -0
- package/dist/memory/frontmatter-edit.d.ts.map +1 -0
- package/dist/memory/frontmatter-edit.js +333 -0
- package/dist/memory/frontmatter-edit.js.map +1 -0
- package/dist/memory/git-log.d.ts +32 -0
- package/dist/memory/git-log.d.ts.map +1 -0
- package/dist/memory/git-log.js +100 -0
- package/dist/memory/git-log.js.map +1 -0
- package/dist/memory/history.d.ts +142 -0
- package/dist/memory/history.d.ts.map +1 -0
- package/dist/memory/history.js +250 -0
- package/dist/memory/history.js.map +1 -0
- package/dist/memory/index.d.ts +58 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +107 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/query.d.ts +150 -0
- package/dist/memory/query.d.ts.map +1 -0
- package/dist/memory/query.js +307 -0
- package/dist/memory/query.js.map +1 -0
- package/dist/memory/reject.d.ts +11 -0
- package/dist/memory/reject.d.ts.map +1 -0
- package/dist/memory/reject.js +32 -0
- package/dist/memory/reject.js.map +1 -0
- package/dist/memory/schema.d.ts +104 -0
- package/dist/memory/schema.d.ts.map +1 -0
- package/dist/memory/schema.js +161 -0
- package/dist/memory/schema.js.map +1 -0
- package/dist/memory/state-machine.d.ts +166 -0
- package/dist/memory/state-machine.d.ts.map +1 -0
- package/dist/memory/state-machine.js +431 -0
- package/dist/memory/state-machine.js.map +1 -0
- package/dist/memory/submit.d.ts +11 -0
- package/dist/memory/submit.d.ts.map +1 -0
- package/dist/memory/submit.js +44 -0
- package/dist/memory/submit.js.map +1 -0
- package/dist/storage/builtin-directives.d.ts +54 -0
- package/dist/storage/builtin-directives.d.ts.map +1 -0
- package/dist/storage/builtin-directives.js +152 -0
- package/dist/storage/builtin-directives.js.map +1 -0
- package/dist/storage/commit.d.ts +92 -0
- package/dist/storage/commit.d.ts.map +1 -0
- package/dist/storage/commit.js +201 -0
- package/dist/storage/commit.js.map +1 -0
- package/dist/storage/confinement.d.ts +93 -0
- package/dist/storage/confinement.d.ts.map +1 -0
- package/dist/storage/confinement.js +164 -0
- package/dist/storage/confinement.js.map +1 -0
- package/dist/storage/document.d.ts +27 -0
- package/dist/storage/document.d.ts.map +1 -0
- package/dist/storage/document.js +52 -0
- package/dist/storage/document.js.map +1 -0
- package/dist/storage/errors.d.ts +36 -0
- package/dist/storage/errors.d.ts.map +1 -0
- package/dist/storage/errors.js +46 -0
- package/dist/storage/errors.js.map +1 -0
- package/dist/storage/frontmatter.d.ts +27 -0
- package/dist/storage/frontmatter.d.ts.map +1 -0
- package/dist/storage/frontmatter.js +37 -0
- package/dist/storage/frontmatter.js.map +1 -0
- package/dist/storage/git-root.d.ts +17 -0
- package/dist/storage/git-root.d.ts.map +1 -0
- package/dist/storage/git-root.js +49 -0
- package/dist/storage/git-root.js.map +1 -0
- package/dist/storage/index.d.ts +29 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +66 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/init-state.d.ts +9 -0
- package/dist/storage/init-state.d.ts.map +1 -0
- package/dist/storage/init-state.js +24 -0
- package/dist/storage/init-state.js.map +1 -0
- package/dist/storage/layout.d.ts +51 -0
- package/dist/storage/layout.d.ts.map +1 -0
- package/dist/storage/layout.js +88 -0
- package/dist/storage/layout.js.map +1 -0
- package/dist/storage/memory-path.d.ts +72 -0
- package/dist/storage/memory-path.d.ts.map +1 -0
- package/dist/storage/memory-path.js +127 -0
- package/dist/storage/memory-path.js.map +1 -0
- package/dist/storage/snapshot.d.ts +16 -0
- package/dist/storage/snapshot.d.ts.map +1 -0
- package/dist/storage/snapshot.js +72 -0
- package/dist/storage/snapshot.js.map +1 -0
- package/dist/storage/templates.d.ts +88 -0
- package/dist/storage/templates.d.ts.map +1 -0
- package/dist/storage/templates.js +422 -0
- package/dist/storage/templates.js.map +1 -0
- package/dist/validation/error-mapper.d.ts +16 -0
- package/dist/validation/error-mapper.d.ts.map +1 -0
- package/dist/validation/error-mapper.js +18 -0
- package/dist/validation/error-mapper.js.map +1 -0
- package/dist/validation/errors.d.ts +69 -0
- package/dist/validation/errors.d.ts.map +1 -0
- package/dist/validation/errors.js +69 -0
- package/dist/validation/errors.js.map +1 -0
- package/dist/validation/id.d.ts +16 -0
- package/dist/validation/id.d.ts.map +1 -0
- package/dist/validation/id.js +177 -0
- package/dist/validation/id.js.map +1 -0
- package/dist/validation/index.d.ts +18 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +37 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/secret-scan.d.ts +129 -0
- package/dist/validation/secret-scan.d.ts.map +1 -0
- package/dist/validation/secret-scan.js +367 -0
- package/dist/validation/secret-scan.js.map +1 -0
- package/dist/validation/two-pass.d.ts +24 -0
- package/dist/validation/two-pass.d.ts.map +1 -0
- package/dist/validation/two-pass.js +24 -0
- package/dist/validation/two-pass.js.map +1 -0
- package/dist/validation/warning.d.ts +31 -0
- package/dist/validation/warning.d.ts.map +1 -0
- package/dist/validation/warning.js +81 -0
- package/dist/validation/warning.js.map +1 -0
- package/dist/validation/yaml.d.ts +9 -0
- package/dist/validation/yaml.d.ts.map +1 -0
- package/dist/validation/yaml.js +30 -0
- package/dist/validation/yaml.js.map +1 -0
- package/dist/workflow/index.d.ts +10 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +16 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/schema.d.ts +89 -0
- package/dist/workflow/schema.d.ts.map +1 -0
- package/dist/workflow/schema.js +73 -0
- package/dist/workflow/schema.js.map +1 -0
- 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
|