specweave 1.0.350 → 1.0.352
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/bin/specweave.js +9 -0
- package/dist/plugins/specweave-ado/lib/ado-client-v2.d.ts +5 -0
- package/dist/plugins/specweave-ado/lib/ado-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-client-v2.js +61 -23
- package/dist/plugins/specweave-ado/lib/ado-client-v2.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js +3 -2
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js +2 -1
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js +25 -9
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/conflict-resolver.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/conflict-resolver.js +17 -1
- package/dist/plugins/specweave-ado/lib/conflict-resolver.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts +3 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.js +14 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.js +10 -7
- package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
- package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client.js +7 -5
- package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js +13 -3
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.d.ts +24 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js +36 -20
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +4 -2
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.js +38 -9
- package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts +1 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-graphql-client.js +32 -22
- package/dist/plugins/specweave-github/lib/github-graphql-client.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +144 -8
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.d.ts +8 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.js +94 -24
- package/dist/plugins/specweave-github/lib/github-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts +1 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js +2 -1
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +25 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -1
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts +3 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/per-us-sync.js +29 -9
- package/dist/plugins/specweave-github/lib/per-us-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/content-format-adapter.d.ts +59 -0
- package/dist/plugins/specweave-jira/lib/content-format-adapter.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/content-format-adapter.js +159 -0
- package/dist/plugins/specweave-jira/lib/content-format-adapter.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.d.ts +45 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.js +92 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js +13 -28
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +2 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +19 -7
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.d.ts +47 -0
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.js +110 -0
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.d.ts +26 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.js +77 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.js +5 -3
- package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts +17 -2
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js +103 -33
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +4 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js +19 -6
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/metadata-paths.d.ts +29 -0
- package/dist/plugins/specweave-jira/lib/metadata-paths.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/metadata-paths.js +73 -0
- package/dist/plugins/specweave-jira/lib/metadata-paths.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/reorganization-detector.d.ts +15 -2
- package/dist/plugins/specweave-jira/lib/reorganization-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/reorganization-detector.js +121 -33
- package/dist/plugins/specweave-jira/lib/reorganization-detector.js.map +1 -1
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +23 -18
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/sync-progress.d.ts +6 -0
- package/dist/src/cli/commands/sync-progress.d.ts.map +1 -1
- package/dist/src/cli/commands/sync-progress.js +37 -0
- package/dist/src/cli/commands/sync-progress.js.map +1 -1
- package/dist/src/cli/commands/sync-task.d.ts +16 -0
- package/dist/src/cli/commands/sync-task.d.ts.map +1 -0
- package/dist/src/cli/commands/sync-task.js +42 -0
- package/dist/src/cli/commands/sync-task.js.map +1 -0
- package/dist/src/cli/helpers/init/instruction-file-merger.js +3 -3
- package/dist/src/cli/helpers/init/instruction-file-merger.js.map +1 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.d.ts +9 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.d.ts.map +1 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.js +26 -8
- package/dist/src/core/hooks/LifecycleHookDispatcher.js.map +1 -1
- package/dist/src/core/increment/metadata-manager.d.ts +13 -0
- package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.js +144 -17
- package/dist/src/core/increment/metadata-manager.js.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.js +2 -1
- package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -1
- package/dist/src/core/increment/status-commands.d.ts.map +1 -1
- package/dist/src/core/increment/status-commands.js +33 -11
- package/dist/src/core/increment/status-commands.js.map +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 +2 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
- package/dist/src/locales/de/cli.json +252 -77
- package/dist/src/locales/en/cli.json +7 -0
- package/dist/src/locales/es/cli.json +245 -3
- package/dist/src/locales/fr/cli.json +259 -84
- package/dist/src/locales/ja/cli.json +253 -78
- package/dist/src/locales/ko/cli.json +253 -78
- package/dist/src/locales/pt/cli.json +252 -77
- package/dist/src/locales/ru/cli.json +17 -3
- package/dist/src/locales/zh/cli.json +258 -83
- package/dist/src/sync/ado-reconciler.d.ts.map +1 -1
- package/dist/src/sync/ado-reconciler.js +5 -1
- package/dist/src/sync/ado-reconciler.js.map +1 -1
- package/dist/src/sync/base-reconciler.d.ts.map +1 -1
- package/dist/src/sync/base-reconciler.js +6 -1
- package/dist/src/sync/base-reconciler.js.map +1 -1
- package/dist/src/sync/config.d.ts +4 -0
- package/dist/src/sync/config.d.ts.map +1 -1
- package/dist/src/sync/config.js +6 -4
- package/dist/src/sync/config.js.map +1 -1
- package/dist/src/sync/external-issue-auto-creator.d.ts +3 -0
- package/dist/src/sync/external-issue-auto-creator.d.ts.map +1 -1
- package/dist/src/sync/external-issue-auto-creator.js +53 -17
- package/dist/src/sync/external-issue-auto-creator.js.map +1 -1
- package/dist/src/sync/external-item-sync-service.d.ts +9 -0
- package/dist/src/sync/external-item-sync-service.d.ts.map +1 -1
- package/dist/src/sync/external-item-sync-service.js +210 -9
- package/dist/src/sync/external-item-sync-service.js.map +1 -1
- package/dist/src/sync/github-reconciler.d.ts +30 -0
- package/dist/src/sync/github-reconciler.d.ts.map +1 -1
- package/dist/src/sync/github-reconciler.js +242 -3
- package/dist/src/sync/github-reconciler.js.map +1 -1
- package/dist/src/sync/jira-reconciler.d.ts.map +1 -1
- package/dist/src/sync/jira-reconciler.js +5 -1
- package/dist/src/sync/jira-reconciler.js.map +1 -1
- package/dist/src/sync/provider-router.d.ts.map +1 -1
- package/dist/src/sync/provider-router.js +2 -1
- package/dist/src/sync/provider-router.js.map +1 -1
- package/dist/src/sync/providers/ado.d.ts +4 -0
- package/dist/src/sync/providers/ado.d.ts.map +1 -1
- package/dist/src/sync/providers/ado.js +36 -11
- package/dist/src/sync/providers/ado.js.map +1 -1
- package/dist/src/sync/providers/github.d.ts.map +1 -1
- package/dist/src/sync/providers/github.js +48 -35
- package/dist/src/sync/providers/github.js.map +1 -1
- package/dist/src/sync/providers/jira.d.ts.map +1 -1
- package/dist/src/sync/providers/jira.js +42 -26
- package/dist/src/sync/providers/jira.js.map +1 -1
- package/dist/src/sync/status-mapper.d.ts +3 -1
- package/dist/src/sync/status-mapper.d.ts.map +1 -1
- package/dist/src/sync/status-mapper.js +10 -2
- package/dist/src/sync/status-mapper.js.map +1 -1
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
- package/dist/src/sync/sync-coordinator.js +29 -19
- package/dist/src/sync/sync-coordinator.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +31 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.d.ts +13 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +144 -17
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
- package/plugins/specweave/lib/vendor/sync/github-reconciler.d.ts +30 -0
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js +242 -3
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
- package/plugins/specweave/skills/architect/SKILL.md +2 -0
- package/plugins/specweave/skills/grill/SKILL.md +2 -0
- package/plugins/specweave/skills/team-lead/SKILL.md +43 -320
- package/plugins/specweave/skills/team-lead/agents/backend.md +60 -0
- package/plugins/specweave/skills/team-lead/agents/database.md +51 -0
- package/plugins/specweave/skills/team-lead/agents/frontend.md +61 -0
- package/plugins/specweave/skills/team-lead/agents/security.md +52 -0
- package/plugins/specweave/skills/team-lead/agents/testing.md +57 -0
- package/plugins/specweave/skills/test-aware-planner/SKILL.md +2 -0
- package/plugins/specweave-ado/hooks/post-task-completion.sh +2 -2
- package/plugins/specweave-ado/lib/ado-client-v2.js +51 -21
- package/plugins/specweave-ado/lib/ado-client-v2.ts +62 -23
- package/plugins/specweave-ado/lib/ado-duplicate-detector.js +4 -4
- package/plugins/specweave-ado/lib/ado-duplicate-detector.ts +4 -3
- package/plugins/specweave-ado/lib/ado-hierarchical-sync.js +54 -12
- package/plugins/specweave-ado/lib/ado-hierarchical-sync.ts +88 -18
- package/plugins/specweave-ado/lib/ado-profile-resolver.js +1 -1
- package/plugins/specweave-ado/lib/ado-profile-resolver.ts +3 -1
- package/plugins/specweave-ado/lib/ado-spec-sync.js +22 -9
- package/plugins/specweave-ado/lib/ado-spec-sync.ts +27 -9
- package/plugins/specweave-ado/lib/conflict-resolver.js +17 -1
- package/plugins/specweave-ado/lib/conflict-resolver.ts +17 -1
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +11 -1
- package/plugins/specweave-ado/lib/per-us-sync.js +8 -1
- package/plugins/specweave-ado/lib/per-us-sync.ts +17 -2
- package/plugins/specweave-github/hooks/github-auto-create-handler.sh +28 -2
- package/plugins/specweave-github/hooks/post-task-completion.sh +6 -3
- package/plugins/specweave-github/lib/enhanced-github-sync.js +35 -6
- package/plugins/specweave-github/lib/github-board-resolver.js +4 -4
- package/plugins/specweave-github/lib/github-board-resolver.ts +4 -4
- package/plugins/specweave-github/lib/github-client-v2.js +6 -6
- package/plugins/specweave-github/lib/github-client-v2.ts +11 -7
- package/plugins/specweave-github/lib/github-client.js +5 -4
- package/plugins/specweave-github/lib/github-client.ts +7 -5
- package/plugins/specweave-github/lib/github-cross-repo-sync.js +17 -3
- package/plugins/specweave-github/lib/github-cross-repo-sync.ts +16 -3
- package/plugins/specweave-github/lib/github-feature-sync-cli.js +20 -11
- package/plugins/specweave-github/lib/github-feature-sync-cli.ts +42 -20
- package/plugins/specweave-github/lib/github-feature-sync.js +32 -8
- package/plugins/specweave-github/lib/github-feature-sync.ts +41 -9
- package/plugins/specweave-github/lib/github-graphql-client.js +29 -20
- package/plugins/specweave-github/lib/github-graphql-client.ts +34 -22
- package/plugins/specweave-github/lib/github-hierarchical-sync.js +2 -2
- package/plugins/specweave-github/lib/github-hierarchical-sync.ts +2 -2
- package/plugins/specweave-github/lib/github-multi-project-sync.js +23 -7
- package/plugins/specweave-github/lib/github-multi-project-sync.ts +26 -8
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +110 -5
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.ts +135 -9
- package/plugins/specweave-github/lib/github-spec-sync.js +85 -24
- package/plugins/specweave-github/lib/github-spec-sync.ts +100 -26
- package/plugins/specweave-github/lib/github-sync-orchestrator.js +2 -1
- package/plugins/specweave-github/lib/github-sync-orchestrator.ts +3 -1
- package/plugins/specweave-github/lib/github-us-auto-closer.js +25 -0
- package/plugins/specweave-github/lib/github-us-auto-closer.ts +43 -0
- package/plugins/specweave-github/lib/per-us-sync.js +26 -11
- package/plugins/specweave-github/lib/per-us-sync.ts +29 -11
- package/plugins/specweave-jira/hooks/post-task-completion.sh +2 -1
- package/plugins/specweave-jira/lib/content-format-adapter.js +116 -0
- package/plugins/specweave-jira/lib/content-format-adapter.ts +189 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +21 -5
- package/plugins/specweave-jira/lib/jira-deployment-detector.js +63 -0
- package/plugins/specweave-jira/lib/jira-deployment-detector.ts +113 -0
- package/plugins/specweave-jira/lib/jira-duplicate-detector.js +12 -29
- package/plugins/specweave-jira/lib/jira-duplicate-detector.ts +13 -27
- package/plugins/specweave-jira/lib/jira-epic-sync.js +15 -5
- package/plugins/specweave-jira/lib/jira-epic-sync.ts +22 -7
- package/plugins/specweave-jira/lib/jira-field-discovery.js +76 -0
- package/plugins/specweave-jira/lib/jira-field-discovery.ts +139 -0
- package/plugins/specweave-jira/lib/jira-hierarchical-sync.js +10 -0
- package/plugins/specweave-jira/lib/jira-hierarchical-sync.ts +11 -0
- package/plugins/specweave-jira/lib/jira-multi-project-sync.js +19 -9
- package/plugins/specweave-jira/lib/jira-multi-project-sync.ts +25 -14
- package/plugins/specweave-jira/lib/jira-paginated-search.js +55 -0
- package/plugins/specweave-jira/lib/jira-paginated-search.ts +108 -0
- package/plugins/specweave-jira/lib/jira-spec-commit-sync.js +5 -3
- package/plugins/specweave-jira/lib/jira-spec-commit-sync.ts +5 -3
- package/plugins/specweave-jira/lib/jira-spec-sync.js +102 -31
- package/plugins/specweave-jira/lib/jira-spec-sync.ts +123 -45
- package/plugins/specweave-jira/lib/jira-status-sync.js +18 -5
- package/plugins/specweave-jira/lib/jira-status-sync.ts +21 -6
- package/plugins/specweave-jira/lib/metadata-paths.js +38 -0
- package/plugins/specweave-jira/lib/metadata-paths.ts +73 -0
- package/plugins/specweave-jira/lib/reorganization-detector.js +101 -23
- package/plugins/specweave-jira/lib/reorganization-detector.ts +125 -35
- package/plugins/specweave-jira/scripts/refresh-cache.js +1 -1
- package/plugins/specweave-jira/scripts/refresh-cache.ts +2 -2
- package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +3 -5
|
@@ -86,11 +86,25 @@ export interface ProjectGitHubConfig {
|
|
|
86
86
|
teamBoardId?: number; // For team-board strategy
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** Default cross-team detection keywords (English). Override via `crossTeamKeywords` config. */
|
|
90
|
+
const DEFAULT_CROSS_TEAM_KEYWORDS = [
|
|
91
|
+
'integration',
|
|
92
|
+
'cross-team',
|
|
93
|
+
'cross-project',
|
|
94
|
+
'shared',
|
|
95
|
+
'common',
|
|
96
|
+
'auth',
|
|
97
|
+
'api-contract',
|
|
98
|
+
'sync',
|
|
99
|
+
];
|
|
100
|
+
|
|
89
101
|
export class GitHubSpecSync {
|
|
90
102
|
private specManager: SpecMetadataManager;
|
|
91
103
|
private projectContextManager: ProjectContextManager;
|
|
92
104
|
private projectRoot: string;
|
|
93
105
|
private token?: string;
|
|
106
|
+
/** Configurable keywords for cross-team spec detection. Case-insensitive matching. */
|
|
107
|
+
crossTeamKeywords: string[] = DEFAULT_CROSS_TEAM_KEYWORDS;
|
|
94
108
|
|
|
95
109
|
constructor(projectRoot: string = process.cwd()) {
|
|
96
110
|
this.projectRoot = projectRoot;
|
|
@@ -605,6 +619,11 @@ ${acList}
|
|
|
605
619
|
|
|
606
620
|
/**
|
|
607
621
|
* Resolve conflicts
|
|
622
|
+
*
|
|
623
|
+
* GUARD: Only overwrites local spec title if the user explicitly configured
|
|
624
|
+
* "remote-wins" as the conflict resolution strategy. Default conflicts
|
|
625
|
+
* detected by detectConflicts() use 'remote-wins' but this guard ensures
|
|
626
|
+
* API-sourced titles never silently replace local titles.
|
|
608
627
|
*/
|
|
609
628
|
private async resolveConflicts(
|
|
610
629
|
spec: SpecContent,
|
|
@@ -612,12 +631,27 @@ ${acList}
|
|
|
612
631
|
): Promise<void> {
|
|
613
632
|
for (const conflict of conflicts) {
|
|
614
633
|
if (conflict.resolution === 'remote-wins') {
|
|
615
|
-
|
|
616
|
-
//
|
|
634
|
+
// Guard: never overwrite local title with an API-sourced title
|
|
635
|
+
// unless the conflict was explicitly set to remote-wins by user config.
|
|
636
|
+
// The title field from detectConflicts() defaults to 'remote-wins'
|
|
637
|
+
// but we should only apply it for non-title fields or when the remote
|
|
638
|
+
// value looks like a real title (not a stub/API artifact).
|
|
617
639
|
if (conflict.field === 'title') {
|
|
640
|
+
const remoteTitle = conflict.remoteValue as string;
|
|
641
|
+
// Skip if remote title looks like a prefix-formatted project title
|
|
642
|
+
// (e.g., "[SPEC-001] Real Title") — extract and compare only the suffix
|
|
643
|
+
const remoteSuffix = remoteTitle.replace(/^\[.*?\]\s*/, '');
|
|
644
|
+
const localTitle = spec.metadata.title;
|
|
645
|
+
if (remoteSuffix === localTitle || !remoteSuffix) {
|
|
646
|
+
// No real difference or empty remote — skip
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
console.log(` 🔄 Resolving: ${conflict.description} (GitHub wins — explicit remote-wins)`);
|
|
618
650
|
await this.specManager.saveMetadata(spec.metadata.id, {
|
|
619
651
|
title: conflict.remoteValue
|
|
620
652
|
});
|
|
653
|
+
} else {
|
|
654
|
+
console.log(` 🔄 Resolving: ${conflict.description} (GitHub wins)`);
|
|
621
655
|
}
|
|
622
656
|
}
|
|
623
657
|
}
|
|
@@ -659,23 +693,74 @@ ${acList}
|
|
|
659
693
|
}
|
|
660
694
|
|
|
661
695
|
/**
|
|
662
|
-
* Fetch GitHub Project details
|
|
696
|
+
* Fetch GitHub Project details via GraphQL ProjectV2 API
|
|
663
697
|
*/
|
|
664
698
|
private async fetchGitHubProject(
|
|
665
699
|
owner: string,
|
|
666
700
|
repo: string,
|
|
667
701
|
projectId: number
|
|
668
702
|
): Promise<GitHubProject> {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
703
|
+
const query = `
|
|
704
|
+
query GetProject($owner: String!, $number: Int!) {
|
|
705
|
+
user(login: $owner) {
|
|
706
|
+
projectV2(number: $number) {
|
|
707
|
+
id
|
|
708
|
+
title
|
|
709
|
+
number
|
|
710
|
+
url
|
|
711
|
+
closed
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
`;
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
const result = await this.executeGraphQL(query, {
|
|
719
|
+
owner,
|
|
720
|
+
number: projectId,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// Try user first, fall back to org
|
|
724
|
+
let project = result.data?.user?.projectV2;
|
|
725
|
+
if (!project) {
|
|
726
|
+
const orgQuery = `
|
|
727
|
+
query GetOrgProject($owner: String!, $number: Int!) {
|
|
728
|
+
organization(login: $owner) {
|
|
729
|
+
projectV2(number: $number) {
|
|
730
|
+
id
|
|
731
|
+
title
|
|
732
|
+
number
|
|
733
|
+
url
|
|
734
|
+
closed
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
`;
|
|
739
|
+
const orgResult = await this.executeGraphQL(orgQuery, {
|
|
740
|
+
owner,
|
|
741
|
+
number: projectId,
|
|
742
|
+
});
|
|
743
|
+
project = orgResult.data?.organization?.projectV2;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (!project) {
|
|
747
|
+
throw new Error(`GitHub Project #${projectId} not found for ${owner}`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
id: projectId,
|
|
752
|
+
title: project.title,
|
|
753
|
+
number: project.number,
|
|
754
|
+
url: project.url,
|
|
755
|
+
state: project.closed ? 'closed' : 'open',
|
|
756
|
+
owner,
|
|
757
|
+
repo,
|
|
758
|
+
};
|
|
759
|
+
} catch (error) {
|
|
760
|
+
throw new Error(
|
|
761
|
+
`Failed to fetch GitHub Project #${projectId}: ${error instanceof Error ? error.message : String(error)}`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
679
764
|
}
|
|
680
765
|
|
|
681
766
|
/**
|
|
@@ -932,20 +1017,9 @@ ${acList}
|
|
|
932
1017
|
* - Tags include multiple project names
|
|
933
1018
|
*/
|
|
934
1019
|
private isCrossTeamSpec(spec: SpecContent): boolean {
|
|
935
|
-
const crossTeamKeywords = [
|
|
936
|
-
'integration',
|
|
937
|
-
'cross-team',
|
|
938
|
-
'cross-project',
|
|
939
|
-
'shared',
|
|
940
|
-
'common',
|
|
941
|
-
'auth', // Auth often touches frontend + backend
|
|
942
|
-
'api-contract',
|
|
943
|
-
'sync'
|
|
944
|
-
];
|
|
945
|
-
|
|
946
1020
|
const title = spec.metadata.title.toLowerCase();
|
|
947
|
-
const hasCrossTeamKeyword = crossTeamKeywords.some(keyword =>
|
|
948
|
-
title.includes(keyword)
|
|
1021
|
+
const hasCrossTeamKeyword = this.crossTeamKeywords.some(keyword =>
|
|
1022
|
+
title.toLowerCase().includes(keyword.toLowerCase())
|
|
949
1023
|
);
|
|
950
1024
|
|
|
951
1025
|
// Check tags for multiple project references
|
|
@@ -44,7 +44,8 @@ class GitHubSyncOrchestrator {
|
|
|
44
44
|
projectV2Number: this.config.projectV2Number,
|
|
45
45
|
projectV2Id: this.config.projectV2Id
|
|
46
46
|
});
|
|
47
|
-
const
|
|
47
|
+
const boardName = this.config.boardName || "SpecWeave Sync Board";
|
|
48
|
+
const project = await boardResolver.findOrCreateProject(boardName);
|
|
48
49
|
const nodeIds = pushResult.created.filter((item) => item.issueNodeId).map((item) => item.issueNodeId);
|
|
49
50
|
const itemIds = await boardResolver.addIssuesToProject(project.id, nodeIds);
|
|
50
51
|
const nodeIdToStory = /* @__PURE__ */ new Map();
|
|
@@ -27,6 +27,7 @@ export interface SyncOrchestratorConfig {
|
|
|
27
27
|
projectV2Enabled?: boolean;
|
|
28
28
|
projectV2Number?: number;
|
|
29
29
|
projectV2Id?: string;
|
|
30
|
+
boardName?: string;
|
|
30
31
|
statusFieldMapping?: Record<string, string>;
|
|
31
32
|
priorityFieldMapping?: Record<string, string>;
|
|
32
33
|
}
|
|
@@ -106,7 +107,8 @@ export class GitHubSyncOrchestrator {
|
|
|
106
107
|
projectV2Id: this.config.projectV2Id,
|
|
107
108
|
});
|
|
108
109
|
|
|
109
|
-
const
|
|
110
|
+
const boardName = this.config.boardName || 'SpecWeave Sync Board';
|
|
111
|
+
const project = await boardResolver.findOrCreateProject(boardName);
|
|
110
112
|
|
|
111
113
|
// Collect issue node IDs from created issues
|
|
112
114
|
const nodeIds = pushResult.created
|
|
@@ -60,6 +60,10 @@ async function autoCloseCompletedUserStories(incrementId, affectedUSIds, specPat
|
|
|
60
60
|
execOpts
|
|
61
61
|
);
|
|
62
62
|
if (closeResult.success) {
|
|
63
|
+
await ensureLabelExists("status:completed", repoSlug, execOpts, {
|
|
64
|
+
color: "6f42c1",
|
|
65
|
+
description: "Completed work item"
|
|
66
|
+
});
|
|
63
67
|
await execFileNoThrow(
|
|
64
68
|
"gh",
|
|
65
69
|
["issue", "edit", issueNum, "--remove-label", "status:active", "--add-label", "status:completed", "-R", repoSlug],
|
|
@@ -129,6 +133,27 @@ async function parseIssueLinks(specPath) {
|
|
|
129
133
|
}
|
|
130
134
|
return links;
|
|
131
135
|
}
|
|
136
|
+
async function ensureLabelExists(labelName, repoSlug, execOpts, defaults) {
|
|
137
|
+
const checkResult = await execFileNoThrow(
|
|
138
|
+
"gh",
|
|
139
|
+
["label", "list", "--repo", repoSlug, "--search", labelName, "--json", "name", "--jq", ".[].name"],
|
|
140
|
+
execOpts
|
|
141
|
+
);
|
|
142
|
+
if (checkResult.success) {
|
|
143
|
+
const existing = (checkResult.stdout || "").trim().split("\n").filter(Boolean);
|
|
144
|
+
if (existing.some((name) => name === labelName)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const createResult = await execFileNoThrow(
|
|
149
|
+
"gh",
|
|
150
|
+
["label", "create", labelName, "--repo", repoSlug, "--color", defaults.color, "--description", defaults.description, "--force"],
|
|
151
|
+
execOpts
|
|
152
|
+
);
|
|
153
|
+
if (!createResult.success) {
|
|
154
|
+
console.warn(`\u26A0\uFE0F Could not create label "${labelName}": ${createResult.stderr || "unknown error"} (continuing without label)`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
132
157
|
function parseACStatesForUS(content, usId) {
|
|
133
158
|
const states = [];
|
|
134
159
|
const usNum = String(parseInt(usId.replace("US-", ""), 10));
|
|
@@ -130,6 +130,12 @@ export async function autoCloseCompletedUserStories(
|
|
|
130
130
|
);
|
|
131
131
|
|
|
132
132
|
if (closeResult.success) {
|
|
133
|
+
// Ensure required labels exist before applying them
|
|
134
|
+
await ensureLabelExists('status:completed', repoSlug, execOpts, {
|
|
135
|
+
color: '6f42c1',
|
|
136
|
+
description: 'Completed work item',
|
|
137
|
+
});
|
|
138
|
+
|
|
133
139
|
// Update labels: remove status:active, add status:completed
|
|
134
140
|
await execFileNoThrow(
|
|
135
141
|
'gh',
|
|
@@ -224,6 +230,43 @@ async function parseIssueLinks(specPath: string): Promise<Record<string, ParsedU
|
|
|
224
230
|
return links;
|
|
225
231
|
}
|
|
226
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Ensure a label exists in the repo before applying it.
|
|
235
|
+
* Creates the label if missing; logs a warning and continues on permission errors.
|
|
236
|
+
*/
|
|
237
|
+
async function ensureLabelExists(
|
|
238
|
+
labelName: string,
|
|
239
|
+
repoSlug: string,
|
|
240
|
+
execOpts: { env?: Record<string, string> },
|
|
241
|
+
defaults: { color: string; description: string },
|
|
242
|
+
): Promise<void> {
|
|
243
|
+
// Check if label exists
|
|
244
|
+
const checkResult = await execFileNoThrow(
|
|
245
|
+
'gh',
|
|
246
|
+
['label', 'list', '--repo', repoSlug, '--search', labelName, '--json', 'name', '--jq', '.[].name'],
|
|
247
|
+
execOpts,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (checkResult.success) {
|
|
251
|
+
const existing = (checkResult.stdout || '').trim().split('\n').filter(Boolean);
|
|
252
|
+
if (existing.some(name => name === labelName)) {
|
|
253
|
+
return; // Label already exists
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Create the label
|
|
258
|
+
const createResult = await execFileNoThrow(
|
|
259
|
+
'gh',
|
|
260
|
+
['label', 'create', labelName, '--repo', repoSlug, '--color', defaults.color, '--description', defaults.description, '--force'],
|
|
261
|
+
execOpts,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
if (!createResult.success) {
|
|
265
|
+
// Permission error or other failure — log warning and continue
|
|
266
|
+
console.warn(`⚠️ Could not create label "${labelName}": ${createResult.stderr || 'unknown error'} (continuing without label)`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
227
270
|
/**
|
|
228
271
|
* Extract AC states for a specific user story from spec.md content.
|
|
229
272
|
*/
|
|
@@ -141,20 +141,35 @@ class PerUSGitHubSync {
|
|
|
141
141
|
}
|
|
142
142
|
/**
|
|
143
143
|
* Find existing issue by US ID in title
|
|
144
|
+
*
|
|
145
|
+
* Paginates through all issues to avoid missing matches on repos with 100+ issues.
|
|
146
|
+
* Deduplicates by issue number before returning.
|
|
144
147
|
*/
|
|
145
148
|
async findExistingIssue(mapping, usId) {
|
|
146
149
|
try {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
150
|
+
const seenNumbers = /* @__PURE__ */ new Set();
|
|
151
|
+
let page = 1;
|
|
152
|
+
const perPage = 100;
|
|
153
|
+
while (true) {
|
|
154
|
+
const response = await this.octokit.issues.listForRepo({
|
|
155
|
+
owner: mapping.owner,
|
|
156
|
+
repo: mapping.repo,
|
|
157
|
+
labels: "specweave",
|
|
158
|
+
state: "all",
|
|
159
|
+
per_page: perPage,
|
|
160
|
+
page
|
|
161
|
+
});
|
|
162
|
+
for (const issue of response.data) {
|
|
163
|
+
if (seenNumbers.has(issue.number)) continue;
|
|
164
|
+
seenNumbers.add(issue.number);
|
|
165
|
+
if (issue.title.includes(`[${usId}]`)) {
|
|
166
|
+
return { number: issue.number };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (response.data.length < perPage) break;
|
|
170
|
+
page++;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
158
173
|
} catch {
|
|
159
174
|
return null;
|
|
160
175
|
}
|
|
@@ -249,25 +249,43 @@ export class PerUSGitHubSync {
|
|
|
249
249
|
|
|
250
250
|
/**
|
|
251
251
|
* Find existing issue by US ID in title
|
|
252
|
+
*
|
|
253
|
+
* Paginates through all issues to avoid missing matches on repos with 100+ issues.
|
|
254
|
+
* Deduplicates by issue number before returning.
|
|
252
255
|
*/
|
|
253
256
|
private async findExistingIssue(
|
|
254
257
|
mapping: GitHubMapping,
|
|
255
258
|
usId: string
|
|
256
259
|
): Promise<{ number: number } | null> {
|
|
257
260
|
try {
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
261
|
+
const seenNumbers = new Set<number>();
|
|
262
|
+
let page = 1;
|
|
263
|
+
const perPage = 100;
|
|
264
|
+
|
|
265
|
+
while (true) {
|
|
266
|
+
const response = await this.octokit.issues.listForRepo({
|
|
267
|
+
owner: mapping.owner,
|
|
268
|
+
repo: mapping.repo,
|
|
269
|
+
labels: 'specweave',
|
|
270
|
+
state: 'all',
|
|
271
|
+
per_page: perPage,
|
|
272
|
+
page,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
for (const issue of response.data) {
|
|
276
|
+
if (seenNumbers.has(issue.number)) continue;
|
|
277
|
+
seenNumbers.add(issue.number);
|
|
278
|
+
if (issue.title.includes(`[${usId}]`)) {
|
|
279
|
+
return { number: issue.number };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
265
282
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
283
|
+
// No more pages
|
|
284
|
+
if (response.data.length < perPage) break;
|
|
285
|
+
page++;
|
|
286
|
+
}
|
|
269
287
|
|
|
270
|
-
return
|
|
288
|
+
return null;
|
|
271
289
|
} catch {
|
|
272
290
|
return null;
|
|
273
291
|
}
|
|
@@ -96,7 +96,8 @@ EOF
|
|
|
96
96
|
exit 0
|
|
97
97
|
fi
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
# Read JIRA issue key: canonical path first, then legacy fallbacks
|
|
100
|
+
JIRA_ISSUE=$(jq -r '.external_sync.jira.issueKey // .jira.issueKey // .jira.issue // empty' "$METADATA_FILE" 2>/dev/null)
|
|
100
101
|
|
|
101
102
|
if [ -z "$JIRA_ISSUE" ]; then
|
|
102
103
|
echo "[$(date)] [JIRA] ℹ️ No JIRA issue linked to $CURRENT_INCREMENT, skipping sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { getApiVersion } from "./jira-deployment-detector.js";
|
|
2
|
+
function toADF(text) {
|
|
3
|
+
const lines = text.split("\n");
|
|
4
|
+
const content = [];
|
|
5
|
+
let currentParagraph = [];
|
|
6
|
+
const flushParagraph = () => {
|
|
7
|
+
if (currentParagraph.length > 0) {
|
|
8
|
+
content.push({ type: "paragraph", content: [...currentParagraph] });
|
|
9
|
+
currentParagraph = [];
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
const wikiHeadingMatch = line.match(/^h(\d)\.\s+(.+)$/);
|
|
14
|
+
const mdHeadingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
15
|
+
if (wikiHeadingMatch) {
|
|
16
|
+
flushParagraph();
|
|
17
|
+
const level = parseInt(wikiHeadingMatch[1]);
|
|
18
|
+
content.push({
|
|
19
|
+
type: "heading",
|
|
20
|
+
attrs: { level },
|
|
21
|
+
content: [{ type: "text", text: wikiHeadingMatch[2] }]
|
|
22
|
+
});
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (mdHeadingMatch) {
|
|
26
|
+
flushParagraph();
|
|
27
|
+
const level = mdHeadingMatch[1].length;
|
|
28
|
+
content.push({
|
|
29
|
+
type: "heading",
|
|
30
|
+
attrs: { level },
|
|
31
|
+
content: [{ type: "text", text: mdHeadingMatch[2] }]
|
|
32
|
+
});
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (/^-{3,}$/.test(line.trim())) {
|
|
36
|
+
flushParagraph();
|
|
37
|
+
content.push({ type: "rule" });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const bulletMatch = line.match(/^\*\s+(.+)$/) || line.match(/^-\s+(.+)$/);
|
|
41
|
+
if (bulletMatch) {
|
|
42
|
+
flushParagraph();
|
|
43
|
+
const listItem = {
|
|
44
|
+
type: "listItem",
|
|
45
|
+
content: [{ type: "paragraph", content: [{ type: "text", text: bulletMatch[1] }] }]
|
|
46
|
+
};
|
|
47
|
+
const lastNode = content[content.length - 1];
|
|
48
|
+
if (lastNode && lastNode.type === "bulletList" && lastNode.content) {
|
|
49
|
+
lastNode.content.push(listItem);
|
|
50
|
+
} else {
|
|
51
|
+
content.push({ type: "bulletList", content: [listItem] });
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (line.trim() === "") {
|
|
56
|
+
flushParagraph();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
let textContent = line;
|
|
60
|
+
const textNodes = [];
|
|
61
|
+
const parts = textContent.split(/(\*[^*]+\*|_[^_]+_)/);
|
|
62
|
+
for (const part of parts) {
|
|
63
|
+
if (part.startsWith("*") && part.endsWith("*") && part.length > 2) {
|
|
64
|
+
textNodes.push({
|
|
65
|
+
type: "text",
|
|
66
|
+
text: part.slice(1, -1),
|
|
67
|
+
marks: [{ type: "strong" }]
|
|
68
|
+
});
|
|
69
|
+
} else if (part.startsWith("_") && part.endsWith("_") && part.length > 2) {
|
|
70
|
+
textNodes.push({
|
|
71
|
+
type: "text",
|
|
72
|
+
text: part.slice(1, -1),
|
|
73
|
+
marks: [{ type: "em" }]
|
|
74
|
+
});
|
|
75
|
+
} else if (part) {
|
|
76
|
+
textNodes.push({ type: "text", text: part });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
currentParagraph.push(...textNodes);
|
|
80
|
+
}
|
|
81
|
+
flushParagraph();
|
|
82
|
+
if (content.length === 0) {
|
|
83
|
+
content.push({
|
|
84
|
+
type: "paragraph",
|
|
85
|
+
content: [{ type: "text", text: text || "" }]
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
type: "doc",
|
|
90
|
+
version: 1,
|
|
91
|
+
content
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function toWikiMarkup(text) {
|
|
95
|
+
return text;
|
|
96
|
+
}
|
|
97
|
+
function formatContent(text, domain) {
|
|
98
|
+
const version = getApiVersion(domain);
|
|
99
|
+
if (version === "3") {
|
|
100
|
+
return toADF(text);
|
|
101
|
+
}
|
|
102
|
+
return toWikiMarkup(text);
|
|
103
|
+
}
|
|
104
|
+
function toDescription(text, domain) {
|
|
105
|
+
return formatContent(text, domain);
|
|
106
|
+
}
|
|
107
|
+
function toCommentBody(text, domain) {
|
|
108
|
+
return formatContent(text, domain);
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
formatContent,
|
|
112
|
+
toADF,
|
|
113
|
+
toCommentBody,
|
|
114
|
+
toDescription,
|
|
115
|
+
toWikiMarkup
|
|
116
|
+
};
|