@hoilab/ada-cli 0.84.21 → 0.84.22
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/CHANGELOG.md +20 -0
- package/README.es.md +163 -0
- package/README.md +99 -652
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +11 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +2 -1
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/cli/home-screen.d.ts.map +1 -1
- package/dist/cli/home-screen.js +2 -2
- package/dist/cli/home-screen.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +9 -3
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +57 -7
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +449 -73
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +4 -3
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +3 -0
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +4 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +41 -32
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/defaults.d.ts +1 -0
- package/dist/core/defaults.d.ts.map +1 -1
- package/dist/core/defaults.js +9 -0
- package/dist/core/defaults.js.map +1 -1
- package/dist/core/engine-personality.d.ts +12 -0
- package/dist/core/engine-personality.d.ts.map +1 -0
- package/dist/core/engine-personality.js +53 -0
- package/dist/core/engine-personality.js.map +1 -0
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +10 -5
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +23 -4
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +2 -1
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/memory-engine/audit-export.d.ts +54 -0
- package/dist/core/memory-engine/audit-export.d.ts.map +1 -0
- package/dist/core/memory-engine/audit-export.js +85 -0
- package/dist/core/memory-engine/audit-export.js.map +1 -0
- package/dist/core/memory-engine/audit.d.ts.map +1 -1
- package/dist/core/memory-engine/audit.js +7 -8
- package/dist/core/memory-engine/audit.js.map +1 -1
- package/dist/core/memory-engine/engine.d.ts +2 -3
- package/dist/core/memory-engine/engine.d.ts.map +1 -1
- package/dist/core/memory-engine/engine.js +148 -50
- package/dist/core/memory-engine/engine.js.map +1 -1
- package/dist/core/memory-engine/inverted-index.d.ts.map +1 -1
- package/dist/core/memory-engine/inverted-index.js +67 -10
- package/dist/core/memory-engine/inverted-index.js.map +1 -1
- package/dist/core/memory-engine/keychain.d.ts.map +1 -1
- package/dist/core/memory-engine/keychain.js +1 -6
- package/dist/core/memory-engine/keychain.js.map +1 -1
- package/dist/core/memory-engine/observation-store.d.ts +1 -0
- package/dist/core/memory-engine/observation-store.d.ts.map +1 -1
- package/dist/core/memory-engine/observation-store.js +37 -8
- package/dist/core/memory-engine/observation-store.js.map +1 -1
- package/dist/core/memory-engine/scanner.d.ts.map +1 -1
- package/dist/core/memory-engine/scanner.js +25 -5
- package/dist/core/memory-engine/scanner.js.map +1 -1
- package/dist/core/memory-engine/security.d.ts.map +1 -1
- package/dist/core/memory-engine/security.js +131 -23
- package/dist/core/memory-engine/security.js.map +1 -1
- package/dist/core/memory-engine/session-indexer.d.ts.map +1 -1
- package/dist/core/memory-engine/session-indexer.js +8 -2
- package/dist/core/memory-engine/session-indexer.js.map +1 -1
- package/dist/core/memory-engine/tools.d.ts +2 -2
- package/dist/core/memory-engine/tools.d.ts.map +1 -1
- package/dist/core/memory-engine/tools.js +14 -7
- package/dist/core/memory-engine/tools.js.map +1 -1
- package/dist/core/memory-engine/vault.d.ts.map +1 -1
- package/dist/core/memory-engine/vault.js +2 -2
- package/dist/core/memory-engine/vault.js.map +1 -1
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +2 -1
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +12 -6
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/models-store.d.ts.map +1 -1
- package/dist/core/models-store.js +2 -1
- package/dist/core/models-store.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +11 -6
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/pi-manifest.d.ts.map +1 -1
- package/dist/core/pi-manifest.js +2 -1
- package/dist/core/pi-manifest.js.map +1 -1
- package/dist/core/prompt-optimizer.d.ts +53 -0
- package/dist/core/prompt-optimizer.d.ts.map +1 -0
- package/dist/core/prompt-optimizer.js +138 -0
- package/dist/core/prompt-optimizer.js.map +1 -0
- package/dist/core/provider-attribution.d.ts.map +1 -1
- package/dist/core/provider-attribution.js +1 -1
- package/dist/core/provider-attribution.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +2 -1
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/resource-loader.d.ts +50 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +233 -7
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +8 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +11 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +2 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-diagnostics.d.ts +9 -0
- package/dist/core/settings-diagnostics.d.ts.map +1 -0
- package/dist/core/settings-diagnostics.js +21 -0
- package/dist/core/settings-diagnostics.js.map +1 -0
- package/dist/core/settings-manager.d.ts +52 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +204 -10
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +51 -33
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +2 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +0 -5
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +2 -5
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +20 -4
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +2 -2
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/project-memory-tool.d.ts.map +1 -1
- package/dist/core/tools/project-memory-tool.js +4 -1
- package/dist/core/tools/project-memory-tool.js.map +1 -1
- package/dist/core/trust-manager.d.ts.map +1 -1
- package/dist/core/trust-manager.js +2 -1
- package/dist/core/trust-manager.js.map +1 -1
- package/dist/core/workflows/agent-runner.d.ts +86 -0
- package/dist/core/workflows/agent-runner.d.ts.map +1 -0
- package/dist/core/workflows/agent-runner.js +225 -0
- package/dist/core/workflows/agent-runner.js.map +1 -0
- package/dist/core/workflows/builtins.d.ts +41 -0
- package/dist/core/workflows/builtins.d.ts.map +1 -0
- package/dist/core/workflows/builtins.js +282 -0
- package/dist/core/workflows/builtins.js.map +1 -0
- package/dist/core/workflows/goal-engine/engine.d.ts +134 -0
- package/dist/core/workflows/goal-engine/engine.d.ts.map +1 -0
- package/dist/core/workflows/goal-engine/engine.js +462 -0
- package/dist/core/workflows/goal-engine/engine.js.map +1 -0
- package/dist/core/workflows/goal-engine/errors.d.ts +7 -0
- package/dist/core/workflows/goal-engine/errors.d.ts.map +1 -0
- package/dist/core/workflows/goal-engine/errors.js +9 -0
- package/dist/core/workflows/goal-engine/errors.js.map +1 -0
- package/dist/core/workflows/goal-engine/index.d.ts +13 -0
- package/dist/core/workflows/goal-engine/index.d.ts.map +1 -0
- package/dist/core/workflows/goal-engine/index.js +8 -0
- package/dist/core/workflows/goal-engine/index.js.map +1 -0
- package/dist/core/workflows/goal-engine/persistence.d.ts +31 -0
- package/dist/core/workflows/goal-engine/persistence.d.ts.map +1 -0
- package/dist/core/workflows/goal-engine/persistence.js +145 -0
- package/dist/core/workflows/goal-engine/persistence.js.map +1 -0
- package/dist/core/workflows/goal-engine/prompts.d.ts +37 -0
- package/dist/core/workflows/goal-engine/prompts.d.ts.map +1 -0
- package/dist/core/workflows/goal-engine/prompts.js +131 -0
- package/dist/core/workflows/goal-engine/prompts.js.map +1 -0
- package/dist/core/workflows/goal-engine/state.d.ts +41 -0
- package/dist/core/workflows/goal-engine/state.d.ts.map +1 -0
- package/dist/core/workflows/goal-engine/state.js +397 -0
- package/dist/core/workflows/goal-engine/state.js.map +1 -0
- package/dist/core/workflows/goal-engine/tools.d.ts +31 -0
- package/dist/core/workflows/goal-engine/tools.d.ts.map +1 -0
- package/dist/core/workflows/goal-engine/tools.js +211 -0
- package/dist/core/workflows/goal-engine/tools.js.map +1 -0
- package/dist/core/workflows/goal-engine/types.d.ts +286 -0
- package/dist/core/workflows/goal-engine/types.d.ts.map +1 -0
- package/dist/core/workflows/goal-engine/types.js +17 -0
- package/dist/core/workflows/goal-engine/types.js.map +1 -0
- package/dist/core/workflows/goal-engine/worktree-fingerprint.d.ts +23 -0
- package/dist/core/workflows/goal-engine/worktree-fingerprint.d.ts.map +1 -0
- package/dist/core/workflows/goal-engine/worktree-fingerprint.js +72 -0
- package/dist/core/workflows/goal-engine/worktree-fingerprint.js.map +1 -0
- package/dist/core/workflows/graph-sync.d.ts +67 -0
- package/dist/core/workflows/graph-sync.d.ts.map +1 -0
- package/dist/core/workflows/graph-sync.js +188 -0
- package/dist/core/workflows/graph-sync.js.map +1 -0
- package/dist/core/workflows/graph.d.ts +143 -0
- package/dist/core/workflows/graph.d.ts.map +1 -0
- package/dist/core/workflows/graph.js +319 -0
- package/dist/core/workflows/graph.js.map +1 -0
- package/dist/core/workflows/index.d.ts +18 -0
- package/dist/core/workflows/index.d.ts.map +1 -0
- package/dist/core/workflows/index.js +14 -0
- package/dist/core/workflows/index.js.map +1 -0
- package/dist/core/workflows/journal.d.ts +68 -0
- package/dist/core/workflows/journal.d.ts.map +1 -0
- package/dist/core/workflows/journal.js +83 -0
- package/dist/core/workflows/journal.js.map +1 -0
- package/dist/core/workflows/manager.d.ts +112 -0
- package/dist/core/workflows/manager.d.ts.map +1 -0
- package/dist/core/workflows/manager.js +327 -0
- package/dist/core/workflows/manager.js.map +1 -0
- package/dist/core/workflows/resolve-script.d.ts +35 -0
- package/dist/core/workflows/resolve-script.d.ts.map +1 -0
- package/dist/core/workflows/resolve-script.js +66 -0
- package/dist/core/workflows/resolve-script.js.map +1 -0
- package/dist/core/workflows/runtime.d.ts +49 -0
- package/dist/core/workflows/runtime.d.ts.map +1 -0
- package/dist/core/workflows/runtime.js +301 -0
- package/dist/core/workflows/runtime.js.map +1 -0
- package/dist/core/workflows/shared-store.d.ts +31 -0
- package/dist/core/workflows/shared-store.d.ts.map +1 -0
- package/dist/core/workflows/shared-store.js +80 -0
- package/dist/core/workflows/shared-store.js.map +1 -0
- package/dist/core/workflows/subagent-host.d.ts +50 -0
- package/dist/core/workflows/subagent-host.d.ts.map +1 -0
- package/dist/core/workflows/subagent-host.js +225 -0
- package/dist/core/workflows/subagent-host.js.map +1 -0
- package/dist/core/workflows/tool-workflow.d.ts +22 -0
- package/dist/core/workflows/tool-workflow.d.ts.map +1 -0
- package/dist/core/workflows/tool-workflow.js +120 -0
- package/dist/core/workflows/tool-workflow.js.map +1 -0
- package/dist/core/workflows/types.d.ts +142 -0
- package/dist/core/workflows/types.d.ts.map +1 -0
- package/dist/core/workflows/types.js +26 -0
- package/dist/core/workflows/types.js.map +1 -0
- package/dist/core/workflows/worktree.d.ts +42 -0
- package/dist/core/workflows/worktree.d.ts.map +1 -0
- package/dist/core/workflows/worktree.js +107 -0
- package/dist/core/workflows/worktree.js.map +1 -0
- package/dist/extensions/llama/index.d.ts.map +1 -1
- package/dist/extensions/llama/index.js +2 -0
- package/dist/extensions/llama/index.js.map +1 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +6 -2
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/index.d.ts +16 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -11
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +4 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +20 -15
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +4 -3
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/home-screen.d.ts.map +1 -1
- package/dist/modes/interactive/components/home-screen.js +2 -2
- package/dist/modes/interactive/components/home-screen.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +2 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +16 -6
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/prompt-optimization.d.ts +16 -0
- package/dist/modes/interactive/components/prompt-optimization.d.ts.map +1 -0
- package/dist/modes/interactive/components/prompt-optimization.js +46 -0
- package/dist/modes/interactive/components/prompt-optimization.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +19 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +247 -46
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-submenu.d.ts +72 -0
- package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -0
- package/dist/modes/interactive/components/settings-submenu.js +165 -0
- package/dist/modes/interactive/components/settings-submenu.js.map +1 -0
- package/dist/modes/interactive/components/thinking-selector.d.ts +8 -3
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +29 -3
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts.map +1 -1
- package/dist/modes/interactive/external-editor.js +2 -1
- package/dist/modes/interactive/external-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +24 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +677 -34
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +4 -3
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/json-event.d.ts +16 -13
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +20 -6
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +173 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +29 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +1 -1
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/skills/ada-engine/SKILL.md +153 -0
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +2 -1
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/management-http.d.ts +5 -3
- package/dist/utils/management-http.d.ts.map +1 -1
- package/dist/utils/management-http.js +14 -10
- package/dist/utils/management-http.js.map +1 -1
- package/dist/utils/text.d.ts +8 -0
- package/dist/utils/text.d.ts.map +1 -0
- package/dist/utils/text.js +9 -0
- package/dist/utils/text.js.map +1 -0
- package/docs/compaction.md +15 -0
- package/docs/custom-provider.md +5 -2
- package/docs/extensions.md +11 -2
- package/docs/json.md +7 -2
- package/docs/llama-cpp.md +2 -0
- package/docs/models.md +8 -2
- package/docs/rpc.md +9 -3
- package/docs/settings.md +2 -2
- package/docs/skills.md +4 -3
- package/examples/extensions/border-status-editor.ts +1 -1
- package/examples/extensions/git-checkpoint.ts +2 -2
- package/examples/extensions/notify.ts +3 -1
- package/examples/extensions/subagent/README.md +1 -1
- package/examples/extensions/subagent/index.ts +6 -1
- package/examples/extensions/titlebar-spinner.ts +1 -1
- package/examples/rpc-extension-ui.ts +1 -1
- package/examples/sdk/06-extensions.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/npm-shrinkwrap.json +2 -12
- package/package.json +3 -3
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Huella del árbol de trabajo — detector de no-progreso para el Goal Engine.
|
|
3
|
+
*
|
|
4
|
+
* El fallo autónomo más caro no es equivocarse: es re-ejecutar el mismo verify
|
|
5
|
+
* contra un árbol idéntico y contarlo como intento. El repeat detector cuenta
|
|
6
|
+
* sub-tareas fallidas consecutivas, pero nada observaba si el trabajo cambió
|
|
7
|
+
* algo. Un agente puede "fallar" tres veces sin haber tocado un solo byte.
|
|
8
|
+
*
|
|
9
|
+
* Esto compara el estado observable del repo entre fallo y fallo. Si no cambió,
|
|
10
|
+
* el problema no es el intento: es el plan — así que el goal se pausa y el
|
|
11
|
+
* motivo entra en el prompt de re-planificación en vez de gastar otro turno.
|
|
12
|
+
*
|
|
13
|
+
* Fail-open: fuera de un repo git devuelve undefined y todo sigue igual.
|
|
14
|
+
*/
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { promisify } from "node:util";
|
|
18
|
+
// execFile (sin shell): argumentos en array, nada se interpola en una cadena.
|
|
19
|
+
const execFileAsync = promisify(execFile);
|
|
20
|
+
/** Techo de bytes hasheados de untracked; evita O(worktree) en repos enormes. */
|
|
21
|
+
const MAX_UNTRACKED_BYTES = 2 * 1024 * 1024;
|
|
22
|
+
/**
|
|
23
|
+
* Huella del estado observable del árbol: qué ficheros están modificados, el
|
|
24
|
+
* diff de los tracked, y el contenido de los untracked.
|
|
25
|
+
*
|
|
26
|
+
* `--porcelain=v1 -z -uall` respeta .gitignore, así que node_modules y
|
|
27
|
+
* artefactos de build no entran.
|
|
28
|
+
*/
|
|
29
|
+
export async function workspaceFingerprint(cwd) {
|
|
30
|
+
try {
|
|
31
|
+
const [status, diff] = await Promise.all([
|
|
32
|
+
execFileAsync("git", ["-C", cwd, "status", "--porcelain=v1", "-z", "-uall"], {
|
|
33
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
34
|
+
}),
|
|
35
|
+
execFileAsync("git", ["-C", cwd, "diff", "HEAD"], { maxBuffer: 32 * 1024 * 1024 }),
|
|
36
|
+
]);
|
|
37
|
+
const hash = createHash("sha256");
|
|
38
|
+
hash.update(status.stdout);
|
|
39
|
+
hash.update(diff.stdout);
|
|
40
|
+
// El status ya nombra los untracked, pero no su contenido: un agente que
|
|
41
|
+
// reescribe un fichero nuevo cambiaría nada del status. Hasheamos también
|
|
42
|
+
// el contenido, con tope.
|
|
43
|
+
const untracked = status.stdout
|
|
44
|
+
.split("\0")
|
|
45
|
+
.filter((entry) => entry.startsWith("?? "))
|
|
46
|
+
.map((entry) => entry.slice(3));
|
|
47
|
+
let budget = MAX_UNTRACKED_BYTES;
|
|
48
|
+
const { readFile } = await import("node:fs/promises");
|
|
49
|
+
const { join } = await import("node:path");
|
|
50
|
+
for (const rel of untracked.sort()) {
|
|
51
|
+
if (budget <= 0)
|
|
52
|
+
break;
|
|
53
|
+
try {
|
|
54
|
+
const buf = await readFile(join(cwd, rel));
|
|
55
|
+
const slice = buf.subarray(0, budget);
|
|
56
|
+
budget -= slice.byteLength;
|
|
57
|
+
hash.update(rel);
|
|
58
|
+
hash.update(slice);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// directorio, symlink roto, permisos — el nombre ya cuenta
|
|
62
|
+
hash.update(rel);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return hash.digest("hex");
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// No es un repo git, o git no está disponible: fail-open.
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=worktree-fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-fingerprint.js","sourceRoot":"","sources":["../../../../src/core/workflows/goal-engine/worktree-fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,8EAA8E;AAC9E,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,iFAAiF;AACjF,MAAM,mBAAmB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,GAAW,EAA+B;IACpF,IAAI,CAAC;QACJ,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACxC,aAAa,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE;gBAC5E,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;aAC3B,CAAC;YACF,aAAa,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;SAClF,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAEzB,yEAAyE;QACzE,4EAA0E;QAC1E,0BAA0B;QAC1B,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM;aAC7B,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;aAC1C,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACjC,IAAI,MAAM,GAAG,mBAAmB,CAAC;QACjC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;QACtD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;QAC3C,KAAK,MAAM,GAAG,IAAI,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC;YACpC,IAAI,MAAM,IAAI,CAAC;gBAAE,MAAM;YACvB,IAAI,CAAC;gBACJ,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;gBAC3C,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;gBACtC,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC;gBAC3B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACpB,CAAC;YAAC,MAAM,CAAC;gBACR,6DAA2D;gBAC3D,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAClB,CAAC;QACF,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACR,2DAA0D;QAC1D,OAAO,SAAS,CAAC;IAClB,CAAC;AAAA,CACD","sourcesContent":["/**\n * Huella del árbol de trabajo — detector de no-progreso para el Goal Engine.\n *\n * El fallo autónomo más caro no es equivocarse: es re-ejecutar el mismo verify\n * contra un árbol idéntico y contarlo como intento. El repeat detector cuenta\n * sub-tareas fallidas consecutivas, pero nada observaba si el trabajo cambió\n * algo. Un agente puede \"fallar\" tres veces sin haber tocado un solo byte.\n *\n * Esto compara el estado observable del repo entre fallo y fallo. Si no cambió,\n * el problema no es el intento: es el plan — así que el goal se pausa y el\n * motivo entra en el prompt de re-planificación en vez de gastar otro turno.\n *\n * Fail-open: fuera de un repo git devuelve undefined y todo sigue igual.\n */\n\nimport { execFile } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { promisify } from \"node:util\";\n\n// execFile (sin shell): argumentos en array, nada se interpola en una cadena.\nconst execFileAsync = promisify(execFile);\n\n/** Techo de bytes hasheados de untracked; evita O(worktree) en repos enormes. */\nconst MAX_UNTRACKED_BYTES = 2 * 1024 * 1024;\n\n/**\n * Huella del estado observable del árbol: qué ficheros están modificados, el\n * diff de los tracked, y el contenido de los untracked.\n *\n * `--porcelain=v1 -z -uall` respeta .gitignore, así que node_modules y\n * artefactos de build no entran.\n */\nexport async function workspaceFingerprint(cwd: string): Promise<string | undefined> {\n\ttry {\n\t\tconst [status, diff] = await Promise.all([\n\t\t\texecFileAsync(\"git\", [\"-C\", cwd, \"status\", \"--porcelain=v1\", \"-z\", \"-uall\"], {\n\t\t\t\tmaxBuffer: 32 * 1024 * 1024,\n\t\t\t}),\n\t\t\texecFileAsync(\"git\", [\"-C\", cwd, \"diff\", \"HEAD\"], { maxBuffer: 32 * 1024 * 1024 }),\n\t\t]);\n\t\tconst hash = createHash(\"sha256\");\n\t\thash.update(status.stdout);\n\t\thash.update(diff.stdout);\n\n\t\t// El status ya nombra los untracked, pero no su contenido: un agente que\n\t\t// reescribe un fichero nuevo cambiaría nada del status. Hasheamos también\n\t\t// el contenido, con tope.\n\t\tconst untracked = status.stdout\n\t\t\t.split(\"\\0\")\n\t\t\t.filter((entry) => entry.startsWith(\"?? \"))\n\t\t\t.map((entry) => entry.slice(3));\n\t\tlet budget = MAX_UNTRACKED_BYTES;\n\t\tconst { readFile } = await import(\"node:fs/promises\");\n\t\tconst { join } = await import(\"node:path\");\n\t\tfor (const rel of untracked.sort()) {\n\t\t\tif (budget <= 0) break;\n\t\t\ttry {\n\t\t\t\tconst buf = await readFile(join(cwd, rel));\n\t\t\t\tconst slice = buf.subarray(0, budget);\n\t\t\t\tbudget -= slice.byteLength;\n\t\t\t\thash.update(rel);\n\t\t\t\thash.update(slice);\n\t\t\t} catch {\n\t\t\t\t// directorio, symlink roto, permisos — el nombre ya cuenta\n\t\t\t\thash.update(rel);\n\t\t\t}\n\t\t}\n\t\treturn hash.digest(\"hex\");\n\t} catch {\n\t\t// No es un repo git, o git no está disponible: fail-open.\n\t\treturn undefined;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ToolDefinition } from "../extensions/types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* GraphSync — auto-registro AUTÓNOMO del grafo de trabajo (§16.3).
|
|
4
|
+
*
|
|
5
|
+
* El grafo se construye solo mientras el agente trabaja, sin que el usuario
|
|
6
|
+
* tenga que pedirlo:
|
|
7
|
+
* - write/edit de un archivo → nodo FILE/ARTIFACT + arista `implemented_by`
|
|
8
|
+
* desde el TASK/INTENT activo (el spine se completa solo).
|
|
9
|
+
* - bash que ejecuta un test → nodo TEST + arista `verified_by` (evidencia
|
|
10
|
+
* OBSERVADA: comando + exit code 0).
|
|
11
|
+
* - goal activo → INTENT + REQUIREMENT (doneCriteria) + TASK
|
|
12
|
+
* (tasks del goal) con aristas planned_as / touches.
|
|
13
|
+
* - QA_FINDING / RISK → tool graph_add_nodes (complemento, no dependencia).
|
|
14
|
+
*
|
|
15
|
+
* El gate del grafo solo pasa cuando el spine tiene verified_by REAL (archivo:línea
|
|
16
|
+
* o salida de test) — nunca afirmaciones.
|
|
17
|
+
*/
|
|
18
|
+
import type { Graph } from "./graph.ts";
|
|
19
|
+
export interface GraphSyncOptions {
|
|
20
|
+
/** Grafo del resource-loader (puede ser null si el graph está desactivado). */
|
|
21
|
+
graph: () => Graph | null;
|
|
22
|
+
/** Para registrar qué TASK está activa: el goal activo actual. */
|
|
23
|
+
activeGoal: () => {
|
|
24
|
+
id: string;
|
|
25
|
+
goal: string;
|
|
26
|
+
phase: string;
|
|
27
|
+
tasks: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
status: string;
|
|
31
|
+
}>;
|
|
32
|
+
doneCriteria: string[];
|
|
33
|
+
} | null;
|
|
34
|
+
/** cwd para resolver rutas relativas de archivos. */
|
|
35
|
+
cwd: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* graph_add_nodes — tool para registros puntuales (QA_FINDING, RISK, DECISION,
|
|
39
|
+
* LEARNING). El spine (INTENT/TASK/FILE/TEST) se construye SOLO; esta tool es
|
|
40
|
+
* para lo que el agente descubre durante auditorías.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createGraphTool(graph: () => Graph | null): ToolDefinition;
|
|
43
|
+
/** Único sync del graph con memoria para no duplicar aristas por tool_call. */
|
|
44
|
+
export declare class GraphSync {
|
|
45
|
+
private graph;
|
|
46
|
+
private activeGoal;
|
|
47
|
+
private cwd;
|
|
48
|
+
/** nodos ya creados esta sesión (id → true) para idempotencia. */
|
|
49
|
+
private seenNodes;
|
|
50
|
+
private seenEdges;
|
|
51
|
+
constructor(options: GraphSyncOptions);
|
|
52
|
+
/** afterToolCall: registrar artefactos y verificación observada. */
|
|
53
|
+
onToolResult(toolName: string, args: Record<string, unknown>, result: unknown, isError: boolean): Promise<void>;
|
|
54
|
+
/** syncGoal: cuando el goal cambia, asegura INTENT + REQUIREMENT + TASK en el spine. */
|
|
55
|
+
syncGoal(goal: {
|
|
56
|
+
id: string;
|
|
57
|
+
goal: string;
|
|
58
|
+
phase: string;
|
|
59
|
+
tasks: Array<{
|
|
60
|
+
id: string;
|
|
61
|
+
title: string;
|
|
62
|
+
status: string;
|
|
63
|
+
}>;
|
|
64
|
+
doneCriteria: string[];
|
|
65
|
+
}): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=graph-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-sync.d.ts","sourceRoot":"","sources":["../../../src/core/workflows/graph-sync.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,WAAW,gBAAgB;IAChC,gFAA+E;IAC/E,KAAK,EAAE,MAAM,KAAK,GAAG,IAAI,CAAC;IAC1B,oEAAkE;IAClE,UAAU,EAAE,MAAM;QACjB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,KAAK,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAC5D,YAAY,EAAE,MAAM,EAAE,CAAC;KACvB,GAAG,IAAI,CAAC;IACT,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;CACZ;AAsBD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,KAAK,GAAG,IAAI,GAAG,cAAc,CA8CzE;AAED,gFAA+E;AAC/E,qBAAa,SAAS;IACrB,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,UAAU,CAAiC;IACnD,OAAO,CAAC,GAAG,CAAS;IACpB,qEAAkE;IAClE,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,SAAS,CAAqB;IAEtC,YAAY,OAAO,EAAE,gBAAgB,EAIpC;IAED,qEAAoE;IAC9D,YAAY,CACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,OAAO,GACd,OAAO,CAAC,IAAI,CAAC,CAwDf;IAED,wFAAwF;IAClF,QAAQ,CAAC,IAAI,EAAE;QACpB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,KAAK,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAC5D,YAAY,EAAE,MAAM,EAAE,CAAC;KACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6ChB;CACD","sourcesContent":["import { Type } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\n/**\n * GraphSync — auto-registro AUTÓNOMO del grafo de trabajo (§16.3).\n *\n * El grafo se construye solo mientras el agente trabaja, sin que el usuario\n * tenga que pedirlo:\n * - write/edit de un archivo → nodo FILE/ARTIFACT + arista `implemented_by`\n * desde el TASK/INTENT activo (el spine se completa solo).\n * - bash que ejecuta un test → nodo TEST + arista `verified_by` (evidencia\n * OBSERVADA: comando + exit code 0).\n * - goal activo → INTENT + REQUIREMENT (doneCriteria) + TASK\n * (tasks del goal) con aristas planned_as / touches.\n * - QA_FINDING / RISK → tool graph_add_nodes (complemento, no dependencia).\n *\n * El gate del grafo solo pasa cuando el spine tiene verified_by REAL (archivo:línea\n * o salida de test) — nunca afirmaciones.\n */\n\nimport type { Graph } from \"./graph.ts\";\n\nexport interface GraphSyncOptions {\n\t/** Grafo del resource-loader (puede ser null si el graph está desactivado). */\n\tgraph: () => Graph | null;\n\t/** Para registrar qué TASK está activa: el goal activo actual. */\n\tactiveGoal: () => {\n\t\tid: string;\n\t\tgoal: string;\n\t\tphase: string;\n\t\ttasks: Array<{ id: string; title: string; status: string }>;\n\t\tdoneCriteria: string[];\n\t} | null;\n\t/** cwd para resolver rutas relativas de archivos. */\n\tcwd: string;\n}\n\n/** Normaliza una ruta relativa del agente a una ruta absoluta-corta para el grafo. */\nfunction normalizeFilePath(cwd: string, raw: string): string {\n\tif (!raw) return raw;\n\tif (raw.startsWith(\"/\")) return raw;\n\treturn `${cwd.replace(/\\/$/, \"\")}/${raw}`;\n}\n\n/** ¿Parece un comando de test? (heurística conservadora: no marca falsos). */\nfunction looksLikeTest(command: string): boolean {\n\tconst c = command.trim().toLowerCase();\n\treturn (\n\t\t/\\b(npm test|vitest|jest|pytest|go test|npm run test|cargo test|mvn test|gradle test)\\b/.test(c) ||\n\t\t/^vitest /.test(c) ||\n\t\t/^jest /.test(c) ||\n\t\t/^pytest /.test(c) ||\n\t\t/^go test /.test(c) ||\n\t\t/test\\.[a-z]+$/.test(c.split(/\\s+/)[0] ?? \"\")\n\t);\n}\n\n/**\n * graph_add_nodes — tool para registros puntuales (QA_FINDING, RISK, DECISION,\n * LEARNING). El spine (INTENT/TASK/FILE/TEST) se construye SOLO; esta tool es\n * para lo que el agente descubre durante auditorías.\n */\nexport function createGraphTool(graph: () => Graph | null): ToolDefinition {\n\treturn {\n\t\tname: \"graph_add_nodes\",\n\t\tlabel: \"Añadir nodos al grafo\",\n\t\tdescription:\n\t\t\t\"Registra nodos en el grafo de trabajo (QA_FINDING, RISK, DECISION, LEARNING, METRIC) para auditorías y trazabilidad. El spine (INTENT→TASK→FILE/TEST) se construye automáticamente — no lo uses para eso.\",\n\t\tpromptSnippet: \"Registra hallazgos en el grafo (graph_add_nodes)\",\n\t\tpromptGuidelines: [\n\t\t\t\"Usa QA_FINDING para hallazgos de auditoría/revisión con archivo:línea.\",\n\t\t\t\"Usa RISK para riesgos identificados.\",\n\t\t\t\"El spine del grafo se auto-registra — no añadas FILE/TASK manualmente.\",\n\t\t],\n\t\tparameters: Type.Object({\n\t\t\tnodes: Type.Array(\n\t\t\t\tType.Object({\n\t\t\t\t\ttype: Type.Union([\n\t\t\t\t\t\tType.Literal(\"QA_FINDING\"),\n\t\t\t\t\t\tType.Literal(\"RISK\"),\n\t\t\t\t\t\tType.Literal(\"DECISION\"),\n\t\t\t\t\t\tType.Literal(\"LEARNING\"),\n\t\t\t\t\t]),\n\t\t\t\t\tslug: Type.String({ description: \"Identificador corto (ej. 'auth-timing')\" }),\n\t\t\t\t\tdescription: Type.String({ description: \"Descripción del hallazgo\" }),\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\tasync execute(\n\t\t\t_toolCallId: string,\n\t\t\tinput: {\n\t\t\t\tnodes: Array<{ type: \"QA_FINDING\" | \"RISK\" | \"DECISION\" | \"LEARNING\"; slug: string; description: string }>;\n\t\t\t},\n\t\t) {\n\t\t\tconst g = graph();\n\t\t\tif (!g) {\n\t\t\t\treturn {\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t\tcontent: [{ type: \"text\", text: JSON.stringify({ ok: false, error: \"graph no disponible\" }) }],\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst results = input.nodes.map((n) => {\n\t\t\t\tconst r = g.addNode({ type: n.type, slug: n.slug, description: n.description });\n\t\t\t\treturn { id: `${n.type}:${n.slug}`, ok: r.ok, error: r.error };\n\t\t\t});\n\t\t\treturn { details: undefined, content: [{ type: \"text\", text: JSON.stringify({ ok: true, results }) }] };\n\t\t},\n\t};\n}\n\n/** Único sync del graph con memoria para no duplicar aristas por tool_call. */\nexport class GraphSync {\n\tprivate graph: () => Graph | null;\n\tprivate activeGoal: GraphSyncOptions[\"activeGoal\"];\n\tprivate cwd: string;\n\t/** nodos ya creados esta sesión (id → true) para idempotencia. */\n\tprivate seenNodes = new Set<string>();\n\tprivate seenEdges = new Set<string>();\n\n\tconstructor(options: GraphSyncOptions) {\n\t\tthis.graph = options.graph;\n\t\tthis.activeGoal = options.activeGoal;\n\t\tthis.cwd = options.cwd;\n\t}\n\n\t/** afterToolCall: registrar artefactos y verificación observada. */\n\tasync onToolResult(\n\t\ttoolName: string,\n\t\targs: Record<string, unknown>,\n\t\tresult: unknown,\n\t\tisError: boolean,\n\t): Promise<void> {\n\t\tconst g = this.graph();\n\t\tif (!g) return;\n\t\ttry {\n\t\t\tconst goal = this.activeGoal();\n\n\t\t\tif (toolName === \"write\" || toolName === \"edit\" || toolName === \"file_edit\") {\n\t\t\t\tconst rawPath = typeof args.path === \"string\" ? args.path : \"\";\n\t\t\t\tif (!rawPath) return;\n\t\t\t\tconst path = normalizeFilePath(this.cwd, rawPath);\n\t\t\t\tconst nodeId = `ARTIFACT:${path}`;\n\t\t\t\t// Nodo de artefacto (idempotente).\n\t\t\t\tif (!this.seenNodes.has(nodeId)) {\n\t\t\t\t\tg.addNode({ type: \"ARTIFACT\", slug: path, description: `Archivo ${path}`, file: path });\n\t\t\t\t\tthis.seenNodes.add(nodeId);\n\t\t\t\t}\n\t\t\t\t// Arista desde el TASK activo del goal (o INTENT) → implemented_by.\n\t\t\t\tconst from = goal?.tasks.find((t) => t.status === \"running\" || t.status === \"active\")?.id;\n\t\t\t\tconst fromNode = from ? `TASK:${from}` : goal ? `INTENT:${goal.id}` : null;\n\t\t\t\tif (fromNode) {\n\t\t\t\t\tconst edgeKey = `${fromNode}|implemented_by|${nodeId}`;\n\t\t\t\t\tif (!this.seenEdges.has(edgeKey)) {\n\t\t\t\t\t\tg.addEdges([{ subject: fromNode, predicate: \"implemented_by\", object: nodeId }]);\n\t\t\t\t\t\tthis.seenEdges.add(edgeKey);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (toolName === \"bash\") {\n\t\t\t\tconst command = typeof args.command === \"string\" ? args.command : \"\";\n\t\t\t\tif (!command || isError) return;\n\t\t\t\tconst outcome = (result as { exitCode?: number } | undefined)?.exitCode ?? 0;\n\t\t\t\tif (outcome !== 0 || !looksLikeTest(command)) return;\n\t\t\t\t// Test que pasó → nodo TEST + verified_by desde REQUIREMENT/INTENT.\n\t\t\t\tconst testId = `METRIC:test ${command.slice(0, 50)}`;\n\t\t\t\tif (!this.seenNodes.has(testId)) {\n\t\t\t\t\tg.addNode({\n\t\t\t\t\t\ttype: \"METRIC\",\n\t\t\t\t\t\tslug: `test ${command.slice(0, 50)}`,\n\t\t\t\t\t\tdescription: `Test que pasa: ${command}`,\n\t\t\t\t\t});\n\t\t\t\t\tthis.seenNodes.add(testId);\n\t\t\t\t}\n\t\t\t\tconst from = goal?.tasks.find((t) => t.status === \"running\" || t.status === \"active\")?.id;\n\t\t\t\tconst fromNode = from ? `TASK:${from}` : goal ? `INTENT:${goal.id}` : null;\n\t\t\t\tif (fromNode) {\n\t\t\t\t\tconst edgeKey = `${fromNode}|verified_by|${testId}`;\n\t\t\t\t\tif (!this.seenEdges.has(edgeKey)) {\n\t\t\t\t\t\tg.addEdges([{ subject: fromNode, predicate: \"verified_by\", object: testId }]);\n\t\t\t\t\t\tthis.seenEdges.add(edgeKey);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// El grafo es best-effort: nunca rompe el flujo del agente.\n\t\t}\n\t}\n\n\t/** syncGoal: cuando el goal cambia, asegura INTENT + REQUIREMENT + TASK en el spine. */\n\tasync syncGoal(goal: {\n\t\tid: string;\n\t\tgoal: string;\n\t\tphase: string;\n\t\ttasks: Array<{ id: string; title: string; status: string }>;\n\t\tdoneCriteria: string[];\n\t}): Promise<void> {\n\t\tconst g = this.graph();\n\t\tif (!g) return;\n\t\ttry {\n\t\t\tconst intentId = `INTENT:${goal.id}`;\n\t\t\tif (!this.seenNodes.has(intentId)) {\n\t\t\t\tg.addNode({ type: \"INTENT\", slug: goal.id, description: goal.goal.slice(0, 140) });\n\t\t\t\tthis.seenNodes.add(intentId);\n\t\t\t}\n\t\t\tfor (const criterion of goal.doneCriteria) {\n\t\t\t\tconst reqId = `REQUIREMENT:${goal.id}:${criterion.slice(0, 40)}`;\n\t\t\t\tif (!this.seenNodes.has(reqId)) {\n\t\t\t\t\tg.addNode({\n\t\t\t\t\t\ttype: \"REQUIREMENT\",\n\t\t\t\t\t\tslug: `${goal.id}:${criterion.slice(0, 40)}`,\n\t\t\t\t\t\tdescription: criterion.slice(0, 140),\n\t\t\t\t\t});\n\t\t\t\t\tthis.seenNodes.add(reqId);\n\t\t\t\t\tconst e1 = `${intentId}|motivates|${reqId}`;\n\t\t\t\t\tif (!this.seenEdges.has(e1)) {\n\t\t\t\t\t\tg.addEdges([{ subject: intentId, predicate: \"motivates\", object: reqId }]);\n\t\t\t\t\t\tthis.seenEdges.add(e1);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const task of goal.tasks) {\n\t\t\t\tconst taskId = `TASK:${task.id}`;\n\t\t\t\tif (!this.seenNodes.has(taskId)) {\n\t\t\t\t\tg.addNode({\n\t\t\t\t\t\ttype: \"TASK\",\n\t\t\t\t\t\tslug: task.id,\n\t\t\t\t\t\tdescription: task.title.slice(0, 140),\n\t\t\t\t\t\tstatus: task.status === \"done\" ? \"resolved\" : undefined,\n\t\t\t\t\t});\n\t\t\t\t\tthis.seenNodes.add(taskId);\n\t\t\t\t\tconst e2 = `${intentId}|planned_as|${taskId}`;\n\t\t\t\t\tif (!this.seenEdges.has(e2)) {\n\t\t\t\t\t\tg.addEdges([{ subject: intentId, predicate: \"planned_as\", object: taskId }]);\n\t\t\t\t\t\tthis.seenEdges.add(e2);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// best-effort\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
/** Normaliza una ruta relativa del agente a una ruta absoluta-corta para el grafo. */
|
|
3
|
+
function normalizeFilePath(cwd, raw) {
|
|
4
|
+
if (!raw)
|
|
5
|
+
return raw;
|
|
6
|
+
if (raw.startsWith("/"))
|
|
7
|
+
return raw;
|
|
8
|
+
return `${cwd.replace(/\/$/, "")}/${raw}`;
|
|
9
|
+
}
|
|
10
|
+
/** ¿Parece un comando de test? (heurística conservadora: no marca falsos). */
|
|
11
|
+
function looksLikeTest(command) {
|
|
12
|
+
const c = command.trim().toLowerCase();
|
|
13
|
+
return (/\b(npm test|vitest|jest|pytest|go test|npm run test|cargo test|mvn test|gradle test)\b/.test(c) ||
|
|
14
|
+
/^vitest /.test(c) ||
|
|
15
|
+
/^jest /.test(c) ||
|
|
16
|
+
/^pytest /.test(c) ||
|
|
17
|
+
/^go test /.test(c) ||
|
|
18
|
+
/test\.[a-z]+$/.test(c.split(/\s+/)[0] ?? ""));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* graph_add_nodes — tool para registros puntuales (QA_FINDING, RISK, DECISION,
|
|
22
|
+
* LEARNING). El spine (INTENT/TASK/FILE/TEST) se construye SOLO; esta tool es
|
|
23
|
+
* para lo que el agente descubre durante auditorías.
|
|
24
|
+
*/
|
|
25
|
+
export function createGraphTool(graph) {
|
|
26
|
+
return {
|
|
27
|
+
name: "graph_add_nodes",
|
|
28
|
+
label: "Añadir nodos al grafo",
|
|
29
|
+
description: "Registra nodos en el grafo de trabajo (QA_FINDING, RISK, DECISION, LEARNING, METRIC) para auditorías y trazabilidad. El spine (INTENT→TASK→FILE/TEST) se construye automáticamente — no lo uses para eso.",
|
|
30
|
+
promptSnippet: "Registra hallazgos en el grafo (graph_add_nodes)",
|
|
31
|
+
promptGuidelines: [
|
|
32
|
+
"Usa QA_FINDING para hallazgos de auditoría/revisión con archivo:línea.",
|
|
33
|
+
"Usa RISK para riesgos identificados.",
|
|
34
|
+
"El spine del grafo se auto-registra — no añadas FILE/TASK manualmente.",
|
|
35
|
+
],
|
|
36
|
+
parameters: Type.Object({
|
|
37
|
+
nodes: Type.Array(Type.Object({
|
|
38
|
+
type: Type.Union([
|
|
39
|
+
Type.Literal("QA_FINDING"),
|
|
40
|
+
Type.Literal("RISK"),
|
|
41
|
+
Type.Literal("DECISION"),
|
|
42
|
+
Type.Literal("LEARNING"),
|
|
43
|
+
]),
|
|
44
|
+
slug: Type.String({ description: "Identificador corto (ej. 'auth-timing')" }),
|
|
45
|
+
description: Type.String({ description: "Descripción del hallazgo" }),
|
|
46
|
+
})),
|
|
47
|
+
}),
|
|
48
|
+
async execute(_toolCallId, input) {
|
|
49
|
+
const g = graph();
|
|
50
|
+
if (!g) {
|
|
51
|
+
return {
|
|
52
|
+
details: undefined,
|
|
53
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: "graph no disponible" }) }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const results = input.nodes.map((n) => {
|
|
57
|
+
const r = g.addNode({ type: n.type, slug: n.slug, description: n.description });
|
|
58
|
+
return { id: `${n.type}:${n.slug}`, ok: r.ok, error: r.error };
|
|
59
|
+
});
|
|
60
|
+
return { details: undefined, content: [{ type: "text", text: JSON.stringify({ ok: true, results }) }] };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Único sync del graph con memoria para no duplicar aristas por tool_call. */
|
|
65
|
+
export class GraphSync {
|
|
66
|
+
graph;
|
|
67
|
+
activeGoal;
|
|
68
|
+
cwd;
|
|
69
|
+
/** nodos ya creados esta sesión (id → true) para idempotencia. */
|
|
70
|
+
seenNodes = new Set();
|
|
71
|
+
seenEdges = new Set();
|
|
72
|
+
constructor(options) {
|
|
73
|
+
this.graph = options.graph;
|
|
74
|
+
this.activeGoal = options.activeGoal;
|
|
75
|
+
this.cwd = options.cwd;
|
|
76
|
+
}
|
|
77
|
+
/** afterToolCall: registrar artefactos y verificación observada. */
|
|
78
|
+
async onToolResult(toolName, args, result, isError) {
|
|
79
|
+
const g = this.graph();
|
|
80
|
+
if (!g)
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
const goal = this.activeGoal();
|
|
84
|
+
if (toolName === "write" || toolName === "edit" || toolName === "file_edit") {
|
|
85
|
+
const rawPath = typeof args.path === "string" ? args.path : "";
|
|
86
|
+
if (!rawPath)
|
|
87
|
+
return;
|
|
88
|
+
const path = normalizeFilePath(this.cwd, rawPath);
|
|
89
|
+
const nodeId = `ARTIFACT:${path}`;
|
|
90
|
+
// Nodo de artefacto (idempotente).
|
|
91
|
+
if (!this.seenNodes.has(nodeId)) {
|
|
92
|
+
g.addNode({ type: "ARTIFACT", slug: path, description: `Archivo ${path}`, file: path });
|
|
93
|
+
this.seenNodes.add(nodeId);
|
|
94
|
+
}
|
|
95
|
+
// Arista desde el TASK activo del goal (o INTENT) → implemented_by.
|
|
96
|
+
const from = goal?.tasks.find((t) => t.status === "running" || t.status === "active")?.id;
|
|
97
|
+
const fromNode = from ? `TASK:${from}` : goal ? `INTENT:${goal.id}` : null;
|
|
98
|
+
if (fromNode) {
|
|
99
|
+
const edgeKey = `${fromNode}|implemented_by|${nodeId}`;
|
|
100
|
+
if (!this.seenEdges.has(edgeKey)) {
|
|
101
|
+
g.addEdges([{ subject: fromNode, predicate: "implemented_by", object: nodeId }]);
|
|
102
|
+
this.seenEdges.add(edgeKey);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (toolName === "bash") {
|
|
107
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
108
|
+
if (!command || isError)
|
|
109
|
+
return;
|
|
110
|
+
const outcome = result?.exitCode ?? 0;
|
|
111
|
+
if (outcome !== 0 || !looksLikeTest(command))
|
|
112
|
+
return;
|
|
113
|
+
// Test que pasó → nodo TEST + verified_by desde REQUIREMENT/INTENT.
|
|
114
|
+
const testId = `METRIC:test ${command.slice(0, 50)}`;
|
|
115
|
+
if (!this.seenNodes.has(testId)) {
|
|
116
|
+
g.addNode({
|
|
117
|
+
type: "METRIC",
|
|
118
|
+
slug: `test ${command.slice(0, 50)}`,
|
|
119
|
+
description: `Test que pasa: ${command}`,
|
|
120
|
+
});
|
|
121
|
+
this.seenNodes.add(testId);
|
|
122
|
+
}
|
|
123
|
+
const from = goal?.tasks.find((t) => t.status === "running" || t.status === "active")?.id;
|
|
124
|
+
const fromNode = from ? `TASK:${from}` : goal ? `INTENT:${goal.id}` : null;
|
|
125
|
+
if (fromNode) {
|
|
126
|
+
const edgeKey = `${fromNode}|verified_by|${testId}`;
|
|
127
|
+
if (!this.seenEdges.has(edgeKey)) {
|
|
128
|
+
g.addEdges([{ subject: fromNode, predicate: "verified_by", object: testId }]);
|
|
129
|
+
this.seenEdges.add(edgeKey);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// El grafo es best-effort: nunca rompe el flujo del agente.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** syncGoal: cuando el goal cambia, asegura INTENT + REQUIREMENT + TASK en el spine. */
|
|
139
|
+
async syncGoal(goal) {
|
|
140
|
+
const g = this.graph();
|
|
141
|
+
if (!g)
|
|
142
|
+
return;
|
|
143
|
+
try {
|
|
144
|
+
const intentId = `INTENT:${goal.id}`;
|
|
145
|
+
if (!this.seenNodes.has(intentId)) {
|
|
146
|
+
g.addNode({ type: "INTENT", slug: goal.id, description: goal.goal.slice(0, 140) });
|
|
147
|
+
this.seenNodes.add(intentId);
|
|
148
|
+
}
|
|
149
|
+
for (const criterion of goal.doneCriteria) {
|
|
150
|
+
const reqId = `REQUIREMENT:${goal.id}:${criterion.slice(0, 40)}`;
|
|
151
|
+
if (!this.seenNodes.has(reqId)) {
|
|
152
|
+
g.addNode({
|
|
153
|
+
type: "REQUIREMENT",
|
|
154
|
+
slug: `${goal.id}:${criterion.slice(0, 40)}`,
|
|
155
|
+
description: criterion.slice(0, 140),
|
|
156
|
+
});
|
|
157
|
+
this.seenNodes.add(reqId);
|
|
158
|
+
const e1 = `${intentId}|motivates|${reqId}`;
|
|
159
|
+
if (!this.seenEdges.has(e1)) {
|
|
160
|
+
g.addEdges([{ subject: intentId, predicate: "motivates", object: reqId }]);
|
|
161
|
+
this.seenEdges.add(e1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const task of goal.tasks) {
|
|
166
|
+
const taskId = `TASK:${task.id}`;
|
|
167
|
+
if (!this.seenNodes.has(taskId)) {
|
|
168
|
+
g.addNode({
|
|
169
|
+
type: "TASK",
|
|
170
|
+
slug: task.id,
|
|
171
|
+
description: task.title.slice(0, 140),
|
|
172
|
+
status: task.status === "done" ? "resolved" : undefined,
|
|
173
|
+
});
|
|
174
|
+
this.seenNodes.add(taskId);
|
|
175
|
+
const e2 = `${intentId}|planned_as|${taskId}`;
|
|
176
|
+
if (!this.seenEdges.has(e2)) {
|
|
177
|
+
g.addEdges([{ subject: intentId, predicate: "planned_as", object: taskId }]);
|
|
178
|
+
this.seenEdges.add(e2);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// best-effort
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=graph-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-sync.js","sourceRoot":"","sources":["../../../src/core/workflows/graph-sync.ts"],"names":[],"mappings":"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","sourcesContent":["import { Type } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\n/**\n * GraphSync — auto-registro AUTÓNOMO del grafo de trabajo (§16.3).\n *\n * El grafo se construye solo mientras el agente trabaja, sin que el usuario\n * tenga que pedirlo:\n * - write/edit de un archivo → nodo FILE/ARTIFACT + arista `implemented_by`\n * desde el TASK/INTENT activo (el spine se completa solo).\n * - bash que ejecuta un test → nodo TEST + arista `verified_by` (evidencia\n * OBSERVADA: comando + exit code 0).\n * - goal activo → INTENT + REQUIREMENT (doneCriteria) + TASK\n * (tasks del goal) con aristas planned_as / touches.\n * - QA_FINDING / RISK → tool graph_add_nodes (complemento, no dependencia).\n *\n * El gate del grafo solo pasa cuando el spine tiene verified_by REAL (archivo:línea\n * o salida de test) — nunca afirmaciones.\n */\n\nimport type { Graph } from \"./graph.ts\";\n\nexport interface GraphSyncOptions {\n\t/** Grafo del resource-loader (puede ser null si el graph está desactivado). */\n\tgraph: () => Graph | null;\n\t/** Para registrar qué TASK está activa: el goal activo actual. */\n\tactiveGoal: () => {\n\t\tid: string;\n\t\tgoal: string;\n\t\tphase: string;\n\t\ttasks: Array<{ id: string; title: string; status: string }>;\n\t\tdoneCriteria: string[];\n\t} | null;\n\t/** cwd para resolver rutas relativas de archivos. */\n\tcwd: string;\n}\n\n/** Normaliza una ruta relativa del agente a una ruta absoluta-corta para el grafo. */\nfunction normalizeFilePath(cwd: string, raw: string): string {\n\tif (!raw) return raw;\n\tif (raw.startsWith(\"/\")) return raw;\n\treturn `${cwd.replace(/\\/$/, \"\")}/${raw}`;\n}\n\n/** ¿Parece un comando de test? (heurística conservadora: no marca falsos). */\nfunction looksLikeTest(command: string): boolean {\n\tconst c = command.trim().toLowerCase();\n\treturn (\n\t\t/\\b(npm test|vitest|jest|pytest|go test|npm run test|cargo test|mvn test|gradle test)\\b/.test(c) ||\n\t\t/^vitest /.test(c) ||\n\t\t/^jest /.test(c) ||\n\t\t/^pytest /.test(c) ||\n\t\t/^go test /.test(c) ||\n\t\t/test\\.[a-z]+$/.test(c.split(/\\s+/)[0] ?? \"\")\n\t);\n}\n\n/**\n * graph_add_nodes — tool para registros puntuales (QA_FINDING, RISK, DECISION,\n * LEARNING). El spine (INTENT/TASK/FILE/TEST) se construye SOLO; esta tool es\n * para lo que el agente descubre durante auditorías.\n */\nexport function createGraphTool(graph: () => Graph | null): ToolDefinition {\n\treturn {\n\t\tname: \"graph_add_nodes\",\n\t\tlabel: \"Añadir nodos al grafo\",\n\t\tdescription:\n\t\t\t\"Registra nodos en el grafo de trabajo (QA_FINDING, RISK, DECISION, LEARNING, METRIC) para auditorías y trazabilidad. El spine (INTENT→TASK→FILE/TEST) se construye automáticamente — no lo uses para eso.\",\n\t\tpromptSnippet: \"Registra hallazgos en el grafo (graph_add_nodes)\",\n\t\tpromptGuidelines: [\n\t\t\t\"Usa QA_FINDING para hallazgos de auditoría/revisión con archivo:línea.\",\n\t\t\t\"Usa RISK para riesgos identificados.\",\n\t\t\t\"El spine del grafo se auto-registra — no añadas FILE/TASK manualmente.\",\n\t\t],\n\t\tparameters: Type.Object({\n\t\t\tnodes: Type.Array(\n\t\t\t\tType.Object({\n\t\t\t\t\ttype: Type.Union([\n\t\t\t\t\t\tType.Literal(\"QA_FINDING\"),\n\t\t\t\t\t\tType.Literal(\"RISK\"),\n\t\t\t\t\t\tType.Literal(\"DECISION\"),\n\t\t\t\t\t\tType.Literal(\"LEARNING\"),\n\t\t\t\t\t]),\n\t\t\t\t\tslug: Type.String({ description: \"Identificador corto (ej. 'auth-timing')\" }),\n\t\t\t\t\tdescription: Type.String({ description: \"Descripción del hallazgo\" }),\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\tasync execute(\n\t\t\t_toolCallId: string,\n\t\t\tinput: {\n\t\t\t\tnodes: Array<{ type: \"QA_FINDING\" | \"RISK\" | \"DECISION\" | \"LEARNING\"; slug: string; description: string }>;\n\t\t\t},\n\t\t) {\n\t\t\tconst g = graph();\n\t\t\tif (!g) {\n\t\t\t\treturn {\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t\tcontent: [{ type: \"text\", text: JSON.stringify({ ok: false, error: \"graph no disponible\" }) }],\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst results = input.nodes.map((n) => {\n\t\t\t\tconst r = g.addNode({ type: n.type, slug: n.slug, description: n.description });\n\t\t\t\treturn { id: `${n.type}:${n.slug}`, ok: r.ok, error: r.error };\n\t\t\t});\n\t\t\treturn { details: undefined, content: [{ type: \"text\", text: JSON.stringify({ ok: true, results }) }] };\n\t\t},\n\t};\n}\n\n/** Único sync del graph con memoria para no duplicar aristas por tool_call. */\nexport class GraphSync {\n\tprivate graph: () => Graph | null;\n\tprivate activeGoal: GraphSyncOptions[\"activeGoal\"];\n\tprivate cwd: string;\n\t/** nodos ya creados esta sesión (id → true) para idempotencia. */\n\tprivate seenNodes = new Set<string>();\n\tprivate seenEdges = new Set<string>();\n\n\tconstructor(options: GraphSyncOptions) {\n\t\tthis.graph = options.graph;\n\t\tthis.activeGoal = options.activeGoal;\n\t\tthis.cwd = options.cwd;\n\t}\n\n\t/** afterToolCall: registrar artefactos y verificación observada. */\n\tasync onToolResult(\n\t\ttoolName: string,\n\t\targs: Record<string, unknown>,\n\t\tresult: unknown,\n\t\tisError: boolean,\n\t): Promise<void> {\n\t\tconst g = this.graph();\n\t\tif (!g) return;\n\t\ttry {\n\t\t\tconst goal = this.activeGoal();\n\n\t\t\tif (toolName === \"write\" || toolName === \"edit\" || toolName === \"file_edit\") {\n\t\t\t\tconst rawPath = typeof args.path === \"string\" ? args.path : \"\";\n\t\t\t\tif (!rawPath) return;\n\t\t\t\tconst path = normalizeFilePath(this.cwd, rawPath);\n\t\t\t\tconst nodeId = `ARTIFACT:${path}`;\n\t\t\t\t// Nodo de artefacto (idempotente).\n\t\t\t\tif (!this.seenNodes.has(nodeId)) {\n\t\t\t\t\tg.addNode({ type: \"ARTIFACT\", slug: path, description: `Archivo ${path}`, file: path });\n\t\t\t\t\tthis.seenNodes.add(nodeId);\n\t\t\t\t}\n\t\t\t\t// Arista desde el TASK activo del goal (o INTENT) → implemented_by.\n\t\t\t\tconst from = goal?.tasks.find((t) => t.status === \"running\" || t.status === \"active\")?.id;\n\t\t\t\tconst fromNode = from ? `TASK:${from}` : goal ? `INTENT:${goal.id}` : null;\n\t\t\t\tif (fromNode) {\n\t\t\t\t\tconst edgeKey = `${fromNode}|implemented_by|${nodeId}`;\n\t\t\t\t\tif (!this.seenEdges.has(edgeKey)) {\n\t\t\t\t\t\tg.addEdges([{ subject: fromNode, predicate: \"implemented_by\", object: nodeId }]);\n\t\t\t\t\t\tthis.seenEdges.add(edgeKey);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (toolName === \"bash\") {\n\t\t\t\tconst command = typeof args.command === \"string\" ? args.command : \"\";\n\t\t\t\tif (!command || isError) return;\n\t\t\t\tconst outcome = (result as { exitCode?: number } | undefined)?.exitCode ?? 0;\n\t\t\t\tif (outcome !== 0 || !looksLikeTest(command)) return;\n\t\t\t\t// Test que pasó → nodo TEST + verified_by desde REQUIREMENT/INTENT.\n\t\t\t\tconst testId = `METRIC:test ${command.slice(0, 50)}`;\n\t\t\t\tif (!this.seenNodes.has(testId)) {\n\t\t\t\t\tg.addNode({\n\t\t\t\t\t\ttype: \"METRIC\",\n\t\t\t\t\t\tslug: `test ${command.slice(0, 50)}`,\n\t\t\t\t\t\tdescription: `Test que pasa: ${command}`,\n\t\t\t\t\t});\n\t\t\t\t\tthis.seenNodes.add(testId);\n\t\t\t\t}\n\t\t\t\tconst from = goal?.tasks.find((t) => t.status === \"running\" || t.status === \"active\")?.id;\n\t\t\t\tconst fromNode = from ? `TASK:${from}` : goal ? `INTENT:${goal.id}` : null;\n\t\t\t\tif (fromNode) {\n\t\t\t\t\tconst edgeKey = `${fromNode}|verified_by|${testId}`;\n\t\t\t\t\tif (!this.seenEdges.has(edgeKey)) {\n\t\t\t\t\t\tg.addEdges([{ subject: fromNode, predicate: \"verified_by\", object: testId }]);\n\t\t\t\t\t\tthis.seenEdges.add(edgeKey);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// El grafo es best-effort: nunca rompe el flujo del agente.\n\t\t}\n\t}\n\n\t/** syncGoal: cuando el goal cambia, asegura INTENT + REQUIREMENT + TASK en el spine. */\n\tasync syncGoal(goal: {\n\t\tid: string;\n\t\tgoal: string;\n\t\tphase: string;\n\t\ttasks: Array<{ id: string; title: string; status: string }>;\n\t\tdoneCriteria: string[];\n\t}): Promise<void> {\n\t\tconst g = this.graph();\n\t\tif (!g) return;\n\t\ttry {\n\t\t\tconst intentId = `INTENT:${goal.id}`;\n\t\t\tif (!this.seenNodes.has(intentId)) {\n\t\t\t\tg.addNode({ type: \"INTENT\", slug: goal.id, description: goal.goal.slice(0, 140) });\n\t\t\t\tthis.seenNodes.add(intentId);\n\t\t\t}\n\t\t\tfor (const criterion of goal.doneCriteria) {\n\t\t\t\tconst reqId = `REQUIREMENT:${goal.id}:${criterion.slice(0, 40)}`;\n\t\t\t\tif (!this.seenNodes.has(reqId)) {\n\t\t\t\t\tg.addNode({\n\t\t\t\t\t\ttype: \"REQUIREMENT\",\n\t\t\t\t\t\tslug: `${goal.id}:${criterion.slice(0, 40)}`,\n\t\t\t\t\t\tdescription: criterion.slice(0, 140),\n\t\t\t\t\t});\n\t\t\t\t\tthis.seenNodes.add(reqId);\n\t\t\t\t\tconst e1 = `${intentId}|motivates|${reqId}`;\n\t\t\t\t\tif (!this.seenEdges.has(e1)) {\n\t\t\t\t\t\tg.addEdges([{ subject: intentId, predicate: \"motivates\", object: reqId }]);\n\t\t\t\t\t\tthis.seenEdges.add(e1);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const task of goal.tasks) {\n\t\t\t\tconst taskId = `TASK:${task.id}`;\n\t\t\t\tif (!this.seenNodes.has(taskId)) {\n\t\t\t\t\tg.addNode({\n\t\t\t\t\t\ttype: \"TASK\",\n\t\t\t\t\t\tslug: task.id,\n\t\t\t\t\t\tdescription: task.title.slice(0, 140),\n\t\t\t\t\t\tstatus: task.status === \"done\" ? \"resolved\" : undefined,\n\t\t\t\t\t});\n\t\t\t\t\tthis.seenNodes.add(taskId);\n\t\t\t\t\tconst e2 = `${intentId}|planned_as|${taskId}`;\n\t\t\t\t\tif (!this.seenEdges.has(e2)) {\n\t\t\t\t\t\tg.addEdges([{ subject: intentId, predicate: \"planned_as\", object: taskId }]);\n\t\t\t\t\t\tthis.seenEdges.add(e2);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// best-effort\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ada Intelligence Engine — Grafo de trabajo (Fase 7 / §5).
|
|
3
|
+
*
|
|
4
|
+
* El grafo es memoria ESTRUCTURADA: nodos en 3 capas (product/design/code)
|
|
5
|
+
* y aristas con predicados CERRADOS. Live en `~/.ada/agent/workflows/graph/`
|
|
6
|
+
* (misma raíz que observaciones) con persistencia JSON + índice invertido
|
|
7
|
+
* (patrón memoria; sin SQLite nativo).
|
|
8
|
+
*
|
|
9
|
+
* Reglas no negociables:
|
|
10
|
+
* - Ids reutilizables TYPE:slug (≤72 chars); search antes de inventar.
|
|
11
|
+
* - Predicados fuera de la lista legal → REJECTED con warning.
|
|
12
|
+
* - addEdges es atómico: si un endpoint no existe, rechaza TODO el lote.
|
|
13
|
+
* - retract exige --reason (journal).
|
|
14
|
+
* - Confidence tiers EXTRACTED=1.0 / INFERRED=0.7-0.9 / AMBIGUOUS<0.7 / MANUAL=1.0.
|
|
15
|
+
* - context(id, minConfidence) filtra < 0.6 por defecto.
|
|
16
|
+
* - Gates edge-driven: ada gaps (spine incompleto) / ada gate (criterios con evidencia).
|
|
17
|
+
*/
|
|
18
|
+
export type GraphLayer = "product" | "design" | "code";
|
|
19
|
+
export type GraphNodeType = "INTENT" | "REQUIREMENT" | "CONSTRAINT" | "DECISION" | "RISK" | "TASK" | "QA_FINDING" | "ARTIFACT" | "LEARNING" | "METRIC" | "PERSONA" | "DESIGN_TOKEN" | "UI_PATTERN" | "SCREEN" | "FLOW" | "A11Y_RULE" | "FILE" | "SYMBOL" | "ROUTE" | "TEST" | "DEPENDENCY";
|
|
20
|
+
export declare const NODE_LAYER: Record<GraphNodeType, GraphLayer>;
|
|
21
|
+
/** Predicados cerrados (spine + resto) §5. */
|
|
22
|
+
export declare const SPINE_EDGES: readonly ["motivates", "planned_as", "touches", "implemented_by", "verified_by"];
|
|
23
|
+
export declare const LEGAL_EDGES: Set<string>;
|
|
24
|
+
export type Confidence = 1.0 | 0.9 | 0.8 | 0.7 | 0.5 | 0.4 | 0.3;
|
|
25
|
+
export interface GraphNode {
|
|
26
|
+
id: string;
|
|
27
|
+
type: GraphNodeType;
|
|
28
|
+
description: string;
|
|
29
|
+
status?: "open" | "resolved" | "rejected";
|
|
30
|
+
confidence: Confidence;
|
|
31
|
+
origin?: "inferred";
|
|
32
|
+
file?: string;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
updatedAt: string;
|
|
35
|
+
}
|
|
36
|
+
export interface GraphEdge {
|
|
37
|
+
id: string;
|
|
38
|
+
subject: string;
|
|
39
|
+
predicate: string;
|
|
40
|
+
object: string;
|
|
41
|
+
confidence: Confidence;
|
|
42
|
+
reason?: string;
|
|
43
|
+
rejected?: boolean;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
}
|
|
46
|
+
export interface GraphEdgeInput {
|
|
47
|
+
subject: string;
|
|
48
|
+
predicate: string;
|
|
49
|
+
object: string;
|
|
50
|
+
confidence?: Confidence;
|
|
51
|
+
reason?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface GraphData {
|
|
54
|
+
version: number;
|
|
55
|
+
nodes: Record<string, GraphNode>;
|
|
56
|
+
edges: GraphEdge[];
|
|
57
|
+
}
|
|
58
|
+
export interface GraphResult<T> {
|
|
59
|
+
ok: boolean;
|
|
60
|
+
data?: T;
|
|
61
|
+
error?: string;
|
|
62
|
+
rejected?: string[];
|
|
63
|
+
warnings?: string[];
|
|
64
|
+
}
|
|
65
|
+
export interface GraphStore {
|
|
66
|
+
save(data: GraphData): Promise<void>;
|
|
67
|
+
load(): Promise<GraphData | null>;
|
|
68
|
+
}
|
|
69
|
+
export declare class FileGraphStore implements GraphStore {
|
|
70
|
+
private file;
|
|
71
|
+
constructor(dir: string);
|
|
72
|
+
save(data: GraphData): Promise<void>;
|
|
73
|
+
load(): Promise<GraphData | null>;
|
|
74
|
+
}
|
|
75
|
+
/** Variante en memoria para tests */
|
|
76
|
+
export declare class MemoryGraphStore implements GraphStore {
|
|
77
|
+
data: GraphData | null;
|
|
78
|
+
save(d: GraphData): Promise<void>;
|
|
79
|
+
load(): Promise<GraphData | null>;
|
|
80
|
+
}
|
|
81
|
+
export interface GraphOptions {
|
|
82
|
+
store: GraphStore;
|
|
83
|
+
/** Confianza mínima para context() (default 0.6). */
|
|
84
|
+
minConfidence?: number;
|
|
85
|
+
onEvent?: (type: "graph_update" | "graph_edge_rejected", payload: Record<string, unknown>) => void;
|
|
86
|
+
}
|
|
87
|
+
export declare class Graph {
|
|
88
|
+
private store;
|
|
89
|
+
private minConfidence;
|
|
90
|
+
private onEvent?;
|
|
91
|
+
private data;
|
|
92
|
+
constructor(options: GraphOptions);
|
|
93
|
+
init(): Promise<void>;
|
|
94
|
+
addNode(input: {
|
|
95
|
+
type: GraphNodeType;
|
|
96
|
+
slug: string;
|
|
97
|
+
description: string;
|
|
98
|
+
confidence?: Confidence;
|
|
99
|
+
status?: GraphNode["status"];
|
|
100
|
+
origin?: "inferred";
|
|
101
|
+
file?: string;
|
|
102
|
+
}): GraphResult<GraphNode>;
|
|
103
|
+
addEdges(edges: GraphEdgeInput[]): GraphResult<{
|
|
104
|
+
added: GraphEdge[];
|
|
105
|
+
}>;
|
|
106
|
+
retract(edgeId: string, reason: string): GraphResult<{
|
|
107
|
+
removed: GraphEdge;
|
|
108
|
+
}>;
|
|
109
|
+
search(term: string): Array<{
|
|
110
|
+
node: GraphNode;
|
|
111
|
+
score: number;
|
|
112
|
+
}>;
|
|
113
|
+
context(id: string, minConfidence?: number): GraphResult<{
|
|
114
|
+
node?: GraphNode;
|
|
115
|
+
neighbors: GraphEdge[];
|
|
116
|
+
}>;
|
|
117
|
+
/**
|
|
118
|
+
* gaps(): devuelve el spine incompleto.
|
|
119
|
+
* - TODO INTENT sin motivar un REQUIREMENT
|
|
120
|
+
* - TODO REQUIREMENT sin planned_as → TASK
|
|
121
|
+
* - TODO TASK sin touches → FILE
|
|
122
|
+
* - TODO REQUIREMENT sin verified_by → TEST ni implemented_by → SYMBOL
|
|
123
|
+
*/
|
|
124
|
+
gaps(): Array<{
|
|
125
|
+
id: string;
|
|
126
|
+
description: string;
|
|
127
|
+
missing: string;
|
|
128
|
+
}>;
|
|
129
|
+
/** gate(criteria): devuelve true si TODOS los requisitos tienen verified_by REAL. */
|
|
130
|
+
gate(): GraphResult<{
|
|
131
|
+
passed: boolean;
|
|
132
|
+
failures: string[];
|
|
133
|
+
}>;
|
|
134
|
+
nodeCount(): number;
|
|
135
|
+
edgeCount(): number;
|
|
136
|
+
nodes(): GraphNode[];
|
|
137
|
+
edges(): GraphEdge[];
|
|
138
|
+
private clampConfidence;
|
|
139
|
+
private persist;
|
|
140
|
+
private emit;
|
|
141
|
+
}
|
|
142
|
+
export declare function slugHash(input: string, len?: number): string;
|
|
143
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../src/core/workflows/graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Ada Intelligence Engine — Grafo de trabajo (Fase 7 / §5).\n *\n * El grafo es memoria ESTRUCTURADA: nodos en 3 capas (product/design/code)\n * y aristas con predicados CERRADOS. Live en `~/.ada/agent/workflows/graph/`\n * (misma raíz que observaciones) con persistencia JSON + índice invertido\n * (patrón memoria; sin SQLite nativo).\n *\n * Reglas no negociables:\n * - Ids reutilizables TYPE:slug (≤72 chars); search antes de inventar.\n * - Predicados fuera de la lista legal → REJECTED con warning.\n * - addEdges es atómico: si un endpoint no existe, rechaza TODO el lote.\n * - retract exige --reason (journal).\n * - Confidence tiers EXTRACTED=1.0 / INFERRED=0.7-0.9 / AMBIGUOUS<0.7 / MANUAL=1.0.\n * - context(id, minConfidence) filtra < 0.6 por defecto.\n * - Gates edge-driven: ada gaps (spine incompleto) / ada gate (criterios con evidencia).\n */\n\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tokenize } from \"../memory-engine/inverted-index.ts\";\n\n// ─── Tipos (3 capas) ───\n\nexport type GraphLayer = \"product\" | \"design\" | \"code\";\n\nexport type GraphNodeType =\n\t// product\n\t| \"INTENT\"\n\t| \"REQUIREMENT\"\n\t| \"CONSTRAINT\"\n\t| \"DECISION\"\n\t| \"RISK\"\n\t| \"TASK\"\n\t| \"QA_FINDING\"\n\t| \"ARTIFACT\"\n\t| \"LEARNING\"\n\t| \"METRIC\"\n\t| \"PERSONA\"\n\t// design\n\t| \"DESIGN_TOKEN\"\n\t| \"UI_PATTERN\"\n\t| \"SCREEN\"\n\t| \"FLOW\"\n\t| \"A11Y_RULE\"\n\t// code\n\t| \"FILE\"\n\t| \"SYMBOL\"\n\t| \"ROUTE\"\n\t| \"TEST\"\n\t| \"DEPENDENCY\";\n\nexport const NODE_LAYER: Record<GraphNodeType, GraphLayer> = {\n\tINTENT: \"product\",\n\tREQUIREMENT: \"product\",\n\tCONSTRAINT: \"product\",\n\tDECISION: \"product\",\n\tRISK: \"product\",\n\tTASK: \"product\",\n\tQA_FINDING: \"product\",\n\tARTIFACT: \"product\",\n\tLEARNING: \"product\",\n\tMETRIC: \"product\",\n\tPERSONA: \"product\",\n\tDESIGN_TOKEN: \"design\",\n\tUI_PATTERN: \"design\",\n\tSCREEN: \"design\",\n\tFLOW: \"design\",\n\tA11Y_RULE: \"design\",\n\tFILE: \"code\",\n\tSYMBOL: \"code\",\n\tROUTE: \"code\",\n\tTEST: \"code\",\n\tDEPENDENCY: \"code\",\n};\n\n/** Predicados cerrados (spine + resto) §5. */\nexport const SPINE_EDGES = [\"motivates\", \"planned_as\", \"touches\", \"implemented_by\", \"verified_by\"] as const;\n\nexport const LEGAL_EDGES = new Set<string>([\n\t...SPINE_EDGES,\n\t\"constrains\",\n\t\"mitigated_by\",\n\t\"found\",\n\t\"affects\",\n\t\"regresses\",\n\t\"uses_token\",\n\t\"applied_in\",\n\t\"declared_in\",\n\t\"renders\",\n\t\"calls\",\n\t\"references\",\n\t\"imports\",\n\t\"depends_on\",\n\t\"defines\",\n\t\"served_by\",\n\t\"supersedes\",\n\t\"rejected_in_favour_of\",\n\t\"derives_from\",\n\t\"measured_by\",\n\t\"recalls\",\n]);\n\nexport type Confidence = 1.0 | 0.9 | 0.8 | 0.7 | 0.5 | 0.4 | 0.3;\n\nexport interface GraphNode {\n\tid: string; // TYPE:slug (≤72)\n\ttype: GraphNodeType;\n\tdescription: string; // una frase\n\tstatus?: \"open\" | \"resolved\" | \"rejected\";\n\tconfidence: Confidence;\n\torigin?: \"inferred\";\n\tfile?: string; // source/artefacto\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface GraphEdge {\n\tid: string;\n\tsubject: string; // from\n\tpredicate: string; // legal cerrado\n\tobject: string; // to\n\tconfidence: Confidence;\n\treason?: string;\n\trejected?: boolean; // DECISION status rejected\n\tcreatedAt: string;\n}\n\nexport interface GraphEdgeInput {\n\tsubject: string;\n\tpredicate: string;\n\tobject: string;\n\tconfidence?: Confidence;\n\treason?: string;\n}\n\nexport interface GraphData {\n\tversion: number;\n\tnodes: Record<string, GraphNode>;\n\tedges: GraphEdge[];\n}\n\nexport interface GraphResult<T> {\n\tok: boolean;\n\tdata?: T;\n\terror?: string;\n\trejected?: string[]; // predicados desconocidos\n\twarnings?: string[];\n}\n\n// ─── Persistencia (inyectable para tests) ───\n\nexport interface GraphStore {\n\tsave(data: GraphData): Promise<void>;\n\tload(): Promise<GraphData | null>;\n}\n\nexport class FileGraphStore implements GraphStore {\n\tprivate file: string;\n\tconstructor(dir: string) {\n\t\tmkdirSync(dir, { recursive: true });\n\t\tthis.file = join(dir, \"graph.json\");\n\t}\n\tasync save(data: GraphData): Promise<void> {\n\t\tconst tmp = `${this.file}.tmp`;\n\t\twriteFileSync(tmp, JSON.stringify(data, null, 2), \"utf8\");\n\t\trenameSync(tmp, this.file);\n\t}\n\tasync load(): Promise<GraphData | null> {\n\t\tif (!existsSync(this.file)) return null;\n\t\ttry {\n\t\t\treturn JSON.parse(readFileSync(this.file, \"utf8\")) as GraphData;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n\n/** Variante en memoria para tests */\nexport class MemoryGraphStore implements GraphStore {\n\tdata: GraphData | null = null;\n\tasync save(d: GraphData): Promise<void> {\n\t\tthis.data = structuredClone(d);\n\t}\n\tasync load(): Promise<GraphData | null> {\n\t\treturn this.data ? structuredClone(this.data) : null;\n\t}\n}\n\n// ─── Grafo ───\n\nexport interface GraphOptions {\n\tstore: GraphStore;\n\t/** Confianza mínima para context() (default 0.6). */\n\tminConfidence?: number;\n\tonEvent?: (type: \"graph_update\" | \"graph_edge_rejected\", payload: Record<string, unknown>) => void;\n}\n\nconst GRAPH_VERSION = 1;\n\nexport class Graph {\n\tprivate store: GraphStore;\n\tprivate minConfidence: number;\n\tprivate onEvent?: GraphOptions[\"onEvent\"];\n\tprivate data: GraphData;\n\n\tconstructor(options: GraphOptions) {\n\t\tthis.store = options.store;\n\t\tthis.minConfidence = options.minConfidence ?? 0.6;\n\t\tthis.onEvent = options.onEvent;\n\t\tthis.data = { version: GRAPH_VERSION, nodes: {}, edges: [] };\n\t}\n\n\tasync init(): Promise<void> {\n\t\tconst loaded = await this.store.load();\n\t\tif (loaded?.nodes) {\n\t\t\tthis.data = loaded;\n\t\t} else {\n\t\t\tthis.data = { version: GRAPH_VERSION, nodes: {}, edges: [] };\n\t\t}\n\t}\n\n\t// ─── Nodos ───\n\n\taddNode(input: {\n\t\ttype: GraphNodeType;\n\t\tslug: string;\n\t\tdescription: string;\n\t\tconfidence?: Confidence;\n\t\tstatus?: GraphNode[\"status\"];\n\t\torigin?: \"inferred\";\n\t\tfile?: string;\n\t}): GraphResult<GraphNode> {\n\t\tconst id = `${input.type}:${input.slug}`;\n\t\tif (id.length > 72) {\n\t\t\treturn { ok: false, error: `Id excede 72 chars: ${id.length}` };\n\t\t}\n\t\tconst confidence = this.clampConfidence(input.confidence);\n\t\tconst now = new Date().toISOString();\n\t\tconst existing = this.data.nodes[id];\n\t\tconst node: GraphNode = {\n\t\t\tid,\n\t\t\ttype: input.type,\n\t\t\tdescription: input.description,\n\t\t\tstatus: input.status ?? (input.type === \"DECISION\" ? \"open\" : undefined),\n\t\t\tconfidence,\n\t\t\t...(input.origin ? { origin: \"inferred\" } : {}),\n\t\t\t...(input.file ? { file: input.file } : {}),\n\t\t\tcreatedAt: existing?.createdAt ?? now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.data.nodes[id] = node;\n\t\tvoid this.persist();\n\t\treturn { ok: true, data: node };\n\t}\n\n\t// ─── Aristas (atómico + predicados cerrados) ───\n\n\taddEdges(edges: GraphEdgeInput[]): GraphResult<{ added: GraphEdge[] }> {\n\t\tconst warnings: string[] = [];\n\t\tconst rejected: string[] = [];\n\n\t\t// 1. Validar predicados (lista cerrada).\n\t\tconst legal = edges.every((e) => LEGAL_EDGES.has(e.predicate));\n\t\tif (!legal) {\n\t\t\tfor (const e of edges) {\n\t\t\t\tif (!LEGAL_EDGES.has(e.predicate)) {\n\t\t\t\t\trejected.push(e.predicate);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: `Predicado(s) fuera de la lista legal: ${rejected.join(\", \")}`,\n\t\t\t\trejected,\n\t\t\t};\n\t\t}\n\n\t\t// 2. Validar que todos los endpoints existen (rechazo atómico).\n\t\tconst missing = edges.filter((e) => !this.data.nodes[e.subject] || !this.data.nodes[e.object]);\n\t\tif (missing.length > 0) {\n\t\t\tconst missingIds = missing.map((e) =>\n\t\t\t\t[!this.data.nodes[e.subject] && e.subject, !this.data.nodes[e.object] && e.object]\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(\",\"),\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: `Nodos de paso no existen: ${missingIds.join(\", \")}`,\n\t\t\t\twarnings,\n\t\t\t};\n\t\t}\n\n\t\t// 3. Aplicar.\n\t\tconst now = new Date().toISOString();\n\t\tconst added: GraphEdge[] = [];\n\t\tfor (const e of edges) {\n\t\t\tconst id = `${e.subject}--${e.predicate}-->${e.object}`;\n\t\t\t// Rechazar duplicados exactos.\n\t\t\tif (this.data.edges.some((x) => x.id === id)) continue;\n\t\t\tconst edge: GraphEdge = {\n\t\t\t\tid,\n\t\t\t\tsubject: e.subject,\n\t\t\t\tpredicate: e.predicate,\n\t\t\t\tobject: e.object,\n\t\t\t\tconfidence: this.clampConfidence(e.confidence),\n\t\t\t\t...(e.reason ? { reason: e.reason } : {}),\n\t\t\t\tcreatedAt: now,\n\t\t\t};\n\t\t\tthis.data.edges.push(edge);\n\t\t\tadded.push(edge);\n\t\t\tthis.emit(\"graph_update\", { type: \"edge\", edge });\n\t\t}\n\t\tvoid this.persist();\n\t\treturn { ok: true, data: { added } };\n\t}\n\n\t// ─── Retract (exige reason) ───\n\n\tretract(edgeId: string, reason: string): GraphResult<{ removed: GraphEdge }> {\n\t\tif (!reason || reason.trim().length === 0) {\n\t\t\treturn { ok: false, error: \"retract exige --reason (journal)\" };\n\t\t}\n\t\tconst idx = this.data.edges.findIndex((e) => e.id === edgeId);\n\t\tif (idx === -1) return { ok: false, error: `Arista no encontrada: ${edgeId}` };\n\t\tconst [removed] = this.data.edges.splice(idx, 1);\n\t\tremoved.reason = reason;\n\t\tremoved.rejected = true;\n\t\tvoid this.persist();\n\t\treturn { ok: true, data: { removed } };\n\t}\n\n\t// ─── Búsqueda ───\n\n\tsearch(term: string): Array<{ node: GraphNode; score: number }> {\n\t\tconst tokens = new Set(tokenize(term.toLowerCase()));\n\t\tconst scores: Array<{ node: GraphNode; score: number }> = [];\n\t\tfor (const node of Object.values(this.data.nodes)) {\n\t\t\tconst text = `${node.description} ${node.id}`.toLowerCase();\n\t\t\tlet score = 0;\n\t\t\tfor (const t of tokens) {\n\t\t\t\tif (text.includes(t)) score++;\n\t\t\t}\n\t\t\tif (score > 0) scores.push({ node, score });\n\t\t}\n\t\treturn scores.sort((a, b) => b.score - a.score);\n\t}\n\n\tcontext(id: string, minConfidence?: number): GraphResult<{ node?: GraphNode; neighbors: GraphEdge[] }> {\n\t\tconst threshold = minConfidence ?? this.minConfidence;\n\t\tconst node = this.data.nodes[id];\n\t\tif (!node) return { ok: false, error: `Nodo no encontrado: ${id}` };\n\t\tif (node.confidence < threshold) {\n\t\t\treturn { ok: false, error: `Nodo ${id} tiene confianza ${node.confidence} < ${threshold} (filtrado)` };\n\t\t}\n\t\tconst neighbors = this.data.edges.filter(\n\t\t\t(e) => (e.subject === id || e.object === id) && e.confidence >= threshold,\n\t\t);\n\t\treturn { ok: true, data: { node, neighbors } };\n\t}\n\n\t// ─── Gates edge-driven ───\n\n\t/**\n\t * gaps(): devuelve el spine incompleto.\n\t * - TODO INTENT sin motivar un REQUIREMENT\n\t * - TODO REQUIREMENT sin planned_as → TASK\n\t * - TODO TASK sin touches → FILE\n\t * - TODO REQUIREMENT sin verified_by → TEST ni implemented_by → SYMBOL\n\t */\n\tgaps(): Array<{ id: string; description: string; missing: string }> {\n\t\tconst gaps: Array<{ id: string; description: string; missing: string }> = [];\n\t\tconst edgeFor = (from: string, predicate: string) =>\n\t\t\tthis.data.edges.some((e) => e.subject === from && e.predicate === predicate);\n\n\t\tfor (const n of Object.values(this.data.nodes)) {\n\t\t\tif (n.type === \"INTENT\" && !edgeFor(n.id, \"motivates\"))\n\t\t\t\tgaps.push({ id: n.id, description: n.description, missing: \"REQUIREMENT motivado (motivates)\" });\n\t\t\tif (n.type === \"REQUIREMENT\" && !edgeFor(n.id, \"planned_as\"))\n\t\t\t\tgaps.push({ id: n.id, description: n.description, missing: \"TASK planificada (planned_as)\" });\n\t\t\tif (n.type === \"TASK\" && !edgeFor(n.id, \"touches\"))\n\t\t\t\tgaps.push({ id: n.id, description: n.description, missing: \"FILE tocado (touches)\" });\n\t\t\tif (n.type === \"REQUIREMENT\" && !edgeFor(n.id, \"verified_by\") && !edgeFor(n.id, \"implemented_by\"))\n\t\t\t\tgaps.push({ id: n.id, description: n.description, missing: \"TEST verified_by o SYMBOL implemented_by\" });\n\t\t}\n\t\treturn gaps;\n\t}\n\n\t/** gate(criteria): devuelve true si TODOS los requisitos tienen verified_by REAL. */\n\tgate(): GraphResult<{ passed: boolean; failures: string[] }> {\n\t\tconst requirements = Object.values(this.data.nodes).filter((n) => n.type === \"REQUIREMENT\");\n\t\tif (requirements.length === 0) return { ok: true, data: { passed: true, failures: [] } };\n\t\tconst failures: string[] = [];\n\n\t\t// Una DECISION open bloquea el gate (§5 regla 4: gap que bloquea).\n\t\tfor (const n of Object.values(this.data.nodes)) {\n\t\t\tif (n.type === \"DECISION\" && n.status === \"open\") {\n\t\t\t\tfailures.push(`${n.id}: decisión abierta bloquea el gate`);\n\t\t\t}\n\t\t}\n\n\t\tfor (const req of requirements) {\n\t\t\tconst verified = this.data.edges.some((e) => e.subject === req.id && e.predicate === \"verified_by\");\n\t\t\tif (!verified) failures.push(`${req.id}: sin verified_by`);\n\t\t}\n\t\treturn { ok: true, data: { passed: failures.length === 0, failures } };\n\t}\n\n\tnodeCount(): number {\n\t\treturn Object.keys(this.data.nodes).length;\n\t}\n\tedgeCount(): number {\n\t\treturn this.data.edges.length;\n\t}\n\tnodes(): GraphNode[] {\n\t\treturn Object.values(this.data.nodes);\n\t}\n\tedges(): GraphEdge[] {\n\t\treturn [...this.data.edges];\n\t}\n\n\t// ─── helpers ───\n\n\tprivate clampConfidence(c?: Confidence): Confidence {\n\t\tif (c === undefined) return 1.0;\n\t\tconst n = Math.max(0, Math.min(1, c));\n\t\treturn n as Confidence;\n\t}\n\n\tprivate async persist(): Promise<void> {\n\t\ttry {\n\t\t\tawait this.store.save(this.data);\n\t\t} catch {\n\t\t\t// best-effort: el disco es cache, no fuente de verdad estricta en memoria\n\t\t}\n\t}\n\n\tprivate emit(type: \"graph_update\" | \"graph_edge_rejected\", payload: Record<string, unknown>): void {\n\t\tthis.onEvent?.(type, payload);\n\t}\n}\n\n// ─── Hash de id auxiliar (para slugs estables) ───\n\nexport function slugHash(input: string, len = 6): string {\n\treturn createHash(\"sha256\").update(input).digest(\"hex\").slice(0, len);\n}\n"]}
|