specweave 0.22.13 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/README.md +2 -2
- package/CLAUDE.md +447 -52
- package/README.md +33 -10
- package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts +1 -1
- package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js +1 -1
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.js +1 -1
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-content-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-content-sync.js +4 -1
- package/dist/plugins/specweave-github/lib/github-spec-content-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.d.ts +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.js +1 -1
- package/dist/plugins/specweave-github/lib/github-sync-bidirectional.d.ts +9 -0
- package/dist/plugins/specweave-github/lib/github-sync-bidirectional.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-sync-bidirectional.js +10 -1
- package/dist/plugins/specweave-github/lib/github-sync-bidirectional.js.map +1 -1
- package/dist/plugins/specweave-github/lib/progress-comment-builder.js +2 -2
- package/dist/plugins/specweave-github/lib/progress-comment-builder.js.map +1 -1
- package/dist/plugins/specweave-github/lib/types.d.ts +1 -1
- package/dist/src/cli/commands/import-external.d.ts +22 -0
- package/dist/src/cli/commands/import-external.d.ts.map +1 -0
- package/dist/src/cli/commands/import-external.js +282 -0
- package/dist/src/cli/commands/import-external.js.map +1 -0
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +359 -1
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/helpers/github-repo-selector.d.ts +59 -0
- package/dist/src/cli/helpers/github-repo-selector.d.ts.map +1 -0
- package/dist/src/cli/helpers/github-repo-selector.js +265 -0
- package/dist/src/cli/helpers/github-repo-selector.js.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.js +41 -24
- package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
- package/dist/src/config/import-config.d.ts +69 -0
- package/dist/src/config/import-config.d.ts.map +1 -0
- package/dist/src/config/import-config.js +136 -0
- package/dist/src/config/import-config.js.map +1 -0
- package/dist/src/config/types.d.ts +26 -26
- package/dist/src/core/increment/ac-status-manager.d.ts.map +1 -1
- package/dist/src/core/increment/ac-status-manager.js +4 -2
- package/dist/src/core/increment/ac-status-manager.js.map +1 -1
- package/dist/src/core/increment/completion-validator.d.ts +30 -1
- package/dist/src/core/increment/completion-validator.d.ts.map +1 -1
- package/dist/src/core/increment/completion-validator.js +151 -3
- package/dist/src/core/increment/completion-validator.js.map +1 -1
- package/dist/src/core/increment/increment-archiver.d.ts +25 -0
- package/dist/src/core/increment/increment-archiver.d.ts.map +1 -1
- package/dist/src/core/increment/increment-archiver.js +130 -3
- package/dist/src/core/increment/increment-archiver.js.map +1 -1
- package/dist/src/core/living-docs/feature-archiver.d.ts +37 -0
- package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -1
- package/dist/src/core/living-docs/feature-archiver.js +262 -18
- package/dist/src/core/living-docs/feature-archiver.js.map +1 -1
- package/dist/src/core/living-docs/feature-id-manager.d.ts +17 -0
- package/dist/src/core/living-docs/feature-id-manager.d.ts.map +1 -1
- package/dist/src/core/living-docs/feature-id-manager.js +25 -0
- package/dist/src/core/living-docs/feature-id-manager.js.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.d.ts +16 -0
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +56 -1
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/dist/src/core/living-docs/task-project-specific-generator.d.ts +2 -2
- package/dist/src/core/living-docs/task-project-specific-generator.js +2 -2
- package/dist/src/core/repo-structure/prompt-consolidator.d.ts +2 -2
- package/dist/src/core/repo-structure/prompt-consolidator.d.ts.map +1 -1
- package/dist/src/core/repo-structure/prompt-consolidator.js +3 -15
- package/dist/src/core/repo-structure/prompt-consolidator.js.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js +3 -6
- package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
- package/dist/src/core/spec-content-sync.d.ts +4 -1
- package/dist/src/core/spec-content-sync.d.ts.map +1 -1
- package/dist/src/core/spec-content-sync.js +139 -4
- package/dist/src/core/spec-content-sync.js.map +1 -1
- package/dist/src/core/spec-task-mapper.d.ts.map +1 -1
- package/dist/src/core/spec-task-mapper.js +9 -8
- package/dist/src/core/spec-task-mapper.js.map +1 -1
- package/dist/src/core/status-line-validator.d.ts +63 -0
- package/dist/src/core/status-line-validator.d.ts.map +1 -0
- package/dist/src/core/status-line-validator.js +253 -0
- package/dist/src/core/status-line-validator.js.map +1 -0
- package/dist/src/core/sync/bidirectional-engine.d.ts +10 -1
- package/dist/src/core/sync/bidirectional-engine.d.ts.map +1 -1
- package/dist/src/core/sync/bidirectional-engine.js +10 -1
- package/dist/src/core/sync/bidirectional-engine.js.map +1 -1
- package/dist/src/core/sync/profile-manager.d.ts.map +1 -1
- package/dist/src/core/sync/profile-manager.js +3 -0
- package/dist/src/core/sync/profile-manager.js.map +1 -1
- package/dist/src/core/sync/project-context.d.ts.map +1 -1
- package/dist/src/core/sync/project-context.js +3 -0
- package/dist/src/core/sync/project-context.js.map +1 -1
- package/dist/src/core/sync/status-sync-engine.d.ts +1 -1
- package/dist/src/core/sync/status-sync-engine.js +1 -1
- package/dist/src/core/sync/sync-event-logger.d.ts +15 -1
- package/dist/src/core/sync/sync-event-logger.d.ts.map +1 -1
- package/dist/src/core/sync/sync-event-logger.js +39 -1
- package/dist/src/core/sync/sync-event-logger.js.map +1 -1
- package/dist/src/core/types/origin-metadata.d.ts +153 -0
- package/dist/src/core/types/origin-metadata.d.ts.map +1 -0
- package/dist/src/core/types/origin-metadata.js +166 -0
- package/dist/src/core/types/origin-metadata.js.map +1 -0
- package/dist/src/core/types/sync-config-validator.d.ts +57 -0
- package/dist/src/core/types/sync-config-validator.d.ts.map +1 -0
- package/dist/src/core/types/sync-config-validator.js +116 -0
- package/dist/src/core/types/sync-config-validator.js.map +1 -0
- package/dist/src/core/types/sync-profile.d.ts +8 -2
- package/dist/src/core/types/sync-profile.d.ts.map +1 -1
- package/dist/src/core/types/sync-profile.js.map +1 -1
- package/dist/src/core/types/sync-settings.d.ts +73 -0
- package/dist/src/core/types/sync-settings.d.ts.map +1 -0
- package/dist/src/core/types/sync-settings.js +90 -0
- package/dist/src/core/types/sync-settings.js.map +1 -0
- package/dist/src/core/utils/permission-checker.d.ts +100 -0
- package/dist/src/core/utils/permission-checker.d.ts.map +1 -0
- package/dist/src/core/utils/permission-checker.js +166 -0
- package/dist/src/core/utils/permission-checker.js.map +1 -0
- package/dist/src/generators/spec/spec-parser.js +3 -3
- package/dist/src/generators/spec/spec-parser.js.map +1 -1
- package/dist/src/generators/spec/task-parser.js +4 -4
- package/dist/src/generators/spec/task-parser.js.map +1 -1
- package/dist/src/id-generators/task-id-generator.d.ts +96 -0
- package/dist/src/id-generators/task-id-generator.d.ts.map +1 -0
- package/dist/src/id-generators/task-id-generator.js +143 -0
- package/dist/src/id-generators/task-id-generator.js.map +1 -0
- package/dist/src/id-generators/us-id-generator.d.ts +96 -0
- package/dist/src/id-generators/us-id-generator.d.ts.map +1 -0
- package/dist/src/id-generators/us-id-generator.js +143 -0
- package/dist/src/id-generators/us-id-generator.js.map +1 -0
- package/dist/src/importers/ado-importer.d.ts +43 -0
- package/dist/src/importers/ado-importer.d.ts.map +1 -0
- package/dist/src/importers/ado-importer.js +234 -0
- package/dist/src/importers/ado-importer.js.map +1 -0
- package/dist/src/importers/duplicate-detector.d.ts +107 -0
- package/dist/src/importers/duplicate-detector.d.ts.map +1 -0
- package/dist/src/importers/duplicate-detector.js +189 -0
- package/dist/src/importers/duplicate-detector.js.map +1 -0
- package/dist/src/importers/external-importer.d.ts +96 -0
- package/dist/src/importers/external-importer.d.ts.map +1 -0
- package/dist/src/importers/external-importer.js +13 -0
- package/dist/src/importers/external-importer.js.map +1 -0
- package/dist/src/importers/github-importer.d.ts +37 -0
- package/dist/src/importers/github-importer.d.ts.map +1 -0
- package/dist/src/importers/github-importer.js +161 -0
- package/dist/src/importers/github-importer.js.map +1 -0
- package/dist/src/importers/import-coordinator.d.ts +105 -0
- package/dist/src/importers/import-coordinator.d.ts.map +1 -0
- package/dist/src/importers/import-coordinator.js +224 -0
- package/dist/src/importers/import-coordinator.js.map +1 -0
- package/dist/src/importers/item-converter.d.ts +96 -0
- package/dist/src/importers/item-converter.d.ts.map +1 -0
- package/dist/src/importers/item-converter.js +246 -0
- package/dist/src/importers/item-converter.js.map +1 -0
- package/dist/src/importers/jira-importer.d.ts +42 -0
- package/dist/src/importers/jira-importer.d.ts.map +1 -0
- package/dist/src/importers/jira-importer.js +221 -0
- package/dist/src/importers/jira-importer.js.map +1 -0
- package/dist/src/importers/rate-limiter.d.ts +128 -0
- package/dist/src/importers/rate-limiter.d.ts.map +1 -0
- package/dist/src/importers/rate-limiter.js +200 -0
- package/dist/src/importers/rate-limiter.js.map +1 -0
- package/dist/src/init/compliance/types.d.ts +2 -2
- package/dist/src/init/repo/types.d.ts +2 -2
- package/dist/src/integrations/ado/ado-client.d.ts +6 -0
- package/dist/src/integrations/ado/ado-client.d.ts.map +1 -1
- package/dist/src/integrations/ado/ado-client.js +23 -0
- package/dist/src/integrations/ado/ado-client.js.map +1 -1
- package/dist/src/integrations/jira/jira-client.d.ts +6 -0
- package/dist/src/integrations/jira/jira-client.d.ts.map +1 -1
- package/dist/src/integrations/jira/jira-client.js +38 -0
- package/dist/src/integrations/jira/jira-client.js.map +1 -1
- package/dist/src/integrations/jira/jira-mapper.d.ts +1 -1
- package/dist/src/integrations/jira/jira-mapper.js +1 -1
- package/dist/src/living-docs/fs-id-allocator.d.ts +149 -0
- package/dist/src/living-docs/fs-id-allocator.d.ts.map +1 -0
- package/dist/src/living-docs/fs-id-allocator.js +325 -0
- package/dist/src/living-docs/fs-id-allocator.js.map +1 -0
- package/dist/src/living-docs/id-registry.d.ts +124 -0
- package/dist/src/living-docs/id-registry.d.ts.map +1 -0
- package/dist/src/living-docs/id-registry.js +230 -0
- package/dist/src/living-docs/id-registry.js.map +1 -0
- package/dist/src/progress/us-progress-tracker.d.ts +68 -0
- package/dist/src/progress/us-progress-tracker.d.ts.map +1 -0
- package/dist/src/progress/us-progress-tracker.js +120 -0
- package/dist/src/progress/us-progress-tracker.js.map +1 -0
- package/dist/src/sync/external-item-sync-service.d.ts +150 -0
- package/dist/src/sync/external-item-sync-service.d.ts.map +1 -0
- package/dist/src/sync/external-item-sync-service.js +241 -0
- package/dist/src/sync/external-item-sync-service.js.map +1 -0
- package/dist/src/sync/format-preservation-sync.d.ts +90 -0
- package/dist/src/sync/format-preservation-sync.d.ts.map +1 -0
- package/dist/src/sync/format-preservation-sync.js +173 -0
- package/dist/src/sync/format-preservation-sync.js.map +1 -0
- package/dist/src/sync/index.d.ts +8 -0
- package/dist/src/sync/index.d.ts.map +1 -0
- package/dist/src/sync/index.js +6 -0
- package/dist/src/sync/index.js.map +1 -0
- package/dist/src/sync/sync-coordinator.d.ts +49 -0
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -0
- package/dist/src/sync/sync-coordinator.js +248 -0
- package/dist/src/sync/sync-coordinator.js.map +1 -0
- package/dist/src/sync/sync-metadata.d.ts +75 -0
- package/dist/src/sync/sync-metadata.d.ts.map +1 -0
- package/dist/src/sync/sync-metadata.js +100 -0
- package/dist/src/sync/sync-metadata.js.map +1 -0
- package/dist/src/types/living-docs-us-file.d.ts +63 -0
- package/dist/src/types/living-docs-us-file.d.ts.map +1 -0
- package/dist/src/types/living-docs-us-file.js +27 -0
- package/dist/src/types/living-docs-us-file.js.map +1 -0
- package/dist/src/validators/format-preservation-validator.d.ts +127 -0
- package/dist/src/validators/format-preservation-validator.d.ts.map +1 -0
- package/dist/src/validators/format-preservation-validator.js +187 -0
- package/dist/src/validators/format-preservation-validator.js.map +1 -0
- package/package.json +3 -2
- package/plugins/specweave/.claude-plugin/plugin.json +36 -2
- package/plugins/specweave/agents/architect/AGENT.md +11 -2
- package/plugins/specweave/agents/test-aware-planner/AGENT.md +81 -25
- package/plugins/specweave/commands/specweave-archive-features.md +11 -1
- package/plugins/specweave/commands/specweave-import-external.md +407 -0
- package/plugins/specweave/commands/specweave-progress.md +45 -97
- package/plugins/specweave/hooks/post-edit-spec.sh +41 -0
- package/plugins/specweave/hooks/post-increment-completion.sh +168 -26
- package/plugins/specweave/hooks/post-increment-planning.sh +148 -4
- package/plugins/specweave/hooks/post-spec-update.sh +0 -0
- package/plugins/specweave/hooks/post-task-completion.sh +75 -5
- package/plugins/specweave/hooks/post-write-spec.sh +37 -0
- package/plugins/specweave/lib/hooks/auto-transition.js +1 -1
- package/plugins/specweave/lib/hooks/auto-transition.ts +1 -1
- package/plugins/specweave/lib/hooks/invoke-translator-skill.js +1 -1
- package/plugins/specweave/lib/hooks/invoke-translator-skill.ts +1 -1
- package/plugins/specweave/lib/hooks/sync-cache.js +294 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.js +67 -2
- package/plugins/specweave/lib/hooks/sync-us-tasks.js +200 -14
- package/plugins/specweave/lib/hooks/translate-file.js +1 -1
- package/plugins/specweave/lib/hooks/translate-file.ts +1 -1
- package/plugins/specweave/lib/hooks/update-ac-status.js +1 -1
- package/plugins/specweave/lib/hooks/update-ac-status.ts +1 -1
- package/plugins/specweave/lib/vendor/core/increment/ac-status-manager.d.ts +115 -0
- package/plugins/specweave/lib/vendor/core/increment/ac-status-manager.js +345 -0
- package/plugins/specweave/lib/vendor/core/increment/ac-status-manager.js.map +1 -0
- package/plugins/specweave/lib/vendor/core/increment/active-increment-manager.d.ts +106 -0
- package/plugins/specweave/lib/vendor/core/increment/active-increment-manager.js +220 -0
- package/plugins/specweave/lib/vendor/core/increment/active-increment-manager.js.map +1 -0
- package/plugins/specweave/lib/vendor/core/increment/auto-transition-manager.d.ts +60 -0
- package/plugins/specweave/lib/vendor/core/increment/auto-transition-manager.js +192 -0
- package/plugins/specweave/lib/vendor/core/increment/auto-transition-manager.js.map +1 -0
- package/plugins/specweave/lib/vendor/core/increment/duplicate-detector.d.ts +52 -0
- package/plugins/specweave/lib/vendor/core/increment/duplicate-detector.js +276 -0
- package/plugins/specweave/lib/vendor/core/increment/duplicate-detector.js.map +1 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.d.ts +163 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +541 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -0
- package/plugins/specweave/lib/vendor/core/types/increment-metadata.d.ts +157 -0
- package/plugins/specweave/lib/vendor/core/types/increment-metadata.js +191 -0
- package/plugins/specweave/lib/vendor/core/types/increment-metadata.js.map +1 -0
- package/plugins/specweave/lib/vendor/generators/spec/task-parser.d.ts +95 -0
- package/plugins/specweave/lib/vendor/generators/spec/task-parser.js +301 -0
- package/plugins/specweave/lib/vendor/generators/spec/task-parser.js.map +1 -0
- package/plugins/specweave/lib/vendor/utils/logger.d.ts +48 -0
- package/plugins/specweave/lib/vendor/utils/logger.js +53 -0
- package/plugins/specweave/lib/vendor/utils/logger.js.map +1 -0
- package/plugins/specweave/lib/vendor/utils/translation.d.ts +187 -0
- package/plugins/specweave/lib/vendor/utils/translation.js +414 -0
- package/plugins/specweave/lib/vendor/utils/translation.js.map +1 -0
- package/plugins/specweave-ado/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-ado/lib/ado-multi-project-sync.js +0 -1
- package/plugins/specweave-ado/lib/conflict-resolver.ts +1 -1
- package/plugins/specweave-alternatives/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-backend/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-confluent/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-cost-optimizer/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-diagrams/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-docs/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-docs-preview/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-figma/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-frontend/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-github/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-github/commands/specweave-github-update-user-story.md +1 -1
- package/plugins/specweave-github/hooks/post-task-completion.sh +37 -22
- package/plugins/specweave-github/lib/ThreeLayerSyncManager.ts +1 -1
- package/plugins/specweave-github/lib/enhanced-github-sync.js +1 -1
- package/plugins/specweave-github/lib/enhanced-github-sync.ts +1 -1
- package/plugins/specweave-github/lib/github-spec-content-sync.js +2 -1
- package/plugins/specweave-github/lib/github-spec-content-sync.ts +4 -1
- package/plugins/specweave-github/lib/github-spec-sync.js +1 -1
- package/plugins/specweave-github/lib/github-spec-sync.ts +1 -1
- package/plugins/specweave-github/lib/github-sync-bidirectional.js +1 -1
- package/plugins/specweave-github/lib/github-sync-bidirectional.ts +10 -1
- package/plugins/specweave-github/lib/progress-comment-builder.js +1 -1
- package/plugins/specweave-github/lib/progress-comment-builder.ts +2 -2
- package/plugins/specweave-github/lib/types.ts +1 -1
- package/plugins/specweave-github/skills/github-issue-standard/SKILL.md +1 -1
- package/plugins/specweave-infrastructure/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-jira/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
- package/plugins/specweave-kafka/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kafka-streams/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kubernetes/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-ml/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-mobile/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-n8n/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-payments/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-release/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-testing/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-tooling/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-ui/.claude-plugin/plugin.json +1 -1
- package/src/templates/.env.example +5 -0
- package/src/templates/config-permissions-guide.md +413 -0
- package/src/templates/config.json.template +68 -0
- package/src/templates/tasks.md.template +180 -201
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +0 -170
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +0 -5442
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Sync Performance Cache Module
|
|
4
|
+
*
|
|
5
|
+
* Provides caching layer for living docs sync to meet <500ms target.
|
|
6
|
+
* Caches:
|
|
7
|
+
* - Parsed tasks.md content
|
|
8
|
+
* - File modification timestamps
|
|
9
|
+
* - US-Task mappings
|
|
10
|
+
*
|
|
11
|
+
* Part of increment 0047-us-task-linkage (T-012).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs-extra';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import crypto from 'crypto';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* In-memory cache with TTL
|
|
20
|
+
*/
|
|
21
|
+
class SyncCache {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.cache = new Map();
|
|
24
|
+
this.ttl = 60000; // 60 seconds default TTL
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get cached value
|
|
29
|
+
*
|
|
30
|
+
* @param {string} key - Cache key
|
|
31
|
+
* @returns {any|null} Cached value or null if expired/missing
|
|
32
|
+
*/
|
|
33
|
+
get(key) {
|
|
34
|
+
const entry = this.cache.get(key);
|
|
35
|
+
|
|
36
|
+
if (!entry) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if expired
|
|
41
|
+
if (Date.now() > entry.expiry) {
|
|
42
|
+
this.cache.delete(key);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return entry.value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Set cached value
|
|
51
|
+
*
|
|
52
|
+
* @param {string} key - Cache key
|
|
53
|
+
* @param {any} value - Value to cache
|
|
54
|
+
* @param {number} ttl - Time to live in milliseconds (optional)
|
|
55
|
+
*/
|
|
56
|
+
set(key, value, ttl = this.ttl) {
|
|
57
|
+
this.cache.set(key, {
|
|
58
|
+
value,
|
|
59
|
+
expiry: Date.now() + ttl
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Invalidate cache entry
|
|
65
|
+
*
|
|
66
|
+
* @param {string} key - Cache key
|
|
67
|
+
*/
|
|
68
|
+
invalidate(key) {
|
|
69
|
+
this.cache.delete(key);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clear all cache
|
|
74
|
+
*/
|
|
75
|
+
clear() {
|
|
76
|
+
this.cache.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get cache size
|
|
81
|
+
*
|
|
82
|
+
* @returns {number} Number of cached entries
|
|
83
|
+
*/
|
|
84
|
+
size() {
|
|
85
|
+
return this.cache.size;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Global cache instance
|
|
90
|
+
const globalCache = new SyncCache();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get file hash for change detection
|
|
94
|
+
*
|
|
95
|
+
* @param {string} filePath - Path to file
|
|
96
|
+
* @returns {string} SHA256 hash of file content
|
|
97
|
+
*/
|
|
98
|
+
export function getFileHash(filePath) {
|
|
99
|
+
try {
|
|
100
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
101
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get cached parsed tasks.md
|
|
109
|
+
*
|
|
110
|
+
* @param {string} tasksPath - Path to tasks.md
|
|
111
|
+
* @param {Function} parser - Parser function to call if cache miss
|
|
112
|
+
* @returns {any} Parsed tasks (from cache or fresh)
|
|
113
|
+
*/
|
|
114
|
+
export function getCachedTasks(tasksPath, parser) {
|
|
115
|
+
// Generate cache key from file path + hash
|
|
116
|
+
const fileHash = getFileHash(tasksPath);
|
|
117
|
+
|
|
118
|
+
if (!fileHash) {
|
|
119
|
+
// File doesn't exist, call parser directly
|
|
120
|
+
return parser(tasksPath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const cacheKey = `tasks:${tasksPath}:${fileHash}`;
|
|
124
|
+
|
|
125
|
+
// Try to get from cache
|
|
126
|
+
const cached = globalCache.get(cacheKey);
|
|
127
|
+
|
|
128
|
+
if (cached) {
|
|
129
|
+
// Cache hit
|
|
130
|
+
return cached;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Cache miss - parse and cache
|
|
134
|
+
const parsed = parser(tasksPath);
|
|
135
|
+
globalCache.set(cacheKey, parsed);
|
|
136
|
+
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get cached US file metadata
|
|
142
|
+
*
|
|
143
|
+
* @param {string} usFilePath - Path to US file
|
|
144
|
+
* @returns {object|null} Metadata object or null if file doesn't exist
|
|
145
|
+
*/
|
|
146
|
+
export function getCachedUSMetadata(usFilePath) {
|
|
147
|
+
try {
|
|
148
|
+
const stats = fs.statSync(usFilePath);
|
|
149
|
+
const cacheKey = `us-metadata:${usFilePath}`;
|
|
150
|
+
|
|
151
|
+
const cached = globalCache.get(cacheKey);
|
|
152
|
+
|
|
153
|
+
if (cached && cached.mtime === stats.mtimeMs) {
|
|
154
|
+
// File hasn't changed, return cached metadata
|
|
155
|
+
return cached.metadata;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// File changed or not in cache - read and cache
|
|
159
|
+
const content = fs.readFileSync(usFilePath, 'utf-8');
|
|
160
|
+
const metadata = {
|
|
161
|
+
mtime: stats.mtimeMs,
|
|
162
|
+
metadata: {
|
|
163
|
+
path: usFilePath,
|
|
164
|
+
size: content.length,
|
|
165
|
+
tasksSectionExists: content.includes('## Tasks'),
|
|
166
|
+
acCount: (content.match(/- \[[x ]\] \*\*AC-US\d+-\d{2}\*\*/g) || []).length
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
globalCache.set(cacheKey, metadata);
|
|
171
|
+
|
|
172
|
+
return metadata.metadata;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Batch file operations to reduce I/O
|
|
180
|
+
*
|
|
181
|
+
* @param {Array<{path: string, content: string}>} updates - Files to update
|
|
182
|
+
* @returns {Promise<void>}
|
|
183
|
+
*/
|
|
184
|
+
export async function batchFileUpdates(updates) {
|
|
185
|
+
// Group updates by directory to optimize disk I/O
|
|
186
|
+
const updatesByDir = new Map();
|
|
187
|
+
|
|
188
|
+
updates.forEach(({ path: filePath, content }) => {
|
|
189
|
+
const dir = path.dirname(filePath);
|
|
190
|
+
|
|
191
|
+
if (!updatesByDir.has(dir)) {
|
|
192
|
+
updatesByDir.set(dir, []);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
updatesByDir.get(dir).push({ path: filePath, content });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Write files sequentially within same directory (better disk I/O)
|
|
199
|
+
for (const [dir, fileUpdates] of updatesByDir.entries()) {
|
|
200
|
+
// Ensure directory exists once per directory
|
|
201
|
+
await fs.ensureDir(dir);
|
|
202
|
+
|
|
203
|
+
// Write all files in this directory
|
|
204
|
+
await Promise.all(
|
|
205
|
+
fileUpdates.map(({ path: filePath, content }) =>
|
|
206
|
+
fs.writeFile(filePath, content, 'utf-8')
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check if sync is needed for a US file
|
|
214
|
+
*
|
|
215
|
+
* @param {string} usFilePath - Path to US file
|
|
216
|
+
* @param {Array} tasks - Tasks for this US
|
|
217
|
+
* @param {string} tasksPath - Path to tasks.md
|
|
218
|
+
* @returns {boolean} True if sync is needed
|
|
219
|
+
*/
|
|
220
|
+
export function needsSync(usFilePath, tasks, tasksPath) {
|
|
221
|
+
try {
|
|
222
|
+
// Check if US file exists
|
|
223
|
+
if (!fs.existsSync(usFilePath)) {
|
|
224
|
+
return false; // File doesn't exist, can't sync
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Get file modification times
|
|
228
|
+
const usStats = fs.statSync(usFilePath);
|
|
229
|
+
const tasksStats = fs.statSync(tasksPath);
|
|
230
|
+
|
|
231
|
+
// If tasks.md is newer than US file, sync is needed
|
|
232
|
+
if (tasksStats.mtimeMs > usStats.mtimeMs) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check cache for last sync result
|
|
237
|
+
const cacheKey = `sync-result:${usFilePath}`;
|
|
238
|
+
const cached = globalCache.get(cacheKey);
|
|
239
|
+
|
|
240
|
+
if (cached) {
|
|
241
|
+
// Compare task list with cached
|
|
242
|
+
const currentTaskIds = tasks.map(t => t.id).sort().join(',');
|
|
243
|
+
const cachedTaskIds = cached.taskIds;
|
|
244
|
+
|
|
245
|
+
if (currentTaskIds === cachedTaskIds) {
|
|
246
|
+
// Task list unchanged, no sync needed
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Default: sync is needed
|
|
252
|
+
return true;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
// If error checking, assume sync is needed
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Record sync result for incremental sync
|
|
261
|
+
*
|
|
262
|
+
* @param {string} usFilePath - Path to US file
|
|
263
|
+
* @param {Array} tasks - Tasks that were synced
|
|
264
|
+
*/
|
|
265
|
+
export function recordSync(usFilePath, tasks) {
|
|
266
|
+
const cacheKey = `sync-result:${usFilePath}`;
|
|
267
|
+
const taskIds = tasks.map(t => t.id).sort().join(',');
|
|
268
|
+
|
|
269
|
+
globalCache.set(cacheKey, {
|
|
270
|
+
taskIds,
|
|
271
|
+
timestamp: Date.now()
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get cache statistics
|
|
277
|
+
*
|
|
278
|
+
* @returns {object} Cache stats
|
|
279
|
+
*/
|
|
280
|
+
export function getCacheStats() {
|
|
281
|
+
return {
|
|
282
|
+
size: globalCache.size(),
|
|
283
|
+
entries: Array.from(globalCache.cache.keys())
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Clear cache (for testing)
|
|
289
|
+
*/
|
|
290
|
+
export function clearCache() {
|
|
291
|
+
globalCache.clear();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export default globalCache;
|
|
@@ -37,7 +37,26 @@ async function syncLivingDocs(incrementId) {
|
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
console.log(`\u{1F4C4} Changed/created ${changedDocs.length} file(s)`);
|
|
40
|
-
|
|
40
|
+
|
|
41
|
+
// ========================================================================
|
|
42
|
+
// CHECK PERMISSION: canUpdateExternalItems (v0.24.0 - Three-Permission Architecture)
|
|
43
|
+
// ========================================================================
|
|
44
|
+
// This permission controls whether SpecWeave can UPDATE externally-created items
|
|
45
|
+
// (full content: title, description, ACs, tasks, comments).
|
|
46
|
+
// If false, living docs sync happens locally but doesn't push to external tools.
|
|
47
|
+
const canUpdateExternal = config.sync?.settings?.canUpdateExternalItems ?? false;
|
|
48
|
+
|
|
49
|
+
if (!canUpdateExternal) {
|
|
50
|
+
console.log("\u2139\uFE0F GitHub sync skipped (canUpdateExternalItems = false)");
|
|
51
|
+
console.log(" Living docs updated locally only");
|
|
52
|
+
console.log(" To enable: Set sync.settings.canUpdateExternalItems = true in config.json");
|
|
53
|
+
console.log("\u2705 Living docs sync complete (local only)\n");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// T-034E: Use FormatPreservationSyncService for origin-aware sync
|
|
58
|
+
await syncWithFormatPreservation(incrementId);
|
|
59
|
+
|
|
41
60
|
console.log("\u2705 Living docs sync complete\n");
|
|
42
61
|
} catch (error) {
|
|
43
62
|
console.error("\u274C Error syncing living docs:", error);
|
|
@@ -77,6 +96,18 @@ async function hierarchicalDistribution(incrementId) {
|
|
|
77
96
|
console.log(" \u{1F4CA} Syncing increment to living docs structure...");
|
|
78
97
|
const projectRoot = process.cwd();
|
|
79
98
|
|
|
99
|
+
// ========================================================================
|
|
100
|
+
// USE FEATURE ID FROM ENVIRONMENT (NEW in v0.23.0 - Increment 0047)
|
|
101
|
+
// ========================================================================
|
|
102
|
+
// If FEATURE_ID is provided via environment variable (extracted from spec.md),
|
|
103
|
+
// use it directly instead of auto-generating. This ensures correct traceability.
|
|
104
|
+
const explicitFeatureId = process.env.FEATURE_ID;
|
|
105
|
+
if (explicitFeatureId) {
|
|
106
|
+
console.log(` \u{1F4CE} Using explicit feature ID from spec.md: ${explicitFeatureId}`);
|
|
107
|
+
} else {
|
|
108
|
+
console.log(" \u{1F504} Feature ID will be auto-generated from increment number");
|
|
109
|
+
}
|
|
110
|
+
|
|
80
111
|
// Create logger adapter for LivingDocsSync
|
|
81
112
|
const logger = {
|
|
82
113
|
log: (msg) => console.log(` ${msg}`),
|
|
@@ -87,7 +118,9 @@ async function hierarchicalDistribution(incrementId) {
|
|
|
87
118
|
const sync = new LivingDocsSync(projectRoot, { logger });
|
|
88
119
|
const result = await sync.syncIncrement(incrementId, {
|
|
89
120
|
dryRun: false,
|
|
90
|
-
force: false
|
|
121
|
+
force: false,
|
|
122
|
+
// Pass explicit feature ID if available (v0.23.0+)
|
|
123
|
+
explicitFeatureId: explicitFeatureId || undefined
|
|
91
124
|
});
|
|
92
125
|
|
|
93
126
|
if (!result.success) {
|
|
@@ -303,6 +336,38 @@ if (isMainModule) {
|
|
|
303
336
|
console.error("\u274C Fatal error:", error);
|
|
304
337
|
});
|
|
305
338
|
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Sync with format preservation (T-034E)
|
|
342
|
+
* Routes sync based on origin metadata (external vs internal)
|
|
343
|
+
*/
|
|
344
|
+
async function syncWithFormatPreservation(incrementId) {
|
|
345
|
+
try {
|
|
346
|
+
console.log("\n🔄 Using format-preserving sync...");
|
|
347
|
+
|
|
348
|
+
const { SyncCoordinator } = await import("../../../../dist/src/sync/sync-coordinator.js");
|
|
349
|
+
|
|
350
|
+
const coordinator = new SyncCoordinator({
|
|
351
|
+
projectRoot: process.cwd(),
|
|
352
|
+
incrementId
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const result = await coordinator.syncIncrementCompletion();
|
|
356
|
+
|
|
357
|
+
if (result.success) {
|
|
358
|
+
console.log(`✅ Format-preserving sync complete (${result.syncMode} mode)`);
|
|
359
|
+
console.log(` ${result.userStoriesSynced} user story/stories synced`);
|
|
360
|
+
} else {
|
|
361
|
+
console.log(`⚠️ Sync completed with ${result.errors.length} error(s)`);
|
|
362
|
+
result.errors.forEach(err => console.error(` - ${err}`));
|
|
363
|
+
}
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error("⚠️ Format-preserving sync error:", error);
|
|
366
|
+
console.log(" Falling back to legacy sync...");
|
|
367
|
+
// Don't fail - just log and continue
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
306
371
|
export {
|
|
307
372
|
syncLivingDocs
|
|
308
373
|
};
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
import fs from 'fs-extra';
|
|
14
14
|
import path from 'path';
|
|
15
|
-
import { parseTasksWithUSLinks, getAllTasks } from '
|
|
15
|
+
import { parseTasksWithUSLinks, getAllTasks } from '../vendor/generators/spec/task-parser.js';
|
|
16
16
|
import { glob } from 'glob';
|
|
17
|
+
import { getCachedTasks, needsSync, recordSync, batchFileUpdates } from './sync-cache.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Sync tasks from tasks.md to living docs User Story files
|
|
@@ -36,10 +37,10 @@ export async function syncUSTasksToLivingDocs(incrementId, projectRoot, featureI
|
|
|
36
37
|
return { success: true, updatedFiles: [], errors: [] };
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
// Parse tasks with US linkage
|
|
40
|
+
// Parse tasks with US linkage (with caching for performance)
|
|
40
41
|
let tasksByUS;
|
|
41
42
|
try {
|
|
42
|
-
tasksByUS =
|
|
43
|
+
tasksByUS = getCachedTasks(tasksPath, parseTasksWithUSLinks);
|
|
43
44
|
} catch (error) {
|
|
44
45
|
console.error(` ❌ Failed to parse tasks.md:`, error.message);
|
|
45
46
|
return { success: false, updatedFiles: [], errors: [error.message] };
|
|
@@ -59,6 +60,7 @@ export async function syncUSTasksToLivingDocs(incrementId, projectRoot, featureI
|
|
|
59
60
|
|
|
60
61
|
const updatedFiles = [];
|
|
61
62
|
const errors = [];
|
|
63
|
+
const filesToUpdate = []; // Batch file updates
|
|
62
64
|
|
|
63
65
|
// For each User Story with tasks, update its living docs file
|
|
64
66
|
for (const [usId, tasks] of Object.entries(tasksByUS)) {
|
|
@@ -73,12 +75,19 @@ export async function syncUSTasksToLivingDocs(incrementId, projectRoot, featureI
|
|
|
73
75
|
continue;
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
// Incremental sync: Check if sync is needed
|
|
79
|
+
if (!needsSync(usFilePath, tasks, tasksPath)) {
|
|
80
|
+
console.log(` ⏭️ ${usId} unchanged, skipping sync`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
76
84
|
// Update US file with task list
|
|
77
|
-
const
|
|
85
|
+
const result = await updateUSFile(usFilePath, tasks, incrementId);
|
|
78
86
|
|
|
79
|
-
if (updated) {
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
if (result.updated) {
|
|
88
|
+
filesToUpdate.push({ path: usFilePath, content: result.content });
|
|
89
|
+
recordSync(usFilePath, tasks); // Cache sync result for next run
|
|
90
|
+
console.log(` ✓ Prepared update for ${usId} (${tasks.length} tasks)`);
|
|
82
91
|
}
|
|
83
92
|
} catch (error) {
|
|
84
93
|
console.error(` ❌ Error updating ${usId}:`, error.message);
|
|
@@ -86,6 +95,12 @@ export async function syncUSTasksToLivingDocs(incrementId, projectRoot, featureI
|
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
|
|
98
|
+
// Batch write all file updates (reduce I/O)
|
|
99
|
+
if (filesToUpdate.length > 0) {
|
|
100
|
+
await batchFileUpdates(filesToUpdate);
|
|
101
|
+
updatedFiles.push(...filesToUpdate.map(f => f.path));
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
if (updatedFiles.length > 0) {
|
|
90
105
|
console.log(` ✅ Updated ${updatedFiles.length} User Story file(s)`);
|
|
91
106
|
} else {
|
|
@@ -144,13 +159,20 @@ async function findUSFile(projectRoot, projectId, featureId, usId) {
|
|
|
144
159
|
* @param {string} usFilePath - Path to US markdown file
|
|
145
160
|
* @param {Array} tasks - Tasks linked to this US
|
|
146
161
|
* @param {string} incrementId - Increment ID
|
|
147
|
-
* @returns {Promise<boolean>}
|
|
162
|
+
* @returns {Promise<{updated: boolean, content: string}>} Update result
|
|
148
163
|
*/
|
|
149
164
|
async function updateUSFile(usFilePath, tasks, incrementId) {
|
|
150
165
|
let content = await fs.readFile(usFilePath, 'utf-8');
|
|
151
166
|
let updated = false;
|
|
152
167
|
|
|
153
|
-
// 1. Update
|
|
168
|
+
// 1. Update origin badge (NEW - T-034)
|
|
169
|
+
const updatedOrigin = updateOriginBadge(content, usFilePath);
|
|
170
|
+
if (updatedOrigin !== content) {
|
|
171
|
+
content = updatedOrigin;
|
|
172
|
+
updated = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 2. Update task list section
|
|
154
176
|
const taskList = generateTaskList(tasks, incrementId);
|
|
155
177
|
const newTasksSection = `## Tasks\n\n${taskList}`;
|
|
156
178
|
|
|
@@ -166,19 +188,183 @@ async function updateUSFile(usFilePath, tasks, incrementId) {
|
|
|
166
188
|
}
|
|
167
189
|
}
|
|
168
190
|
|
|
169
|
-
//
|
|
191
|
+
// 3. Update AC checkboxes based on task completion
|
|
170
192
|
const updatedACs = updateACCheckboxes(content, tasks);
|
|
171
193
|
if (updatedACs !== content) {
|
|
172
194
|
content = updatedACs;
|
|
173
195
|
updated = true;
|
|
174
196
|
}
|
|
175
197
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
return { updated, content };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Update origin badge in User Story file
|
|
203
|
+
* Detects origin from US ID (E suffix = external)
|
|
204
|
+
*
|
|
205
|
+
* @param {string} content - US file content
|
|
206
|
+
* @param {string} usFilePath - Path to US file
|
|
207
|
+
* @returns {string} Updated content with origin badge
|
|
208
|
+
*/
|
|
209
|
+
function updateOriginBadge(content, usFilePath) {
|
|
210
|
+
// Extract US ID from filename (e.g., us-001e-title.md -> US-001E)
|
|
211
|
+
const filename = path.basename(usFilePath);
|
|
212
|
+
const usIdMatch = filename.match(/^(us-\d{3}e?)-/i);
|
|
213
|
+
|
|
214
|
+
if (!usIdMatch) {
|
|
215
|
+
return content; // Can't determine US ID, skip
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const usId = usIdMatch[1].toUpperCase();
|
|
219
|
+
const isExternal = usId.endsWith('E');
|
|
220
|
+
|
|
221
|
+
// Validate origin immutability (prevent internal ↔ external changes)
|
|
222
|
+
const existingOrigin = extractExistingOrigin(content);
|
|
223
|
+
if (existingOrigin) {
|
|
224
|
+
const existingIsExternal = existingOrigin !== 'internal';
|
|
225
|
+
if (isExternal !== existingIsExternal) {
|
|
226
|
+
console.log(` ⚠️ Origin immutable: Cannot change ${usId} from ${existingOrigin} to ${isExternal ? 'external' : 'internal'}`);
|
|
227
|
+
return content; // Don't modify origin
|
|
228
|
+
}
|
|
179
229
|
}
|
|
180
230
|
|
|
181
|
-
|
|
231
|
+
// Determine origin badge
|
|
232
|
+
let originBadge;
|
|
233
|
+
if (isExternal) {
|
|
234
|
+
// Try to detect external source from metadata
|
|
235
|
+
const externalSource = detectExternalSource(content);
|
|
236
|
+
originBadge = getExternalOriginBadge(externalSource, content);
|
|
237
|
+
} else {
|
|
238
|
+
originBadge = '🏠 **Internal**';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if origin badge already exists
|
|
242
|
+
const originPattern = /\*\*Origin\*\*:\s*.*/;
|
|
243
|
+
if (originPattern.test(content)) {
|
|
244
|
+
// Replace existing origin badge
|
|
245
|
+
return content.replace(originPattern, `**Origin**: ${originBadge}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Add origin badge after frontmatter (if exists) or at beginning
|
|
249
|
+
const frontmatterEnd = content.match(/^---\n.*?\n---\n/s);
|
|
250
|
+
if (frontmatterEnd) {
|
|
251
|
+
const insertPos = frontmatterEnd[0].length;
|
|
252
|
+
return content.slice(0, insertPos) + `\n**Origin**: ${originBadge}\n\n` + content.slice(insertPos);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// No frontmatter, add at beginning after title
|
|
256
|
+
const titleMatch = content.match(/^#\s+.+\n/);
|
|
257
|
+
if (titleMatch) {
|
|
258
|
+
const insertPos = titleMatch[0].length;
|
|
259
|
+
return content.slice(0, insertPos) + `\n**Origin**: ${originBadge}\n\n` + content.slice(insertPos);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fallback: add at very beginning
|
|
263
|
+
return `**Origin**: ${originBadge}\n\n` + content;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Extract existing origin from content (for immutability validation)
|
|
268
|
+
*
|
|
269
|
+
* @param {string} content - US file content
|
|
270
|
+
* @returns {string|null} Existing origin (internal, github, jira, ado) or null
|
|
271
|
+
*/
|
|
272
|
+
function extractExistingOrigin(content) {
|
|
273
|
+
const originMatch = content.match(/\*\*Origin\*\*:\s*(.+)/);
|
|
274
|
+
if (!originMatch) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const originText = originMatch[1].toLowerCase();
|
|
279
|
+
if (originText.includes('internal')) {
|
|
280
|
+
return 'internal';
|
|
281
|
+
}
|
|
282
|
+
if (originText.includes('github')) {
|
|
283
|
+
return 'github';
|
|
284
|
+
}
|
|
285
|
+
if (originText.includes('jira')) {
|
|
286
|
+
return 'jira';
|
|
287
|
+
}
|
|
288
|
+
if (originText.includes('ado') || originText.includes('azure')) {
|
|
289
|
+
return 'ado';
|
|
290
|
+
}
|
|
291
|
+
if (originText.includes('external')) {
|
|
292
|
+
return 'external'; // Generic external
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Detect external source from content metadata
|
|
300
|
+
*
|
|
301
|
+
* @param {string} content - US file content
|
|
302
|
+
* @returns {string} External source (github, jira, ado) or 'unknown'
|
|
303
|
+
*/
|
|
304
|
+
function detectExternalSource(content) {
|
|
305
|
+
// Check frontmatter for external_source or externalSource
|
|
306
|
+
const frontmatterMatch = content.match(/^---\n(.*?)\n---/s);
|
|
307
|
+
if (frontmatterMatch) {
|
|
308
|
+
const frontmatter = frontmatterMatch[1];
|
|
309
|
+
if (frontmatter.includes('external_source: github') || frontmatter.includes('externalSource: github')) {
|
|
310
|
+
return 'github';
|
|
311
|
+
}
|
|
312
|
+
if (frontmatter.includes('external_source: jira') || frontmatter.includes('externalSource: jira')) {
|
|
313
|
+
return 'jira';
|
|
314
|
+
}
|
|
315
|
+
if (frontmatter.includes('external_source: ado') || frontmatter.includes('externalSource: ado')) {
|
|
316
|
+
return 'ado';
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check for external ID patterns
|
|
321
|
+
if (content.includes('externalId: GH-') || content.includes('external_id: GH-')) {
|
|
322
|
+
return 'github';
|
|
323
|
+
}
|
|
324
|
+
if (content.includes('externalId: JIRA-') || content.includes('external_id: JIRA-')) {
|
|
325
|
+
return 'jira';
|
|
326
|
+
}
|
|
327
|
+
if (content.includes('externalId: ADO-') || content.includes('external_id: ADO-')) {
|
|
328
|
+
return 'ado';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return 'unknown';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get external origin badge with link if available
|
|
336
|
+
*
|
|
337
|
+
* @param {string} source - External source (github, jira, ado)
|
|
338
|
+
* @param {string} content - US file content
|
|
339
|
+
* @returns {string} Origin badge markdown
|
|
340
|
+
*/
|
|
341
|
+
function getExternalOriginBadge(source, content) {
|
|
342
|
+
// Extract external ID and URL from content
|
|
343
|
+
const externalIdMatch = content.match(/external_?[iI]d:\s*([^\n]+)/);
|
|
344
|
+
const externalUrlMatch = content.match(/external_?[uU]rl:\s*([^\n]+)/);
|
|
345
|
+
|
|
346
|
+
const externalId = externalIdMatch ? externalIdMatch[1].trim() : null;
|
|
347
|
+
const externalUrl = externalUrlMatch ? externalUrlMatch[1].trim() : null;
|
|
348
|
+
|
|
349
|
+
switch (source) {
|
|
350
|
+
case 'github':
|
|
351
|
+
if (externalId && externalUrl) {
|
|
352
|
+
return `🔗 [GitHub ${externalId}](${externalUrl})`;
|
|
353
|
+
}
|
|
354
|
+
return '🔗 **GitHub**';
|
|
355
|
+
case 'jira':
|
|
356
|
+
if (externalId && externalUrl) {
|
|
357
|
+
return `🎫 [JIRA ${externalId}](${externalUrl})`;
|
|
358
|
+
}
|
|
359
|
+
return '🎫 **JIRA**';
|
|
360
|
+
case 'ado':
|
|
361
|
+
if (externalId && externalUrl) {
|
|
362
|
+
return `📋 [ADO ${externalId}](${externalUrl})`;
|
|
363
|
+
}
|
|
364
|
+
return '📋 **Azure DevOps**';
|
|
365
|
+
default:
|
|
366
|
+
return '🔗 **External**';
|
|
367
|
+
}
|
|
182
368
|
}
|
|
183
369
|
|
|
184
370
|
/**
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
validateTranslation,
|
|
7
7
|
getLanguageName,
|
|
8
8
|
formatCost
|
|
9
|
-
} from "
|
|
9
|
+
} from "../vendor/utils/translation.js";
|
|
10
10
|
async function translateFile(options) {
|
|
11
11
|
const { filePath, targetLang, preview, verbose } = options;
|
|
12
12
|
if (!await fs.pathExists(filePath)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ACStatusManager } from "
|
|
2
|
+
import { ACStatusManager } from "../vendor/core/increment/ac-status-manager.js";
|
|
3
3
|
async function updateACStatus(incrementId) {
|
|
4
4
|
try {
|
|
5
5
|
const projectRoot = process.cwd();
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* - spec.md: - [ ] AC-US1-02 → NO CHANGE (partial completion)
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import { ACStatusManager } from '
|
|
29
|
+
import { ACStatusManager } from '../vendor/core/increment/ac-status-manager.js';
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Main entry point - uses ACStatusManager for sophisticated sync
|