@devtrack-solution/codesdd 1.2.2
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/.sdd/skills/curated/api-clean-flask-langgraph/SKILL.md +2751 -0
- package/.sdd/skills/curated/devtrack-api/SKILL.md +137 -0
- package/.sdd/skills/curated/devtrack-api/agents/openai.yaml +4 -0
- package/.sdd/skills/curated/devtrack-api/references/application-presentation.md +381 -0
- package/.sdd/skills/curated/devtrack-api/references/architecture-governance.md +219 -0
- package/.sdd/skills/curated/devtrack-api/references/domain-modeling.md +359 -0
- package/.sdd/skills/curated/devtrack-api/references/implementation-checklist.md +127 -0
- package/.sdd/skills/curated/devtrack-api/references/imports-lint.md +207 -0
- package/.sdd/skills/curated/devtrack-api/references/testing-validation.md +167 -0
- package/.sdd/skills/curated/devtrack-api/references/typeorm-infrastructure.md +334 -0
- package/LICENSE +21 -0
- package/README.md +842 -0
- package/bin/codesdd.js +10 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +560 -0
- package/dist/commands/change.d.ts +35 -0
- package/dist/commands/change.js +296 -0
- package/dist/commands/completion.d.ts +72 -0
- package/dist/commands/completion.js +258 -0
- package/dist/commands/config.d.ts +36 -0
- package/dist/commands/config.js +552 -0
- package/dist/commands/feedback.d.ts +9 -0
- package/dist/commands/feedback.js +184 -0
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/schema.js +870 -0
- package/dist/commands/sdd/execution.d.ts +3 -0
- package/dist/commands/sdd/execution.js +409 -0
- package/dist/commands/sdd/shared.d.ts +9 -0
- package/dist/commands/sdd/shared.js +84 -0
- package/dist/commands/sdd/skills.d.ts +3 -0
- package/dist/commands/sdd/skills.js +154 -0
- package/dist/commands/sdd.d.ts +3 -0
- package/dist/commands/sdd.js +769 -0
- package/dist/commands/show.d.ts +14 -0
- package/dist/commands/show.js +133 -0
- package/dist/commands/spec.d.ts +15 -0
- package/dist/commands/spec.js +228 -0
- package/dist/commands/validate.d.ts +24 -0
- package/dist/commands/validate.js +295 -0
- package/dist/commands/workflow/index.d.ts +17 -0
- package/dist/commands/workflow/index.js +12 -0
- package/dist/commands/workflow/instructions.d.ts +29 -0
- package/dist/commands/workflow/instructions.js +383 -0
- package/dist/commands/workflow/new-change.d.ts +11 -0
- package/dist/commands/workflow/new-change.js +45 -0
- package/dist/commands/workflow/schemas.d.ts +10 -0
- package/dist/commands/workflow/schemas.js +34 -0
- package/dist/commands/workflow/shared.d.ts +57 -0
- package/dist/commands/workflow/shared.js +117 -0
- package/dist/commands/workflow/status.d.ts +14 -0
- package/dist/commands/workflow/status.js +76 -0
- package/dist/commands/workflow/templates.d.ts +16 -0
- package/dist/commands/workflow/templates.js +68 -0
- package/dist/core/archive.d.ts +16 -0
- package/dist/core/archive.js +487 -0
- package/dist/core/artifact-graph/graph.d.ts +56 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +7 -0
- package/dist/core/artifact-graph/index.js +13 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +143 -0
- package/dist/core/artifact-graph/instruction-loader.js +215 -0
- package/dist/core/artifact-graph/resolver.d.ts +81 -0
- package/dist/core/artifact-graph/resolver.js +258 -0
- package/dist/core/artifact-graph/schema.d.ts +13 -0
- package/dist/core/artifact-graph/schema.js +108 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +54 -0
- package/dist/core/artifact-graph/types.d.ts +45 -0
- package/dist/core/artifact-graph/types.js +43 -0
- package/dist/core/available-tools.d.ts +16 -0
- package/dist/core/available-tools.js +30 -0
- package/dist/core/branding.d.ts +8 -0
- package/dist/core/branding.js +12 -0
- package/dist/core/cli/command-matrix.d.ts +23 -0
- package/dist/core/cli/command-matrix.js +123 -0
- package/dist/core/command-generation/adapters/amazon-q.d.ts +13 -0
- package/dist/core/command-generation/adapters/amazon-q.js +26 -0
- package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
- package/dist/core/command-generation/adapters/antigravity.js +26 -0
- package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
- package/dist/core/command-generation/adapters/auggie.js +27 -0
- package/dist/core/command-generation/adapters/claude.d.ts +13 -0
- package/dist/core/command-generation/adapters/claude.js +50 -0
- package/dist/core/command-generation/adapters/cline.d.ts +14 -0
- package/dist/core/command-generation/adapters/cline.js +27 -0
- package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
- package/dist/core/command-generation/adapters/codebuddy.js +28 -0
- package/dist/core/command-generation/adapters/codex.d.ts +16 -0
- package/dist/core/command-generation/adapters/codex.js +39 -0
- package/dist/core/command-generation/adapters/continue.d.ts +13 -0
- package/dist/core/command-generation/adapters/continue.js +28 -0
- package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
- package/dist/core/command-generation/adapters/costrict.js +27 -0
- package/dist/core/command-generation/adapters/crush.d.ts +13 -0
- package/dist/core/command-generation/adapters/crush.js +30 -0
- package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
- package/dist/core/command-generation/adapters/cursor.js +44 -0
- package/dist/core/command-generation/adapters/factory.d.ts +13 -0
- package/dist/core/command-generation/adapters/factory.js +27 -0
- package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
- package/dist/core/command-generation/adapters/gemini.js +26 -0
- package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
- package/dist/core/command-generation/adapters/github-copilot.js +26 -0
- package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
- package/dist/core/command-generation/adapters/iflow.js +29 -0
- package/dist/core/command-generation/adapters/index.d.ts +29 -0
- package/dist/core/command-generation/adapters/index.js +29 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/kilocode.js +23 -0
- package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
- package/dist/core/command-generation/adapters/kiro.js +26 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +13 -0
- package/dist/core/command-generation/adapters/opencode.js +29 -0
- package/dist/core/command-generation/adapters/pi.d.ts +14 -0
- package/dist/core/command-generation/adapters/pi.js +41 -0
- package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
- package/dist/core/command-generation/adapters/qoder.js +30 -0
- package/dist/core/command-generation/adapters/qwen.d.ts +13 -0
- package/dist/core/command-generation/adapters/qwen.js +26 -0
- package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/roocode.js +27 -0
- package/dist/core/command-generation/adapters/windsurf.d.ts +14 -0
- package/dist/core/command-generation/adapters/windsurf.js +51 -0
- package/dist/core/command-generation/generator.d.ts +21 -0
- package/dist/core/command-generation/generator.js +27 -0
- package/dist/core/command-generation/index.d.ts +22 -0
- package/dist/core/command-generation/index.js +24 -0
- package/dist/core/command-generation/registry.d.ts +36 -0
- package/dist/core/command-generation/registry.js +92 -0
- package/dist/core/command-generation/types.d.ts +56 -0
- package/dist/core/command-generation/types.js +8 -0
- package/dist/core/completions/command-registry.d.ts +7 -0
- package/dist/core/completions/command-registry.js +461 -0
- package/dist/core/completions/completion-provider.d.ts +60 -0
- package/dist/core/completions/completion-provider.js +102 -0
- package/dist/core/completions/factory.d.ts +64 -0
- package/dist/core/completions/factory.js +75 -0
- package/dist/core/completions/generators/bash-generator.d.ts +32 -0
- package/dist/core/completions/generators/bash-generator.js +174 -0
- package/dist/core/completions/generators/fish-generator.d.ts +32 -0
- package/dist/core/completions/generators/fish-generator.js +157 -0
- package/dist/core/completions/generators/powershell-generator.d.ts +33 -0
- package/dist/core/completions/generators/powershell-generator.js +207 -0
- package/dist/core/completions/generators/zsh-generator.d.ts +44 -0
- package/dist/core/completions/generators/zsh-generator.js +250 -0
- package/dist/core/completions/installers/bash-installer.d.ts +87 -0
- package/dist/core/completions/installers/bash-installer.js +318 -0
- package/dist/core/completions/installers/fish-installer.d.ts +43 -0
- package/dist/core/completions/installers/fish-installer.js +143 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +88 -0
- package/dist/core/completions/installers/powershell-installer.js +327 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +125 -0
- package/dist/core/completions/installers/zsh-installer.js +452 -0
- package/dist/core/completions/templates/bash-templates.d.ts +6 -0
- package/dist/core/completions/templates/bash-templates.js +24 -0
- package/dist/core/completions/templates/fish-templates.d.ts +7 -0
- package/dist/core/completions/templates/fish-templates.js +39 -0
- package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
- package/dist/core/completions/templates/powershell-templates.js +25 -0
- package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
- package/dist/core/completions/templates/zsh-templates.js +36 -0
- package/dist/core/completions/types.d.ts +79 -0
- package/dist/core/completions/types.js +2 -0
- package/dist/core/config-prompts.d.ts +9 -0
- package/dist/core/config-prompts.js +34 -0
- package/dist/core/config-schema.d.ts +86 -0
- package/dist/core/config-schema.js +213 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.js +33 -0
- package/dist/core/converters/json-converter.d.ts +6 -0
- package/dist/core/converters/json-converter.js +51 -0
- package/dist/core/global-config.d.ts +44 -0
- package/dist/core/global-config.js +125 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -0
- package/dist/core/init.d.ts +36 -0
- package/dist/core/init.js +576 -0
- package/dist/core/legacy-cleanup.d.ts +162 -0
- package/dist/core/legacy-cleanup.js +512 -0
- package/dist/core/list.d.ts +9 -0
- package/dist/core/list.js +173 -0
- package/dist/core/migration.d.ts +23 -0
- package/dist/core/migration.js +108 -0
- package/dist/core/parsers/change-parser.d.ts +13 -0
- package/dist/core/parsers/change-parser.js +193 -0
- package/dist/core/parsers/markdown-parser.d.ts +22 -0
- package/dist/core/parsers/markdown-parser.js +187 -0
- package/dist/core/parsers/requirement-blocks.d.ts +37 -0
- package/dist/core/parsers/requirement-blocks.js +201 -0
- package/dist/core/profile-sync-drift.d.ts +38 -0
- package/dist/core/profile-sync-drift.js +201 -0
- package/dist/core/profiles.d.ts +26 -0
- package/dist/core/profiles.js +41 -0
- package/dist/core/project-config.d.ts +64 -0
- package/dist/core/project-config.js +223 -0
- package/dist/core/schemas/base.schema.d.ts +13 -0
- package/dist/core/schemas/base.schema.js +13 -0
- package/dist/core/schemas/change.schema.d.ts +73 -0
- package/dist/core/schemas/change.schema.js +31 -0
- package/dist/core/schemas/index.d.ts +4 -0
- package/dist/core/schemas/index.js +4 -0
- package/dist/core/schemas/spec.schema.d.ts +18 -0
- package/dist/core/schemas/spec.schema.js +15 -0
- package/dist/core/sdd/adr-policy.d.ts +7 -0
- package/dist/core/sdd/adr-policy.js +47 -0
- package/dist/core/sdd/adr.d.ts +4 -0
- package/dist/core/sdd/adr.js +27 -0
- package/dist/core/sdd/bootstrap.d.ts +28 -0
- package/dist/core/sdd/bootstrap.js +353 -0
- package/dist/core/sdd/check.d.ts +51 -0
- package/dist/core/sdd/check.js +831 -0
- package/dist/core/sdd/coordination/coordination-adapters.d.ts +73 -0
- package/dist/core/sdd/coordination/coordination-adapters.js +87 -0
- package/dist/core/sdd/coordination/index.d.ts +2 -0
- package/dist/core/sdd/coordination/index.js +2 -0
- package/dist/core/sdd/dedup.d.ts +23 -0
- package/dist/core/sdd/dedup.js +62 -0
- package/dist/core/sdd/default-bootstrap-files.d.ts +23 -0
- package/dist/core/sdd/default-bootstrap-files.js +385 -0
- package/dist/core/sdd/default-skills.d.ts +16 -0
- package/dist/core/sdd/default-skills.js +427 -0
- package/dist/core/sdd/diagnose.d.ts +25 -0
- package/dist/core/sdd/diagnose.js +1312 -0
- package/dist/core/sdd/docs-sync.d.ts +21 -0
- package/dist/core/sdd/docs-sync.js +231 -0
- package/dist/core/sdd/domain/helpers.d.ts +6 -0
- package/dist/core/sdd/domain/helpers.js +37 -0
- package/dist/core/sdd/domain/lifecycle-guardrails.d.ts +22 -0
- package/dist/core/sdd/domain/lifecycle-guardrails.js +31 -0
- package/dist/core/sdd/domain/lifecycle-hooks.d.ts +16 -0
- package/dist/core/sdd/domain/lifecycle-hooks.js +27 -0
- package/dist/core/sdd/domain/post-active-validation.d.ts +15 -0
- package/dist/core/sdd/domain/post-active-validation.js +71 -0
- package/dist/core/sdd/domain/traceability.d.ts +8 -0
- package/dist/core/sdd/domain/traceability.js +83 -0
- package/dist/core/sdd/domain/transition-engine.d.ts +49 -0
- package/dist/core/sdd/domain/transition-engine.js +120 -0
- package/dist/core/sdd/fingerprint.d.ts +23 -0
- package/dist/core/sdd/fingerprint.js +146 -0
- package/dist/core/sdd/import-openspec.d.ts +31 -0
- package/dist/core/sdd/import-openspec.js +232 -0
- package/dist/core/sdd/init.d.ts +36 -0
- package/dist/core/sdd/init.js +65 -0
- package/dist/core/sdd/json-schema.d.ts +6 -0
- package/dist/core/sdd/json-schema.js +59 -0
- package/dist/core/sdd/legacy-operations.d.ts +286 -0
- package/dist/core/sdd/legacy-operations.js +2175 -0
- package/dist/core/sdd/lenses.d.ts +14 -0
- package/dist/core/sdd/lenses.js +97 -0
- package/dist/core/sdd/merge-catalog.d.ts +9 -0
- package/dist/core/sdd/merge-catalog.js +70 -0
- package/dist/core/sdd/migrate-workspace.d.ts +36 -0
- package/dist/core/sdd/migrate-workspace.js +344 -0
- package/dist/core/sdd/migrate.d.ts +24 -0
- package/dist/core/sdd/migrate.js +385 -0
- package/dist/core/sdd/resolve-project-root.d.ts +15 -0
- package/dist/core/sdd/resolve-project-root.js +46 -0
- package/dist/core/sdd/root-resolver.d.ts +16 -0
- package/dist/core/sdd/root-resolver.js +62 -0
- package/dist/core/sdd/sanitize.d.ts +35 -0
- package/dist/core/sdd/sanitize.js +750 -0
- package/dist/core/sdd/services/approve.service.d.ts +20 -0
- package/dist/core/sdd/services/approve.service.js +82 -0
- package/dist/core/sdd/services/audit.service.d.ts +53 -0
- package/dist/core/sdd/services/audit.service.js +136 -0
- package/dist/core/sdd/services/breakdown.service.d.ts +35 -0
- package/dist/core/sdd/services/breakdown.service.js +185 -0
- package/dist/core/sdd/services/context.service.d.ts +346 -0
- package/dist/core/sdd/services/context.service.js +278 -0
- package/dist/core/sdd/services/debate.service.d.ts +16 -0
- package/dist/core/sdd/services/debate.service.js +73 -0
- package/dist/core/sdd/services/decide.service.d.ts +23 -0
- package/dist/core/sdd/services/decide.service.js +81 -0
- package/dist/core/sdd/services/dedup-apply.service.d.ts +39 -0
- package/dist/core/sdd/services/dedup-apply.service.js +259 -0
- package/dist/core/sdd/services/feature-lint.service.d.ts +29 -0
- package/dist/core/sdd/services/feature-lint.service.js +146 -0
- package/dist/core/sdd/services/finalize.service.d.ts +33 -0
- package/dist/core/sdd/services/finalize.service.js +707 -0
- package/dist/core/sdd/services/frontend-gap.service.d.ts +23 -0
- package/dist/core/sdd/services/frontend-gap.service.js +117 -0
- package/dist/core/sdd/services/frontend-impact.service.d.ts +19 -0
- package/dist/core/sdd/services/frontend-impact.service.js +46 -0
- package/dist/core/sdd/services/ingest-deposito.service.d.ts +32 -0
- package/dist/core/sdd/services/ingest-deposito.service.js +231 -0
- package/dist/core/sdd/services/insight.service.d.ts +21 -0
- package/dist/core/sdd/services/insight.service.js +81 -0
- package/dist/core/sdd/services/legacy-capability.service.d.ts +24 -0
- package/dist/core/sdd/services/legacy-capability.service.js +59 -0
- package/dist/core/sdd/services/mcp-runtime.service.d.ts +42 -0
- package/dist/core/sdd/services/mcp-runtime.service.js +144 -0
- package/dist/core/sdd/services/metrics.service.d.ts +49 -0
- package/dist/core/sdd/services/metrics.service.js +181 -0
- package/dist/core/sdd/services/next.service.d.ts +35 -0
- package/dist/core/sdd/services/next.service.js +54 -0
- package/dist/core/sdd/services/onboard.service.d.ts +9 -0
- package/dist/core/sdd/services/onboard.service.js +165 -0
- package/dist/core/sdd/services/rebuild.service.d.ts +31 -0
- package/dist/core/sdd/services/rebuild.service.js +482 -0
- package/dist/core/sdd/services/scan-naming.service.d.ts +43 -0
- package/dist/core/sdd/services/scan-naming.service.js +246 -0
- package/dist/core/sdd/services/skills-invoke.service.d.ts +24 -0
- package/dist/core/sdd/services/skills-invoke.service.js +63 -0
- package/dist/core/sdd/services/skills-sync.service.d.ts +15 -0
- package/dist/core/sdd/services/skills-sync.service.js +117 -0
- package/dist/core/sdd/services/start.service.d.ts +26 -0
- package/dist/core/sdd/services/start.service.js +237 -0
- package/dist/core/sdd/skills.d.ts +15 -0
- package/dist/core/sdd/skills.js +46 -0
- package/dist/core/sdd/state-lock.d.ts +19 -0
- package/dist/core/sdd/state-lock.js +144 -0
- package/dist/core/sdd/state.d.ts +155 -0
- package/dist/core/sdd/state.js +1000 -0
- package/dist/core/sdd/store/in-memory-adapter.d.ts +12 -0
- package/dist/core/sdd/store/in-memory-adapter.js +27 -0
- package/dist/core/sdd/store/index.d.ts +5 -0
- package/dist/core/sdd/store/index.js +5 -0
- package/dist/core/sdd/store/sdd-stores.d.ts +25 -0
- package/dist/core/sdd/store/sdd-stores.js +59 -0
- package/dist/core/sdd/store/state-store.d.ts +32 -0
- package/dist/core/sdd/store/state-store.js +2 -0
- package/dist/core/sdd/store/yaml-file-adapter.d.ts +12 -0
- package/dist/core/sdd/store/yaml-file-adapter.js +43 -0
- package/dist/core/sdd/structural-health.d.ts +557 -0
- package/dist/core/sdd/structural-health.js +187 -0
- package/dist/core/sdd/transaction.d.ts +14 -0
- package/dist/core/sdd/transaction.js +100 -0
- package/dist/core/sdd/types.d.ts +1570 -0
- package/dist/core/sdd/types.js +617 -0
- package/dist/core/sdd/views.d.ts +3 -0
- package/dist/core/sdd/views.js +560 -0
- package/dist/core/sdd/workspace-schemas.d.ts +620 -0
- package/dist/core/sdd/workspace-schemas.js +254 -0
- package/dist/core/sdd/write-manifest.d.ts +25 -0
- package/dist/core/sdd/write-manifest.js +353 -0
- package/dist/core/shared/index.d.ts +8 -0
- package/dist/core/shared/index.js +8 -0
- package/dist/core/shared/skill-generation.d.ts +49 -0
- package/dist/core/shared/skill-generation.js +106 -0
- package/dist/core/shared/tool-detection.d.ts +71 -0
- package/dist/core/shared/tool-detection.js +158 -0
- package/dist/core/specs-apply.d.ts +73 -0
- package/dist/core/specs-apply.js +385 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/index.d.ts +8 -0
- package/dist/core/templates/index.js +9 -0
- package/dist/core/templates/skill-templates.d.ts +20 -0
- package/dist/core/templates/skill-templates.js +19 -0
- package/dist/core/templates/types.d.ts +19 -0
- package/dist/core/templates/types.js +5 -0
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +308 -0
- package/dist/core/templates/workflows/archive-change.d.ts +10 -0
- package/dist/core/templates/workflows/archive-change.js +277 -0
- package/dist/core/templates/workflows/bulk-archive-change.d.ts +10 -0
- package/dist/core/templates/workflows/bulk-archive-change.js +502 -0
- package/dist/core/templates/workflows/continue-change.d.ts +10 -0
- package/dist/core/templates/workflows/continue-change.js +232 -0
- package/dist/core/templates/workflows/explore.d.ts +10 -0
- package/dist/core/templates/workflows/explore.js +475 -0
- package/dist/core/templates/workflows/feedback.d.ts +9 -0
- package/dist/core/templates/workflows/feedback.js +108 -0
- package/dist/core/templates/workflows/ff-change.d.ts +10 -0
- package/dist/core/templates/workflows/ff-change.js +206 -0
- package/dist/core/templates/workflows/new-change.d.ts +10 -0
- package/dist/core/templates/workflows/new-change.js +151 -0
- package/dist/core/templates/workflows/onboard.d.ts +10 -0
- package/dist/core/templates/workflows/onboard.js +573 -0
- package/dist/core/templates/workflows/propose.d.ts +10 -0
- package/dist/core/templates/workflows/propose.js +224 -0
- package/dist/core/templates/workflows/sdd.d.ts +10 -0
- package/dist/core/templates/workflows/sdd.js +107 -0
- package/dist/core/templates/workflows/sync-specs.d.ts +10 -0
- package/dist/core/templates/workflows/sync-specs.js +286 -0
- package/dist/core/templates/workflows/verify-change.d.ts +10 -0
- package/dist/core/templates/workflows/verify-change.js +346 -0
- package/dist/core/update.d.ts +77 -0
- package/dist/core/update.js +538 -0
- package/dist/core/validation/constants.d.ts +34 -0
- package/dist/core/validation/constants.js +40 -0
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.js +2 -0
- package/dist/core/validation/validator.d.ts +33 -0
- package/dist/core/validation/validator.js +409 -0
- package/dist/core/view.d.ts +8 -0
- package/dist/core/view.js +170 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/prompts/searchable-multi-select.d.ts +28 -0
- package/dist/prompts/searchable-multi-select.js +159 -0
- package/dist/telemetry/config.d.ts +32 -0
- package/dist/telemetry/config.js +68 -0
- package/dist/telemetry/index.d.ts +44 -0
- package/dist/telemetry/index.js +207 -0
- package/dist/ui/ascii-patterns.d.ts +16 -0
- package/dist/ui/ascii-patterns.js +133 -0
- package/dist/ui/welcome-screen.d.ts +10 -0
- package/dist/ui/welcome-screen.js +146 -0
- package/dist/utils/change-metadata.d.ts +51 -0
- package/dist/utils/change-metadata.js +147 -0
- package/dist/utils/change-utils.d.ts +62 -0
- package/dist/utils/change-utils.js +121 -0
- package/dist/utils/command-references.d.ts +18 -0
- package/dist/utils/command-references.js +20 -0
- package/dist/utils/file-system.d.ts +36 -0
- package/dist/utils/file-system.js +281 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/interactive.d.ts +18 -0
- package/dist/utils/interactive.js +21 -0
- package/dist/utils/item-discovery.d.ts +4 -0
- package/dist/utils/item-discovery.js +73 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/openspec-compat.d.ts +2 -0
- package/dist/utils/openspec-compat.js +2 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +41 -0
- package/dist/utils/task-progress.d.ts +8 -0
- package/dist/utils/task-progress.js +36 -0
- package/package.json +111 -0
- package/schemas/sdd/1-spec.schema.json +221 -0
- package/schemas/sdd/2-plan.schema.json +199 -0
- package/schemas/sdd/3-tasks.schema.json +102 -0
- package/schemas/sdd/4-changelog.schema.json +55 -0
- package/schemas/sdd/5-quality.schema.json +427 -0
- package/schemas/sdd/workspace-catalog.schema.json +1012 -0
- package/schemas/spec-driven/schema.yaml +153 -0
- package/schemas/spec-driven/templates/design.md +19 -0
- package/schemas/spec-driven/templates/proposal.md +23 -0
- package/schemas/spec-driven/templates/spec.md +8 -0
- package/schemas/spec-driven/templates/tasks.md +9 -0
|
@@ -0,0 +1,2175 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { SddWriteTransaction } from './write-manifest.js';
|
|
4
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import * as yaml from 'yaml';
|
|
8
|
+
import { CLI_NAME } from '../branding.js';
|
|
9
|
+
import { LENSES, validateDocumentAgainstLens } from './lenses.js';
|
|
10
|
+
import { TransitionEngine } from './domain/transition-engine.js';
|
|
11
|
+
import { resolveOpenSpecSubpath } from './services/legacy-capability.service.js';
|
|
12
|
+
import { allocateEntityId, loadProjectSddConfig, loadStateSnapshot, nowIso, resolveSddPaths, } from './state.js';
|
|
13
|
+
import { BUILT_IN_SDD_SKILLS } from './default-skills.js';
|
|
14
|
+
import { renderViews } from './views.js';
|
|
15
|
+
import { normalizeSemanticText } from './dedup.js';
|
|
16
|
+
import { stringifyWorkspaceYamlDocument } from './workspace-schemas.js';
|
|
17
|
+
export const execFileAsync = promisify(execFile);
|
|
18
|
+
export const RADAR_TO_DISCOVERY_STATUS = {
|
|
19
|
+
READY: 'READY',
|
|
20
|
+
PLANNED: 'PLANNED',
|
|
21
|
+
SPLIT: 'SPLIT',
|
|
22
|
+
IN_PROGRESS: 'IN_PROGRESS',
|
|
23
|
+
DONE: 'DONE',
|
|
24
|
+
CANCELLED: 'CANCELLED',
|
|
25
|
+
};
|
|
26
|
+
export function slugify(value) {
|
|
27
|
+
return value
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.normalize('NFD')
|
|
30
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
31
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
32
|
+
.replace(/^-+|-+$/g, '')
|
|
33
|
+
.replace(/-{2,}/g, '-');
|
|
34
|
+
}
|
|
35
|
+
export function stripFunctionalTitlePrefixes(value) {
|
|
36
|
+
return value
|
|
37
|
+
.replace(/^\s*debate:\s*/i, '')
|
|
38
|
+
.replace(/^\s*insight:\s*/i, '')
|
|
39
|
+
.trim();
|
|
40
|
+
}
|
|
41
|
+
export function computeCanonicalTitle(value) {
|
|
42
|
+
const normalized = stripFunctionalTitlePrefixes(value)
|
|
43
|
+
.replace(/\s+/g, ' ')
|
|
44
|
+
.trim();
|
|
45
|
+
const fallback = normalized || 'Untitled canonical item';
|
|
46
|
+
return fallback.slice(0, 60).trim();
|
|
47
|
+
}
|
|
48
|
+
export function ensureMemoryInitialized(paths) {
|
|
49
|
+
return fs.access(paths.memoryRoot).catch(() => {
|
|
50
|
+
throw new Error(`Directory ${paths.memoryRoot} not found. Run "${CLI_NAME} sdd init".`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function findDiscoveryRecord(records, id) {
|
|
54
|
+
return records.find((record) => record.id === id);
|
|
55
|
+
}
|
|
56
|
+
export function markdownDebateTemplate(insight, debateId) {
|
|
57
|
+
const now = nowIso();
|
|
58
|
+
return `# Debate ${debateId}
|
|
59
|
+
|
|
60
|
+
## 1) Decision Question (Required)
|
|
61
|
+
Decide ____ instead of ____ to solve ____.
|
|
62
|
+
|
|
63
|
+
## 2) Decision Criteria (Required)
|
|
64
|
+
- User impact
|
|
65
|
+
- Implementation complexity
|
|
66
|
+
- Technical risk
|
|
67
|
+
- Operational cost
|
|
68
|
+
- Delivery time
|
|
69
|
+
|
|
70
|
+
## 3) Options Considered (Minimum 2)
|
|
71
|
+
### Option A
|
|
72
|
+
- Proposal:
|
|
73
|
+
- Pros:
|
|
74
|
+
- Cons:
|
|
75
|
+
|
|
76
|
+
### Option B
|
|
77
|
+
- Proposal:
|
|
78
|
+
- Pros:
|
|
79
|
+
- Cons:
|
|
80
|
+
|
|
81
|
+
### Option C (Optional)
|
|
82
|
+
- Proposal:
|
|
83
|
+
- Pros:
|
|
84
|
+
- Cons:
|
|
85
|
+
|
|
86
|
+
## 4) Evidence-Based Argument Round
|
|
87
|
+
### Agent A (Defends A)
|
|
88
|
+
- Argument:
|
|
89
|
+
- Evidence:
|
|
90
|
+
|
|
91
|
+
### Agent B (Defends B)
|
|
92
|
+
- Argument:
|
|
93
|
+
- Evidence:
|
|
94
|
+
|
|
95
|
+
## 5) Cross-Critique Round
|
|
96
|
+
### A Critiques B
|
|
97
|
+
- Concrete risks:
|
|
98
|
+
|
|
99
|
+
### B Critiques A
|
|
100
|
+
- Concrete risks:
|
|
101
|
+
|
|
102
|
+
## 6) Scoring Matrix (0-5)
|
|
103
|
+
| Criterion | Weight | A | B | C |
|
|
104
|
+
| --- | --- | --- | --- | --- |
|
|
105
|
+
| User impact | 3 | | | |
|
|
106
|
+
| Implementation complexity | 2 | | | |
|
|
107
|
+
| Technical risk | 3 | | | |
|
|
108
|
+
| Operational cost | 2 | | | |
|
|
109
|
+
| Delivery time | 2 | | | |
|
|
110
|
+
|
|
111
|
+
## 7) Mediator Decision (Required)
|
|
112
|
+
- Choice (A/B/C):
|
|
113
|
+
- Rationale:
|
|
114
|
+
- Accepted risks:
|
|
115
|
+
- Reversal conditions:
|
|
116
|
+
|
|
117
|
+
## 8) Quality Contract (Required)
|
|
118
|
+
- Touched scope target: 95% unit coverage and 95% integration/e2e coverage when applicable.
|
|
119
|
+
- Required evidence:
|
|
120
|
+
- Unit coverage:
|
|
121
|
+
- Integration/e2e or equivalent:
|
|
122
|
+
- Acceptance and risk matrix:
|
|
123
|
+
- Exceptions:
|
|
124
|
+
- None, or formal exception with scope, reason, accepted risk, compensating control, review deadline, and approver.
|
|
125
|
+
|
|
126
|
+
## 9) Output
|
|
127
|
+
- APPROVED -> EPIC-####
|
|
128
|
+
- DISCARDED -> Record in discarded
|
|
129
|
+
|
|
130
|
+
## Metadata
|
|
131
|
+
- Source insight: ${insight.id}
|
|
132
|
+
- Insight title: ${insight.title}
|
|
133
|
+
- Created at: ${insight.created_at || now}
|
|
134
|
+
- Debate opened at: ${now}
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
export function markdownInsightTemplate(id, title, text) {
|
|
138
|
+
return `# Insight ${id}
|
|
139
|
+
|
|
140
|
+
## Title
|
|
141
|
+
${title}
|
|
142
|
+
|
|
143
|
+
## Description
|
|
144
|
+
${text}
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
export function markdownRadarTemplate(debate, radarId, rationale) {
|
|
148
|
+
return `# Epic ${radarId}
|
|
149
|
+
|
|
150
|
+
## Origin
|
|
151
|
+
- Debate: ${debate.id}
|
|
152
|
+
- Base title: ${debate.title}
|
|
153
|
+
|
|
154
|
+
## Approved Summary
|
|
155
|
+
${rationale || 'Approved summary pending explicit expansion from the source debate.'}
|
|
156
|
+
|
|
157
|
+
## Quality Contract
|
|
158
|
+
- Default scope: touched_scope.
|
|
159
|
+
- Unit target: 95%.
|
|
160
|
+
- Integration/e2e target: 95% when applicable.
|
|
161
|
+
- Evidence mode: hybrid coverage plus equivalence matrix.
|
|
162
|
+
- Exceptions require scope, reason, accepted risk, compensating control, review deadline, and approver.
|
|
163
|
+
|
|
164
|
+
## Status
|
|
165
|
+
READY
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
export function markdownRadarFromDepositoTemplate(radarId, title, sourceCount, rationale) {
|
|
169
|
+
return `# Epic ${radarId}
|
|
170
|
+
|
|
171
|
+
## Origin
|
|
172
|
+
- Origin: source intake
|
|
173
|
+
- Indexed sources: ${sourceCount}
|
|
174
|
+
|
|
175
|
+
## Approved Summary
|
|
176
|
+
${rationale || 'Initial planning generated from source inputs.'}
|
|
177
|
+
|
|
178
|
+
## Quality Contract
|
|
179
|
+
- Default scope: touched_scope.
|
|
180
|
+
- Unit target: 95%.
|
|
181
|
+
- Integration/e2e target: 95% when applicable.
|
|
182
|
+
- Evidence mode: hybrid coverage plus equivalence matrix.
|
|
183
|
+
- Exceptions require scope, reason, accepted risk, compensating control, review deadline, and approver.
|
|
184
|
+
|
|
185
|
+
## Title
|
|
186
|
+
${title}
|
|
187
|
+
|
|
188
|
+
## Status
|
|
189
|
+
READY
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
export function markdownDiscardTemplate(debate, rationale) {
|
|
193
|
+
return `# Discarded ${debate.id}
|
|
194
|
+
|
|
195
|
+
## Origin
|
|
196
|
+
- Debate: ${debate.id}
|
|
197
|
+
- Title: ${debate.title}
|
|
198
|
+
|
|
199
|
+
## Discard Reason
|
|
200
|
+
${rationale || '(reason not provided)'}
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
export const DEFAULT_META_EVOLUTION_CONFIG = {
|
|
204
|
+
enabled: true,
|
|
205
|
+
audit_interval_days: 180,
|
|
206
|
+
placeholder_markers: ['(fill in', '(placeholder', 'TODO:', 'TBD:'],
|
|
207
|
+
health_alert_threshold: 75,
|
|
208
|
+
};
|
|
209
|
+
export function buildDefaultQualityContract(options) {
|
|
210
|
+
return {
|
|
211
|
+
version: 1,
|
|
212
|
+
enabled: true,
|
|
213
|
+
inherited_from: options?.inheritedFrom || '',
|
|
214
|
+
scope: options?.scope || 'touched_scope',
|
|
215
|
+
unit_target_percent: 95,
|
|
216
|
+
integration_target_percent: 95,
|
|
217
|
+
evidence_mode: options?.evidenceMode || 'hybrid',
|
|
218
|
+
enforcement: options?.enforcement || 'blocking',
|
|
219
|
+
stack_profile: options?.stackProfile || 'default',
|
|
220
|
+
required_axes: [
|
|
221
|
+
'requirements',
|
|
222
|
+
'critical_flows',
|
|
223
|
+
'contracts',
|
|
224
|
+
'acceptance_criteria',
|
|
225
|
+
'risks',
|
|
226
|
+
],
|
|
227
|
+
required_evidence: [
|
|
228
|
+
{
|
|
229
|
+
kind: 'unit_coverage',
|
|
230
|
+
required: true,
|
|
231
|
+
status: 'planned',
|
|
232
|
+
command: 'project-specific unit coverage command',
|
|
233
|
+
artifact: 'coverage report for touched scope',
|
|
234
|
+
notes: 'Default target is 95% unless a formal exception is recorded.',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
kind: 'integration_or_e2e',
|
|
238
|
+
required: true,
|
|
239
|
+
status: 'planned',
|
|
240
|
+
command: 'project-specific integration/e2e command or equivalent matrix',
|
|
241
|
+
artifact: 'integration/e2e result or equivalence matrix',
|
|
242
|
+
notes: 'Required for applicable critical flows in the touched scope.',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
kind: 'acceptance_risk_matrix',
|
|
246
|
+
required: true,
|
|
247
|
+
status: 'planned',
|
|
248
|
+
command: 'manual review recorded in quality.md',
|
|
249
|
+
artifact: 'requirements, acceptance criteria, and risk traceability matrix',
|
|
250
|
+
notes: 'Used when numeric coverage cannot express the main risk.',
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
exceptions: [],
|
|
254
|
+
updated_at: nowIso(),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
export function ensureFeatureQualityContract(feature, inheritedFrom) {
|
|
258
|
+
if (!feature.quality_contract) {
|
|
259
|
+
feature.quality_contract = buildDefaultQualityContract({
|
|
260
|
+
inheritedFrom: inheritedFrom || feature.origin_ref || feature.id,
|
|
261
|
+
});
|
|
262
|
+
return feature.quality_contract;
|
|
263
|
+
}
|
|
264
|
+
feature.quality_contract.required_axes = Array.from(new Set([
|
|
265
|
+
...((feature.quality_contract.required_axes || []).filter(Boolean)),
|
|
266
|
+
'requirements',
|
|
267
|
+
'critical_flows',
|
|
268
|
+
'contracts',
|
|
269
|
+
'acceptance_criteria',
|
|
270
|
+
'risks',
|
|
271
|
+
]));
|
|
272
|
+
if (feature.quality_contract.required_evidence.length === 0) {
|
|
273
|
+
feature.quality_contract.required_evidence = buildDefaultQualityContract({
|
|
274
|
+
inheritedFrom: inheritedFrom || feature.quality_contract.inherited_from || feature.origin_ref || feature.id,
|
|
275
|
+
}).required_evidence;
|
|
276
|
+
}
|
|
277
|
+
if (feature.quality_contract.enforcement === 'advisory' && feature.quality_contract.exceptions.length === 0) {
|
|
278
|
+
feature.quality_contract.enforcement = 'blocking';
|
|
279
|
+
}
|
|
280
|
+
feature.quality_contract.updated_at = feature.quality_contract.updated_at || nowIso();
|
|
281
|
+
return feature.quality_contract;
|
|
282
|
+
}
|
|
283
|
+
export const FORCED_TRANSITION_MARKERS = [
|
|
284
|
+
'--force-transition',
|
|
285
|
+
'forced_transition',
|
|
286
|
+
'transição forçada',
|
|
287
|
+
'transicao forcada',
|
|
288
|
+
];
|
|
289
|
+
export function normalizePercent(part, total) {
|
|
290
|
+
if (total <= 0)
|
|
291
|
+
return 100;
|
|
292
|
+
return Math.round((part / total) * 10000) / 100;
|
|
293
|
+
}
|
|
294
|
+
export async function readMetaEvolutionConfig(paths) {
|
|
295
|
+
try {
|
|
296
|
+
const raw = yaml.parse(await fs.readFile(paths.configFile, 'utf-8'));
|
|
297
|
+
const candidate = raw && typeof raw === 'object' && raw.meta_evolution && typeof raw.meta_evolution === 'object'
|
|
298
|
+
? raw.meta_evolution
|
|
299
|
+
: {};
|
|
300
|
+
const placeholderMarkers = Array.isArray(candidate.placeholder_markers)
|
|
301
|
+
? candidate.placeholder_markers
|
|
302
|
+
.map((value) => String(value).trim().toLowerCase())
|
|
303
|
+
.filter((value) => value.length > 0)
|
|
304
|
+
: DEFAULT_META_EVOLUTION_CONFIG.placeholder_markers;
|
|
305
|
+
return {
|
|
306
|
+
enabled: typeof candidate.enabled === 'boolean'
|
|
307
|
+
? candidate.enabled
|
|
308
|
+
: DEFAULT_META_EVOLUTION_CONFIG.enabled,
|
|
309
|
+
audit_interval_days: typeof candidate.audit_interval_days === 'number' &&
|
|
310
|
+
Number.isFinite(candidate.audit_interval_days) &&
|
|
311
|
+
candidate.audit_interval_days > 0
|
|
312
|
+
? candidate.audit_interval_days
|
|
313
|
+
: DEFAULT_META_EVOLUTION_CONFIG.audit_interval_days,
|
|
314
|
+
placeholder_markers: placeholderMarkers.length > 0
|
|
315
|
+
? placeholderMarkers
|
|
316
|
+
: DEFAULT_META_EVOLUTION_CONFIG.placeholder_markers,
|
|
317
|
+
health_alert_threshold: typeof candidate.health_alert_threshold === 'number' &&
|
|
318
|
+
Number.isFinite(candidate.health_alert_threshold) &&
|
|
319
|
+
candidate.health_alert_threshold >= 0 &&
|
|
320
|
+
candidate.health_alert_threshold <= 100
|
|
321
|
+
? candidate.health_alert_threshold
|
|
322
|
+
: DEFAULT_META_EVOLUTION_CONFIG.health_alert_threshold,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return { ...DEFAULT_META_EVOLUTION_CONFIG };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
export async function listFilesRecursive(rootDir) {
|
|
330
|
+
const files = [];
|
|
331
|
+
async function walk(currentDir) {
|
|
332
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true }).catch(() => []);
|
|
333
|
+
for (const entry of entries) {
|
|
334
|
+
const full = path.join(currentDir, entry.name);
|
|
335
|
+
if (entry.isDirectory()) {
|
|
336
|
+
await walk(full);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
files.push(full);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
await walk(rootDir);
|
|
344
|
+
return files;
|
|
345
|
+
}
|
|
346
|
+
export function hasPlaceholder(content, markers) {
|
|
347
|
+
const normalized = content.toLowerCase();
|
|
348
|
+
return markers.some((marker) => {
|
|
349
|
+
const normalizedMarker = marker.toLowerCase().trim();
|
|
350
|
+
if (!normalizedMarker)
|
|
351
|
+
return false;
|
|
352
|
+
if (/^[a-z0-9_-]+$/i.test(normalizedMarker)) {
|
|
353
|
+
const escaped = normalizedMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
354
|
+
return new RegExp(`\\b${escaped}\\b`, 'i').test(content);
|
|
355
|
+
}
|
|
356
|
+
return normalized.includes(normalizedMarker);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
export async function collectAuditArtifacts(paths) {
|
|
360
|
+
const roots = [paths.activeDir, paths.archivedDir, paths.discoveryDir, path.join(paths.coreDir, 'adrs')];
|
|
361
|
+
const artifactFiles = [];
|
|
362
|
+
for (const root of roots) {
|
|
363
|
+
const files = await listFilesRecursive(root);
|
|
364
|
+
for (const filePath of files) {
|
|
365
|
+
if (filePath.endsWith('.md') || filePath.endsWith('.yaml'))
|
|
366
|
+
artifactFiles.push(filePath);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return artifactFiles;
|
|
370
|
+
}
|
|
371
|
+
export function debateHasRealDeliberation(content) {
|
|
372
|
+
const lower = content.toLowerCase();
|
|
373
|
+
const sectionIndexCandidates = [
|
|
374
|
+
lower.indexOf('## 7) decisao do mediador'),
|
|
375
|
+
lower.indexOf('## 7) decisão do mediador'),
|
|
376
|
+
lower.indexOf('## 7) mediator decision'),
|
|
377
|
+
].filter((index) => index >= 0);
|
|
378
|
+
const sectionIndex = sectionIndexCandidates.length > 0 ? Math.min(...sectionIndexCandidates) : -1;
|
|
379
|
+
if (sectionIndex < 0)
|
|
380
|
+
return false;
|
|
381
|
+
const tail = content.slice(sectionIndex);
|
|
382
|
+
const choiceMatch = tail.match(/- Escolha \(A\/B\/C\):\s*(.+)/i) ||
|
|
383
|
+
tail.match(/- \*\*Escolha \(A\/B\/C\):\*\*\s*(.+)/i) ||
|
|
384
|
+
tail.match(/- Escolha:\s*(?:op(?:ç|c)[aã]o\s*)?([ABC])\b/i) ||
|
|
385
|
+
tail.match(/- Op(?:ç|c)[aã]o escolhida:\s*([ABC])\b/i) ||
|
|
386
|
+
tail.match(/- Choice \(A\/B\/C\):\s*(.+)/i) ||
|
|
387
|
+
tail.match(/- \*\*Choice \(A\/B\/C\):\*\*\s*(.+)/i) ||
|
|
388
|
+
tail.match(/- Choice:\s*(?:option\s*)?([ABC])\b/i) ||
|
|
389
|
+
tail.match(/- Selected option:\s*([ABC])\b/i);
|
|
390
|
+
const rationaleMatch = tail.match(/- Justificativa:\s*(.+)/i) ||
|
|
391
|
+
tail.match(/- \*\*Justificativa:\*\*\s*([\s\S]+?)(?:\n\s*-\s|\n##|\n---|$)/i) ||
|
|
392
|
+
tail.match(/- Rationale:\s*(.+)/i) ||
|
|
393
|
+
tail.match(/- \*\*Rationale:\*\*\s*([\s\S]+?)(?:\n\s*-\s|\n##|\n---|$)/i);
|
|
394
|
+
const approvedOutcome = /\bAPPROVED\b|aprovad[ao]/i.test(tail);
|
|
395
|
+
if ((!choiceMatch && !approvedOutcome) || !rationaleMatch)
|
|
396
|
+
return false;
|
|
397
|
+
const choice = (choiceMatch?.[1] || 'approved').trim().toLowerCase();
|
|
398
|
+
const rationale = rationaleMatch[1].trim().toLowerCase();
|
|
399
|
+
if (!choice || !rationale)
|
|
400
|
+
return false;
|
|
401
|
+
const invalidTokens = ['(preencher', '(fill in', '____', '-'];
|
|
402
|
+
const isInvalid = (value, token) => value === token || (token !== '-' && value.includes(token));
|
|
403
|
+
const choiceInvalid = invalidTokens.some((token) => isInvalid(choice, token));
|
|
404
|
+
const rationaleInvalid = invalidTokens.some((token) => isInvalid(rationale, token));
|
|
405
|
+
return !choiceInvalid && !rationaleInvalid;
|
|
406
|
+
}
|
|
407
|
+
export async function collectForcedTransitions(paths) {
|
|
408
|
+
const roots = [paths.activeDir, paths.archivedDir];
|
|
409
|
+
let count = 0;
|
|
410
|
+
const featureRefs = new Set();
|
|
411
|
+
for (const root of roots) {
|
|
412
|
+
const files = await listFilesRecursive(root);
|
|
413
|
+
for (const filePath of files) {
|
|
414
|
+
if (!/4-(changelog|historico)\.(md|yaml)$/i.test(filePath))
|
|
415
|
+
continue;
|
|
416
|
+
const content = (await fs.readFile(filePath, 'utf-8').catch(() => '')).toLowerCase();
|
|
417
|
+
if (FORCED_TRANSITION_MARKERS.some((marker) => content.includes(marker))) {
|
|
418
|
+
count += 1;
|
|
419
|
+
const match = filePath.match(/FEAT-\d{3,}/);
|
|
420
|
+
if (match?.[0])
|
|
421
|
+
featureRefs.add(match[0]);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { total: count, featureRefs: Array.from(featureRefs).sort() };
|
|
426
|
+
}
|
|
427
|
+
export function sourceTypeFromRelativePath(relativePath) {
|
|
428
|
+
const normalized = relativePath.replace(/\\/g, '/').toLowerCase();
|
|
429
|
+
if (normalized.includes('/prds/'))
|
|
430
|
+
return 'prd';
|
|
431
|
+
if (normalized.includes('/rfcs/'))
|
|
432
|
+
return 'rfc';
|
|
433
|
+
if (normalized.includes('/briefings/'))
|
|
434
|
+
return 'briefing';
|
|
435
|
+
if (normalized.includes('/historias/'))
|
|
436
|
+
return 'historia';
|
|
437
|
+
if (normalized.includes('/wireframes/'))
|
|
438
|
+
return 'wireframe';
|
|
439
|
+
if (normalized.includes('/html-mocks/'))
|
|
440
|
+
return 'html_mock';
|
|
441
|
+
if (normalized.includes('/referencias-visuais/'))
|
|
442
|
+
return 'referencia_visual';
|
|
443
|
+
if (normalized.includes('/entrevistas/'))
|
|
444
|
+
return 'entrevista';
|
|
445
|
+
if (normalized.includes('/anexos/'))
|
|
446
|
+
return 'anexo';
|
|
447
|
+
if (normalized.includes('/legado/'))
|
|
448
|
+
return 'legado';
|
|
449
|
+
return 'outro';
|
|
450
|
+
}
|
|
451
|
+
export function defaultConsolidationTargets(type) {
|
|
452
|
+
switch (type) {
|
|
453
|
+
case 'prd':
|
|
454
|
+
case 'briefing':
|
|
455
|
+
case 'historia':
|
|
456
|
+
return ['contexto', 'epic', 'backlog'];
|
|
457
|
+
case 'wireframe':
|
|
458
|
+
case 'html_mock':
|
|
459
|
+
case 'referencia_visual':
|
|
460
|
+
return ['frontend-map', 'frontend-gaps', 'frontend-decisions', 'backlog'];
|
|
461
|
+
case 'rfc':
|
|
462
|
+
return ['arquitetura', 'servicos', 'integration-contracts', 'backlog'];
|
|
463
|
+
case 'legado':
|
|
464
|
+
return ['repo-map', 'arquitetura', 'backlog'];
|
|
465
|
+
case 'entrevista':
|
|
466
|
+
return ['insights', 'epic'];
|
|
467
|
+
default:
|
|
468
|
+
return ['contexto'];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
export function deriveInitialFeatureTitles(sources, frontendEnabled) {
|
|
472
|
+
const types = new Set(sources.map((source) => source.type));
|
|
473
|
+
const titles = [];
|
|
474
|
+
if (types.has('prd') || types.has('historia') || types.has('briefing') || types.has('rfc')) {
|
|
475
|
+
titles.push('Initial business core and primary contracts');
|
|
476
|
+
}
|
|
477
|
+
if (frontendEnabled &&
|
|
478
|
+
(types.has('wireframe') || types.has('html_mock') || types.has('referencia_visual'))) {
|
|
479
|
+
titles.push('Initial frontend structure based on references');
|
|
480
|
+
}
|
|
481
|
+
if (types.has('legado')) {
|
|
482
|
+
titles.push('Legacy mapping and adaptation to current architecture');
|
|
483
|
+
}
|
|
484
|
+
titles.push('Consolidate operational documentation and handoff trail');
|
|
485
|
+
return Array.from(new Set(titles));
|
|
486
|
+
}
|
|
487
|
+
export async function listFilesRecursively(rootDir) {
|
|
488
|
+
const entries = await fs.readdir(rootDir, { withFileTypes: true }).catch(() => []);
|
|
489
|
+
const files = [];
|
|
490
|
+
for (const entry of entries) {
|
|
491
|
+
const absolute = path.join(rootDir, entry.name);
|
|
492
|
+
if (entry.isDirectory()) {
|
|
493
|
+
files.push(...(await listFilesRecursively(absolute)));
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (!entry.isFile())
|
|
497
|
+
continue;
|
|
498
|
+
files.push(absolute);
|
|
499
|
+
}
|
|
500
|
+
return files;
|
|
501
|
+
}
|
|
502
|
+
export function sourceTitleFromPath(filePath) {
|
|
503
|
+
const base = path.basename(filePath, path.extname(filePath));
|
|
504
|
+
const cleaned = base.replace(/[-_]+/g, ' ').trim();
|
|
505
|
+
if (!cleaned)
|
|
506
|
+
return base || 'fonte sem titulo';
|
|
507
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
508
|
+
}
|
|
509
|
+
export function normalizeSourceStatus(current, desired) {
|
|
510
|
+
const order = {
|
|
511
|
+
RAW: 0,
|
|
512
|
+
INDEXED: 1,
|
|
513
|
+
NORMALIZED: 2,
|
|
514
|
+
PLANNED: 3,
|
|
515
|
+
ARCHIVED: 4,
|
|
516
|
+
};
|
|
517
|
+
return order[current] >= order[desired] ? current : desired;
|
|
518
|
+
}
|
|
519
|
+
export function nextSourceId(existingIds) {
|
|
520
|
+
let max = 0;
|
|
521
|
+
for (const id of existingIds) {
|
|
522
|
+
const match = /^SRC-(\d+)$/.exec(id);
|
|
523
|
+
if (!match)
|
|
524
|
+
continue;
|
|
525
|
+
const numeric = Number(match[1]);
|
|
526
|
+
if (Number.isFinite(numeric)) {
|
|
527
|
+
max = Math.max(max, numeric);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return `SRC-${String(max + 1).padStart(4, '0')}`;
|
|
531
|
+
}
|
|
532
|
+
export async function extractSourceSummary(filePath) {
|
|
533
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
534
|
+
const textLike = new Set([
|
|
535
|
+
'.md',
|
|
536
|
+
'.txt',
|
|
537
|
+
'.rst',
|
|
538
|
+
'.adoc',
|
|
539
|
+
'.json',
|
|
540
|
+
'.yaml',
|
|
541
|
+
'.yml',
|
|
542
|
+
'.html',
|
|
543
|
+
'.htm',
|
|
544
|
+
'.csv',
|
|
545
|
+
]);
|
|
546
|
+
if (!textLike.has(ext)) {
|
|
547
|
+
return '';
|
|
548
|
+
}
|
|
549
|
+
const raw = await fs.readFile(filePath, 'utf-8').catch(() => '');
|
|
550
|
+
if (!raw.trim())
|
|
551
|
+
return '';
|
|
552
|
+
const lines = raw
|
|
553
|
+
.split(/\r?\n/)
|
|
554
|
+
.map((line) => line.replace(/^#+\s*/, '').replace(/<[^>]+>/g, '').trim())
|
|
555
|
+
.filter((line) => line.length > 0);
|
|
556
|
+
const candidate = lines.find((line) => line.length > 20) || lines[0] || '';
|
|
557
|
+
return candidate.slice(0, 220);
|
|
558
|
+
}
|
|
559
|
+
export async function findDebateFile(paths, debateId) {
|
|
560
|
+
const debateDir = paths.discoveryDebatesDir;
|
|
561
|
+
const entries = await fs.readdir(debateDir, { withFileTypes: true }).catch(() => []);
|
|
562
|
+
const found = entries.find((entry) => entry.isFile() && entry.name.startsWith(`${debateId}-`));
|
|
563
|
+
if (!found)
|
|
564
|
+
return null;
|
|
565
|
+
return path.join(debateDir, found.name);
|
|
566
|
+
}
|
|
567
|
+
export function validateDebateDocument(content) {
|
|
568
|
+
const missing = validateDocumentAgainstLens(content, LENSES.debate);
|
|
569
|
+
if (content.includes('Decidir ____ em vez de ____ para resolver ____.') ||
|
|
570
|
+
content.includes('Decide ____ instead of ____ to solve ____.')) {
|
|
571
|
+
missing.push('Fill the decision question with real context');
|
|
572
|
+
}
|
|
573
|
+
if (/\- (?:Escolha|Choice) \(A\/B\/C\):\s*$/m.test(content)) {
|
|
574
|
+
missing.push('Inform the mediator choice in "Choice (A/B/C)"');
|
|
575
|
+
}
|
|
576
|
+
if (/\- (?:Justificativa|Rationale):\s*$/m.test(content)) {
|
|
577
|
+
missing.push('Inform the mediator decision rationale');
|
|
578
|
+
}
|
|
579
|
+
if (/\- Unit coverage:\s*$/m.test(content)) {
|
|
580
|
+
missing.push('Inform required unit coverage evidence in the quality contract');
|
|
581
|
+
}
|
|
582
|
+
if (/\- Integration\/e2e or equivalent:\s*$/m.test(content)) {
|
|
583
|
+
missing.push('Inform integration/e2e or equivalence evidence in the quality contract');
|
|
584
|
+
}
|
|
585
|
+
if (/\- Acceptance and risk matrix:\s*$/m.test(content)) {
|
|
586
|
+
missing.push('Inform acceptance and risk matrix evidence in the quality contract');
|
|
587
|
+
}
|
|
588
|
+
if (/\- Exceptions:\s*\n\s+- None, or formal exception/m.test(content)) {
|
|
589
|
+
missing.push('Confirm there is no quality exception or document the formal exception');
|
|
590
|
+
}
|
|
591
|
+
return missing;
|
|
592
|
+
}
|
|
593
|
+
export function pickTopSkills(catalogSkills, ids, max = 3) {
|
|
594
|
+
if (!ids || ids.length === 0)
|
|
595
|
+
return [];
|
|
596
|
+
const known = new Set(catalogSkills.map((skill) => skill.id));
|
|
597
|
+
return ids.filter((id) => known.has(id)).slice(0, max);
|
|
598
|
+
}
|
|
599
|
+
export function bundlesForSkills(catalog, skillIds) {
|
|
600
|
+
return Array.from(new Set(catalog.skills
|
|
601
|
+
.filter((skill) => skillIds.includes(skill.id))
|
|
602
|
+
.flatMap((skill) => skill.bundle_ids)));
|
|
603
|
+
}
|
|
604
|
+
export function syncCounterFromId(discoveryIndex, id) {
|
|
605
|
+
const [prefix, numeric] = id.split('-');
|
|
606
|
+
if (!prefix || !numeric)
|
|
607
|
+
return;
|
|
608
|
+
if (!['INS', 'DEB', 'RAD', 'EPIC', 'FEAT', 'FGAP', 'TD'].includes(prefix))
|
|
609
|
+
return;
|
|
610
|
+
const value = Number(numeric);
|
|
611
|
+
if (!Number.isFinite(value))
|
|
612
|
+
return;
|
|
613
|
+
const counterKey = prefix;
|
|
614
|
+
discoveryIndex.counters[counterKey] = Math.max(discoveryIndex.counters[counterKey] || 0, value);
|
|
615
|
+
}
|
|
616
|
+
export async function getRuntime(projectRoot) {
|
|
617
|
+
const { resolveProjectRoot } = await import('./resolve-project-root.js');
|
|
618
|
+
const resolved = resolveProjectRoot(projectRoot === '.' ? undefined : projectRoot);
|
|
619
|
+
const config = await loadProjectSddConfig(resolved);
|
|
620
|
+
const paths = resolveSddPaths(resolved, config);
|
|
621
|
+
await ensureMemoryInitialized(paths);
|
|
622
|
+
return { config, paths };
|
|
623
|
+
}
|
|
624
|
+
export async function persistAndRender(paths, config, render) {
|
|
625
|
+
if (!render && render !== undefined)
|
|
626
|
+
return;
|
|
627
|
+
if (!config.views.autoRender && render === undefined)
|
|
628
|
+
return;
|
|
629
|
+
const snapshot = await loadStateSnapshot(paths, config);
|
|
630
|
+
await renderViews(paths, config, snapshot);
|
|
631
|
+
}
|
|
632
|
+
export function relProjectPath(paths, absolutePath) {
|
|
633
|
+
return path.relative(paths.projectRoot, absolutePath).replace(/\\/g, '/');
|
|
634
|
+
}
|
|
635
|
+
export function coreDocRef(paths, name) {
|
|
636
|
+
return relProjectPath(paths, path.join(paths.coreDir, name));
|
|
637
|
+
}
|
|
638
|
+
export function planningDocRef(paths, name) {
|
|
639
|
+
return relProjectPath(paths, path.join(paths.pendenciasDir, name));
|
|
640
|
+
}
|
|
641
|
+
export function activeFeatureRef(paths, featureId, fileName) {
|
|
642
|
+
return relProjectPath(paths, path.join(paths.activeDir, featureId, fileName));
|
|
643
|
+
}
|
|
644
|
+
export function featureActiveDir(paths, featureId) {
|
|
645
|
+
return path.join(paths.activeDir, featureId);
|
|
646
|
+
}
|
|
647
|
+
export function featurePlannedDir(paths, featureId) {
|
|
648
|
+
return path.join(paths.plannedDir, featureId);
|
|
649
|
+
}
|
|
650
|
+
export function activeDocNamesForLayout(config) {
|
|
651
|
+
return {
|
|
652
|
+
spec: '1-spec.yaml',
|
|
653
|
+
plan: '2-plan.yaml',
|
|
654
|
+
quality: '5-quality.yaml',
|
|
655
|
+
tasks: '3-tasks.yaml',
|
|
656
|
+
changelog: '4-changelog.yaml',
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
export function activeDocCandidateNames(config) {
|
|
660
|
+
const preferred = activeDocNamesForLayout(config);
|
|
661
|
+
const canonical = ['1-spec.yaml', '2-plan.yaml', '5-quality.yaml', '3-tasks.yaml', '4-changelog.yaml'];
|
|
662
|
+
const english = ['1-spec.md', '2-plan.md', 'quality.md', '3-tasks.md', '4-changelog.md'];
|
|
663
|
+
const portuguese = ['1-especificacao.md', '2-planejamento.md', 'quality.md', '3-tarefas.md', '4-historico.md'];
|
|
664
|
+
return Array.from(new Set([preferred.spec, preferred.plan, preferred.quality, preferred.tasks, preferred.changelog, ...canonical, ...english, ...portuguese]));
|
|
665
|
+
}
|
|
666
|
+
export async function resolveActiveDocRefs(paths, featureId, config) {
|
|
667
|
+
const activePath = featureActiveDir(paths, featureId);
|
|
668
|
+
const names = activeDocCandidateNames(config);
|
|
669
|
+
const refs = [];
|
|
670
|
+
for (const name of names) {
|
|
671
|
+
const filePath = path.join(activePath, name);
|
|
672
|
+
if (await pathExists(filePath)) {
|
|
673
|
+
refs.push(activeFeatureRef(paths, featureId, name));
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return refs;
|
|
677
|
+
}
|
|
678
|
+
function workspaceYamlDoc(fileName, value) {
|
|
679
|
+
return stringifyWorkspaceYamlDocument(fileName, value);
|
|
680
|
+
}
|
|
681
|
+
function ensureMinLength(value, fallback, minLength) {
|
|
682
|
+
const text = (value || fallback).trim();
|
|
683
|
+
if (text.length >= minLength) {
|
|
684
|
+
return text;
|
|
685
|
+
}
|
|
686
|
+
return `${text} ${fallback}`.trim().padEnd(minLength, '.');
|
|
687
|
+
}
|
|
688
|
+
function workspaceOriginType(feature) {
|
|
689
|
+
if (feature.origin_type === 'frontend_gap')
|
|
690
|
+
return 'fgap';
|
|
691
|
+
if (feature.origin_type === 'tech_debt')
|
|
692
|
+
return 'td';
|
|
693
|
+
if (feature.origin_type === 'fast_track')
|
|
694
|
+
return 'direct';
|
|
695
|
+
if (feature.origin_type === 'radar')
|
|
696
|
+
return 'radar';
|
|
697
|
+
if (feature.origin_type === 'epic')
|
|
698
|
+
return 'epic';
|
|
699
|
+
return 'direct';
|
|
700
|
+
}
|
|
701
|
+
function isPrivacyComplianceFeature(feature) {
|
|
702
|
+
if (feature.origin_ref === 'EPIC-0020' || feature.origin_ref === 'EPIC-0021')
|
|
703
|
+
return true;
|
|
704
|
+
const refs = new Set(feature.acceptance_refs || []);
|
|
705
|
+
return refs.has('EPIC-0020') || refs.has('EPIC-0021') || refs.has('DEB-0021') || refs.has('INS-0021');
|
|
706
|
+
}
|
|
707
|
+
export function stackContextTokens(snapshot) {
|
|
708
|
+
return (snapshot.techStack?.items || [])
|
|
709
|
+
.flatMap((item) => [item.layer, item.technology])
|
|
710
|
+
.map((value) => value.trim())
|
|
711
|
+
.filter(Boolean);
|
|
712
|
+
}
|
|
713
|
+
function featureTokenText(feature) {
|
|
714
|
+
return [
|
|
715
|
+
feature.title,
|
|
716
|
+
feature.summary || '',
|
|
717
|
+
feature.execution_kind,
|
|
718
|
+
feature.planning_mode,
|
|
719
|
+
...feature.touches,
|
|
720
|
+
...feature.lock_domains,
|
|
721
|
+
...feature.produces,
|
|
722
|
+
...feature.consumes,
|
|
723
|
+
...feature.recommended_skills,
|
|
724
|
+
...(feature.quality_contract?.required_axes || []),
|
|
725
|
+
feature.quality_contract?.stack_profile || '',
|
|
726
|
+
]
|
|
727
|
+
.join(' ')
|
|
728
|
+
.toLowerCase();
|
|
729
|
+
}
|
|
730
|
+
function workspaceExecutionProfileForFeature(feature) {
|
|
731
|
+
const tokens = featureTokenText(feature);
|
|
732
|
+
const isDevTrackTypescript = includesAny(tokens, ['devtrack', 'nestjs', 'nest.js', 'nest js', 'typeorm']) ||
|
|
733
|
+
feature.quality_contract?.stack_profile === 'typescript_node';
|
|
734
|
+
const hasPythonAgenticStack = feature.quality_contract?.stack_profile === 'python_agentic_backend' ||
|
|
735
|
+
includesAny(tokens, ['api-clean-flask-langgraph', 'python', 'flask', 'langgraph', 'langchain']);
|
|
736
|
+
const hasApiSurface = includesAny(tokens, ['/api', 'api/', 'endpoint']);
|
|
737
|
+
const hasAgentApiSurface = hasApiSurface && includesAny(tokens, ['agent', 'agente']);
|
|
738
|
+
const isPythonAgentic = !isDevTrackTypescript &&
|
|
739
|
+
(hasPythonAgenticStack || hasAgentApiSurface);
|
|
740
|
+
if (isPythonAgentic) {
|
|
741
|
+
return {
|
|
742
|
+
id: 'python_agentic_backend',
|
|
743
|
+
label: 'Python Flask/LangGraph backend workflow',
|
|
744
|
+
description: 'Python/Flask/LangGraph API routes, application services, adapters, and tests',
|
|
745
|
+
implementationFiles: ['app/', 'src/', 'tests/', 'pyproject.toml'],
|
|
746
|
+
unitCommand: 'python -m pytest tests -q',
|
|
747
|
+
integrationCommand: 'python -m pytest tests -q',
|
|
748
|
+
buildCommand: 'python -m compileall .',
|
|
749
|
+
buildExpected: 'Python sources compile and pytest validation passes for the touched scope.',
|
|
750
|
+
architectureTreeAscii: `project/\n app/\n api/\n application/\n domain/\n infrastructure/\n tests/\n .sdd/\n active/${feature.id}/`,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
if (includesAny(tokens, ['frontend', 'ui', 'route']) && !includesAny(tokens, ['/api', 'api/'])) {
|
|
754
|
+
return {
|
|
755
|
+
id: 'frontend_typescript',
|
|
756
|
+
label: 'frontend workflow',
|
|
757
|
+
description: 'frontend routes, UI coverage, and OpenSDD frontend guardrails',
|
|
758
|
+
implementationFiles: ['src/', 'app/', 'pages/', 'components/', 'tests/'],
|
|
759
|
+
unitCommand: 'pnpm test -- test/core/sdd-operations.test.ts',
|
|
760
|
+
integrationCommand: 'pnpm test -- test/core/sdd-operations.test.ts',
|
|
761
|
+
buildCommand: 'pnpm run build',
|
|
762
|
+
buildExpected: 'TypeScript build completes successfully after the scoped changes.',
|
|
763
|
+
architectureTreeAscii: `project/\n src/\n app/\n components/\n .sdd/\n active/${feature.id}/`,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
id: 'opensdd_typescript',
|
|
768
|
+
label: 'OpenSDD backend workflow',
|
|
769
|
+
description: 'OpenSDD state, command services, and backend validation flows',
|
|
770
|
+
implementationFiles: ['src/core/sdd/legacy-operations.ts'],
|
|
771
|
+
unitCommand: 'pnpm test -- test/core/sdd-operations.test.ts',
|
|
772
|
+
integrationCommand: 'pnpm test -- test/core/sdd-operations.test.ts',
|
|
773
|
+
buildCommand: 'pnpm run build',
|
|
774
|
+
buildExpected: 'TypeScript build completes successfully after the scoped changes.',
|
|
775
|
+
architectureTreeAscii: `project/\n src/\n .sdd/\n active/${feature.id}/`,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
export function buildActiveSpecDoc(feature) {
|
|
779
|
+
const now = nowIso();
|
|
780
|
+
const title = ensureMinLength(feature.title, `Workspace execution for ${feature.id}`, 10);
|
|
781
|
+
const objective = ensureMinLength(feature.summary, `Deliver ${feature.title} with traceability from SDD state to implementation evidence, preserving the planned scope, dependencies, quality gates, and handoff requirements for ${feature.id}.`, 100);
|
|
782
|
+
const baseDocument = {
|
|
783
|
+
schema_version: 1,
|
|
784
|
+
feature_id: feature.id,
|
|
785
|
+
title,
|
|
786
|
+
origin: {
|
|
787
|
+
type: workspaceOriginType(feature),
|
|
788
|
+
ref: feature.origin_ref || '',
|
|
789
|
+
},
|
|
790
|
+
objective,
|
|
791
|
+
expected_outcome: ensureMinLength(`${feature.title} is implemented with acceptance refs, dependencies, and quality evidence visible to the next agent.`, `Feature ${feature.id} reaches a verifiable done state with generated workspace artifacts and canonical SDD state in sync.`, 50),
|
|
792
|
+
system_impact: ensureMinLength('Preserve canonical SDD state, generated views, documentation sync, and handoff context without hidden operational memory.', `System impact for ${feature.id} remains visible in OpenSDD state and workspace artifacts.`, 30),
|
|
793
|
+
acceptance_refs: feature.acceptance_refs.length > 0 ? feature.acceptance_refs : [feature.origin_ref || feature.id],
|
|
794
|
+
gates: feature.gates,
|
|
795
|
+
created_at: now,
|
|
796
|
+
updated_at: now,
|
|
797
|
+
};
|
|
798
|
+
if (isPrivacyComplianceFeature(feature)) {
|
|
799
|
+
baseDocument.compliance_context = {
|
|
800
|
+
jurisdictions: ['BR-LGPD', 'EU-GDPR', 'US-MULTI-STATE'],
|
|
801
|
+
data_classes: ['personal_data', 'sensitive_data', 'children_data', 'online_identifiers'],
|
|
802
|
+
source_refs: ['SRC-0001'],
|
|
803
|
+
legal_review_required: true,
|
|
804
|
+
legal_review_notes: 'Human legal validation is required before final legal interpretation or jurisdiction-specific enforcement decisions.',
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
return workspaceYamlDoc('1-spec.yaml', baseDocument);
|
|
808
|
+
}
|
|
809
|
+
export function buildActivePlanDoc(feature, recommendedBundles) {
|
|
810
|
+
const profile = workspaceExecutionProfileForFeature(feature);
|
|
811
|
+
const affectedContracts = [
|
|
812
|
+
...feature.consumes.map((name) => ({ name, change_type: 'consumes' })),
|
|
813
|
+
...feature.produces.map((name) => ({ name, change_type: 'produces' })),
|
|
814
|
+
];
|
|
815
|
+
const baseDocument = {
|
|
816
|
+
schema_version: 1,
|
|
817
|
+
feature_id: feature.id,
|
|
818
|
+
architectural_impact: {
|
|
819
|
+
description: `Touches ${feature.touches.join(', ') || 'the active SDD workspace'} with lock domains ${feature.lock_domains.join(', ') || 'none declared'}.`,
|
|
820
|
+
affected_modules: feature.touches,
|
|
821
|
+
},
|
|
822
|
+
frontend_impact: {
|
|
823
|
+
status: feature.frontend_impact_status,
|
|
824
|
+
description: feature.frontend_impact_reason || 'Frontend impact must be declared before finalize when applicable.',
|
|
825
|
+
},
|
|
826
|
+
affected_contracts: affectedContracts,
|
|
827
|
+
quality_strategy: {
|
|
828
|
+
unit_target: feature.quality_contract?.unit_target_percent ?? 95,
|
|
829
|
+
integration_target: feature.quality_contract?.integration_target_percent ?? 95,
|
|
830
|
+
approach: feature.quality_contract?.evidence_mode || 'hybrid',
|
|
831
|
+
},
|
|
832
|
+
suggested_skills: Array.from(new Set([...feature.recommended_skills, ...recommendedBundles])),
|
|
833
|
+
skill_conformance: {
|
|
834
|
+
selected_skills: feature.recommended_skills,
|
|
835
|
+
required_skills: feature.recommended_skills,
|
|
836
|
+
detected_conflicts: [],
|
|
837
|
+
architecture_tree_ascii: profile.architectureTreeAscii,
|
|
838
|
+
},
|
|
839
|
+
};
|
|
840
|
+
if (isPrivacyComplianceFeature(feature)) {
|
|
841
|
+
baseDocument.privacy_controls = {
|
|
842
|
+
source_registry_refs: ['SRC-0001'],
|
|
843
|
+
jurisdiction_profiles: ['JP-BR-LGPD', 'JP-EU-GDPR', 'JP-US-PRIVACY-MODEL'],
|
|
844
|
+
control_ids: ['CTRL-TRANSPARENCY-001', 'CTRL-LEGAL-BASIS-001', 'CTRL-SECURITY-001'],
|
|
845
|
+
legal_review_gate: 'required',
|
|
846
|
+
unresolved_legal_questions: [
|
|
847
|
+
'Confirm lawful basis fit for each jurisdiction before production go-live.',
|
|
848
|
+
],
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
return workspaceYamlDoc('2-plan.yaml', baseDocument);
|
|
852
|
+
}
|
|
853
|
+
export function buildActiveQualityDoc(feature) {
|
|
854
|
+
const contract = ensureFeatureQualityContract(feature);
|
|
855
|
+
const profile = workspaceExecutionProfileForFeature(feature);
|
|
856
|
+
const targetedTestCommand = testCommandForFeature(feature);
|
|
857
|
+
const requiredSkillIds = Array.from(new Set(feature.recommended_skills.filter(Boolean)));
|
|
858
|
+
const baseDocument = {
|
|
859
|
+
schema_version: 1,
|
|
860
|
+
feature_id: feature.id,
|
|
861
|
+
coverage_targets: {
|
|
862
|
+
unit: contract.unit_target_percent,
|
|
863
|
+
integration: contract.integration_target_percent,
|
|
864
|
+
},
|
|
865
|
+
validation_strategies: [
|
|
866
|
+
{
|
|
867
|
+
name: 'Targeted tests',
|
|
868
|
+
command: targetedTestCommand,
|
|
869
|
+
expected: 'Changed behavior passes targeted validation for the touched scope.',
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
name: 'Build',
|
|
873
|
+
command: profile.buildCommand,
|
|
874
|
+
expected: profile.buildExpected,
|
|
875
|
+
},
|
|
876
|
+
],
|
|
877
|
+
evidence_log: [],
|
|
878
|
+
skill_evidence: {
|
|
879
|
+
required_skill_ids: requiredSkillIds,
|
|
880
|
+
evidence: [],
|
|
881
|
+
verification_rule: requiredSkillIds.length > 0
|
|
882
|
+
? `Record one evidence entry per required skill before finalize: ${requiredSkillIds.join(', ')}.`
|
|
883
|
+
: 'No skill evidence required for this feature.',
|
|
884
|
+
},
|
|
885
|
+
acceptance_matrix: contract.required_axes.map((axis) => ({
|
|
886
|
+
criterion: axis,
|
|
887
|
+
status: 'not_met',
|
|
888
|
+
evidence: 'Evidence must be recorded before finalize.',
|
|
889
|
+
})),
|
|
890
|
+
exceptions: contract.exceptions.map((exception) => ({
|
|
891
|
+
scope: exception.scope,
|
|
892
|
+
reason: exception.reason,
|
|
893
|
+
accepted_risk: exception.accepted_risk,
|
|
894
|
+
compensating_control: exception.compensating_control,
|
|
895
|
+
})),
|
|
896
|
+
remediation_policy: {
|
|
897
|
+
on_coverage_miss: contract.exceptions.length > 0 ? 'exception' : 'block',
|
|
898
|
+
max_rounds: 3,
|
|
899
|
+
},
|
|
900
|
+
traceability: {
|
|
901
|
+
spec_anchor: {
|
|
902
|
+
spec_updated_at: '',
|
|
903
|
+
changelog_refs: [],
|
|
904
|
+
},
|
|
905
|
+
requirements: [],
|
|
906
|
+
},
|
|
907
|
+
};
|
|
908
|
+
if (isPrivacyComplianceFeature(feature)) {
|
|
909
|
+
baseDocument.security_integrity = {
|
|
910
|
+
endpoint_auth_review: 'pending',
|
|
911
|
+
sensitive_data_exposure_review: 'pending',
|
|
912
|
+
incident_response_review: 'pending',
|
|
913
|
+
notes: 'Record explicit evidence for exposed endpoints, sensitive-data handling, and incident response readiness before finalize.',
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
return workspaceYamlDoc('5-quality.yaml', baseDocument);
|
|
917
|
+
}
|
|
918
|
+
export function buildActiveTasksDoc(feature, paths) {
|
|
919
|
+
const memoryAgentGuide = relProjectPath(paths, path.join(paths.memoryRoot, 'AGENT.md'));
|
|
920
|
+
const coreDocsDir = relProjectPath(paths, paths.coreDir);
|
|
921
|
+
const scope = taskScopeForFeature(feature);
|
|
922
|
+
const touchedFiles = filesForFeatureScope(feature, paths);
|
|
923
|
+
const targetedTestCommand = testCommandForFeature(feature);
|
|
924
|
+
const profile = workspaceExecutionProfileForFeature(feature);
|
|
925
|
+
const skills = feature.recommended_skills.length > 0
|
|
926
|
+
? feature.recommended_skills.join(', ')
|
|
927
|
+
: 'architecture and api-design-principles';
|
|
928
|
+
const summary = feature.summary || `Deliver ${feature.title} for ${scope.description}.`;
|
|
929
|
+
return workspaceYamlDoc('3-tasks.yaml', {
|
|
930
|
+
schema_version: 1,
|
|
931
|
+
feature_id: feature.id,
|
|
932
|
+
tasks: [
|
|
933
|
+
{
|
|
934
|
+
id: `${feature.id}-T1`,
|
|
935
|
+
phase: 'preparation',
|
|
936
|
+
title: `Frame ${feature.title}`,
|
|
937
|
+
description: `Run ${CLI_NAME} sdd context ${feature.id}, confirm ${scope.description}, review ${skills}, and turn the feature summary into concrete acceptance checks: ${summary}`,
|
|
938
|
+
files_touched: ['.sdd/state/backlog.yaml', '.sdd/state/discovery-index.yaml'],
|
|
939
|
+
test_scripts: [],
|
|
940
|
+
acceptance: `The implementation plan names the ${scope.description} files, expected validation commands, and any dependency or lock-domain risk before edits begin.`,
|
|
941
|
+
status: 'pending',
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
id: `${feature.id}-T2`,
|
|
945
|
+
phase: 'implementation',
|
|
946
|
+
title: `Implement ${scope.label}`,
|
|
947
|
+
description: `Apply the ${feature.title} changes in ${touchedFiles.join(', ')} while preserving existing public contracts and SDD state transitions.`,
|
|
948
|
+
files_touched: touchedFiles,
|
|
949
|
+
test_scripts: [],
|
|
950
|
+
acceptance: `The ${scope.label} behavior is implemented, uses existing OpenSDD patterns, and keeps generated workspace/state artifacts valid.`,
|
|
951
|
+
status: 'pending',
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
id: `${feature.id}-T3`,
|
|
955
|
+
phase: 'testing',
|
|
956
|
+
title: `Validate ${scope.label}`,
|
|
957
|
+
description: `Run focused validation for ${scope.description}, then record unit and integration evidence in 5-quality.yaml so coverage targets can be evaluated before finalize.`,
|
|
958
|
+
files_touched: ['.sdd/active/<FEAT-ID>/5-quality.yaml', ...touchedFiles],
|
|
959
|
+
test_scripts: [
|
|
960
|
+
{
|
|
961
|
+
command: targetedTestCommand,
|
|
962
|
+
type: 'unit',
|
|
963
|
+
expected: `Targeted tests for ${scope.description} pass and provide evidence for unit coverage.`,
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
command: profile.buildCommand,
|
|
967
|
+
type: 'build',
|
|
968
|
+
expected: profile.buildExpected,
|
|
969
|
+
},
|
|
970
|
+
],
|
|
971
|
+
acceptance: 'Testing task includes executable commands and produces evidence entries for unit and integration coverage targets.',
|
|
972
|
+
status: 'pending',
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
id: `${feature.id}-T4`,
|
|
976
|
+
phase: 'documentation',
|
|
977
|
+
title: `Document ${scope.label}`,
|
|
978
|
+
description: `Update README.md, ${memoryAgentGuide}, AGENTS.md, AGENT.md, and affected docs under ${coreDocsDir} when ${feature.title} changes workflow, validation, or contributor expectations.`,
|
|
979
|
+
files_touched: ['README.md', memoryAgentGuide, 'AGENTS.md', 'AGENT.md', coreDocsDir],
|
|
980
|
+
test_scripts: [],
|
|
981
|
+
acceptance: `Docs either describe the ${scope.label} behavior and validation loop or record a clear no-doc-impact rationale.`,
|
|
982
|
+
status: 'pending',
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
id: `${feature.id}-T5`,
|
|
986
|
+
phase: 'finalization',
|
|
987
|
+
title: `Finalize ${feature.id}`,
|
|
988
|
+
description: `Declare frontend impact, ensure 5-quality.yaml has evidence meeting coverage targets or a formal exception, then finalize with ${CLI_NAME} sdd finalize --ref ${feature.id}.`,
|
|
989
|
+
files_touched: ['.sdd/active/<FEAT-ID>/5-quality.yaml', '.sdd/state/backlog.yaml', '.sdd/state/transition-log.yaml'],
|
|
990
|
+
test_scripts: [
|
|
991
|
+
{
|
|
992
|
+
command: profile.buildCommand,
|
|
993
|
+
type: 'build',
|
|
994
|
+
expected: profile.buildExpected,
|
|
995
|
+
},
|
|
996
|
+
],
|
|
997
|
+
acceptance: 'Frontend impact is declared, quality coverage targets are met or formally excepted, and SDD finalize succeeds without unresolved re-task rounds.',
|
|
998
|
+
status: 'pending',
|
|
999
|
+
},
|
|
1000
|
+
],
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
function taskScopeForFeature(feature) {
|
|
1004
|
+
const tokens = [
|
|
1005
|
+
...feature.touches,
|
|
1006
|
+
...feature.recommended_skills,
|
|
1007
|
+
feature.title,
|
|
1008
|
+
feature.summary || '',
|
|
1009
|
+
].join(' ').toLowerCase();
|
|
1010
|
+
const profile = workspaceExecutionProfileForFeature(feature);
|
|
1011
|
+
if (profile.id !== 'opensdd_typescript') {
|
|
1012
|
+
return { label: profile.label, description: profile.description };
|
|
1013
|
+
}
|
|
1014
|
+
if (tokens.includes('skill')) {
|
|
1015
|
+
return { label: 'skill distribution workflow', description: 'built-in skills, skill catalog state, and distribution templates' };
|
|
1016
|
+
}
|
|
1017
|
+
if (tokens.includes('schema') || tokens.includes('yaml') || tokens.includes('workspace')) {
|
|
1018
|
+
return { label: 'workspace schema workflow', description: 'workspace YAML documents, schemas, and SDD write validation' };
|
|
1019
|
+
}
|
|
1020
|
+
if (tokens.includes('sdd') || tokens.includes('backend') || tokens.includes('architecture')) {
|
|
1021
|
+
return { label: 'OpenSDD backend workflow', description: 'OpenSDD state, command services, and backend validation flows' };
|
|
1022
|
+
}
|
|
1023
|
+
return { label: 'feature scope', description: feature.touches.join(', ') || 'the declared feature scope' };
|
|
1024
|
+
}
|
|
1025
|
+
function filesForFeatureScope(feature, paths) {
|
|
1026
|
+
const tokens = [
|
|
1027
|
+
...feature.touches,
|
|
1028
|
+
...feature.recommended_skills,
|
|
1029
|
+
feature.title,
|
|
1030
|
+
feature.summary || '',
|
|
1031
|
+
].join(' ').toLowerCase();
|
|
1032
|
+
const profile = workspaceExecutionProfileForFeature(feature);
|
|
1033
|
+
if (profile.id === 'python_agentic_backend') {
|
|
1034
|
+
return [
|
|
1035
|
+
...profile.implementationFiles,
|
|
1036
|
+
'.sdd/active/<FEAT-ID>/3-tasks.yaml',
|
|
1037
|
+
'.sdd/active/<FEAT-ID>/5-quality.yaml',
|
|
1038
|
+
];
|
|
1039
|
+
}
|
|
1040
|
+
const files = new Set();
|
|
1041
|
+
if (tokens.includes('schema') || tokens.includes('yaml') || tokens.includes('workspace')) {
|
|
1042
|
+
files.add('src/core/sdd/workspace-schemas.ts');
|
|
1043
|
+
files.add('src/core/sdd/legacy-operations.ts');
|
|
1044
|
+
files.add('src/core/sdd/write-manifest.ts');
|
|
1045
|
+
}
|
|
1046
|
+
if (tokens.includes('quality') || tokens.includes('finalize') || tokens.includes('task')) {
|
|
1047
|
+
files.add('src/core/sdd/legacy-operations.ts');
|
|
1048
|
+
files.add('src/core/sdd/services/finalize.service.ts');
|
|
1049
|
+
}
|
|
1050
|
+
if (tokens.includes('skill')) {
|
|
1051
|
+
files.add('src/core/sdd/default-skills.ts');
|
|
1052
|
+
files.add(path.relative(paths.projectRoot, paths.skillsDir).replace(/\\/g, '/'));
|
|
1053
|
+
}
|
|
1054
|
+
if (tokens.includes('frontend')) {
|
|
1055
|
+
files.add('src/core/sdd/services/frontend-impact.service.ts');
|
|
1056
|
+
files.add('src/core/sdd/services/frontend-gap.service.ts');
|
|
1057
|
+
}
|
|
1058
|
+
if (files.size === 0) {
|
|
1059
|
+
files.add('src/core/sdd/legacy-operations.ts');
|
|
1060
|
+
}
|
|
1061
|
+
files.add('.sdd/active/<FEAT-ID>/3-tasks.yaml');
|
|
1062
|
+
files.add('.sdd/active/<FEAT-ID>/5-quality.yaml');
|
|
1063
|
+
return Array.from(files);
|
|
1064
|
+
}
|
|
1065
|
+
function testCommandForFeature(feature) {
|
|
1066
|
+
const profile = workspaceExecutionProfileForFeature(feature);
|
|
1067
|
+
if (profile.id === 'python_agentic_backend') {
|
|
1068
|
+
return profile.unitCommand;
|
|
1069
|
+
}
|
|
1070
|
+
const tokens = [
|
|
1071
|
+
...feature.touches,
|
|
1072
|
+
...feature.recommended_skills,
|
|
1073
|
+
feature.title,
|
|
1074
|
+
feature.summary || '',
|
|
1075
|
+
].join(' ').toLowerCase();
|
|
1076
|
+
if (tokens.includes('quality') || tokens.includes('task') || tokens.includes('finalize')) {
|
|
1077
|
+
return 'pnpm test -- test/core/sdd/task-quality-schemas.test.ts';
|
|
1078
|
+
}
|
|
1079
|
+
if (tokens.includes('schema') || tokens.includes('yaml') || tokens.includes('workspace')) {
|
|
1080
|
+
return 'pnpm test -- test/core/sdd/workspace-schemas.test.ts';
|
|
1081
|
+
}
|
|
1082
|
+
if (tokens.includes('frontend')) {
|
|
1083
|
+
return 'pnpm test -- test/core/sdd-operations.test.ts';
|
|
1084
|
+
}
|
|
1085
|
+
if (tokens.includes('skill')) {
|
|
1086
|
+
return 'pnpm test -- test/core/sdd-skills.test.ts';
|
|
1087
|
+
}
|
|
1088
|
+
return 'pnpm test -- test/core/sdd-operations.test.ts';
|
|
1089
|
+
}
|
|
1090
|
+
export function buildActiveChangelogDoc(feature) {
|
|
1091
|
+
return workspaceYamlDoc('4-changelog.yaml', {
|
|
1092
|
+
schema_version: 1,
|
|
1093
|
+
feature_id: feature.id,
|
|
1094
|
+
entries: [
|
|
1095
|
+
{
|
|
1096
|
+
timestamp: nowIso(),
|
|
1097
|
+
action: 'workspace_created',
|
|
1098
|
+
details: 'Workspace created automatically for feature execution.',
|
|
1099
|
+
},
|
|
1100
|
+
],
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
export async function ensureFeaturePlannedWorkspace(paths, config, feature, recommendedBundles) {
|
|
1104
|
+
const plannedPath = featurePlannedDir(paths, feature.id);
|
|
1105
|
+
const names = activeDocNamesForLayout(config);
|
|
1106
|
+
const docs = [
|
|
1107
|
+
path.join(plannedPath, names.spec),
|
|
1108
|
+
path.join(plannedPath, names.plan),
|
|
1109
|
+
path.join(plannedPath, names.quality),
|
|
1110
|
+
path.join(plannedPath, names.tasks),
|
|
1111
|
+
path.join(plannedPath, names.changelog),
|
|
1112
|
+
];
|
|
1113
|
+
ensureFeatureQualityContract(feature);
|
|
1114
|
+
const tx = new SddWriteTransaction();
|
|
1115
|
+
let hasWrites = false;
|
|
1116
|
+
if (!(await pathExists(docs[0]))) {
|
|
1117
|
+
tx.writeFile(docs[0], buildActiveSpecDoc(feature));
|
|
1118
|
+
hasWrites = true;
|
|
1119
|
+
}
|
|
1120
|
+
if (!(await pathExists(docs[1]))) {
|
|
1121
|
+
tx.writeFile(docs[1], buildActivePlanDoc(feature, recommendedBundles));
|
|
1122
|
+
hasWrites = true;
|
|
1123
|
+
}
|
|
1124
|
+
if (!(await pathExists(docs[2]))) {
|
|
1125
|
+
tx.writeFile(docs[2], buildActiveQualityDoc(feature));
|
|
1126
|
+
hasWrites = true;
|
|
1127
|
+
}
|
|
1128
|
+
if (!(await pathExists(docs[3]))) {
|
|
1129
|
+
tx.writeFile(docs[3], buildActiveTasksDoc(feature, paths));
|
|
1130
|
+
hasWrites = true;
|
|
1131
|
+
}
|
|
1132
|
+
if (!(await pathExists(docs[4]))) {
|
|
1133
|
+
tx.writeFile(docs[4], buildActiveChangelogDoc(feature));
|
|
1134
|
+
hasWrites = true;
|
|
1135
|
+
}
|
|
1136
|
+
if (hasWrites) {
|
|
1137
|
+
await tx.commit(paths.projectRoot, paths.memoryRoot, 'ensureFeaturePlannedWorkspace');
|
|
1138
|
+
}
|
|
1139
|
+
const handoffSeedRefs = [feature.id, feature.origin_ref].filter((v) => Boolean(v));
|
|
1140
|
+
return {
|
|
1141
|
+
plannedPath,
|
|
1142
|
+
generatedDocs: docs.map((doc) => path.relative(paths.projectRoot, doc)),
|
|
1143
|
+
handoffSeedRefs,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
export async function ensureFeatureActiveWorkspace(paths, config, feature, recommendedBundles) {
|
|
1147
|
+
const activePath = featureActiveDir(paths, feature.id);
|
|
1148
|
+
const names = activeDocNamesForLayout(config);
|
|
1149
|
+
const docs = [
|
|
1150
|
+
path.join(activePath, names.spec),
|
|
1151
|
+
path.join(activePath, names.plan),
|
|
1152
|
+
path.join(activePath, names.quality),
|
|
1153
|
+
path.join(activePath, names.tasks),
|
|
1154
|
+
path.join(activePath, names.changelog),
|
|
1155
|
+
];
|
|
1156
|
+
ensureFeatureQualityContract(feature);
|
|
1157
|
+
const tx = new SddWriteTransaction();
|
|
1158
|
+
let hasWrites = false;
|
|
1159
|
+
if (!(await pathExists(docs[0]))) {
|
|
1160
|
+
tx.writeFile(docs[0], buildActiveSpecDoc(feature));
|
|
1161
|
+
hasWrites = true;
|
|
1162
|
+
}
|
|
1163
|
+
if (!(await pathExists(docs[1]))) {
|
|
1164
|
+
tx.writeFile(docs[1], buildActivePlanDoc(feature, recommendedBundles));
|
|
1165
|
+
hasWrites = true;
|
|
1166
|
+
}
|
|
1167
|
+
if (!(await pathExists(docs[2]))) {
|
|
1168
|
+
tx.writeFile(docs[2], buildActiveQualityDoc(feature));
|
|
1169
|
+
hasWrites = true;
|
|
1170
|
+
}
|
|
1171
|
+
if (!(await pathExists(docs[3]))) {
|
|
1172
|
+
tx.writeFile(docs[3], buildActiveTasksDoc(feature, paths));
|
|
1173
|
+
hasWrites = true;
|
|
1174
|
+
}
|
|
1175
|
+
if (!(await pathExists(docs[4]))) {
|
|
1176
|
+
tx.writeFile(docs[4], buildActiveChangelogDoc(feature));
|
|
1177
|
+
hasWrites = true;
|
|
1178
|
+
}
|
|
1179
|
+
if (hasWrites) {
|
|
1180
|
+
await tx.commit(paths.projectRoot, paths.memoryRoot, 'ensureFeatureActiveWorkspace');
|
|
1181
|
+
}
|
|
1182
|
+
const handoffSeedRefs = [feature.id, feature.origin_ref].filter((v) => Boolean(v));
|
|
1183
|
+
return {
|
|
1184
|
+
activePath,
|
|
1185
|
+
generatedDocs: docs.map((doc) => path.relative(paths.projectRoot, doc)),
|
|
1186
|
+
handoffSeedRefs,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
export function resolveRadar(record) {
|
|
1190
|
+
if (!record || (record.type !== 'RAD' && record.type !== 'EPIC')) {
|
|
1191
|
+
throw new Error('Referencia EPIC/RAD invalida.');
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
export function resolveFeat(items, id) {
|
|
1195
|
+
const feat = items.find((item) => item.id === id);
|
|
1196
|
+
if (!feat) {
|
|
1197
|
+
throw new Error(`Feature ${id} nao encontrada no backlog.`);
|
|
1198
|
+
}
|
|
1199
|
+
return feat;
|
|
1200
|
+
}
|
|
1201
|
+
export function buildBacklogItem(id, title, originType, originRef, scale, recommendedSkills, options) {
|
|
1202
|
+
const flowMode = options?.flowMode || 'padrao';
|
|
1203
|
+
const profileSeed = [
|
|
1204
|
+
title,
|
|
1205
|
+
...(options?.touches || []),
|
|
1206
|
+
...recommendedSkills,
|
|
1207
|
+
...(options?.produces || []),
|
|
1208
|
+
...(options?.consumes || []),
|
|
1209
|
+
].join(' ').toLowerCase();
|
|
1210
|
+
const profileHasPythonAgenticStack = includesAny(profileSeed, [
|
|
1211
|
+
'api-clean-flask-langgraph',
|
|
1212
|
+
'python',
|
|
1213
|
+
'flask',
|
|
1214
|
+
'langgraph',
|
|
1215
|
+
'langchain',
|
|
1216
|
+
]);
|
|
1217
|
+
const profileHasApiSurface = includesAny(profileSeed, ['/api', 'api/', 'endpoint']);
|
|
1218
|
+
const profileHasAgentApiSurface = profileHasApiSurface && includesAny(profileSeed, ['agent', 'agente']);
|
|
1219
|
+
const stackProfile = (profileHasPythonAgenticStack || profileHasAgentApiSurface)
|
|
1220
|
+
? 'python_agentic_backend'
|
|
1221
|
+
: includesAny(profileSeed, ['typescript', 'nodejs', 'node.js', 'nestjs', 'typeorm', 'devtrack'])
|
|
1222
|
+
? 'typescript_node'
|
|
1223
|
+
: 'default';
|
|
1224
|
+
const gates = flowMode === 'direto'
|
|
1225
|
+
? {
|
|
1226
|
+
proposta: { status: 'nao_exigida', approved_at: '', approved_by: '', note: '' },
|
|
1227
|
+
planejamento: { status: 'nao_exigida', approved_at: '', approved_by: '', note: '' },
|
|
1228
|
+
tarefas: { status: 'rascunho', approved_at: '', approved_by: '', note: '' },
|
|
1229
|
+
}
|
|
1230
|
+
: {
|
|
1231
|
+
proposta: { status: 'rascunho', approved_at: '', approved_by: '', note: '' },
|
|
1232
|
+
planejamento: { status: 'rascunho', approved_at: '', approved_by: '', note: '' },
|
|
1233
|
+
tarefas: { status: 'rascunho', approved_at: '', approved_by: '', note: '' },
|
|
1234
|
+
};
|
|
1235
|
+
return {
|
|
1236
|
+
id,
|
|
1237
|
+
title,
|
|
1238
|
+
status: 'READY',
|
|
1239
|
+
origin_type: originType,
|
|
1240
|
+
origin_ref: originRef,
|
|
1241
|
+
scale,
|
|
1242
|
+
summary: '',
|
|
1243
|
+
blocked_by: [],
|
|
1244
|
+
touches: options?.touches || [],
|
|
1245
|
+
lock_domains: options?.lockDomains || [],
|
|
1246
|
+
parallel_group: options?.parallelGroup || '',
|
|
1247
|
+
execution_kind: options?.executionKind || 'feature',
|
|
1248
|
+
planning_mode: options?.planningMode || 'local_plan',
|
|
1249
|
+
flow_mode: flowMode,
|
|
1250
|
+
current_stage: 'proposta',
|
|
1251
|
+
gates,
|
|
1252
|
+
acceptance_refs: options?.acceptanceRefs || [],
|
|
1253
|
+
produces: options?.produces || [],
|
|
1254
|
+
consumes: options?.consumes || [],
|
|
1255
|
+
priority_score: 0,
|
|
1256
|
+
dependency_count: 0,
|
|
1257
|
+
agent_role: '',
|
|
1258
|
+
recommended_skills: recommendedSkills,
|
|
1259
|
+
change_name: '',
|
|
1260
|
+
branch_name: '',
|
|
1261
|
+
worktree_path: '',
|
|
1262
|
+
start_commit_sha: '',
|
|
1263
|
+
requires_adr: false,
|
|
1264
|
+
quality_contract: buildDefaultQualityContract({
|
|
1265
|
+
inheritedFrom: originRef || originType,
|
|
1266
|
+
scope: 'touched_scope',
|
|
1267
|
+
stackProfile,
|
|
1268
|
+
}),
|
|
1269
|
+
frontend_impact_status: 'unknown',
|
|
1270
|
+
frontend_impact_reason: '',
|
|
1271
|
+
frontend_impact_declared_at: '',
|
|
1272
|
+
frontend_surface_tokens: [],
|
|
1273
|
+
frontend_gap_refs: [],
|
|
1274
|
+
warning_links: [],
|
|
1275
|
+
spec_refs: [],
|
|
1276
|
+
last_sync_at: nowIso(),
|
|
1277
|
+
archived_at: '',
|
|
1278
|
+
done_at: '',
|
|
1279
|
+
unblocked_at: '',
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
export function includesAny(haystack, needles) {
|
|
1283
|
+
return needles.some((needle) => {
|
|
1284
|
+
if (needle.length <= 3) {
|
|
1285
|
+
return new RegExp(`(^|[^a-z0-9])${needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^a-z0-9]|$)`, 'i').test(haystack);
|
|
1286
|
+
}
|
|
1287
|
+
return haystack.includes(needle);
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
export function classifyFeatureShape(title, contextTokens = []) {
|
|
1291
|
+
const titleNormalized = title.toLowerCase();
|
|
1292
|
+
const contextNormalized = contextTokens.join(' ').toLowerCase();
|
|
1293
|
+
const normalized = [titleNormalized, contextNormalized].join(' ').toLowerCase();
|
|
1294
|
+
if (includesAny(normalized, ['migracao', 'migration', 'seed', 'backfill'])) {
|
|
1295
|
+
return {
|
|
1296
|
+
executionKind: 'migration',
|
|
1297
|
+
touches: ['database'],
|
|
1298
|
+
lockDomains: ['schema-change'],
|
|
1299
|
+
produces: ['dados-migrados'],
|
|
1300
|
+
consumes: ['modelo-de-dominio'],
|
|
1301
|
+
planningMode: 'local_plan',
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
const routeTarget = parseRouteToken(title);
|
|
1305
|
+
const isApiRoute = !!routeTarget && routeTarget.startsWith('/api');
|
|
1306
|
+
const contextHasPythonAgenticStack = includesAny(contextNormalized, ['python', 'flask', 'langgraph', 'langchain']);
|
|
1307
|
+
const titleHasExplicitApiSurface = includesAny(titleNormalized, [
|
|
1308
|
+
'/api',
|
|
1309
|
+
'api/',
|
|
1310
|
+
'endpoint',
|
|
1311
|
+
'flask',
|
|
1312
|
+
'python',
|
|
1313
|
+
'langgraph',
|
|
1314
|
+
'langchain',
|
|
1315
|
+
]);
|
|
1316
|
+
const titleHasBackendWork = contextHasPythonAgenticStack && titleNormalized.includes('backend');
|
|
1317
|
+
const titleHasAgentApiWork = contextHasPythonAgenticStack &&
|
|
1318
|
+
includesAny(titleNormalized, ['agent', 'agente']) &&
|
|
1319
|
+
includesAny(titleNormalized, ['api', '/api', 'api/', 'endpoint', 'route', 'rota', 'backend']);
|
|
1320
|
+
const isApiBackend = isApiRoute || titleHasExplicitApiSurface || titleHasBackendWork || titleHasAgentApiWork;
|
|
1321
|
+
if (isApiBackend) {
|
|
1322
|
+
const touches = ['backend', 'api'];
|
|
1323
|
+
const lockDomains = ['api-contract'];
|
|
1324
|
+
const produces = ['api-contract'];
|
|
1325
|
+
if (routeTarget)
|
|
1326
|
+
produces.push(`route:${routeTarget}`);
|
|
1327
|
+
if (includesAny(normalized, ['python']))
|
|
1328
|
+
touches.push('python');
|
|
1329
|
+
if (includesAny(normalized, ['flask']))
|
|
1330
|
+
touches.push('flask');
|
|
1331
|
+
if (includesAny(normalized, ['langgraph', 'langchain'])) {
|
|
1332
|
+
touches.push('langgraph');
|
|
1333
|
+
lockDomains.push('agent-runtime');
|
|
1334
|
+
produces.push('agent-workflow');
|
|
1335
|
+
}
|
|
1336
|
+
if (includesAny(normalized, ['agent', 'agente']))
|
|
1337
|
+
touches.push('agent');
|
|
1338
|
+
if (includesAny(normalized, ['pdf']))
|
|
1339
|
+
produces.push('pdf-report');
|
|
1340
|
+
if (includesAny(normalized, ['i18n', 'internationalization', 'localization', 'copy']))
|
|
1341
|
+
touches.push('i18n');
|
|
1342
|
+
return {
|
|
1343
|
+
executionKind: 'feature',
|
|
1344
|
+
touches: Array.from(new Set(touches)),
|
|
1345
|
+
lockDomains: Array.from(new Set(lockDomains)),
|
|
1346
|
+
produces: Array.from(new Set(produces)),
|
|
1347
|
+
consumes: [],
|
|
1348
|
+
planningMode: 'local_plan',
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
if (includesAny(normalized, ['frontend', 'tela', 'ui', 'pagina', 'rota'])) {
|
|
1352
|
+
return {
|
|
1353
|
+
executionKind: 'frontend_coverage',
|
|
1354
|
+
touches: ['frontend'],
|
|
1355
|
+
lockDomains: ['frontend-route'],
|
|
1356
|
+
produces: ['interface-usuario'],
|
|
1357
|
+
consumes: ['api-ou-contrato'],
|
|
1358
|
+
planningMode: 'local_plan',
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
if (includesAny(normalized, ['infra', 'deploy', 'helm', 'terraform', 'pipeline'])) {
|
|
1362
|
+
return {
|
|
1363
|
+
executionKind: 'infra',
|
|
1364
|
+
touches: ['infra'],
|
|
1365
|
+
lockDomains: ['infra-shared'],
|
|
1366
|
+
produces: ['infra-configurada'],
|
|
1367
|
+
consumes: [],
|
|
1368
|
+
planningMode: 'local_plan',
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
if (includesAny(normalized, ['docs', 'documentacao', 'adr', 'manual'])) {
|
|
1372
|
+
return {
|
|
1373
|
+
executionKind: 'documentation',
|
|
1374
|
+
touches: ['docs'],
|
|
1375
|
+
lockDomains: [],
|
|
1376
|
+
produces: ['documentacao-atualizada'],
|
|
1377
|
+
consumes: ['implementacao-concluida'],
|
|
1378
|
+
planningMode: 'direct_tasks',
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
const authLock = includesAny(normalized, ['auth', 'autoriz', 'permiss'])
|
|
1382
|
+
? ['auth-rules']
|
|
1383
|
+
: [];
|
|
1384
|
+
const backendTouches = ['backend'];
|
|
1385
|
+
if (includesAny(normalized, ['devtrack', 'foundation api', 'foundation-api', 'foundation'])) {
|
|
1386
|
+
backendTouches.push('devtrack');
|
|
1387
|
+
}
|
|
1388
|
+
if (includesAny(normalized, ['nestjs', 'nest.js', 'nest js'])) {
|
|
1389
|
+
backendTouches.push('nestjs');
|
|
1390
|
+
}
|
|
1391
|
+
if (includesAny(normalized, ['typeorm'])) {
|
|
1392
|
+
backendTouches.push('typeorm');
|
|
1393
|
+
}
|
|
1394
|
+
return {
|
|
1395
|
+
executionKind: 'feature',
|
|
1396
|
+
touches: Array.from(new Set(backendTouches)),
|
|
1397
|
+
lockDomains: authLock,
|
|
1398
|
+
produces: ['capacidade-de-negocio'],
|
|
1399
|
+
consumes: [],
|
|
1400
|
+
planningMode: 'local_plan',
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
export function normalizeTitle(value) {
|
|
1404
|
+
return normalizeSemanticText(value).replace(/\s+/g, '-');
|
|
1405
|
+
}
|
|
1406
|
+
export function intersects(a, b) {
|
|
1407
|
+
const setB = new Set(b);
|
|
1408
|
+
return a.some((v) => setB.has(v));
|
|
1409
|
+
}
|
|
1410
|
+
export function unresolvedDependencies(item, all) {
|
|
1411
|
+
const byId = new Map(all.map((entry) => [entry.id, entry]));
|
|
1412
|
+
return item.blocked_by.filter((depId) => {
|
|
1413
|
+
const dep = byId.get(depId);
|
|
1414
|
+
return !dep || dep.status !== 'DONE';
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
export function lockConflictWithActive(item, all) {
|
|
1418
|
+
if (item.lock_domains.length === 0)
|
|
1419
|
+
return [];
|
|
1420
|
+
const conflicts = [];
|
|
1421
|
+
for (const other of all) {
|
|
1422
|
+
if (other.id === item.id)
|
|
1423
|
+
continue;
|
|
1424
|
+
if (other.status !== 'IN_PROGRESS')
|
|
1425
|
+
continue;
|
|
1426
|
+
if (intersects(item.lock_domains, other.lock_domains)) {
|
|
1427
|
+
conflicts.push(other.id);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return conflicts;
|
|
1431
|
+
}
|
|
1432
|
+
export function featureReadiness(item, all) {
|
|
1433
|
+
if (unresolvedDependencies(item, all).length > 0 || item.status === 'BLOCKED')
|
|
1434
|
+
return 'BLOCKED';
|
|
1435
|
+
if (lockConflictWithActive(item, all).length > 0)
|
|
1436
|
+
return 'LOCK_CONFLICT';
|
|
1437
|
+
return 'READY';
|
|
1438
|
+
}
|
|
1439
|
+
export function updateDependencyMetadata(items) {
|
|
1440
|
+
for (const item of items) {
|
|
1441
|
+
item.dependency_count = item.blocked_by.length;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
export function parseRouteToken(value) {
|
|
1445
|
+
const token = value.trim();
|
|
1446
|
+
if (!token)
|
|
1447
|
+
return null;
|
|
1448
|
+
if (token.startsWith('route:')) {
|
|
1449
|
+
const raw = token.slice('route:'.length).trim();
|
|
1450
|
+
if (!raw)
|
|
1451
|
+
return null;
|
|
1452
|
+
return raw.startsWith('/') ? raw : `/${raw}`;
|
|
1453
|
+
}
|
|
1454
|
+
if (token.startsWith('/'))
|
|
1455
|
+
return token;
|
|
1456
|
+
const inlineRoute = token.match(/\/[a-z0-9_\-/:]*/i);
|
|
1457
|
+
if (inlineRoute && inlineRoute[0])
|
|
1458
|
+
return inlineRoute[0];
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
export function inferRouteTargetsFromFeature(feature) {
|
|
1462
|
+
const routes = [
|
|
1463
|
+
...feature.produces.map(parseRouteToken),
|
|
1464
|
+
...feature.consumes.map(parseRouteToken),
|
|
1465
|
+
...(feature.frontend_surface_tokens || []).map(parseRouteToken),
|
|
1466
|
+
].filter((value) => Boolean(value));
|
|
1467
|
+
return Array.from(new Set(routes));
|
|
1468
|
+
}
|
|
1469
|
+
export function inferSurfaceTargetsFromFeature(feature) {
|
|
1470
|
+
return Array.from(new Set((feature.frontend_surface_tokens || []).map((token) => token.trim()).filter(Boolean)));
|
|
1471
|
+
}
|
|
1472
|
+
export async function gitHeadCommit(projectRoot) {
|
|
1473
|
+
try {
|
|
1474
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: projectRoot });
|
|
1475
|
+
return stdout.trim();
|
|
1476
|
+
}
|
|
1477
|
+
catch {
|
|
1478
|
+
return '';
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
export async function gitChangedFiles(projectRoot, baseRef) {
|
|
1482
|
+
if (!baseRef) {
|
|
1483
|
+
return { files: [], warning: 'base_ref_ausente' };
|
|
1484
|
+
}
|
|
1485
|
+
try {
|
|
1486
|
+
const { stdout } = await execFileAsync('git', ['diff', '--name-only', `${baseRef}..HEAD`], {
|
|
1487
|
+
cwd: projectRoot,
|
|
1488
|
+
});
|
|
1489
|
+
const files = stdout
|
|
1490
|
+
.split(/\r?\n/)
|
|
1491
|
+
.map((line) => line.trim())
|
|
1492
|
+
.filter(Boolean);
|
|
1493
|
+
return { files, warning: '' };
|
|
1494
|
+
}
|
|
1495
|
+
catch {
|
|
1496
|
+
return { files: [], warning: 'git_diff_indisponivel' };
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
export function isFrontendPath(filePath) {
|
|
1500
|
+
const normalized = filePath.replace(/\\/g, '/').toLowerCase();
|
|
1501
|
+
const frontendDirs = [
|
|
1502
|
+
'/frontend/',
|
|
1503
|
+
'/web/',
|
|
1504
|
+
'/ui/',
|
|
1505
|
+
'/app/',
|
|
1506
|
+
'/pages/',
|
|
1507
|
+
'/components/',
|
|
1508
|
+
'/routes/',
|
|
1509
|
+
'/views/',
|
|
1510
|
+
'/templates/',
|
|
1511
|
+
'/public/',
|
|
1512
|
+
];
|
|
1513
|
+
if (frontendDirs.some((segment) => normalized.includes(segment)))
|
|
1514
|
+
return true;
|
|
1515
|
+
if (normalized.includes('/src/') &&
|
|
1516
|
+
(normalized.includes('/src/app/') ||
|
|
1517
|
+
normalized.includes('/src/pages/') ||
|
|
1518
|
+
normalized.includes('/src/components/'))) {
|
|
1519
|
+
return true;
|
|
1520
|
+
}
|
|
1521
|
+
return /\.(tsx|jsx|vue|svelte|css|scss|sass|less|html)$/.test(normalized);
|
|
1522
|
+
}
|
|
1523
|
+
export function detectFrontendImpactEvidence(feature, changedFiles) {
|
|
1524
|
+
const metadataRoutes = inferRouteTargetsFromFeature(feature);
|
|
1525
|
+
const metadataSurfaces = inferSurfaceTargetsFromFeature(feature);
|
|
1526
|
+
const diffFiles = changedFiles.filter(isFrontendPath);
|
|
1527
|
+
const metadataEvidence = metadataRoutes.length > 0 ||
|
|
1528
|
+
metadataSurfaces.length > 0 ||
|
|
1529
|
+
feature.execution_kind === 'frontend_coverage' ||
|
|
1530
|
+
feature.touches.includes('frontend');
|
|
1531
|
+
const sources = [];
|
|
1532
|
+
if (metadataEvidence)
|
|
1533
|
+
sources.push('metadata');
|
|
1534
|
+
if (diffFiles.length > 0)
|
|
1535
|
+
sources.push('diff');
|
|
1536
|
+
return {
|
|
1537
|
+
metadata_routes: metadataRoutes,
|
|
1538
|
+
metadata_surfaces: metadataSurfaces,
|
|
1539
|
+
diff_files: diffFiles,
|
|
1540
|
+
evidence_sources: sources,
|
|
1541
|
+
has_frontend_evidence: sources.length > 0,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
export function ensureRouteInFrontendMap(snapshot, routePath, gapId) {
|
|
1545
|
+
if (!snapshot.frontendMap)
|
|
1546
|
+
return;
|
|
1547
|
+
const routeId = `route-${slugify(routePath) || 'root'}`;
|
|
1548
|
+
const existing = snapshot.frontendMap.routes.find((route) => route.id === routeId);
|
|
1549
|
+
if (existing) {
|
|
1550
|
+
existing.ui_status = existing.ui_status === 'OK' ? 'PARTIAL' : existing.ui_status;
|
|
1551
|
+
existing.source_gap_ids = Array.from(new Set([...(existing.source_gap_ids || []), gapId]));
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
snapshot.frontendMap.routes.push({
|
|
1555
|
+
id: routeId,
|
|
1556
|
+
path: routePath,
|
|
1557
|
+
parent_id: '',
|
|
1558
|
+
label: '',
|
|
1559
|
+
nav_surface: '',
|
|
1560
|
+
ui_status: 'GAP',
|
|
1561
|
+
source_gap_ids: [gapId],
|
|
1562
|
+
implemented_files: [],
|
|
1563
|
+
notes: '',
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
export async function maybeCreateAutomaticFrontendGap(paths, snapshot, feature, options) {
|
|
1567
|
+
if (!snapshot.frontendGaps || !snapshot.frontendMap)
|
|
1568
|
+
return '';
|
|
1569
|
+
if (feature.frontend_gap_refs.length > 0)
|
|
1570
|
+
return '';
|
|
1571
|
+
const backendImpact = feature.touches.includes('backend') ||
|
|
1572
|
+
feature.execution_kind === 'feature' ||
|
|
1573
|
+
feature.execution_kind === 'migration';
|
|
1574
|
+
if (!backendImpact && !options?.force)
|
|
1575
|
+
return '';
|
|
1576
|
+
const gapId = await allocateEntityId(paths, 'FGAP');
|
|
1577
|
+
syncCounterFromId(snapshot.discoveryIndex, gapId);
|
|
1578
|
+
const now = nowIso();
|
|
1579
|
+
const routeTargets = Array.from(new Set([...(options?.routeTargets || []), ...inferRouteTargetsFromFeature(feature)]));
|
|
1580
|
+
snapshot.frontendGaps.items.push({
|
|
1581
|
+
id: gapId,
|
|
1582
|
+
title: `Cobertura frontend pendente para ${feature.id}: ${feature.title}`,
|
|
1583
|
+
status: 'OPEN',
|
|
1584
|
+
origin_kind: 'automatic',
|
|
1585
|
+
detection_sources: options?.detectionSources || ['metadata'],
|
|
1586
|
+
origin_feature: feature.id,
|
|
1587
|
+
backend_refs: [feature.id],
|
|
1588
|
+
frontend_scope: '',
|
|
1589
|
+
route_targets: routeTargets,
|
|
1590
|
+
menu_targets: [],
|
|
1591
|
+
suggested_files: [],
|
|
1592
|
+
implemented_files: [],
|
|
1593
|
+
resolved_by_feature: '',
|
|
1594
|
+
related_route_ids: routeTargets.map((route) => `route-${slugify(route) || 'root'}`),
|
|
1595
|
+
notes: 'Gerado automaticamente no finalize para evitar perda de rastreabilidade.',
|
|
1596
|
+
created_at: now,
|
|
1597
|
+
updated_at: now,
|
|
1598
|
+
});
|
|
1599
|
+
feature.frontend_gap_refs = Array.from(new Set([...feature.frontend_gap_refs, gapId]));
|
|
1600
|
+
feature.summary = [feature.summary || '', `FGAP automático criado: ${gapId}`]
|
|
1601
|
+
.filter(Boolean)
|
|
1602
|
+
.join('\n');
|
|
1603
|
+
for (const routePath of routeTargets) {
|
|
1604
|
+
ensureRouteInFrontendMap(snapshot, routePath, gapId);
|
|
1605
|
+
}
|
|
1606
|
+
return gapId;
|
|
1607
|
+
}
|
|
1608
|
+
export function resolveRoutedSkills(snapshot, touches) {
|
|
1609
|
+
const knownSkills = new Set((snapshot.skillCatalog?.skills || []).map((skill) => skill.id));
|
|
1610
|
+
let injectedSkills = [];
|
|
1611
|
+
if (touches.length > 0 && snapshot.skillRouting && snapshot.skillRouting.routes) {
|
|
1612
|
+
for (const touch of touches) {
|
|
1613
|
+
const route = snapshot.skillRouting.routes.find((r) => r.domain === touch);
|
|
1614
|
+
if (route && route.skills) {
|
|
1615
|
+
injectedSkills.push(...route.skills);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
if (injectedSkills.length === 0 && snapshot.skillRouting && snapshot.skillRouting.default_skills) {
|
|
1620
|
+
injectedSkills = [...snapshot.skillRouting.default_skills];
|
|
1621
|
+
}
|
|
1622
|
+
// fallback extremo
|
|
1623
|
+
if (injectedSkills.length === 0) {
|
|
1624
|
+
injectedSkills = ['architecture', 'concise-planning', 'context-window-management'];
|
|
1625
|
+
}
|
|
1626
|
+
const validSkills = Array.from(new Set(injectedSkills)).filter((skillId) => knownSkills.has(skillId));
|
|
1627
|
+
if (validSkills.length > 0) {
|
|
1628
|
+
return validSkills.slice(0, 5);
|
|
1629
|
+
}
|
|
1630
|
+
const canonicalFallback = ['architecture', 'concise-planning', 'context-window-management'].filter((skillId) => knownSkills.has(skillId));
|
|
1631
|
+
return canonicalFallback.slice(0, 5);
|
|
1632
|
+
}
|
|
1633
|
+
export function inferOriginType(input) {
|
|
1634
|
+
if (/^(?:RAD|EPIC)-\d{3,}$/.test(input))
|
|
1635
|
+
return 'epic';
|
|
1636
|
+
if (/^FGAP-\d{3,}$/.test(input))
|
|
1637
|
+
return 'frontend_gap';
|
|
1638
|
+
if (/^TD-\d{3,}$/.test(input))
|
|
1639
|
+
return 'tech_debt';
|
|
1640
|
+
return 'direct';
|
|
1641
|
+
}
|
|
1642
|
+
export async function buildFinalizeQueue(paths, backlogItems, queueItems) {
|
|
1643
|
+
const queueByFeature = new Map(queueItems.map((item) => [item.feature_id, item]));
|
|
1644
|
+
const archiveRoot = resolveOpenSpecSubpath(paths.projectRoot, 'changes', 'archive');
|
|
1645
|
+
for (const item of backlogItems) {
|
|
1646
|
+
if (!item.change_name)
|
|
1647
|
+
continue;
|
|
1648
|
+
if (item.status === 'DONE')
|
|
1649
|
+
continue;
|
|
1650
|
+
const archivedPath = path.join(archiveRoot, item.change_name);
|
|
1651
|
+
const archived = await fs
|
|
1652
|
+
.access(archivedPath)
|
|
1653
|
+
.then(() => true)
|
|
1654
|
+
.catch(() => false);
|
|
1655
|
+
if (!archived)
|
|
1656
|
+
continue;
|
|
1657
|
+
if (!queueByFeature.has(item.id)) {
|
|
1658
|
+
queueByFeature.set(item.id, {
|
|
1659
|
+
feature_id: item.id,
|
|
1660
|
+
status: 'PENDING',
|
|
1661
|
+
summary: `Consolidar memoria da ${item.id} (${item.change_name})`,
|
|
1662
|
+
created_at: nowIso(),
|
|
1663
|
+
completed_at: '',
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
if (item.status === 'ARCHIVED') {
|
|
1667
|
+
item.archived_at = item.archived_at || nowIso();
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return Array.from(queueByFeature.values()).sort((a, b) => a.feature_id.localeCompare(b.feature_id));
|
|
1671
|
+
}
|
|
1672
|
+
export async function evaluateFeatureQuality(paths, config, feature) {
|
|
1673
|
+
const contract = feature.quality_contract;
|
|
1674
|
+
if (!contract || contract.enabled === false) {
|
|
1675
|
+
return {
|
|
1676
|
+
ok: false,
|
|
1677
|
+
blocking: true,
|
|
1678
|
+
reasons: ['quality_contract missing or disabled'],
|
|
1679
|
+
artifact: '',
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
const names = activeDocNamesForLayout(config);
|
|
1683
|
+
const activePath = path.join(paths.activeDir, feature.id);
|
|
1684
|
+
const artifactName = [names.quality, ...activeDocCandidateNames(config)].find((name) => (name === '5-quality.yaml' || name === 'quality.md') && existsSync(path.join(activePath, name))) || names.quality;
|
|
1685
|
+
const artifactPath = path.join(activePath, artifactName);
|
|
1686
|
+
const content = await fs.readFile(artifactPath, 'utf-8').catch(() => '');
|
|
1687
|
+
const reasons = [];
|
|
1688
|
+
if (!content.trim()) {
|
|
1689
|
+
reasons.push(`quality artifact missing: ${relProjectPath(paths, artifactPath)}`);
|
|
1690
|
+
}
|
|
1691
|
+
if (contract.unit_target_percent < 95 && contract.exceptions.length === 0) {
|
|
1692
|
+
reasons.push('unit target below 95% without formal exception');
|
|
1693
|
+
}
|
|
1694
|
+
if (contract.integration_target_percent < 95 && contract.exceptions.length === 0) {
|
|
1695
|
+
reasons.push('integration/e2e target below 95% without formal exception');
|
|
1696
|
+
}
|
|
1697
|
+
if (contract.required_axes.length === 0) {
|
|
1698
|
+
reasons.push('required quality axes missing');
|
|
1699
|
+
}
|
|
1700
|
+
if (contract.required_evidence.length === 0 && contract.exceptions.length === 0) {
|
|
1701
|
+
reasons.push('required evidence missing without formal exception');
|
|
1702
|
+
}
|
|
1703
|
+
return {
|
|
1704
|
+
ok: reasons.length === 0,
|
|
1705
|
+
blocking: contract.enforcement === 'blocking' && contract.exceptions.length === 0,
|
|
1706
|
+
reasons,
|
|
1707
|
+
artifact: relProjectPath(paths, artifactPath),
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
export function buildAdrMarkdown(feature, unlocked, timestamp) {
|
|
1711
|
+
const refs = [feature.id, feature.origin_ref].filter(Boolean).join(', ') || '-';
|
|
1712
|
+
return `# ADR ${feature.id}
|
|
1713
|
+
|
|
1714
|
+
## Context
|
|
1715
|
+
- Feature: ${feature.id} - ${feature.title}
|
|
1716
|
+
- Origin: ${feature.origin_type}${feature.origin_ref ? ` (${feature.origin_ref})` : ''}
|
|
1717
|
+
- Finalized at: ${timestamp}
|
|
1718
|
+
|
|
1719
|
+
## Decision
|
|
1720
|
+
Consolidate feature ${feature.id} and make the result official in SDD memory.
|
|
1721
|
+
|
|
1722
|
+
## Changes
|
|
1723
|
+
- Associated change: ${feature.change_name || '-'}
|
|
1724
|
+
- Execution type: ${feature.execution_kind}
|
|
1725
|
+
- Planning mode: ${feature.planning_mode}
|
|
1726
|
+
- Lock domains: ${feature.lock_domains.length > 0 ? feature.lock_domains.join(', ') : '-'}
|
|
1727
|
+
|
|
1728
|
+
## Consequences
|
|
1729
|
+
- Residual risk documented in feature summary: ${feature.summary || '-'}
|
|
1730
|
+
|
|
1731
|
+
## Released Dependents
|
|
1732
|
+
${unlocked.length > 0 ? unlocked.map((id) => `- ${id}`).join('\n') : '- None'}
|
|
1733
|
+
|
|
1734
|
+
## References
|
|
1735
|
+
- ${refs}
|
|
1736
|
+
`;
|
|
1737
|
+
}
|
|
1738
|
+
export function applyLoggedTransition(transitionLog, entityType, entity, toStatus, options) {
|
|
1739
|
+
TransitionEngine.assertValid(entityType, entity.status, toStatus, {
|
|
1740
|
+
forceTransition: options.forceTransition,
|
|
1741
|
+
lensViolations: options.lensViolations,
|
|
1742
|
+
});
|
|
1743
|
+
const fromStatus = entity.status;
|
|
1744
|
+
entity.status = toStatus;
|
|
1745
|
+
transitionLog.push({
|
|
1746
|
+
timestamp: options.timestamp || new Date().toISOString(),
|
|
1747
|
+
entity_type: entityType,
|
|
1748
|
+
entity_id: entity.id,
|
|
1749
|
+
from: fromStatus,
|
|
1750
|
+
to: toStatus,
|
|
1751
|
+
actor: options.actor || 'system',
|
|
1752
|
+
reason: options.reason || '',
|
|
1753
|
+
source_command: options.sourceCommand || '',
|
|
1754
|
+
force_transition: options.forceTransition || false,
|
|
1755
|
+
lens_violations: options.lensViolations || [],
|
|
1756
|
+
});
|
|
1757
|
+
if (options.afterTransition) {
|
|
1758
|
+
options.afterTransition(entity);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
export function gateSatisfied(status) {
|
|
1762
|
+
return status === 'aprovada' || status === 'nao_exigida';
|
|
1763
|
+
}
|
|
1764
|
+
export function detectContextType(ref) {
|
|
1765
|
+
if (/^FEAT-\d{3,}$/.test(ref))
|
|
1766
|
+
return 'FEAT';
|
|
1767
|
+
if (/^EPIC-\d{3,}$/.test(ref))
|
|
1768
|
+
return 'EPIC';
|
|
1769
|
+
if (/^RAD-\d{3,}$/.test(ref))
|
|
1770
|
+
return 'RAD';
|
|
1771
|
+
if (/^FGAP-\d{3,}$/.test(ref))
|
|
1772
|
+
return 'FGAP';
|
|
1773
|
+
if (/^TD-\d{3,}$/.test(ref))
|
|
1774
|
+
return 'TD';
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
export async function pathExists(targetPath) {
|
|
1778
|
+
try {
|
|
1779
|
+
await fs.access(targetPath);
|
|
1780
|
+
return true;
|
|
1781
|
+
}
|
|
1782
|
+
catch {
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
export async function listAdrRefs(paths, refs) {
|
|
1787
|
+
const adrDir = path.join(paths.coreDir, 'adrs');
|
|
1788
|
+
const entries = await fs.readdir(adrDir).catch(() => []);
|
|
1789
|
+
const normalized = refs.filter(Boolean);
|
|
1790
|
+
return entries
|
|
1791
|
+
.filter((name) => name.endsWith('.md'))
|
|
1792
|
+
.filter((name) => normalized.some((ref) => name.includes(ref)))
|
|
1793
|
+
.map((name) => relProjectPath(paths, path.join(adrDir, name)))
|
|
1794
|
+
.sort();
|
|
1795
|
+
}
|
|
1796
|
+
export function computeReadyFeatures(items, options) {
|
|
1797
|
+
return computeReadyFeatureWaves(items, { rank: options?.rank || 'impact', maxAgents: options?.maxAgents });
|
|
1798
|
+
}
|
|
1799
|
+
const SESSION_TEMP_SENSITIVE_SEGMENTS = ['.env', 'secrets', 'secret', 'credentials', 'token', '.aws', '.ssh', 'id_rsa', 'pem'];
|
|
1800
|
+
export function buildSessionStateFingerprint(items) {
|
|
1801
|
+
const normalized = items
|
|
1802
|
+
.map((item) => ({
|
|
1803
|
+
id: item.id,
|
|
1804
|
+
status: item.status,
|
|
1805
|
+
blocked_by: [...item.blocked_by].sort(),
|
|
1806
|
+
lock_domains: [...item.lock_domains].sort(),
|
|
1807
|
+
parallel_group: item.parallel_group || '',
|
|
1808
|
+
dependency_count: item.dependency_count,
|
|
1809
|
+
change_name: item.change_name || '',
|
|
1810
|
+
start_commit_sha: item.start_commit_sha || '',
|
|
1811
|
+
}))
|
|
1812
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1813
|
+
const payload = JSON.stringify(normalized);
|
|
1814
|
+
return createHash('sha1').update(payload).digest('hex');
|
|
1815
|
+
}
|
|
1816
|
+
export function buildSessionManifest(featureId, projectRoot, items) {
|
|
1817
|
+
const activeFeatureIds = items.filter((item) => item.status === 'IN_PROGRESS').map((item) => item.id).sort();
|
|
1818
|
+
return {
|
|
1819
|
+
version: 1,
|
|
1820
|
+
feature_id: featureId,
|
|
1821
|
+
project_root: projectRoot,
|
|
1822
|
+
created_at: nowIso(),
|
|
1823
|
+
state_fingerprint: buildSessionStateFingerprint(items),
|
|
1824
|
+
active_feature_ids: activeFeatureIds,
|
|
1825
|
+
lock_domain_snapshot: Array.from(new Set(items.flatMap((item) => item.lock_domains))).sort(),
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
export function isSessionPackStale(manifest, items) {
|
|
1829
|
+
const reasons = [];
|
|
1830
|
+
const currentFingerprint = buildSessionStateFingerprint(items);
|
|
1831
|
+
if (manifest.state_fingerprint !== currentFingerprint) {
|
|
1832
|
+
reasons.push('state_fingerprint mismatch');
|
|
1833
|
+
}
|
|
1834
|
+
const active = items
|
|
1835
|
+
.filter((item) => item.status === 'IN_PROGRESS')
|
|
1836
|
+
.map((item) => item.id)
|
|
1837
|
+
.sort();
|
|
1838
|
+
if (manifest.active_feature_ids.join(',') !== active.join(',')) {
|
|
1839
|
+
reasons.push('active_feature_ids mismatch');
|
|
1840
|
+
}
|
|
1841
|
+
return { stale: reasons.length > 0, reasons };
|
|
1842
|
+
}
|
|
1843
|
+
export function isSafeSessionTempPath(projectRoot, candidatePath) {
|
|
1844
|
+
const resolvedRoot = path.resolve(projectRoot);
|
|
1845
|
+
const resolvedCandidate = path.resolve(candidatePath);
|
|
1846
|
+
const relative = path.relative(resolvedRoot, resolvedCandidate);
|
|
1847
|
+
const inside = relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
1848
|
+
if (!inside)
|
|
1849
|
+
return false;
|
|
1850
|
+
const lowerCandidate = resolvedCandidate.toLowerCase();
|
|
1851
|
+
const hasSensitiveSegment = SESSION_TEMP_SENSITIVE_SEGMENTS.some((segment) => lowerCandidate.includes(`/${segment.toLowerCase()}/`) || lowerCandidate.endsWith(`/${segment.toLowerCase()}`));
|
|
1852
|
+
return !hasSensitiveSegment;
|
|
1853
|
+
}
|
|
1854
|
+
export async function cleanupSessionPath(sessionRoot, candidatePath) {
|
|
1855
|
+
if (!isSafeSessionTempPath(sessionRoot, candidatePath)) {
|
|
1856
|
+
return {
|
|
1857
|
+
removed: false,
|
|
1858
|
+
already_absent: false,
|
|
1859
|
+
reason: 'caminho fora da raiz do projeto ou padrao sensível detectado',
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
const exists = await fs
|
|
1863
|
+
.access(candidatePath)
|
|
1864
|
+
.then(() => true)
|
|
1865
|
+
.catch(() => false);
|
|
1866
|
+
if (!exists) {
|
|
1867
|
+
return { removed: false, already_absent: true, reason: 'ja inexistente' };
|
|
1868
|
+
}
|
|
1869
|
+
await fs.rm(candidatePath, { recursive: true, force: true });
|
|
1870
|
+
return { removed: true, already_absent: false, reason: 'removido' };
|
|
1871
|
+
}
|
|
1872
|
+
export function computeReadyFeatureWaves(items, options) {
|
|
1873
|
+
const rank = options?.rank || 'impact';
|
|
1874
|
+
const requestedAgents = typeof options?.maxAgents === 'number' && Number.isFinite(options.maxAgents)
|
|
1875
|
+
? Math.floor(options.maxAgents)
|
|
1876
|
+
: undefined;
|
|
1877
|
+
const maxAgents = typeof requestedAgents === 'number' && requestedAgents > 0 ? requestedAgents : Number.MAX_SAFE_INTEGER;
|
|
1878
|
+
const byId = new Map(items.map((item) => [item.id, item]));
|
|
1879
|
+
const dependentsMap = buildDependentsMap(items);
|
|
1880
|
+
const blocked = [];
|
|
1881
|
+
const conflicts = [];
|
|
1882
|
+
const deferred = [];
|
|
1883
|
+
const candidateById = new Map();
|
|
1884
|
+
const blockedByReasons = new Map();
|
|
1885
|
+
const readyLikeStates = new Set(['READY', 'SYNC_REQUIRED', 'VERIFY_FAILED']);
|
|
1886
|
+
for (const item of items) {
|
|
1887
|
+
if (item.status === 'DONE' || item.status === 'ARCHIVED')
|
|
1888
|
+
continue;
|
|
1889
|
+
const unresolvedDeps = item.blocked_by.filter((depId) => {
|
|
1890
|
+
const dep = byId.get(depId);
|
|
1891
|
+
return !dep || dep.status !== 'DONE';
|
|
1892
|
+
});
|
|
1893
|
+
if (item.status === 'BLOCKED' || unresolvedDeps.length > 0) {
|
|
1894
|
+
blocked.push(item);
|
|
1895
|
+
const reasons = [`blocked_by=${unresolvedDeps.join(',') || 'pending'}`];
|
|
1896
|
+
blockedByReasons.set(item.id, reasons);
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
if (!readyLikeStates.has(item.status)) {
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
const activeConflicts = lockConflictWithActive(item, items);
|
|
1903
|
+
if (activeConflicts.length > 0) {
|
|
1904
|
+
const reasons = [`active_lock_conflict=${activeConflicts.join(',')}`];
|
|
1905
|
+
blockedByReasons.set(item.id, reasons);
|
|
1906
|
+
conflicts.push(item);
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
const scored = scoreReadyItem(item, items, dependentsMap, rank);
|
|
1910
|
+
candidateById.set(item.id, {
|
|
1911
|
+
item,
|
|
1912
|
+
score: scored.score,
|
|
1913
|
+
score_reasons: scored.reasons,
|
|
1914
|
+
deferredReasons: [...scored.reasons],
|
|
1915
|
+
readinessBlockers: [],
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
const candidates = Array.from(candidateById.values()).sort((a, b) => {
|
|
1919
|
+
if (b.score !== a.score)
|
|
1920
|
+
return b.score - a.score;
|
|
1921
|
+
return extractFeatureNumber(a.item.id) - extractFeatureNumber(b.item.id);
|
|
1922
|
+
});
|
|
1923
|
+
const waves = [];
|
|
1924
|
+
const remaining = [...candidates];
|
|
1925
|
+
while (remaining.length > 0) {
|
|
1926
|
+
const wave = [];
|
|
1927
|
+
const waveLocks = new Map();
|
|
1928
|
+
const selectedFeatureIds = new Set();
|
|
1929
|
+
let picked = false;
|
|
1930
|
+
for (const candidate of remaining) {
|
|
1931
|
+
const reasoned = candidate;
|
|
1932
|
+
if (wave.length >= maxAgents) {
|
|
1933
|
+
reasoned.readinessBlockers.push(`max_agents=${maxAgents} atingiu limite da onda`);
|
|
1934
|
+
deferred.push({ id: reasoned.item.id, reasons: [...reasoned.readinessBlockers] });
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
const lockConflicts = candidate.item.lock_domains.filter((lockDomain) => waveLocks.has(lockDomain));
|
|
1938
|
+
if (lockConflicts.length > 0) {
|
|
1939
|
+
const blockerIds = new Set();
|
|
1940
|
+
for (const lockDomain of lockConflicts) {
|
|
1941
|
+
const owners = waveLocks.get(lockDomain) || [];
|
|
1942
|
+
for (const owner of owners)
|
|
1943
|
+
blockerIds.add(owner);
|
|
1944
|
+
}
|
|
1945
|
+
reasoned.readinessBlockers.push(`lock_conflict_wave=(${Array.from(blockerIds).join(',') || 'sem-owner'})`);
|
|
1946
|
+
deferred.push({ id: reasoned.item.id, reasons: [...reasoned.readinessBlockers] });
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
wave.push(reasoned);
|
|
1950
|
+
picked = true;
|
|
1951
|
+
selectedFeatureIds.add(reasoned.item.id);
|
|
1952
|
+
for (const lockDomain of reasoned.item.lock_domains) {
|
|
1953
|
+
const owners = waveLocks.get(lockDomain) || [];
|
|
1954
|
+
owners.push(reasoned.item.id);
|
|
1955
|
+
waveLocks.set(lockDomain, owners);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
if (!picked) {
|
|
1959
|
+
const fallback = remaining.shift();
|
|
1960
|
+
if (!fallback)
|
|
1961
|
+
break;
|
|
1962
|
+
waves.push([fallback.item.id]);
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
const selectedIds = new Set(wave.map((entry) => entry.item.id));
|
|
1966
|
+
waves.push(wave.map((entry) => entry.item.id));
|
|
1967
|
+
for (let index = remaining.length - 1; index >= 0; index -= 1) {
|
|
1968
|
+
if (selectedIds.has(remaining[index].item.id)) {
|
|
1969
|
+
remaining.splice(index, 1);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
const readyIds = new Set(waves[0] || []);
|
|
1974
|
+
const ready = candidates
|
|
1975
|
+
.filter((entry) => readyIds.has(entry.item.id))
|
|
1976
|
+
.map((entry) => entry.item);
|
|
1977
|
+
if (ready.length > 0) {
|
|
1978
|
+
ready.sort((a, b) => extractFeatureNumber(a.id) - extractFeatureNumber(b.id));
|
|
1979
|
+
}
|
|
1980
|
+
const deferredMap = new Map();
|
|
1981
|
+
for (const entry of candidateById.values()) {
|
|
1982
|
+
if (!readyIds.has(entry.item.id)) {
|
|
1983
|
+
const reasons = [
|
|
1984
|
+
...(entry.readinessBlockers.length > 0 ? entry.readinessBlockers : ['deferred_by_wave']),
|
|
1985
|
+
];
|
|
1986
|
+
deferredMap.set(entry.item.id, reasons);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
for (const [id, reasons] of deferredMap) {
|
|
1990
|
+
if (!blockedByReasons.has(id)) {
|
|
1991
|
+
deferred.push({ id, reasons });
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
const conflictReasonsSet = new Set(deferred.map((item) => item.id));
|
|
1995
|
+
for (const entry of blockedByReasons.entries()) {
|
|
1996
|
+
const [id] = entry;
|
|
1997
|
+
if (conflictReasonsSet.has(id) && !blocked.find((item) => item.id === id)) {
|
|
1998
|
+
const item = byId.get(id);
|
|
1999
|
+
if (item) {
|
|
2000
|
+
conflicts.push(item);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
return {
|
|
2005
|
+
ready,
|
|
2006
|
+
blocked,
|
|
2007
|
+
conflicts: conflicts.filter((value, index, all) => index === all.findIndex((entry) => entry.id === value.id)),
|
|
2008
|
+
waves,
|
|
2009
|
+
deferred: Array.from(new Map(deferred.map((entry) => [entry.id, entry])).values()).filter((entry) => !readyIds.has(entry.id)),
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
export function extractFeatureNumber(featureId) {
|
|
2013
|
+
const value = Number(featureId.split('-')[1]);
|
|
2014
|
+
return Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
|
|
2015
|
+
}
|
|
2016
|
+
export function buildDependentsMap(items) {
|
|
2017
|
+
const dependents = new Map();
|
|
2018
|
+
for (const item of items) {
|
|
2019
|
+
for (const dep of item.blocked_by) {
|
|
2020
|
+
const list = dependents.get(dep) || [];
|
|
2021
|
+
list.push(item.id);
|
|
2022
|
+
dependents.set(dep, list);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return dependents;
|
|
2026
|
+
}
|
|
2027
|
+
export function countIndirectDependents(featureId, dependentsMap) {
|
|
2028
|
+
const directSet = new Set(dependentsMap.get(featureId) || []);
|
|
2029
|
+
const visited = new Set([featureId]);
|
|
2030
|
+
const queue = [...directSet];
|
|
2031
|
+
for (const id of queue)
|
|
2032
|
+
visited.add(id);
|
|
2033
|
+
while (queue.length > 0) {
|
|
2034
|
+
const current = queue.shift();
|
|
2035
|
+
const next = dependentsMap.get(current) || [];
|
|
2036
|
+
for (const candidate of next) {
|
|
2037
|
+
if (visited.has(candidate))
|
|
2038
|
+
continue;
|
|
2039
|
+
visited.add(candidate);
|
|
2040
|
+
queue.push(candidate);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return {
|
|
2044
|
+
direct: directSet.size,
|
|
2045
|
+
indirect: Math.max(0, visited.size - 1 - directSet.size),
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
export function lockCriticality(lockDomains) {
|
|
2049
|
+
if (lockDomains.length === 0)
|
|
2050
|
+
return 0;
|
|
2051
|
+
const weights = {
|
|
2052
|
+
'auth-rules': 3,
|
|
2053
|
+
payment: 3,
|
|
2054
|
+
'schema-change': 3,
|
|
2055
|
+
};
|
|
2056
|
+
let total = 0;
|
|
2057
|
+
for (const lock of lockDomains) {
|
|
2058
|
+
total += weights[lock] ?? 1;
|
|
2059
|
+
}
|
|
2060
|
+
return total;
|
|
2061
|
+
}
|
|
2062
|
+
export function lockConflictRisk(item, items) {
|
|
2063
|
+
if (item.lock_domains.length === 0)
|
|
2064
|
+
return 0;
|
|
2065
|
+
let risk = 0;
|
|
2066
|
+
for (const other of items) {
|
|
2067
|
+
if (other.id === item.id)
|
|
2068
|
+
continue;
|
|
2069
|
+
if (other.status !== 'IN_PROGRESS')
|
|
2070
|
+
continue;
|
|
2071
|
+
if (intersects(item.lock_domains, other.lock_domains)) {
|
|
2072
|
+
risk++;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return risk;
|
|
2076
|
+
}
|
|
2077
|
+
export function scoreReadyItem(item, items, dependentsMap, rank) {
|
|
2078
|
+
const { direct, indirect } = countIndirectDependents(item.id, dependentsMap);
|
|
2079
|
+
const criticality = lockCriticality(item.lock_domains);
|
|
2080
|
+
const conflictRisk = lockConflictRisk(item, items);
|
|
2081
|
+
if (rank === 'fifo') {
|
|
2082
|
+
const score = 1_000_000 - extractFeatureNumber(item.id);
|
|
2083
|
+
return {
|
|
2084
|
+
score,
|
|
2085
|
+
reasons: ['ordem FIFO por numero da feature'],
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
if (rank === 'criticality') {
|
|
2089
|
+
const score = criticality * 5 + direct * 2 + indirect - conflictRisk;
|
|
2090
|
+
return {
|
|
2091
|
+
score,
|
|
2092
|
+
reasons: [
|
|
2093
|
+
`criticidade_lock=${criticality}`,
|
|
2094
|
+
`dependentes_diretos=${direct}`,
|
|
2095
|
+
`dependentes_indiretos=${indirect}`,
|
|
2096
|
+
`risco_conflito=${conflictRisk}`,
|
|
2097
|
+
],
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
const score = direct * 5 + indirect * 3 + criticality * 2 - conflictRisk;
|
|
2101
|
+
return {
|
|
2102
|
+
score,
|
|
2103
|
+
reasons: [
|
|
2104
|
+
`dependentes_diretos=${direct}`,
|
|
2105
|
+
`dependentes_indiretos=${indirect}`,
|
|
2106
|
+
`criticidade_lock=${criticality}`,
|
|
2107
|
+
`risco_conflito=${conflictRisk}`,
|
|
2108
|
+
],
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
export function buildCuratedSkillContent(entry) {
|
|
2112
|
+
const builtInSkill = BUILT_IN_SDD_SKILLS[entry.id];
|
|
2113
|
+
if (typeof builtInSkill === 'string') {
|
|
2114
|
+
return builtInSkill;
|
|
2115
|
+
}
|
|
2116
|
+
if (builtInSkill && typeof builtInSkill === 'object' && !('kind' in builtInSkill)) {
|
|
2117
|
+
const markdown = builtInSkill['SKILL.md'];
|
|
2118
|
+
if (typeof markdown === 'string' && markdown.trim().length > 0) {
|
|
2119
|
+
return markdown;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
const domainLine = entry.domains.length > 0 ? entry.domains.join(', ') : 'geral';
|
|
2123
|
+
const phaseLine = entry.phases.length > 0 ? entry.phases.join(', ') : 'all';
|
|
2124
|
+
return `---
|
|
2125
|
+
name: ${entry.id}
|
|
2126
|
+
description: ${entry.description || entry.title}
|
|
2127
|
+
---
|
|
2128
|
+
|
|
2129
|
+
# ${entry.title}
|
|
2130
|
+
|
|
2131
|
+
## Contexto
|
|
2132
|
+
- Dominios: ${domainLine}
|
|
2133
|
+
- Fases: ${phaseLine}
|
|
2134
|
+
|
|
2135
|
+
## Instrucoes
|
|
2136
|
+
${entry.description || 'Aplicar esta skill como apoio especializado durante o planejamento e execucao.'}
|
|
2137
|
+
`;
|
|
2138
|
+
}
|
|
2139
|
+
export async function resolveSkillFileRef(paths, skillId) {
|
|
2140
|
+
const candidates = [
|
|
2141
|
+
path.join(paths.skillsCuratedDir, skillId, 'SKILL.md'),
|
|
2142
|
+
path.join(paths.skillsCuratedDir, `sdd-curated-${skillId}`, 'SKILL.md'),
|
|
2143
|
+
path.join(paths.skillsDir, 'curated', skillId, 'SKILL.md'),
|
|
2144
|
+
path.join(paths.skillsDir, 'curated', `sdd-curated-${skillId}`, 'SKILL.md'),
|
|
2145
|
+
];
|
|
2146
|
+
for (const candidate of candidates) {
|
|
2147
|
+
if (await pathExists(candidate)) {
|
|
2148
|
+
return relProjectPath(paths, candidate);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
return relProjectPath(paths, path.join(paths.skillsCuratedDir, skillId, 'SKILL.md'));
|
|
2152
|
+
}
|
|
2153
|
+
export function buildSkillInvocationPrompt(input) {
|
|
2154
|
+
const headerObjective = input.objective?.trim()
|
|
2155
|
+
? `Objetivo: ${input.objective.trim()}`
|
|
2156
|
+
: 'Objetivo: executar a tarefa usando as skills selecionadas.';
|
|
2157
|
+
const refLine = input.ref ? `Contexto de referencia: ${input.ref}` : '';
|
|
2158
|
+
const skillsLines = input.skills
|
|
2159
|
+
.map((skill, index) => `${index + 1}. ${skill.id} (${skill.path})${skill.reason ? ` - ${skill.reason}` : ''}`)
|
|
2160
|
+
.join('\n');
|
|
2161
|
+
return `Use as skills abaixo nesta ordem e execute a tarefa com rastreabilidade:
|
|
2162
|
+
|
|
2163
|
+
${headerObjective}
|
|
2164
|
+
${refLine}
|
|
2165
|
+
|
|
2166
|
+
Skills obrigatorias:
|
|
2167
|
+
${skillsLines}
|
|
2168
|
+
|
|
2169
|
+
Regras:
|
|
2170
|
+
1. Cite as skills que esta aplicando no inicio da resposta.
|
|
2171
|
+
2. Siga a ordem definida acima.
|
|
2172
|
+
3. Se faltar contexto, consulte primeiro o estado canônico em .sdd/state/*.yaml.
|
|
2173
|
+
4. Finalize com proximos comandos OpenSDD recomendados.`;
|
|
2174
|
+
}
|
|
2175
|
+
//# sourceMappingURL=legacy-operations.js.map
|