convoke-agents 3.1.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +37 -10
- package/_bmad/bme/_artifacts/config.yaml +15 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/SKILL.md +6 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-01-scope.md +138 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-02-dryrun.md +199 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-03-resolve.md +174 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-04-execute.md +213 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/workflow.md +85 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/SKILL.md +6 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-01-scan.md +131 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-02-explore.md +131 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-03-recommend.md +149 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/workflow.md +78 -0
- package/_bmad/bme/_portability/skills/bmad-export-skill/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-export-skill/workflow.md +74 -0
- package/_bmad/bme/_portability/skills/bmad-generate-catalog/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-generate-catalog/workflow.md +42 -0
- package/_bmad/bme/_portability/skills/bmad-seed-catalog/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-seed-catalog/workflow.md +61 -0
- package/_bmad/bme/_portability/skills/bmad-validate-exports/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-validate-exports/workflow.md +43 -0
- package/_bmad/bme/_team-factory/agents/team-factory.md +128 -0
- package/_bmad/bme/_team-factory/config.yaml +13 -0
- package/_bmad/bme/_team-factory/lib/cascade-logic.js +184 -0
- package/_bmad/bme/_team-factory/lib/collision-detector.js +228 -0
- package/_bmad/bme/_team-factory/lib/manifest-tracker.js +214 -0
- package/_bmad/bme/_team-factory/lib/spec-differ.js +176 -0
- package/_bmad/bme/_team-factory/lib/spec-parser.js +201 -0
- package/_bmad/bme/_team-factory/lib/spec-writer.js +128 -0
- package/_bmad/bme/_team-factory/lib/types/factory-types.js +193 -0
- package/_bmad/bme/_team-factory/lib/utils/csv-utils.js +62 -0
- package/_bmad/bme/_team-factory/lib/utils/naming-utils.js +45 -0
- package/_bmad/bme/_team-factory/lib/validators/end-to-end-validator.js +898 -0
- package/_bmad/bme/_team-factory/lib/writers/activation-validator.js +175 -0
- package/_bmad/bme/_team-factory/lib/writers/config-appender.js +192 -0
- package/_bmad/bme/_team-factory/lib/writers/config-creator.js +215 -0
- package/_bmad/bme/_team-factory/lib/writers/csv-appender.js +118 -0
- package/_bmad/bme/_team-factory/lib/writers/csv-creator.js +190 -0
- package/_bmad/bme/_team-factory/lib/writers/registry-appender.js +372 -0
- package/_bmad/bme/_team-factory/lib/writers/registry-writer.js +409 -0
- package/_bmad/bme/_team-factory/module-help.csv +3 -0
- package/_bmad/bme/_team-factory/schemas/schema-independent.json +147 -0
- package/_bmad/bme/_team-factory/schemas/schema-sequential.json +242 -0
- package/_bmad/bme/_team-factory/templates/team-spec-template.yaml +86 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-01-scope.md +105 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-02-connect.md +110 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-03-review.md +116 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-04-generate.md +160 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-05-validate.md +146 -0
- package/_bmad/bme/_team-factory/workflows/step-00-route.md +76 -0
- package/_bmad/bme/_vortex/config.yaml +4 -4
- package/package.json +13 -7
- package/scripts/convoke-doctor.js +172 -1
- package/scripts/install-gyre-agents.js +0 -0
- package/scripts/lib/artifact-utils.js +521 -13
- package/scripts/lib/portfolio/portfolio-engine.js +301 -34
- package/scripts/lib/portfolio/rules/artifact-chain-rule.js +33 -3
- package/scripts/lib/portfolio/rules/conflict-resolver.js +22 -0
- package/scripts/migrate-artifacts.js +69 -10
- package/scripts/portability/catalog-generator.js +353 -0
- package/scripts/portability/classify-skills.js +646 -0
- package/scripts/portability/convoke-export.js +522 -0
- package/scripts/portability/export-engine.js +1156 -0
- package/scripts/portability/generate-adapters.js +79 -0
- package/scripts/portability/manifest-csv.js +147 -0
- package/scripts/portability/seed-catalog-repo.js +427 -0
- package/scripts/portability/templates/canonical-example.md +102 -0
- package/scripts/portability/templates/canonical-format.md +218 -0
- package/scripts/portability/templates/readme-template.md +72 -0
- package/scripts/portability/test-constants.js +42 -0
- package/scripts/portability/validate-classification.js +529 -0
- package/scripts/portability/validate-exports.js +348 -0
- package/scripts/update/lib/agent-registry.js +35 -0
- package/scripts/update/lib/config-merger.js +140 -10
- package/scripts/update/lib/refresh-installation.js +293 -8
- package/scripts/update/lib/utils.js +27 -1
- package/scripts/update/lib/validator.js +114 -4
|
@@ -452,6 +452,169 @@ function inferInitiative(remainder, taxonomy) {
|
|
|
452
452
|
return { initiative: null, confidence: 'low', source: 'unresolved', candidates };
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
+
// --- Suggested Initiative (Story 6.2) ---
|
|
456
|
+
// inferInitiative() is intentionally cautious — it never guesses. The suggester
|
|
457
|
+
// layers ON TOP, providing reviewable defaults for AMBIGUOUS entries based on
|
|
458
|
+
// content keywords, folder defaults, and git context. Suggestions are guidance,
|
|
459
|
+
// not decisions: the manifest entry stays AMBIGUOUS, but the operator gets a
|
|
460
|
+
// "REVIEW SUGGESTION: accept '{X}' or specify" prompt instead of a bare wall.
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Folder-default map for initiative inference.
|
|
464
|
+
* - planning-artifacts → convoke (platform-level artifacts default to convoke)
|
|
465
|
+
* - vortex-artifacts → null (Vortex spans multiple initiatives, no safe default)
|
|
466
|
+
* - gyre-artifacts → gyre (all gyre-artifacts/* belong to gyre)
|
|
467
|
+
*
|
|
468
|
+
* @type {Object<string, string|null>}
|
|
469
|
+
*/
|
|
470
|
+
const FOLDER_DEFAULT_MAP = Object.freeze({
|
|
471
|
+
'planning-artifacts': 'convoke',
|
|
472
|
+
'vortex-artifacts': null,
|
|
473
|
+
'gyre-artifacts': 'gyre'
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
/** Cap on git queries per migration run to preserve NFR2 (dry-run < 10s for 200 files) */
|
|
477
|
+
const MAX_GIT_SUGGESTER_QUERIES = 50;
|
|
478
|
+
let _gitSuggesterQueryCount = 0;
|
|
479
|
+
let _gitSuggesterWarned = false;
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Reset the per-run git query counter.
|
|
483
|
+
* Called by generateManifest() at the start of each run, and by tests
|
|
484
|
+
* via the exported helper to avoid cross-test state pollution.
|
|
485
|
+
*/
|
|
486
|
+
function _resetGitSuggesterCounter() {
|
|
487
|
+
_gitSuggesterQueryCount = 0;
|
|
488
|
+
_gitSuggesterWarned = false;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Scan a text corpus for any taxonomy initiative ID or alias as a whole word.
|
|
493
|
+
* Returns the first match (longest first to prefer specific over generic).
|
|
494
|
+
*
|
|
495
|
+
* Uses hyphen-aware lookarounds rather than `\b` because JS `\b` treats `-` as a
|
|
496
|
+
* word boundary, which would cause `pre-gyre` to match the `gyre` initiative.
|
|
497
|
+
* The boundary class `[a-z0-9-]` keeps kebab-case identifiers atomic.
|
|
498
|
+
*
|
|
499
|
+
* @param {string} corpus - Lowercased text to scan
|
|
500
|
+
* @param {import('./types').TaxonomyConfig} taxonomy - Taxonomy with initiatives and aliases
|
|
501
|
+
* @returns {string|null} Resolved initiative ID, or null if no match
|
|
502
|
+
*/
|
|
503
|
+
function _scanCorpusForInitiative(corpus, taxonomy) {
|
|
504
|
+
if (!corpus) return null;
|
|
505
|
+
if (!taxonomy || !taxonomy.initiatives) return null;
|
|
506
|
+
|
|
507
|
+
const platform = Array.isArray(taxonomy.initiatives.platform) ? taxonomy.initiatives.platform : [];
|
|
508
|
+
const user = Array.isArray(taxonomy.initiatives.user) ? taxonomy.initiatives.user : [];
|
|
509
|
+
const allInitiatives = [...platform, ...user];
|
|
510
|
+
const aliasKeys = taxonomy.aliases ? Object.keys(taxonomy.aliases) : [];
|
|
511
|
+
|
|
512
|
+
// Combine and sort by length descending — prefer 'strategy-perimeter' over 'strategy'.
|
|
513
|
+
const candidates = [...allInitiatives, ...aliasKeys].sort((a, b) => b.length - a.length);
|
|
514
|
+
|
|
515
|
+
for (const candidate of candidates) {
|
|
516
|
+
const escaped = candidate.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
517
|
+
// Boundary class: not preceded/followed by [a-z0-9-]. Treats hyphen as word-internal,
|
|
518
|
+
// so 'gyrescope' rejects (preceded by 'gyre' won't trigger; 'scope' = letter), 'pre-gyre'
|
|
519
|
+
// also rejects (the leading 'pre-' counts as boundary since hyphen is in the class).
|
|
520
|
+
const re = new RegExp(`(?:^|[^a-z0-9-])${escaped}(?:$|[^a-z0-9-])`, 'i');
|
|
521
|
+
if (re.test(corpus)) {
|
|
522
|
+
// Resolve aliases to canonical initiative
|
|
523
|
+
if (allInitiatives.includes(candidate)) return candidate;
|
|
524
|
+
if (taxonomy.aliases && taxonomy.aliases[candidate]) return taxonomy.aliases[candidate];
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Suggest a likely initiative for a file when inferInitiative() returns null.
|
|
532
|
+
* Three-step priority chain:
|
|
533
|
+
* 1. Content keyword scan (frontmatter title + first 10 lines) → 'medium' confidence
|
|
534
|
+
* 2. Folder default (FOLDER_DEFAULT_MAP) → 'low' confidence
|
|
535
|
+
* 3. Git creation commit message → 'low' confidence
|
|
536
|
+
*
|
|
537
|
+
* Suggestions are GUIDANCE for the operator, not auto-resolutions. The action
|
|
538
|
+
* label remains AMBIGUOUS so the operator must still confirm.
|
|
539
|
+
*
|
|
540
|
+
* @param {string} filename - The file's basename
|
|
541
|
+
* @param {string} dirName - The parent directory name (e.g., 'planning-artifacts')
|
|
542
|
+
* @param {string} fileContent - Already-loaded file content (avoids double-read)
|
|
543
|
+
* @param {import('./types').TaxonomyConfig} taxonomy - Taxonomy config
|
|
544
|
+
* @param {string} projectRoot - Absolute path to project root (for git queries)
|
|
545
|
+
* @returns {{initiative: string|null, source: 'content-keyword'|'folder-default'|'git-context'|null, confidence: 'medium'|'low'|null}}
|
|
546
|
+
*/
|
|
547
|
+
function suggestInitiative(filename, dirName, fileContent, taxonomy, projectRoot) {
|
|
548
|
+
// Step 1: Content keyword scan (highest priority)
|
|
549
|
+
// Scan frontmatter title + first 10 lines, lowercased.
|
|
550
|
+
// Note: a file titled "Comparing Gyre vs Vortex" matches both — first match wins
|
|
551
|
+
// (longest first via _scanCorpusForInitiative). This is a documented trade-off.
|
|
552
|
+
let title = '';
|
|
553
|
+
let firstLines;
|
|
554
|
+
try {
|
|
555
|
+
const parsed = matter(fileContent);
|
|
556
|
+
if (parsed.data && typeof parsed.data.title === 'string') {
|
|
557
|
+
title = parsed.data.title;
|
|
558
|
+
}
|
|
559
|
+
const body = parsed.content || fileContent;
|
|
560
|
+
firstLines = body.split('\n').slice(0, 10).join(' ');
|
|
561
|
+
} catch {
|
|
562
|
+
// Frontmatter parse failed — fall back to raw content
|
|
563
|
+
firstLines = fileContent.split('\n').slice(0, 10).join(' ');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const corpus = `${title} ${firstLines}`.toLowerCase();
|
|
567
|
+
const contentMatch = _scanCorpusForInitiative(corpus, taxonomy);
|
|
568
|
+
if (contentMatch) {
|
|
569
|
+
return { initiative: contentMatch, source: 'content-keyword', confidence: 'medium' };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Step 2: Folder default
|
|
573
|
+
if (Object.prototype.hasOwnProperty.call(FOLDER_DEFAULT_MAP, dirName)) {
|
|
574
|
+
const folderDefault = FOLDER_DEFAULT_MAP[dirName];
|
|
575
|
+
if (folderDefault) {
|
|
576
|
+
return { initiative: folderDefault, source: 'folder-default', confidence: 'low' };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Step 3: Git context (lowest priority, capped to preserve NFR2).
|
|
581
|
+
// Skip git entirely if we don't have a project root to run inside.
|
|
582
|
+
if (!projectRoot) {
|
|
583
|
+
return { initiative: null, source: null, confidence: null };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Cap check: emit a one-time warning when we first hit the cap, then short-circuit.
|
|
587
|
+
if (_gitSuggesterQueryCount >= MAX_GIT_SUGGESTER_QUERIES) {
|
|
588
|
+
if (!_gitSuggesterWarned) {
|
|
589
|
+
_gitSuggesterWarned = true;
|
|
590
|
+
console.warn(
|
|
591
|
+
`Warning: git-context suggester cap reached (${MAX_GIT_SUGGESTER_QUERIES} queries). ` +
|
|
592
|
+
`Remaining ambiguous files will not get git-based suggestions.`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
return { initiative: null, source: null, confidence: null };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
_gitSuggesterQueryCount++;
|
|
599
|
+
try {
|
|
600
|
+
const relPath = path.join('_bmad-output', dirName, filename);
|
|
601
|
+
const raw = execFileSync(
|
|
602
|
+
'git', ['log', '--diff-filter=A', '--format=%s', '--', relPath],
|
|
603
|
+
{ cwd: projectRoot, encoding: 'utf8', stdio: 'pipe' }
|
|
604
|
+
).trim();
|
|
605
|
+
if (raw) {
|
|
606
|
+
const gitMatch = _scanCorpusForInitiative(raw.toLowerCase(), taxonomy);
|
|
607
|
+
if (gitMatch) {
|
|
608
|
+
return { initiative: gitMatch, source: 'git-context', confidence: 'low' };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
} catch {
|
|
612
|
+
// Git unavailable, file not tracked, or other failure — silent
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return { initiative: null, source: null, confidence: null };
|
|
616
|
+
}
|
|
617
|
+
|
|
455
618
|
/**
|
|
456
619
|
* Determine the governance state of a file based on filename convention and frontmatter.
|
|
457
620
|
*
|
|
@@ -760,7 +923,7 @@ async function getCrossReferences(targetFilename, scopeFiles, _projectRoot) {
|
|
|
760
923
|
* @param {string} _projectRoot - Project root (reserved)
|
|
761
924
|
* @returns {Promise<import('./types').ManifestEntry>}
|
|
762
925
|
*/
|
|
763
|
-
async function buildManifestEntry(fileInfo, taxonomy,
|
|
926
|
+
async function buildManifestEntry(fileInfo, taxonomy, projectRoot) {
|
|
764
927
|
const { filename, dir, fullPath } = fileInfo;
|
|
765
928
|
const oldPath = `${dir}/${filename}`;
|
|
766
929
|
|
|
@@ -771,7 +934,8 @@ async function buildManifestEntry(fileInfo, taxonomy, _projectRoot) {
|
|
|
771
934
|
confidence: 'low', source: 'non-markdown', action: 'SKIP',
|
|
772
935
|
dir, contextClues: null, crossReferences: null, candidates: [],
|
|
773
936
|
collisionWith: null, frontmatterInitiative: null, fileInitiative: null,
|
|
774
|
-
typeConfidence: 'low', typeSource: 'none'
|
|
937
|
+
typeConfidence: 'low', typeSource: 'none',
|
|
938
|
+
suggestedInitiative: null, suggestedFrom: null, suggestedConfidence: null
|
|
775
939
|
};
|
|
776
940
|
}
|
|
777
941
|
|
|
@@ -784,7 +948,8 @@ async function buildManifestEntry(fileInfo, taxonomy, _projectRoot) {
|
|
|
784
948
|
confidence: 'low', source: 'unreadable', action: 'AMBIGUOUS',
|
|
785
949
|
dir, contextClues: null, crossReferences: null, candidates: [],
|
|
786
950
|
collisionWith: null, frontmatterInitiative: null, fileInitiative: null,
|
|
787
|
-
typeConfidence: 'low', typeSource: 'none'
|
|
951
|
+
typeConfidence: 'low', typeSource: 'none',
|
|
952
|
+
suggestedInitiative: null, suggestedFrom: null, suggestedConfidence: null
|
|
788
953
|
};
|
|
789
954
|
}
|
|
790
955
|
|
|
@@ -811,15 +976,29 @@ async function buildManifestEntry(fileInfo, taxonomy, _projectRoot) {
|
|
|
811
976
|
candidates: govState.candidates || [],
|
|
812
977
|
collisionWith: null,
|
|
813
978
|
frontmatterInitiative: govState.frontmatterInitiative,
|
|
814
|
-
fileInitiative: govState.fileInitiative
|
|
979
|
+
fileInitiative: govState.fileInitiative,
|
|
980
|
+
// Suggestion fields (Story 6.2)
|
|
981
|
+
// - suggestedInitiative/From/Confidence: populated for AMBIGUOUS entries by suggestInitiative()
|
|
982
|
+
// - suggestedNewPath: populated for colliding RENAME entries by suggestDifferentiator()
|
|
983
|
+
// (set to null here so consumers always see a defined field)
|
|
984
|
+
suggestedInitiative: null,
|
|
985
|
+
suggestedFrom: null,
|
|
986
|
+
suggestedConfidence: null,
|
|
987
|
+
suggestedNewPath: null
|
|
815
988
|
};
|
|
816
989
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
990
|
+
// For ambiguous/ungoverned entries, layer a suggestion on top via suggestInitiative.
|
|
991
|
+
// The action stays AMBIGUOUS — the operator must still confirm. This is guidance, not auto-resolution.
|
|
992
|
+
if (govState.state === 'ungoverned' || govState.state === 'ambiguous') {
|
|
993
|
+
const suggestion = suggestInitiative(filename, dir, fileContent, taxonomy, projectRoot);
|
|
994
|
+
return {
|
|
995
|
+
...base,
|
|
996
|
+
newPath: null,
|
|
997
|
+
action: 'AMBIGUOUS',
|
|
998
|
+
suggestedInitiative: suggestion.initiative,
|
|
999
|
+
suggestedFrom: suggestion.source,
|
|
1000
|
+
suggestedConfidence: suggestion.confidence
|
|
1001
|
+
};
|
|
823
1002
|
}
|
|
824
1003
|
|
|
825
1004
|
if (govState.state === 'invalid-governed') {
|
|
@@ -851,6 +1030,111 @@ async function buildManifestEntry(fileInfo, taxonomy, _projectRoot) {
|
|
|
851
1030
|
return { ...base, newPath, action: 'RENAME' };
|
|
852
1031
|
}
|
|
853
1032
|
|
|
1033
|
+
/**
|
|
1034
|
+
* Suggest a differentiator suffix for two source filenames colliding on the same target.
|
|
1035
|
+
* Strategy:
|
|
1036
|
+
* 1. Strip date suffix from sources and target
|
|
1037
|
+
* 2. For each source, find the longest unique segment chain that the OTHER sources don't share
|
|
1038
|
+
* 3. Insert that differentiator into the target before the date suffix
|
|
1039
|
+
*
|
|
1040
|
+
* Returns null if sources are too similar to differentiate (e.g., exact duplicates).
|
|
1041
|
+
*
|
|
1042
|
+
* @param {string[]} sourcePaths - List of colliding source paths (e.g., 'vortex-artifacts/lean-persona-strategic-navigator-2026-04-04.md')
|
|
1043
|
+
* @param {string} targetPath - The collision target (e.g., 'vortex-artifacts/helm-lean-persona-2026-04-04.md')
|
|
1044
|
+
* @returns {Map<string, string|null>} Map of sourcePath → suggestedNewPath (or null)
|
|
1045
|
+
*/
|
|
1046
|
+
function suggestDifferentiator(sourcePaths, targetPath) {
|
|
1047
|
+
const result = new Map();
|
|
1048
|
+
|
|
1049
|
+
// Filter out sentinel entries like '(existing) ...' that aren't real source files
|
|
1050
|
+
const realSources = sourcePaths.filter(s => !s.startsWith('(existing) '));
|
|
1051
|
+
if (realSources.length < 2) {
|
|
1052
|
+
for (const s of sourcePaths) result.set(s, null);
|
|
1053
|
+
return result;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Parse target: directory + stem + date + ext
|
|
1057
|
+
const targetMatch = targetPath.match(/^(.*\/)?([^/]+?)(-(\d{4}-\d{2}-\d{2}))?\.(md|yaml)$/);
|
|
1058
|
+
if (!targetMatch) {
|
|
1059
|
+
for (const s of sourcePaths) result.set(s, null);
|
|
1060
|
+
return result;
|
|
1061
|
+
}
|
|
1062
|
+
const targetDir = targetMatch[1] || '';
|
|
1063
|
+
const targetStem = targetMatch[2];
|
|
1064
|
+
const targetDate = targetMatch[4] || '';
|
|
1065
|
+
const targetExt = targetMatch[5];
|
|
1066
|
+
|
|
1067
|
+
// For each real source, extract segments that are not in the target stem.
|
|
1068
|
+
// Then verify uniqueness against the other sources.
|
|
1069
|
+
const sourceData = realSources.map(srcPath => {
|
|
1070
|
+
const srcMatch = srcPath.match(/^(.*\/)?([^/]+?)(-(\d{4}-\d{2}-\d{2}))?\.(md|yaml)$/);
|
|
1071
|
+
if (!srcMatch) return { srcPath, segments: [] };
|
|
1072
|
+
const srcStem = srcMatch[2];
|
|
1073
|
+
const srcSegments = srcStem.split('-');
|
|
1074
|
+
const targetSegments = new Set(targetStem.split('-'));
|
|
1075
|
+
// Keep only segments not present in the target stem
|
|
1076
|
+
const unique = srcSegments.filter(s => !targetSegments.has(s));
|
|
1077
|
+
return { srcPath, segments: unique, srcStem };
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// Cross-check: each source's segments must also distinguish it from OTHER sources
|
|
1081
|
+
for (const { srcPath, segments } of sourceData) {
|
|
1082
|
+
const otherSegSets = sourceData
|
|
1083
|
+
.filter(d => d.srcPath !== srcPath)
|
|
1084
|
+
.map(d => new Set(d.segments));
|
|
1085
|
+
|
|
1086
|
+
// Find segments that are unique to THIS source (not in any other source's segments)
|
|
1087
|
+
const uniqueToMe = segments.filter(seg => otherSegSets.every(other => !other.has(seg)));
|
|
1088
|
+
|
|
1089
|
+
if (uniqueToMe.length === 0) {
|
|
1090
|
+
// No distinguishing segments — can't differentiate
|
|
1091
|
+
result.set(srcPath, null);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Build the differentiator (join unique segments)
|
|
1096
|
+
const differentiator = uniqueToMe.join('-');
|
|
1097
|
+
|
|
1098
|
+
// Construct the suggested new path: targetDir + targetStem + '-' + differentiator + dateSuffix + ext
|
|
1099
|
+
const dateSuffix = targetDate ? `-${targetDate}` : '';
|
|
1100
|
+
const suggestedFilename = `${targetStem}-${differentiator}${dateSuffix}.${targetExt}`;
|
|
1101
|
+
const suggestedNewPath = `${targetDir}${suggestedFilename}`;
|
|
1102
|
+
result.set(srcPath, suggestedNewPath);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Edge case: if the suggested new paths themselves collide, append a numeric suffix.
|
|
1106
|
+
// First pass: count duplicates. Second pass: rename ALL duplicates (not just 2nd+).
|
|
1107
|
+
// Uses the same greedy regex as the source/target parser above to avoid the
|
|
1108
|
+
// lazy-`.*?` empty-stem bug.
|
|
1109
|
+
const dupCounts = new Map();
|
|
1110
|
+
for (const suggested of result.values()) {
|
|
1111
|
+
if (!suggested) continue;
|
|
1112
|
+
dupCounts.set(suggested, (dupCounts.get(suggested) || 0) + 1);
|
|
1113
|
+
}
|
|
1114
|
+
const assignedSuffix = new Map(); // suggested → next index to assign
|
|
1115
|
+
for (const [src, suggested] of result) {
|
|
1116
|
+
if (!suggested) continue;
|
|
1117
|
+
if ((dupCounts.get(suggested) || 0) < 2) continue; // not a dup, skip
|
|
1118
|
+
const idx = (assignedSuffix.get(suggested) || 0) + 1;
|
|
1119
|
+
assignedSuffix.set(suggested, idx);
|
|
1120
|
+
const reMatch = suggested.match(/^(.*\/)?([^/]+?)(-(\d{4}-\d{2}-\d{2}))?\.(md|yaml)$/);
|
|
1121
|
+
if (reMatch) {
|
|
1122
|
+
const dirPrefix = reMatch[1] || '';
|
|
1123
|
+
const stem = reMatch[2];
|
|
1124
|
+
const dateSuffix = reMatch[4] ? `-${reMatch[4]}` : '';
|
|
1125
|
+
const ext = reMatch[5];
|
|
1126
|
+
result.set(src, `${dirPrefix}${stem}-${idx}${dateSuffix}.${ext}`);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Sentinels get null
|
|
1131
|
+
for (const s of sourcePaths) {
|
|
1132
|
+
if (!result.has(s)) result.set(s, null);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return result;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
854
1138
|
/**
|
|
855
1139
|
* Detect target filename collisions in manifest entries.
|
|
856
1140
|
*
|
|
@@ -917,6 +1201,9 @@ async function generateManifest(projectRoot, options = {}) {
|
|
|
917
1201
|
const scopeFiles = await scanArtifactDirs(projectRoot, includeDirs, excludeDirs);
|
|
918
1202
|
const entries = [];
|
|
919
1203
|
|
|
1204
|
+
// Reset the per-run git query counter (Story 6.2 — caps git suggestions to preserve NFR2)
|
|
1205
|
+
_resetGitSuggesterCounter();
|
|
1206
|
+
|
|
920
1207
|
for (const fileInfo of scopeFiles) {
|
|
921
1208
|
const entry = await buildManifestEntry(fileInfo, taxonomy, projectRoot);
|
|
922
1209
|
entries.push(entry);
|
|
@@ -925,9 +1212,12 @@ async function generateManifest(projectRoot, options = {}) {
|
|
|
925
1212
|
// Detect collisions and annotate entries
|
|
926
1213
|
const collisions = detectCollisions(entries);
|
|
927
1214
|
for (const [target, sources] of collisions) {
|
|
1215
|
+
// Compute differentiator suggestions for this collision (Story 6.2)
|
|
1216
|
+
const differentiators = suggestDifferentiator(sources, target);
|
|
928
1217
|
for (const entry of entries) {
|
|
929
1218
|
if (entry.newPath === target && entry.action === 'RENAME') {
|
|
930
1219
|
entry.collisionWith = sources.filter(s => s !== entry.oldPath);
|
|
1220
|
+
entry.suggestedNewPath = differentiators.get(entry.oldPath) || null;
|
|
931
1221
|
}
|
|
932
1222
|
}
|
|
933
1223
|
}
|
|
@@ -991,6 +1281,9 @@ function formatManifest(manifest, options = {}) {
|
|
|
991
1281
|
lines.push(` Type: ${entry.artifactType} (confidence: ${entry.typeConfidence || 'high'}, source: ${entry.typeSource || 'prefix'})`);
|
|
992
1282
|
if (entry.collisionWith && entry.collisionWith.length > 0) {
|
|
993
1283
|
lines.push(` [!] COLLISION: same target as ${entry.collisionWith.join(', ')}`);
|
|
1284
|
+
if (entry.suggestedNewPath) {
|
|
1285
|
+
lines.push(` Suggested rename: ${entry.suggestedNewPath}`);
|
|
1286
|
+
}
|
|
994
1287
|
}
|
|
995
1288
|
break;
|
|
996
1289
|
|
|
@@ -1026,7 +1319,13 @@ function formatManifest(manifest, options = {}) {
|
|
|
1026
1319
|
if (entry.candidates.length > 0) {
|
|
1027
1320
|
lines.push(` Candidates: ${entry.candidates.join(', ')}`);
|
|
1028
1321
|
}
|
|
1029
|
-
|
|
1322
|
+
// Suggestion (Story 6.2): if a default exists, surface it and switch the action label
|
|
1323
|
+
if (entry.suggestedInitiative) {
|
|
1324
|
+
lines.push(` Suggested: ${entry.suggestedInitiative} (source: ${entry.suggestedFrom}, confidence: ${entry.suggestedConfidence})`);
|
|
1325
|
+
lines.push(` REVIEW SUGGESTION: Accept '${entry.suggestedInitiative}' or specify initiative`);
|
|
1326
|
+
} else {
|
|
1327
|
+
lines.push(' ACTION REQUIRED: Specify initiative for this file');
|
|
1328
|
+
}
|
|
1030
1329
|
break;
|
|
1031
1330
|
}
|
|
1032
1331
|
}
|
|
@@ -1396,24 +1695,101 @@ async function promptInitiative(filename, candidates) {
|
|
|
1396
1695
|
}
|
|
1397
1696
|
|
|
1398
1697
|
/**
|
|
1399
|
-
* Resolve ambiguous manifest entries interactively or auto-skip in force mode.
|
|
1698
|
+
* Resolve ambiguous manifest entries interactively, via a resolution map, or auto-skip in force mode.
|
|
1400
1699
|
* Mutates manifest entries in-place.
|
|
1401
1700
|
*
|
|
1701
|
+
* **Caller responsibility:** after this function returns, the caller MUST re-run `detectCollisions()`
|
|
1702
|
+
* on the manifest. Resolution-map and interactive renames may produce target-path collisions that
|
|
1703
|
+
* the original `generateManifest()` did not see. The CLI's `main()` does this; programmatic callers
|
|
1704
|
+
* must do the same.
|
|
1705
|
+
*
|
|
1706
|
+
* Priority order for AMBIGUOUS entries:
|
|
1707
|
+
* 1. Resolution map (Story 6.4) — operator decisions passed via --resolution-file
|
|
1708
|
+
* 2. No-candidates auto-skip — entries the engine couldn't even generate candidates for
|
|
1709
|
+
* 3. Force auto-skip — `--force` flag bypasses interactive prompts
|
|
1710
|
+
* 4. Interactive prompt — fallback
|
|
1711
|
+
*
|
|
1402
1712
|
* @param {import('./types').ManifestResult} manifest - Manifest to resolve
|
|
1403
1713
|
* @param {import('./types').TaxonomyConfig} taxonomy - Taxonomy for filename generation
|
|
1404
1714
|
* @param {string} _projectRoot - Project root (reserved)
|
|
1405
1715
|
* @param {Object} [options={}]
|
|
1406
1716
|
* @param {boolean} [options.force=false] - Auto-skip all ambiguous in force mode
|
|
1717
|
+
* @param {Function} [options.promptFn=promptInitiative] - Prompt function for tests
|
|
1718
|
+
* @param {Object|null} [options.resolutionMap=null] - Pre-loaded resolutions keyed by oldPath.
|
|
1719
|
+
* Each value: `{ action: 'rename'|'skip', initiative?: string }`. Validated by loadResolutionMap()
|
|
1720
|
+
* before being passed in here. When a resolution-map entry exists for an oldPath, it takes
|
|
1721
|
+
* precedence over BOTH the no-candidates auto-skip and the force flag — the operator's
|
|
1722
|
+
* explicit decision wins. If `entry.artifactType` is null (engine couldn't infer type) and the
|
|
1723
|
+
* resolution says rename, falls back to a synthetic 'note' type so generateNewFilename can run.
|
|
1407
1724
|
* @returns {Promise<{resolved: number, skipped: number}>}
|
|
1408
1725
|
*/
|
|
1409
1726
|
async function resolveAmbiguous(manifest, taxonomy, _projectRoot, options = {}) {
|
|
1410
|
-
const { force = false, promptFn = promptInitiative } = options;
|
|
1727
|
+
const { force = false, promptFn = promptInitiative, resolutionMap = null } = options;
|
|
1411
1728
|
let resolved = 0;
|
|
1412
1729
|
let skipped = 0;
|
|
1413
1730
|
|
|
1414
1731
|
for (const entry of manifest.entries) {
|
|
1415
1732
|
if (entry.action !== 'AMBIGUOUS') continue;
|
|
1416
1733
|
|
|
1734
|
+
// Story 6.4: Resolution map takes precedence over all other guards.
|
|
1735
|
+
// Operator decisions passed via --resolution-file are honored even when the engine
|
|
1736
|
+
// would have auto-skipped (no candidates) or when --force is set.
|
|
1737
|
+
if (resolutionMap && Object.prototype.hasOwnProperty.call(resolutionMap, entry.oldPath)) {
|
|
1738
|
+
const resolution = resolutionMap[entry.oldPath];
|
|
1739
|
+
if (resolution.action === 'skip') {
|
|
1740
|
+
entry.action = 'SKIP';
|
|
1741
|
+
entry.source = 'operator';
|
|
1742
|
+
skipped++;
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
if (resolution.action === 'rename') {
|
|
1746
|
+
// Initiative pre-validated against taxonomy by loadResolutionMap()
|
|
1747
|
+
entry.initiative = resolution.initiative;
|
|
1748
|
+
// Fallback type for entries the engine couldn't classify (e.g. bare `notes.md`).
|
|
1749
|
+
// We synthesize 'note' ONLY if the taxonomy actually declares 'note' as an artifact type.
|
|
1750
|
+
// Otherwise we leave the entry as AMBIGUOUS rather than producing a path that downstream
|
|
1751
|
+
// schema validation would reject. The operator can extend taxonomy.artifact_types and re-run.
|
|
1752
|
+
if (!entry.artifactType) {
|
|
1753
|
+
const validTypes = Array.isArray(taxonomy && taxonomy.artifact_types) ? taxonomy.artifact_types : [];
|
|
1754
|
+
if (!validTypes.includes('note')) {
|
|
1755
|
+
console.warn(
|
|
1756
|
+
`Warning: cannot honor resolution for ${entry.oldPath} — no artifact type inferable ` +
|
|
1757
|
+
`and 'note' is not in taxonomy.artifact_types. Entry left as AMBIGUOUS.`
|
|
1758
|
+
);
|
|
1759
|
+
// Skip the rename logic and let the entry fall through to whatever the next guard says.
|
|
1760
|
+
// We don't continue here so the existing no-candidates / force / interactive paths apply.
|
|
1761
|
+
} else {
|
|
1762
|
+
entry.artifactType = 'note';
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
// Only proceed with the rename if we now have a type (either real or synthesized).
|
|
1766
|
+
if (entry.artifactType) {
|
|
1767
|
+
// Guard: entry.dir must be set or the rename target becomes 'undefined/foo.md'.
|
|
1768
|
+
// Derive from oldPath as a safety net.
|
|
1769
|
+
if (!entry.dir) {
|
|
1770
|
+
const lastSlash = entry.oldPath.lastIndexOf('/');
|
|
1771
|
+
entry.dir = lastSlash >= 0 ? entry.oldPath.slice(0, lastSlash) : '';
|
|
1772
|
+
}
|
|
1773
|
+
const filename = entry.oldPath.split('/').pop();
|
|
1774
|
+
const newFilename = generateNewFilename(filename, resolution.initiative, entry.artifactType, taxonomy);
|
|
1775
|
+
entry.newPath = entry.dir ? `${entry.dir}/${newFilename}` : newFilename;
|
|
1776
|
+
entry.action = 'RENAME';
|
|
1777
|
+
entry.confidence = 'high';
|
|
1778
|
+
entry.source = 'operator';
|
|
1779
|
+
resolved++;
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
// Otherwise fall through to the normal AMBIGUOUS handling below.
|
|
1783
|
+
} else {
|
|
1784
|
+
// Unknown action — loadResolutionMap should have caught this. Throw rather than silently
|
|
1785
|
+
// dropping the operator's intent.
|
|
1786
|
+
throw new ArtifactMigrationError(
|
|
1787
|
+
`Resolution map for ${entry.oldPath} has unknown action '${resolution.action}'`,
|
|
1788
|
+
{ phase: 'rename', recoverable: true }
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1417
1793
|
// Non-resolvable: no type or no candidates — auto-skip
|
|
1418
1794
|
if (!entry.artifactType || !entry.candidates || entry.candidates.length === 0) {
|
|
1419
1795
|
entry.action = 'SKIP';
|
|
@@ -1454,6 +1830,134 @@ async function resolveAmbiguous(manifest, taxonomy, _projectRoot, options = {})
|
|
|
1454
1830
|
return { resolved, skipped };
|
|
1455
1831
|
}
|
|
1456
1832
|
|
|
1833
|
+
/**
|
|
1834
|
+
* Load and validate a resolution-map JSON file for use with `resolveAmbiguous()`.
|
|
1835
|
+
*
|
|
1836
|
+
* Expected file shape:
|
|
1837
|
+
* ```json
|
|
1838
|
+
* {
|
|
1839
|
+
* "schemaVersion": 1,
|
|
1840
|
+
* "resolutions": {
|
|
1841
|
+
* "dir/file.md": { "action": "rename", "initiative": "convoke" },
|
|
1842
|
+
* "dir/other.md": { "action": "skip" }
|
|
1843
|
+
* }
|
|
1844
|
+
* }
|
|
1845
|
+
* ```
|
|
1846
|
+
*
|
|
1847
|
+
* Throws an `ArtifactMigrationError` (phase: 'rename', recoverable: true) on any validation
|
|
1848
|
+
* failure with a clear message. Validation is strict — partial files are not accepted.
|
|
1849
|
+
*
|
|
1850
|
+
* @param {string} filePath - Absolute path to the JSON file
|
|
1851
|
+
* @param {import('./types').TaxonomyConfig} taxonomy - Taxonomy for initiative validation
|
|
1852
|
+
* @returns {Object} The validated `resolutions` map (oldPath → { action, initiative? })
|
|
1853
|
+
* @throws {ArtifactMigrationError}
|
|
1854
|
+
*/
|
|
1855
|
+
function loadResolutionMap(filePath, taxonomy) {
|
|
1856
|
+
if (!fs.existsSync(filePath)) {
|
|
1857
|
+
throw new ArtifactMigrationError(
|
|
1858
|
+
`Resolution file not found: ${filePath}`,
|
|
1859
|
+
{ phase: 'rename', recoverable: true }
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
let raw;
|
|
1864
|
+
try {
|
|
1865
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
1866
|
+
} catch (err) {
|
|
1867
|
+
throw new ArtifactMigrationError(
|
|
1868
|
+
`Cannot read resolution file ${filePath}: ${err.message}`,
|
|
1869
|
+
{ phase: 'rename', recoverable: true }
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
let parsed;
|
|
1874
|
+
try {
|
|
1875
|
+
parsed = JSON.parse(raw);
|
|
1876
|
+
} catch (err) {
|
|
1877
|
+
throw new ArtifactMigrationError(
|
|
1878
|
+
`Invalid JSON in resolution file ${filePath}: ${err.message}`,
|
|
1879
|
+
{ phase: 'rename', recoverable: true }
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
1884
|
+
throw new ArtifactMigrationError(
|
|
1885
|
+
`Resolution file must contain a JSON object: ${filePath}`,
|
|
1886
|
+
{ phase: 'rename', recoverable: true }
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
if (parsed.schemaVersion !== 1) {
|
|
1891
|
+
throw new ArtifactMigrationError(
|
|
1892
|
+
`Unsupported schemaVersion ${parsed.schemaVersion} in ${filePath} (expected 1)`,
|
|
1893
|
+
{ phase: 'rename', recoverable: true }
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
if (!parsed.resolutions || typeof parsed.resolutions !== 'object') {
|
|
1898
|
+
throw new ArtifactMigrationError(
|
|
1899
|
+
`Resolution file ${filePath} missing required 'resolutions' object`,
|
|
1900
|
+
{ phase: 'rename', recoverable: true }
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const validInitiatives = new Set([
|
|
1905
|
+
...(Array.isArray(taxonomy && taxonomy.initiatives && taxonomy.initiatives.platform) ? taxonomy.initiatives.platform : []),
|
|
1906
|
+
...(Array.isArray(taxonomy && taxonomy.initiatives && taxonomy.initiatives.user) ? taxonomy.initiatives.user : [])
|
|
1907
|
+
]);
|
|
1908
|
+
|
|
1909
|
+
// Use Object.create(null) for the output map so consumers can do plain `map[key]`
|
|
1910
|
+
// lookups without prototype pollution risk. We still validate the keys themselves.
|
|
1911
|
+
const safeMap = Object.create(null);
|
|
1912
|
+
|
|
1913
|
+
for (const [oldPath, resolution] of Object.entries(parsed.resolutions)) {
|
|
1914
|
+
// Reject keys that could pollute the prototype chain or are otherwise unsafe.
|
|
1915
|
+
if (oldPath === '__proto__' || oldPath === 'constructor' || oldPath === 'prototype') {
|
|
1916
|
+
throw new ArtifactMigrationError(
|
|
1917
|
+
`Unsafe resolution key '${oldPath}' rejected`,
|
|
1918
|
+
{ phase: 'rename', recoverable: true }
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
if (typeof oldPath !== 'string' || oldPath.length === 0) {
|
|
1922
|
+
throw new ArtifactMigrationError(
|
|
1923
|
+
`Resolution keys must be non-empty strings`,
|
|
1924
|
+
{ phase: 'rename', recoverable: true }
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
if (!resolution || typeof resolution !== 'object') {
|
|
1928
|
+
throw new ArtifactMigrationError(
|
|
1929
|
+
`Invalid resolution entry for ${oldPath}: must be an object`,
|
|
1930
|
+
{ phase: 'rename', recoverable: true }
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
if (resolution.action !== 'rename' && resolution.action !== 'skip') {
|
|
1934
|
+
throw new ArtifactMigrationError(
|
|
1935
|
+
`Invalid action '${resolution.action}' for ${oldPath} (expected 'rename' or 'skip')`,
|
|
1936
|
+
{ phase: 'rename', recoverable: true }
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
if (resolution.action === 'rename') {
|
|
1940
|
+
if (typeof resolution.initiative !== 'string' || resolution.initiative.length === 0) {
|
|
1941
|
+
throw new ArtifactMigrationError(
|
|
1942
|
+
`Resolution for ${oldPath} has action='rename' but no initiative`,
|
|
1943
|
+
{ phase: 'rename', recoverable: true }
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
if (!validInitiatives.has(resolution.initiative)) {
|
|
1947
|
+
throw new ArtifactMigrationError(
|
|
1948
|
+
`Unknown initiative '${resolution.initiative}' for ${oldPath} (not in taxonomy)`,
|
|
1949
|
+
{ phase: 'rename', recoverable: true }
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
safeMap[oldPath] = { action: 'rename', initiative: resolution.initiative };
|
|
1953
|
+
} else {
|
|
1954
|
+
safeMap[oldPath] = { action: 'skip' };
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
return safeMap;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1457
1961
|
/**
|
|
1458
1962
|
* Generate artifact-rename-map.md content as a markdown table.
|
|
1459
1963
|
*
|
|
@@ -1647,6 +2151,9 @@ module.exports = {
|
|
|
1647
2151
|
ARTIFACT_TYPE_ALIASES,
|
|
1648
2152
|
inferArtifactType,
|
|
1649
2153
|
inferInitiative,
|
|
2154
|
+
suggestInitiative,
|
|
2155
|
+
suggestDifferentiator,
|
|
2156
|
+
FOLDER_DEFAULT_MAP,
|
|
1650
2157
|
getGovernanceState,
|
|
1651
2158
|
generateNewFilename,
|
|
1652
2159
|
// Git
|
|
@@ -1667,6 +2174,7 @@ module.exports = {
|
|
|
1667
2174
|
// Interactive & Recovery
|
|
1668
2175
|
promptInitiative,
|
|
1669
2176
|
resolveAmbiguous,
|
|
2177
|
+
loadResolutionMap,
|
|
1670
2178
|
generateRenameMap,
|
|
1671
2179
|
detectMigrationState,
|
|
1672
2180
|
generateGovernanceADR,
|