mustflow 2.114.6 → 2.114.11
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/dist/cli/commands/skill.js +35 -1
- package/dist/cli/lib/validation/index.js +112 -0
- package/dist/core/check-issues.js +13 -1
- package/dist/core/skill-route-resolution.js +242 -9
- package/package.json +1 -1
- package/schemas/README.md +3 -3
- package/schemas/skill-route-report.schema.json +110 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +54 -0
- package/templates/default/manifest.toml +1 -1
|
@@ -111,7 +111,29 @@ function createSkillRouteScriptPackSuggestions(mustflowRoot, report, paths) {
|
|
|
111
111
|
issues: suggestionReport.issues,
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
function routeDependencyReason(candidate) {
|
|
115
|
+
return candidate.selection_reasons.find((reason) => reason.startsWith('route_dependency:')) ?? null;
|
|
116
|
+
}
|
|
117
|
+
function conflictHintCandidates(report) {
|
|
118
|
+
const candidates = [
|
|
119
|
+
...(report.selected.main ? [report.selected.main] : []),
|
|
120
|
+
...report.selected.adjuncts,
|
|
121
|
+
...report.candidates,
|
|
122
|
+
];
|
|
123
|
+
const seenSkills = new Set();
|
|
124
|
+
return candidates.filter((candidate) => {
|
|
125
|
+
if (seenSkills.has(candidate.skill) || candidate.route_card.route_dependencies.conflicts_with.length === 0) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
seenSkills.add(candidate.skill);
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
114
132
|
function renderSkillRouteReport(report, lang, warnings = []) {
|
|
133
|
+
const dependencyReads = report.selected.adjuncts
|
|
134
|
+
.map((candidate) => ({ candidate, reason: routeDependencyReason(candidate) }))
|
|
135
|
+
.filter((entry) => entry.reason !== null);
|
|
136
|
+
const conflictHints = conflictHintCandidates(report);
|
|
115
137
|
const lines = [
|
|
116
138
|
'mustflow skill route',
|
|
117
139
|
`${t(lang, 'label.mustflowRoot')}: ${resolveMustflowRoot()}`,
|
|
@@ -125,7 +147,19 @@ function renderSkillRouteReport(report, lang, warnings = []) {
|
|
|
125
147
|
}
|
|
126
148
|
else {
|
|
127
149
|
for (const candidate of report.candidates) {
|
|
128
|
-
lines.push(`- ${candidate.skill} (${candidate.route_type}, score ${candidate.score.toFixed(1)})`, ` path: ${candidate.skill_path}`, ` reasons: ${candidate.selection_reasons.join(', ') || t(lang, 'value.none')}`);
|
|
150
|
+
lines.push(`- ${candidate.skill} (${candidate.route_type}, score ${candidate.score.toFixed(1)})`, ` path: ${candidate.skill_path}`, ` reasons: ${candidate.selection_reasons.join(', ') || t(lang, 'value.none')}`, ` matched_dimensions: ${candidate.matched_dimensions.join(', ') || t(lang, 'value.none')}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (dependencyReads.length > 0) {
|
|
154
|
+
lines.push('', 'Dependency reads');
|
|
155
|
+
for (const { candidate, reason } of dependencyReads) {
|
|
156
|
+
lines.push(`- ${candidate.skill}`, ` reason: ${reason}`, ` path: ${candidate.skill_path}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (conflictHints.length > 0) {
|
|
160
|
+
lines.push('', 'Conflict hints');
|
|
161
|
+
for (const candidate of conflictHints) {
|
|
162
|
+
lines.push(`- ${candidate.skill}`, ` conflicts_with: ${candidate.route_card.route_dependencies.conflicts_with.join(', ')}`, ` path: ${candidate.skill_path}`);
|
|
129
163
|
}
|
|
130
164
|
}
|
|
131
165
|
lines.push('', 'Read plan', ...report.read_plan.selected_skill_paths.map((skillPath) => `- read selected skill: ${skillPath}`), `- avoid by default: ${report.read_plan.avoid_by_default.join(', ') || t(lang, 'value.none')}`, `- fallback route metadata: ${report.read_plan.fallback_route_metadata.path}`, '', 'Source files', ...report.source_files.map((sourceFile) => `- ${sourceFile}`));
|
|
@@ -683,6 +683,63 @@ function readOptionalStringArray(value, label, issues) {
|
|
|
683
683
|
}
|
|
684
684
|
return value.map((entry) => entry.trim());
|
|
685
685
|
}
|
|
686
|
+
function readOptionalSlugArray(value, label, issues) {
|
|
687
|
+
const values = readOptionalStringArray(value, label, issues);
|
|
688
|
+
for (const value of values) {
|
|
689
|
+
if (!/^[a-z][a-z0-9_-]*$/u.test(value)) {
|
|
690
|
+
pushStrictIssue(issues, `${label} entry "${value}" must use lowercase slug text`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return values;
|
|
694
|
+
}
|
|
695
|
+
function readSkillRouteMetadataContexts(value, label, issues) {
|
|
696
|
+
if (value !== undefined && !isRecord(value)) {
|
|
697
|
+
pushStrictIssue(issues, `${label}.contexts must be a TOML table`);
|
|
698
|
+
}
|
|
699
|
+
const contexts = isRecord(value) ? value : {};
|
|
700
|
+
return {
|
|
701
|
+
fileTypes: readOptionalSlugArray(contexts.file_types, `${label}.contexts.file_types`, issues),
|
|
702
|
+
frameworks: readOptionalSlugArray(contexts.frameworks, `${label}.contexts.frameworks`, issues),
|
|
703
|
+
layers: readOptionalSlugArray(contexts.layers, `${label}.contexts.layers`, issues),
|
|
704
|
+
patternCategories: readOptionalSlugArray(contexts.pattern_categories, `${label}.contexts.pattern_categories`, issues),
|
|
705
|
+
positiveTerms: readOptionalSlugArray(contexts.positive_terms, `${label}.contexts.positive_terms`, issues),
|
|
706
|
+
negativeTerms: readOptionalSlugArray(contexts.negative_terms, `${label}.contexts.negative_terms`, issues),
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function readSkillRouteUnlockRules(value, label, issues) {
|
|
710
|
+
if (value === undefined) {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
if (!Array.isArray(value) || value.some((entry) => !isRecord(entry))) {
|
|
714
|
+
pushStrictIssue(issues, `${label} must be an array of TOML tables`);
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
return value
|
|
718
|
+
.map((entry, index) => {
|
|
719
|
+
const signal = typeof entry.signal === 'string' ? entry.signal.trim() : '';
|
|
720
|
+
const skill = typeof entry.skill === 'string' ? entry.skill.trim() : '';
|
|
721
|
+
if (!/^[a-z][a-z0-9_-]*$/u.test(signal)) {
|
|
722
|
+
pushStrictIssue(issues, `${label}[${index}].signal must use lowercase slug text`);
|
|
723
|
+
}
|
|
724
|
+
if (!/^[a-z][a-z0-9-]*$/u.test(skill)) {
|
|
725
|
+
pushStrictIssue(issues, `${label}[${index}].skill must be a skill folder name`);
|
|
726
|
+
}
|
|
727
|
+
return { signal, skill };
|
|
728
|
+
})
|
|
729
|
+
.filter((entry) => entry.signal && entry.skill);
|
|
730
|
+
}
|
|
731
|
+
function readSkillRouteMetadataDependencies(value, label, issues) {
|
|
732
|
+
if (value !== undefined && !isRecord(value)) {
|
|
733
|
+
pushStrictIssue(issues, `${label}.dependencies must be a TOML table`);
|
|
734
|
+
}
|
|
735
|
+
const dependencies = isRecord(value) ? value : {};
|
|
736
|
+
return {
|
|
737
|
+
requiresSkills: readOptionalSlugArray(dependencies.requires_skills, `${label}.dependencies.requires_skills`, issues),
|
|
738
|
+
suggestsAdjuncts: readOptionalSlugArray(dependencies.suggests_adjuncts, `${label}.dependencies.suggests_adjuncts`, issues),
|
|
739
|
+
conflictsWith: readOptionalSlugArray(dependencies.conflicts_with, `${label}.dependencies.conflicts_with`, issues),
|
|
740
|
+
unlocksOn: readSkillRouteUnlockRules(dependencies.unlocks_on, `${label}.dependencies.unlocks_on`, issues),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
686
743
|
function validateSkillRouteMetadataTable(skillName, route, issues) {
|
|
687
744
|
const label = `${SKILL_ROUTES_METADATA_PATH} routes.${skillName}`;
|
|
688
745
|
const rawCategory = typeof route.category === 'string' ? route.category : undefined;
|
|
@@ -693,6 +750,8 @@ function validateSkillRouteMetadataTable(skillName, route, issues) {
|
|
|
693
750
|
const profiles = readOptionalStringArray(route.profiles, `${label}.profiles`, issues);
|
|
694
751
|
const appliesToReasons = readOptionalStringArray(route.applies_to_reasons, `${label}.applies_to_reasons`, issues);
|
|
695
752
|
const mutuallyExclusiveWith = readOptionalStringArray(route.mutually_exclusive_with, `${label}.mutually_exclusive_with`, issues);
|
|
753
|
+
const contexts = readSkillRouteMetadataContexts(route.contexts, label, issues);
|
|
754
|
+
const dependencies = readSkillRouteMetadataDependencies(route.dependencies, label, issues);
|
|
696
755
|
if (!category) {
|
|
697
756
|
pushStrictIssue(issues, `${label}.category must be one of ${[...ALLOWED_SKILL_ROUTE_CATEGORIES].join(', ')}`);
|
|
698
757
|
}
|
|
@@ -718,6 +777,8 @@ function validateSkillRouteMetadataTable(skillName, route, issues) {
|
|
|
718
777
|
routeType,
|
|
719
778
|
priority: route.priority,
|
|
720
779
|
mutuallyExclusiveWith,
|
|
780
|
+
contexts,
|
|
781
|
+
dependencies,
|
|
721
782
|
};
|
|
722
783
|
}
|
|
723
784
|
function readSkillRouteMetadata(projectRoot, issues) {
|
|
@@ -759,6 +820,16 @@ function readSkillRouteMetadata(projectRoot, issues) {
|
|
|
759
820
|
}
|
|
760
821
|
return metadata;
|
|
761
822
|
}
|
|
823
|
+
function validateAdjunctRouteDependency(metadata, issues, skillName, targetSkill, relation) {
|
|
824
|
+
const targetRoute = metadata.get(targetSkill);
|
|
825
|
+
if (!targetRoute || targetRoute.routeType === 'adjunct') {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
pushStrictIssue(issues, [
|
|
829
|
+
`${SKILL_ROUTES_METADATA_PATH} route "${skillName}" ${relation} route "${targetSkill}"`,
|
|
830
|
+
` must point to an adjunct route, found "${targetRoute.routeType ?? 'unknown'}"`,
|
|
831
|
+
].join(''));
|
|
832
|
+
}
|
|
762
833
|
function validateSkillRouteMetadataAlignment(metadata, routeSkillNames, expectedSkillNames, issues) {
|
|
763
834
|
for (const skillName of routeSkillNames) {
|
|
764
835
|
if (!metadata.has(skillName)) {
|
|
@@ -783,6 +854,47 @@ function validateSkillRouteMetadataAlignment(metadata, routeSkillNames, expected
|
|
|
783
854
|
pushStrictWarning(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" lists "${otherSkillName}" as mutually exclusive but the reverse route does not`);
|
|
784
855
|
}
|
|
785
856
|
}
|
|
857
|
+
for (const requiredSkill of route.dependencies.requiresSkills) {
|
|
858
|
+
if (requiredSkill === skillName) {
|
|
859
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" cannot require itself`);
|
|
860
|
+
}
|
|
861
|
+
else if (!metadata.has(requiredSkill)) {
|
|
862
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" requires unknown route "${requiredSkill}"`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
for (const adjunctSkill of route.dependencies.suggestsAdjuncts) {
|
|
866
|
+
if (adjunctSkill === skillName) {
|
|
867
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" cannot suggest itself as an adjunct`);
|
|
868
|
+
}
|
|
869
|
+
else if (!metadata.has(adjunctSkill)) {
|
|
870
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" suggests unknown adjunct route "${adjunctSkill}"`);
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
validateAdjunctRouteDependency(metadata, issues, skillName, adjunctSkill, 'suggests adjunct');
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
for (const conflictingSkill of route.dependencies.conflictsWith) {
|
|
877
|
+
if (conflictingSkill === skillName) {
|
|
878
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" cannot conflict with itself`);
|
|
879
|
+
}
|
|
880
|
+
else if (!metadata.has(conflictingSkill)) {
|
|
881
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" conflicts with unknown route "${conflictingSkill}"`);
|
|
882
|
+
}
|
|
883
|
+
else if (!metadata.get(conflictingSkill)?.dependencies.conflictsWith.includes(skillName)) {
|
|
884
|
+
pushStrictWarning(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" conflicts with "${conflictingSkill}" but the reverse route does not`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
for (const unlockRule of route.dependencies.unlocksOn) {
|
|
888
|
+
if (unlockRule.skill === skillName) {
|
|
889
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" cannot unlock itself`);
|
|
890
|
+
}
|
|
891
|
+
else if (!metadata.has(unlockRule.skill)) {
|
|
892
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" unlocks unknown route "${unlockRule.skill}" on signal "${unlockRule.signal}"`);
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
validateAdjunctRouteDependency(metadata, issues, skillName, unlockRule.skill, `unlocks on signal "${unlockRule.signal}"`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
786
898
|
}
|
|
787
899
|
}
|
|
788
900
|
function validateSkillIndexRoutes(projectRoot, commandsToml, skillFiles, issues) {
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
const ROUTE_METADATA_UNKNOWN_REFERENCE_PATTERN = new RegExp('^Strict: \\.mustflow\\/skills\\/routes\\.toml route "[^"]+" ' +
|
|
2
|
+
'(?:references unknown mutually exclusive route|requires unknown route|' +
|
|
3
|
+
'suggests unknown adjunct route|conflicts with unknown route|unlocks unknown route) ' +
|
|
4
|
+
'"[^"]+"(?: on signal "[^"]+")?$', 'u');
|
|
5
|
+
const ROUTE_METADATA_INVALID_DEPENDENCY_PATTERN = new RegExp('^Strict: \\.mustflow\\/skills\\/routes\\.toml route "[^"]+" ' +
|
|
6
|
+
'(?:suggests adjunct|unlocks on signal "[^"]+") route "[^"]+" ' +
|
|
7
|
+
'must point to an adjunct route, found "[^"]+"$', 'u');
|
|
1
8
|
const CHECK_ISSUE_ID_RULES = [
|
|
2
9
|
['mustflow.command_contract.intent_table_missing', /^Missing \[intents\] table in \.mustflow\/config\/commands\.toml$/u],
|
|
3
10
|
['mustflow.command_contract.intent_not_table', /^Intent [^\s]+ must be a TOML table$/u],
|
|
@@ -58,8 +65,13 @@ const CHECK_ISSUE_ID_RULES = [
|
|
|
58
65
|
['mustflow.skill.route_metadata_unlisted', /^Strict: \.mustflow\/skills\/routes\.toml route "[^"]+" is not listed in \.mustflow\/skills\/INDEX\.md$/u],
|
|
59
66
|
['mustflow.skill.route_metadata_missing_document', /^Strict: \.mustflow\/skills\/routes\.toml route "[^"]+" points to a missing skill document$/u],
|
|
60
67
|
['mustflow.skill.route_metadata_category_mismatch', /^Strict: \.mustflow\/skills\/INDEX\.md route "[^"]+" must appear under the .+ category section from \.mustflow\/skills\/routes\.toml$/u],
|
|
61
|
-
[
|
|
68
|
+
[
|
|
69
|
+
'mustflow.skill.route_metadata_unknown_reference',
|
|
70
|
+
ROUTE_METADATA_UNKNOWN_REFERENCE_PATTERN,
|
|
71
|
+
],
|
|
72
|
+
['mustflow.skill.route_metadata_invalid_dependency', ROUTE_METADATA_INVALID_DEPENDENCY_PATTERN],
|
|
62
73
|
['mustflow.skill.route_metadata_asymmetric_exclusion', /^Strict warning: \.mustflow\/skills\/routes\.toml route "[^"]+" lists "[^"]+" as mutually exclusive but the reverse route does not$/u],
|
|
74
|
+
['mustflow.skill.route_metadata_asymmetric_conflict', /^Strict warning: \.mustflow\/skills\/routes\.toml route "[^"]+" conflicts with "[^"]+" but the reverse route does not$/u],
|
|
63
75
|
['mustflow.skill.route_fixture_invalid', /^Strict: \.mustflow\/skills\/route-fixtures\.json (?:must|is not valid JSON|cases\[\d+\])/u],
|
|
64
76
|
['mustflow.skill.route_fixture_mismatch', /^Strict: Skill route fixture "[^"]+" (?:expected|forbids)/u],
|
|
65
77
|
['mustflow.skill.template_profile_empty_category', /^Strict: template profile "[^"]+" (?:skill index category ".+" has no route rows|route category gate references ".+" without route rows)$/u],
|
|
@@ -13,6 +13,10 @@ const DEFAULT_MAX_CANDIDATES = 5;
|
|
|
13
13
|
const DEFAULT_MAX_MAIN = 1;
|
|
14
14
|
const DEFAULT_MAX_ADJUNCTS = 2;
|
|
15
15
|
const PATH_SKILL_HINT_SCORE = 25;
|
|
16
|
+
const PATTERN_SIGNAL_TERM_SCORE = 12;
|
|
17
|
+
const PATTERN_SIGNAL_MAX_SCORE = 48;
|
|
18
|
+
const NEGATIVE_SIGNAL_TERM_PENALTY = -25;
|
|
19
|
+
const NEGATIVE_SIGNAL_MAX_PENALTY = -75;
|
|
16
20
|
const DOCS_TREE_MARKDOWN_PATH_PATTERN = /(?:^|\/)(?:docs|docs-site|documentation|\.mustflow\/docs|\.mustflow\/context)\/.+\.(?:md|mdx)$/u;
|
|
17
21
|
const ROOT_DOCUMENT_BASENAMES = [
|
|
18
22
|
'readme',
|
|
@@ -33,6 +37,12 @@ const ROOT_DOCUMENT_BASENAMES = [
|
|
|
33
37
|
'architecture',
|
|
34
38
|
'api',
|
|
35
39
|
];
|
|
40
|
+
const EMPTY_ROUTE_DEPENDENCIES = {
|
|
41
|
+
requires_skills: [],
|
|
42
|
+
suggests_adjuncts: [],
|
|
43
|
+
conflicts_with: [],
|
|
44
|
+
unlocks_on: [],
|
|
45
|
+
};
|
|
36
46
|
const ROUTE_TYPE_WEIGHTS = {
|
|
37
47
|
primary: 18,
|
|
38
48
|
authoring: 16,
|
|
@@ -101,6 +111,46 @@ function readStringArrayFromTable(table, key) {
|
|
|
101
111
|
? value.map((entry) => entry.trim()).filter(Boolean)
|
|
102
112
|
: [];
|
|
103
113
|
}
|
|
114
|
+
function readRouteSignalProfile(route) {
|
|
115
|
+
const contexts = route.contexts;
|
|
116
|
+
if (!isRecord(contexts)) {
|
|
117
|
+
return {
|
|
118
|
+
positiveTerms: [],
|
|
119
|
+
negativeTerms: [],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
positiveTerms: tokenize(readStringArrayFromTable(contexts, 'positive_terms').join(' ')),
|
|
124
|
+
negativeTerms: tokenize(readStringArrayFromTable(contexts, 'negative_terms').join(' ')),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function readRouteUnlockRules(table) {
|
|
128
|
+
const value = table.unlocks_on;
|
|
129
|
+
if (!Array.isArray(value)) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
return value
|
|
133
|
+
.filter(isRecord)
|
|
134
|
+
.map((entry) => {
|
|
135
|
+
return {
|
|
136
|
+
signal: typeof entry.signal === 'string' ? entry.signal.trim() : '',
|
|
137
|
+
skill: typeof entry.skill === 'string' ? entry.skill.trim() : '',
|
|
138
|
+
};
|
|
139
|
+
})
|
|
140
|
+
.filter((entry) => entry.signal && entry.skill);
|
|
141
|
+
}
|
|
142
|
+
function readRouteDependencies(route) {
|
|
143
|
+
const dependencies = route.dependencies;
|
|
144
|
+
if (!isRecord(dependencies)) {
|
|
145
|
+
return EMPTY_ROUTE_DEPENDENCIES;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
requires_skills: readStringArrayFromTable(dependencies, 'requires_skills'),
|
|
149
|
+
suggests_adjuncts: readStringArrayFromTable(dependencies, 'suggests_adjuncts'),
|
|
150
|
+
conflicts_with: readStringArrayFromTable(dependencies, 'conflicts_with'),
|
|
151
|
+
unlocks_on: readRouteUnlockRules(dependencies),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
104
154
|
function readFrontmatterBlock(content) {
|
|
105
155
|
if (!content.startsWith('---')) {
|
|
106
156
|
return [];
|
|
@@ -174,6 +224,8 @@ function readSkillRouteMetadata(projectRoot) {
|
|
|
174
224
|
priority: Number.isInteger(route.priority) ? Number(route.priority) : 0,
|
|
175
225
|
appliesToReasons: readStringArrayFromTable(route, 'applies_to_reasons'),
|
|
176
226
|
mutuallyExclusiveWith: readStringArrayFromTable(route, 'mutually_exclusive_with'),
|
|
227
|
+
signalProfile: readRouteSignalProfile(route),
|
|
228
|
+
dependencies: readRouteDependencies(route),
|
|
177
229
|
});
|
|
178
230
|
}
|
|
179
231
|
}
|
|
@@ -271,6 +323,10 @@ function countMatches(needles, haystack) {
|
|
|
271
323
|
const haystackSet = new Set(haystack);
|
|
272
324
|
return needles.filter((needle) => haystackSet.has(needle)).length;
|
|
273
325
|
}
|
|
326
|
+
function collectMatchedTerms(needles, haystack) {
|
|
327
|
+
const haystackSet = new Set(haystack);
|
|
328
|
+
return [...new Set(needles.filter((needle) => haystackSet.has(needle)))].sort((left, right) => left.localeCompare(right));
|
|
329
|
+
}
|
|
274
330
|
function routeTextTerms(route, skillName) {
|
|
275
331
|
return tokenize([
|
|
276
332
|
skillName,
|
|
@@ -282,6 +338,65 @@ function routeTextTerms(route, skillName) {
|
|
|
282
338
|
route.skillPath,
|
|
283
339
|
].join(' '));
|
|
284
340
|
}
|
|
341
|
+
function createExcerptReference(skillPath, section) {
|
|
342
|
+
return {
|
|
343
|
+
source_path: skillPath,
|
|
344
|
+
section,
|
|
345
|
+
read_when: [
|
|
346
|
+
'candidate scores tie within the same route category',
|
|
347
|
+
'matched dimensions are too broad to choose a single skill',
|
|
348
|
+
'task language overlaps a listed negative or conflicting signal',
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function createRouteCard(skillPath, matchedDimensions, routeDependencies) {
|
|
353
|
+
return {
|
|
354
|
+
source: 'route_metadata_and_skill_frontmatter',
|
|
355
|
+
index_read_policy: 'fallback_only',
|
|
356
|
+
compact_fields: [
|
|
357
|
+
'skill',
|
|
358
|
+
'skill_path',
|
|
359
|
+
'trigger',
|
|
360
|
+
'category',
|
|
361
|
+
'route_type',
|
|
362
|
+
'priority',
|
|
363
|
+
'applies_to_reasons',
|
|
364
|
+
'score_breakdown',
|
|
365
|
+
'selection_reasons',
|
|
366
|
+
'verification_intents',
|
|
367
|
+
'route_dependencies',
|
|
368
|
+
],
|
|
369
|
+
matched_dimensions: matchedDimensions,
|
|
370
|
+
route_dependencies: routeDependencies,
|
|
371
|
+
use_when_excerpt: createExcerptReference(skillPath, 'use-when'),
|
|
372
|
+
do_not_use_excerpt: createExcerptReference(skillPath, 'do-not-use-when'),
|
|
373
|
+
read_strategy: [
|
|
374
|
+
'Read the selected SKILL.md before editing matching scope.',
|
|
375
|
+
'For close ties, compare only Use When and Do Not Use When excerpts before loading full competing skills.',
|
|
376
|
+
'Use route_dependencies to add required or suggested adjunct skill reads without loading the expanded index.',
|
|
377
|
+
'Keep .mustflow/skills/INDEX.md out of the prompt unless route metadata and excerpts are insufficient.',
|
|
378
|
+
],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function createPatternSignalBreakdown(signalProfile, taskTerms, pathTerms) {
|
|
382
|
+
if (signalProfile.positiveTerms.length === 0 && signalProfile.negativeTerms.length === 0) {
|
|
383
|
+
return {
|
|
384
|
+
positiveMatches: [],
|
|
385
|
+
negativeMatches: [],
|
|
386
|
+
patternScore: 0,
|
|
387
|
+
negativePenalty: 0,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const inputTerms = [...new Set([...taskTerms, ...pathTerms])];
|
|
391
|
+
const positiveMatches = collectMatchedTerms(signalProfile.positiveTerms, inputTerms);
|
|
392
|
+
const negativeMatches = collectMatchedTerms(signalProfile.negativeTerms, inputTerms);
|
|
393
|
+
return {
|
|
394
|
+
positiveMatches,
|
|
395
|
+
negativeMatches,
|
|
396
|
+
patternScore: Math.min(positiveMatches.length * PATTERN_SIGNAL_TERM_SCORE, PATTERN_SIGNAL_MAX_SCORE),
|
|
397
|
+
negativePenalty: Math.max(negativeMatches.length * NEGATIVE_SIGNAL_TERM_PENALTY, NEGATIVE_SIGNAL_MAX_PENALTY),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
285
400
|
function createCandidate(route, metadata, taskTerms, pathTerms, pathSkillHints, reasons) {
|
|
286
401
|
const skill = skillNameFromPath(route.skillPath);
|
|
287
402
|
const terms = routeTextTerms(route, skill);
|
|
@@ -289,19 +404,38 @@ function createCandidate(route, metadata, taskTerms, pathTerms, pathSkillHints,
|
|
|
289
404
|
const taskMatches = countMatches(taskTerms, terms);
|
|
290
405
|
const pathMatches = countMatches(pathTerms, terms);
|
|
291
406
|
const pathSkillHintMatched = pathSkillHints.has(skill);
|
|
407
|
+
const patternSignals = createPatternSignalBreakdown(metadata.signalProfile, taskTerms, pathTerms);
|
|
292
408
|
const breakdown = {
|
|
293
409
|
reason_match: matchedReasons.length * 35,
|
|
294
410
|
task_text_match: taskMatches * 6,
|
|
295
411
|
path_match: pathMatches * 6 + (pathSkillHintMatched ? PATH_SKILL_HINT_SCORE : 0),
|
|
412
|
+
pattern_signal_match: patternSignals.patternScore,
|
|
413
|
+
negative_signal_penalty: patternSignals.negativePenalty,
|
|
296
414
|
route_type_weight: ROUTE_TYPE_WEIGHTS[metadata.routeType] ?? 0,
|
|
297
415
|
priority_weight: Math.max(0, Math.min(metadata.priority, 100)) / 5,
|
|
298
416
|
};
|
|
299
417
|
const score = Object.values(breakdown).reduce((total, value) => total + value, 0);
|
|
418
|
+
const matchedDimensions = [
|
|
419
|
+
...(matchedReasons.length > 0 ? ['reason'] : []),
|
|
420
|
+
...(taskMatches > 0 ? ['task_terms'] : []),
|
|
421
|
+
...(pathMatches > 0 ? ['path_terms'] : []),
|
|
422
|
+
...(pathSkillHintMatched ? ['path_skill_hint'] : []),
|
|
423
|
+
...(patternSignals.positiveMatches.length > 0 ? ['pattern_signal'] : []),
|
|
424
|
+
...(patternSignals.negativeMatches.length > 0 ? ['negative_signal'] : []),
|
|
425
|
+
...(metadata.routeType !== 'unknown' ? ['route_type'] : []),
|
|
426
|
+
...(metadata.priority > 0 ? ['priority'] : []),
|
|
427
|
+
];
|
|
300
428
|
const selectionReasons = [
|
|
301
429
|
...matchedReasons.map((reason) => `reason:${reason}`),
|
|
302
430
|
...(taskMatches > 0 ? [`task_terms:${taskMatches}`] : []),
|
|
303
431
|
...(pathMatches > 0 ? [`path_terms:${pathMatches}`] : []),
|
|
304
432
|
...(pathSkillHintMatched ? [`path_skill_hint:${skill}`] : []),
|
|
433
|
+
...(patternSignals.positiveMatches.length > 0
|
|
434
|
+
? [`pattern_terms:${patternSignals.positiveMatches.join('|')}`]
|
|
435
|
+
: []),
|
|
436
|
+
...(patternSignals.negativeMatches.length > 0
|
|
437
|
+
? [`negative_terms:${patternSignals.negativeMatches.join('|')}`]
|
|
438
|
+
: []),
|
|
305
439
|
`route_type:${metadata.routeType}`,
|
|
306
440
|
`priority:${metadata.priority}`,
|
|
307
441
|
];
|
|
@@ -316,6 +450,8 @@ function createCandidate(route, metadata, taskTerms, pathTerms, pathSkillHints,
|
|
|
316
450
|
score,
|
|
317
451
|
score_breakdown: breakdown,
|
|
318
452
|
selection_reasons: selectionReasons,
|
|
453
|
+
matched_dimensions: matchedDimensions,
|
|
454
|
+
route_card: createRouteCard(route.skillPath, matchedDimensions, metadata.dependencies),
|
|
319
455
|
verification_intents: route.commandIntents,
|
|
320
456
|
};
|
|
321
457
|
}
|
|
@@ -333,20 +469,107 @@ function sortCandidates(left, right) {
|
|
|
333
469
|
function isSelectableMain(candidate) {
|
|
334
470
|
return candidate.route_type === 'primary' || candidate.route_type === 'authoring' || candidate.route_type === 'external';
|
|
335
471
|
}
|
|
336
|
-
function
|
|
472
|
+
function hasDependencySignal(signal, dependencySignals, taskTerms, pathTerms) {
|
|
473
|
+
if (dependencySignals.has(signal)) {
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
const inputTerms = new Set([...taskTerms, ...pathTerms]);
|
|
477
|
+
const signalTerms = signal
|
|
478
|
+
.split('_')
|
|
479
|
+
.map((term) => (term === 'changed' ? 'change' : term))
|
|
480
|
+
.filter((term) => term !== 'or');
|
|
481
|
+
return signalTerms.length > 0 && signalTerms.every((term) => inputTerms.has(term));
|
|
482
|
+
}
|
|
483
|
+
function collectDependencySignals(paths, reasons, taskTerms, pathTerms) {
|
|
484
|
+
const dependencySignals = new Set(reasons);
|
|
485
|
+
const inputTerms = new Set([...taskTerms, ...pathTerms]);
|
|
486
|
+
if (inputTerms.has('output') &&
|
|
487
|
+
(inputTerms.has('machine') || inputTerms.has('json') || inputTerms.has('jsonl') || inputTerms.has('cli'))) {
|
|
488
|
+
dependencySignals.add('machine_output_changed');
|
|
489
|
+
}
|
|
490
|
+
if (inputTerms.has('schema') ||
|
|
491
|
+
inputTerms.has('fixture') ||
|
|
492
|
+
paths.some((pathValue) => /(?:^|\/)(?:schemas|fixtures|tests\/fixtures)(?:\/|$)/u.test(pathValue))) {
|
|
493
|
+
dependencySignals.add('schema_or_fixture_changed');
|
|
494
|
+
}
|
|
495
|
+
if (inputTerms.has('followup') ||
|
|
496
|
+
(inputTerms.has('follow') && inputTerms.has('up')) ||
|
|
497
|
+
(inputTerms.has('next') && inputTerms.has('action'))) {
|
|
498
|
+
dependencySignals.add('concrete_followup_exists');
|
|
499
|
+
}
|
|
500
|
+
return dependencySignals;
|
|
501
|
+
}
|
|
502
|
+
function addDependencySelectionReason(candidate, reason) {
|
|
503
|
+
const matchedDimensions = [...new Set([...candidate.matched_dimensions, 'route_dependency'])];
|
|
504
|
+
return {
|
|
505
|
+
...candidate,
|
|
506
|
+
selection_reasons: [...new Set([...candidate.selection_reasons, reason])],
|
|
507
|
+
matched_dimensions: matchedDimensions,
|
|
508
|
+
route_card: {
|
|
509
|
+
...candidate.route_card,
|
|
510
|
+
matched_dimensions: matchedDimensions,
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function routeConflictsFor(candidate, metadata) {
|
|
515
|
+
const routeMetadata = metadata.get(candidate.skill);
|
|
516
|
+
return new Set([
|
|
517
|
+
...(routeMetadata?.mutuallyExclusiveWith ?? []),
|
|
518
|
+
...candidate.route_card.route_dependencies.conflicts_with,
|
|
519
|
+
]);
|
|
520
|
+
}
|
|
521
|
+
function routesConflict(left, right, metadata) {
|
|
522
|
+
return routeConflictsFor(left, metadata).has(right.skill) || routeConflictsFor(right, metadata).has(left.skill);
|
|
523
|
+
}
|
|
524
|
+
function selectAdjuncts(main, scoredCandidates, allCandidatesBySkill, metadata, dependencySignals, taskTerms, pathTerms) {
|
|
337
525
|
if (!main) {
|
|
338
526
|
return [];
|
|
339
527
|
}
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
|
|
528
|
+
const selectedMain = main;
|
|
529
|
+
const mainMetadata = metadata.get(selectedMain.skill);
|
|
530
|
+
const excluded = new Set([
|
|
531
|
+
selectedMain.skill,
|
|
532
|
+
...(mainMetadata?.mutuallyExclusiveWith ?? []),
|
|
533
|
+
...selectedMain.route_card.route_dependencies.conflicts_with,
|
|
534
|
+
]);
|
|
535
|
+
const selected = [];
|
|
536
|
+
function addDependencySkill(skill, reason) {
|
|
537
|
+
const dependencyCandidate = allCandidatesBySkill.get(skill);
|
|
538
|
+
if (!dependencyCandidate || excluded.has(dependencyCandidate.skill)) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if ([selectedMain, ...selected].some((candidate) => routesConflict(candidate, dependencyCandidate, metadata))) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (selected.some((candidate) => candidate.skill === dependencyCandidate.skill)) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
selected.push(addDependencySelectionReason(dependencyCandidate, reason));
|
|
548
|
+
}
|
|
549
|
+
for (const skill of selectedMain.route_card.route_dependencies.requires_skills) {
|
|
550
|
+
addDependencySkill(skill, `route_dependency:requires:${selectedMain.skill}`);
|
|
551
|
+
}
|
|
552
|
+
for (const skill of selectedMain.route_card.route_dependencies.suggests_adjuncts) {
|
|
553
|
+
addDependencySkill(skill, `route_dependency:suggested_by:${selectedMain.skill}`);
|
|
554
|
+
}
|
|
555
|
+
for (const unlockRule of selectedMain.route_card.route_dependencies.unlocks_on) {
|
|
556
|
+
if (hasDependencySignal(unlockRule.signal, dependencySignals, taskTerms, pathTerms)) {
|
|
557
|
+
addDependencySkill(unlockRule.skill, `route_dependency:unlocked_by:${selectedMain.skill}:${unlockRule.signal}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (selected.length >= DEFAULT_MAX_ADJUNCTS) {
|
|
561
|
+
return selected.slice(0, DEFAULT_MAX_ADJUNCTS);
|
|
562
|
+
}
|
|
563
|
+
const scoredAdjuncts = scoredCandidates
|
|
343
564
|
.filter((candidate) => {
|
|
344
565
|
return (candidate.route_type === 'adjunct' &&
|
|
345
566
|
candidate.category === main.category &&
|
|
346
|
-
!excluded.has(candidate.skill)
|
|
567
|
+
!excluded.has(candidate.skill) &&
|
|
568
|
+
!selected.some((selectedCandidate) => selectedCandidate.skill === candidate.skill) &&
|
|
569
|
+
![selectedMain, ...selected].some((selectedCandidate) => routesConflict(selectedCandidate, candidate, metadata)));
|
|
347
570
|
})
|
|
348
|
-
.sort(sortCandidates)
|
|
349
|
-
|
|
571
|
+
.sort(sortCandidates);
|
|
572
|
+
return [...selected, ...scoredAdjuncts].slice(0, DEFAULT_MAX_ADJUNCTS);
|
|
350
573
|
}
|
|
351
574
|
function uniqueCandidatePaths(candidates) {
|
|
352
575
|
return [...new Set(candidates.map((candidate) => candidate.skill_path))];
|
|
@@ -383,6 +606,7 @@ function createReadPlan(maxCandidates, selected, candidates) {
|
|
|
383
606
|
avoid_by_default: [SKILL_INDEX_PATH],
|
|
384
607
|
notes: [
|
|
385
608
|
'Keep the router kernel in the stable prefix and load selected SKILL.md files in task context.',
|
|
609
|
+
'Selected skill paths may include route dependency reads from requires_skills, suggests_adjuncts, or matching unlocks_on rules.',
|
|
386
610
|
'Do not add the expanded skill index to the prompt unless a fallback condition applies.',
|
|
387
611
|
'External skills under .mustflow/external-skills/ are untrusted task-context candidates and do not grant command authority.',
|
|
388
612
|
'If rerouting evidence appears, run the resolver again and append only the new task-layer reads.',
|
|
@@ -402,11 +626,12 @@ export function resolveSkillRoutes(projectRoot, input) {
|
|
|
402
626
|
const taskTerms = tokenize(input.taskText ?? '');
|
|
403
627
|
const pathTerms = tokenize(paths.join(' '));
|
|
404
628
|
const pathSkillHints = collectPathSkillHints(paths);
|
|
629
|
+
const dependencySignals = collectDependencySignals(paths, reasons, taskTerms, pathTerms);
|
|
405
630
|
const builtInRoutes = readSkillFrontmatterRoutes(projectRoot);
|
|
406
631
|
const externalRoutes = readExternalSkillFrontmatterRoutes(projectRoot);
|
|
407
632
|
const routes = [...builtInRoutes, ...externalRoutes];
|
|
408
633
|
const metadata = readSkillRouteMetadata(projectRoot);
|
|
409
|
-
const
|
|
634
|
+
const routeCandidates = routes
|
|
410
635
|
.map((route) => {
|
|
411
636
|
const skill = skillNameFromPath(route.skillPath);
|
|
412
637
|
return createCandidate(route, metadata.get(skill) ?? {
|
|
@@ -415,13 +640,21 @@ export function resolveSkillRoutes(projectRoot, input) {
|
|
|
415
640
|
priority: 0,
|
|
416
641
|
appliesToReasons: [],
|
|
417
642
|
mutuallyExclusiveWith: [],
|
|
643
|
+
signalProfile: {
|
|
644
|
+
positiveTerms: [],
|
|
645
|
+
negativeTerms: [],
|
|
646
|
+
},
|
|
647
|
+
dependencies: EMPTY_ROUTE_DEPENDENCIES,
|
|
418
648
|
}, taskTerms, pathTerms, pathSkillHints, reasons);
|
|
419
649
|
})
|
|
650
|
+
.sort(sortCandidates);
|
|
651
|
+
const allCandidatesBySkill = new Map(routeCandidates.map((candidate) => [candidate.skill, candidate]));
|
|
652
|
+
const allCandidates = routeCandidates
|
|
420
653
|
.filter((candidate) => candidate.score > 0)
|
|
421
654
|
.sort(sortCandidates);
|
|
422
655
|
const candidates = allCandidates.slice(0, maxCandidates);
|
|
423
656
|
const main = candidates.find(isSelectableMain) ?? null;
|
|
424
|
-
const adjuncts = selectAdjuncts(main, candidates, metadata);
|
|
657
|
+
const adjuncts = selectAdjuncts(main, candidates, allCandidatesBySkill, metadata, dependencySignals, taskTerms, pathTerms);
|
|
425
658
|
const selected = {
|
|
426
659
|
main,
|
|
427
660
|
adjuncts,
|
package/package.json
CHANGED
package/schemas/README.md
CHANGED
|
@@ -189,9 +189,9 @@ Current schemas:
|
|
|
189
189
|
candidates from direct imports, importers, same-basename siblings, parent configuration files, and
|
|
190
190
|
package boundaries for source-oriented repository navigation
|
|
191
191
|
- `skill-route-report.schema.json`: output of `mf skill route --json`, containing compact route
|
|
192
|
-
candidates, selected main and adjunct skills, score breakdowns, route
|
|
193
|
-
shards, and optional read-only script-pack helper suggestions without
|
|
194
|
-
or replacing selected `SKILL.md` reads
|
|
192
|
+
candidates, selected main and adjunct skills, score breakdowns, route cards with dependencies,
|
|
193
|
+
route read plans, source route shards, and optional read-only script-pack helper suggestions without
|
|
194
|
+
granting command authority or replacing selected `SKILL.md` reads
|
|
195
195
|
- `skill-import-report.schema.json`: output of `mf skill import <github-url> --json`, containing
|
|
196
196
|
GitHub source provenance, target `.mustflow/external-skills/<name>/` paths, imported file hashes,
|
|
197
197
|
warnings for inert external scripts, rejection issues, and whether files were written
|
|
@@ -350,6 +350,8 @@
|
|
|
350
350
|
"reason_match": { "type": "number" },
|
|
351
351
|
"task_text_match": { "type": "number" },
|
|
352
352
|
"path_match": { "type": "number" },
|
|
353
|
+
"pattern_signal_match": { "type": "number" },
|
|
354
|
+
"negative_signal_penalty": { "type": "number" },
|
|
353
355
|
"route_type_weight": { "type": "number" },
|
|
354
356
|
"priority_weight": { "type": "number" }
|
|
355
357
|
}
|
|
@@ -358,12 +360,120 @@
|
|
|
358
360
|
"type": "array",
|
|
359
361
|
"items": { "type": "string" }
|
|
360
362
|
},
|
|
363
|
+
"matched_dimensions": {
|
|
364
|
+
"type": "array",
|
|
365
|
+
"items": {
|
|
366
|
+
"type": "string",
|
|
367
|
+
"enum": [
|
|
368
|
+
"reason",
|
|
369
|
+
"task_terms",
|
|
370
|
+
"path_terms",
|
|
371
|
+
"path_skill_hint",
|
|
372
|
+
"pattern_signal",
|
|
373
|
+
"negative_signal",
|
|
374
|
+
"route_type",
|
|
375
|
+
"priority"
|
|
376
|
+
]
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
"route_card": {
|
|
380
|
+
"type": "object",
|
|
381
|
+
"additionalProperties": false,
|
|
382
|
+
"required": [
|
|
383
|
+
"source",
|
|
384
|
+
"index_read_policy",
|
|
385
|
+
"compact_fields",
|
|
386
|
+
"matched_dimensions",
|
|
387
|
+
"route_dependencies",
|
|
388
|
+
"use_when_excerpt",
|
|
389
|
+
"do_not_use_excerpt",
|
|
390
|
+
"read_strategy"
|
|
391
|
+
],
|
|
392
|
+
"properties": {
|
|
393
|
+
"source": { "const": "route_metadata_and_skill_frontmatter" },
|
|
394
|
+
"index_read_policy": { "const": "fallback_only" },
|
|
395
|
+
"compact_fields": {
|
|
396
|
+
"type": "array",
|
|
397
|
+
"items": { "type": "string" }
|
|
398
|
+
},
|
|
399
|
+
"matched_dimensions": {
|
|
400
|
+
"type": "array",
|
|
401
|
+
"items": {
|
|
402
|
+
"type": "string",
|
|
403
|
+
"enum": [
|
|
404
|
+
"reason",
|
|
405
|
+
"task_terms",
|
|
406
|
+
"path_terms",
|
|
407
|
+
"path_skill_hint",
|
|
408
|
+
"pattern_signal",
|
|
409
|
+
"negative_signal",
|
|
410
|
+
"route_type",
|
|
411
|
+
"priority"
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
"route_dependencies": { "$ref": "#/$defs/routeDependencies" },
|
|
416
|
+
"use_when_excerpt": { "$ref": "#/$defs/routeExcerptReference" },
|
|
417
|
+
"do_not_use_excerpt": { "$ref": "#/$defs/routeExcerptReference" },
|
|
418
|
+
"read_strategy": {
|
|
419
|
+
"type": "array",
|
|
420
|
+
"items": { "type": "string" },
|
|
421
|
+
"minItems": 1
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
},
|
|
361
425
|
"verification_intents": {
|
|
362
426
|
"type": "array",
|
|
363
427
|
"items": { "type": "string" }
|
|
364
428
|
}
|
|
365
429
|
}
|
|
366
430
|
},
|
|
431
|
+
"routeDependencies": {
|
|
432
|
+
"type": "object",
|
|
433
|
+
"additionalProperties": false,
|
|
434
|
+
"required": ["requires_skills", "suggests_adjuncts", "conflicts_with", "unlocks_on"],
|
|
435
|
+
"properties": {
|
|
436
|
+
"requires_skills": {
|
|
437
|
+
"type": "array",
|
|
438
|
+
"items": { "type": "string" }
|
|
439
|
+
},
|
|
440
|
+
"suggests_adjuncts": {
|
|
441
|
+
"type": "array",
|
|
442
|
+
"items": { "type": "string" }
|
|
443
|
+
},
|
|
444
|
+
"conflicts_with": {
|
|
445
|
+
"type": "array",
|
|
446
|
+
"items": { "type": "string" }
|
|
447
|
+
},
|
|
448
|
+
"unlocks_on": {
|
|
449
|
+
"type": "array",
|
|
450
|
+
"items": { "$ref": "#/$defs/routeUnlockRule" }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
"routeUnlockRule": {
|
|
455
|
+
"type": "object",
|
|
456
|
+
"additionalProperties": false,
|
|
457
|
+
"required": ["signal", "skill"],
|
|
458
|
+
"properties": {
|
|
459
|
+
"signal": { "type": "string" },
|
|
460
|
+
"skill": { "type": "string" }
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
"routeExcerptReference": {
|
|
464
|
+
"type": "object",
|
|
465
|
+
"additionalProperties": false,
|
|
466
|
+
"required": ["source_path", "section", "read_when"],
|
|
467
|
+
"properties": {
|
|
468
|
+
"source_path": { "type": "string" },
|
|
469
|
+
"section": { "type": "string", "enum": ["use-when", "do-not-use-when"] },
|
|
470
|
+
"read_when": {
|
|
471
|
+
"type": "array",
|
|
472
|
+
"items": { "type": "string" },
|
|
473
|
+
"minItems": 1
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
},
|
|
367
477
|
"readPlanFile": {
|
|
368
478
|
"type": "object",
|
|
369
479
|
"additionalProperties": false,
|
|
@@ -48,18 +48,42 @@ route_type = "primary"
|
|
|
48
48
|
priority = 80
|
|
49
49
|
applies_to_reasons = ["code_change"]
|
|
50
50
|
|
|
51
|
+
[routes."composition-over-inheritance".contexts]
|
|
52
|
+
file_types = ["ts", "tsx", "js", "jsx", "py", "go", "rs"]
|
|
53
|
+
layers = ["domain", "service", "model"]
|
|
54
|
+
pattern_categories = ["delegation", "class_hierarchy"]
|
|
55
|
+
positive_terms = ["class", "composition", "delegate", "delegation", "extend", "inheritance", "mixin", "subclass"]
|
|
56
|
+
negative_terms = ["command", "facade", "state", "strategy"]
|
|
57
|
+
|
|
51
58
|
[routes."strategy-pattern"]
|
|
52
59
|
category = "architecture_patterns"
|
|
53
60
|
route_type = "primary"
|
|
54
61
|
priority = 80
|
|
55
62
|
applies_to_reasons = ["code_change"]
|
|
56
63
|
|
|
64
|
+
[routes."strategy-pattern".dependencies]
|
|
65
|
+
conflicts_with = ["state-machine-pattern"]
|
|
66
|
+
|
|
67
|
+
[routes."strategy-pattern".contexts]
|
|
68
|
+
file_types = ["ts", "tsx", "js", "jsx", "py", "go", "rs"]
|
|
69
|
+
layers = ["domain", "service", "policy", "pricing", "permission", "provider_selection"]
|
|
70
|
+
pattern_categories = ["variant_selection", "algorithm_policy", "branch_extraction"]
|
|
71
|
+
positive_terms = ["algorithm", "branch", "discount", "plan", "policy", "pricing", "provider", "region", "selection", "strategy", "switch", "variant"]
|
|
72
|
+
negative_terms = ["adapter", "audit", "history", "idempotency", "lifecycle", "phase", "protocol", "retry", "state", "status", "transaction", "transition"]
|
|
73
|
+
|
|
57
74
|
[routes."command-pattern"]
|
|
58
75
|
category = "architecture_patterns"
|
|
59
76
|
route_type = "primary"
|
|
60
77
|
priority = 80
|
|
61
78
|
applies_to_reasons = ["code_change", "behavior_change"]
|
|
62
79
|
|
|
80
|
+
[routes."command-pattern".contexts]
|
|
81
|
+
file_types = ["ts", "tsx", "js", "jsx", "py", "go", "rs"]
|
|
82
|
+
layers = ["application", "service", "workflow", "queue", "mutation"]
|
|
83
|
+
pattern_categories = ["durable_mutation", "undo_redo", "transaction_command"]
|
|
84
|
+
positive_terms = ["audit", "command", "durable", "execute", "idempotency", "mutation", "queue", "redo", "retry", "transaction", "undo"]
|
|
85
|
+
negative_terms = ["facade", "pricing", "provider", "state", "strategy", "transition", "variant"]
|
|
86
|
+
|
|
63
87
|
[routes."command-contract-authoring"]
|
|
64
88
|
category = "workflow_contracts"
|
|
65
89
|
route_type = "authoring"
|
|
@@ -90,12 +114,25 @@ route_type = "primary"
|
|
|
90
114
|
priority = 82
|
|
91
115
|
applies_to_reasons = ["public_api_change", "behavior_change", "docs_change", "test_change", "package_metadata_change", "release_risk"]
|
|
92
116
|
|
|
117
|
+
[routes."public-json-contract-change".dependencies]
|
|
118
|
+
suggests_adjuncts = ["cli-output-contract-review", "completion-evidence-gate"]
|
|
119
|
+
unlocks_on = [
|
|
120
|
+
{ signal = "machine_output_changed", skill = "cli-output-contract-review" },
|
|
121
|
+
{ signal = "schema_or_fixture_changed", skill = "completion-evidence-gate" },
|
|
122
|
+
]
|
|
123
|
+
|
|
93
124
|
[routes."completion-evidence-gate"]
|
|
94
125
|
category = "workflow_contracts"
|
|
95
126
|
route_type = "adjunct"
|
|
96
127
|
priority = 85
|
|
97
128
|
applies_to_reasons = ["unknown_change", "code_change", "behavior_change", "public_api_change", "test_change", "docs_change", "mustflow_docs_change", "mustflow_config_change", "package_metadata_change", "release_risk"]
|
|
98
129
|
|
|
130
|
+
[routes."completion-evidence-gate".dependencies]
|
|
131
|
+
suggests_adjuncts = ["next-action-menu"]
|
|
132
|
+
unlocks_on = [
|
|
133
|
+
{ signal = "concrete_followup_exists", skill = "next-action-menu" },
|
|
134
|
+
]
|
|
135
|
+
|
|
99
136
|
[routes."next-action-menu"]
|
|
100
137
|
category = "workflow_contracts"
|
|
101
138
|
route_type = "adjunct"
|
|
@@ -126,6 +163,13 @@ route_type = "primary"
|
|
|
126
163
|
priority = 80
|
|
127
164
|
applies_to_reasons = ["code_change"]
|
|
128
165
|
|
|
166
|
+
[routes."facade-pattern".contexts]
|
|
167
|
+
file_types = ["ts", "tsx", "js", "jsx", "py", "go", "rs"]
|
|
168
|
+
layers = ["application", "service", "orchestration", "integration"]
|
|
169
|
+
pattern_categories = ["subsystem_simplification", "orchestration_boundary"]
|
|
170
|
+
positive_terms = ["coordinate", "coordination", "facade", "orchestrate", "orchestration", "simplify", "subsystem", "wrapper"]
|
|
171
|
+
negative_terms = ["algorithm", "state", "strategy", "transition", "variant"]
|
|
172
|
+
|
|
129
173
|
[routes."pure-core-imperative-shell"]
|
|
130
174
|
category = "architecture_patterns"
|
|
131
175
|
route_type = "primary"
|
|
@@ -138,6 +182,16 @@ route_type = "primary"
|
|
|
138
182
|
priority = 80
|
|
139
183
|
applies_to_reasons = ["code_change", "behavior_change"]
|
|
140
184
|
|
|
185
|
+
[routes."state-machine-pattern".dependencies]
|
|
186
|
+
conflicts_with = ["strategy-pattern"]
|
|
187
|
+
|
|
188
|
+
[routes."state-machine-pattern".contexts]
|
|
189
|
+
file_types = ["ts", "tsx", "js", "jsx", "py", "go", "rs"]
|
|
190
|
+
layers = ["domain", "workflow", "lifecycle", "state"]
|
|
191
|
+
pattern_categories = ["state_transition", "lifecycle_model", "allowed_action"]
|
|
192
|
+
positive_terms = ["allowed", "history", "irreversible", "lifecycle", "phase", "state", "status", "transition", "workflow"]
|
|
193
|
+
negative_terms = ["algorithm", "pricing", "provider", "strategy", "variant", "wrapper"]
|
|
194
|
+
|
|
141
195
|
[routes."result-option"]
|
|
142
196
|
category = "architecture_patterns"
|
|
143
197
|
route_type = "primary"
|