mustflow 2.58.1 → 2.68.6
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/README.md +4 -0
- package/dist/cli/commands/context.js +81 -6
- package/dist/cli/commands/quality.js +89 -0
- package/dist/cli/commands/skill.js +116 -0
- package/dist/cli/i18n/en.js +16 -0
- package/dist/cli/i18n/es.js +16 -0
- package/dist/cli/i18n/fr.js +16 -0
- package/dist/cli/i18n/hi.js +16 -0
- package/dist/cli/i18n/ko.js +16 -0
- package/dist/cli/i18n/zh.js +16 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/lib/agent-context.js +981 -8
- package/dist/cli/lib/command-registry.js +12 -0
- package/dist/cli/lib/local-index/constants.js +4 -5
- package/dist/cli/lib/local-index/freshness.js +5 -1
- package/dist/cli/lib/local-index/index.js +1 -1
- package/dist/cli/lib/repo-map.js +7 -1
- package/dist/cli/lib/validation/constants.js +3 -0
- package/dist/cli/lib/validation/index.js +41 -2
- package/dist/core/check-issues.js +5 -0
- package/dist/core/prompt-cache-rendering.js +19 -0
- package/dist/core/public-json-contracts.js +35 -0
- package/dist/core/quality-gaming.js +304 -0
- package/dist/core/skill-route-fixtures.js +173 -0
- package/dist/core/skill-route-resolution.js +398 -0
- package/dist/core/source-anchors.js +91 -5
- package/package.json +1 -1
- package/schemas/README.md +9 -1
- package/schemas/context-report.schema.json +442 -0
- package/schemas/quality-gaming-report.schema.json +96 -0
- package/schemas/route-fixture.schema.json +57 -0
- package/schemas/skill-route-report.schema.json +242 -0
- package/templates/default/common/.mustflow/config/commands.toml +34 -2
- package/templates/default/common/.mustflow/config/mustflow.toml +16 -10
- package/templates/default/i18n.toml +14 -8
- package/templates/default/locales/en/.mustflow/context/PROJECT.md +5 -3
- package/templates/default/locales/en/.mustflow/docs/agent-workflow.md +13 -9
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +3 -2
- package/templates/default/locales/en/.mustflow/skills/llm-token-cost-control-review/SKILL.md +5 -2
- package/templates/default/locales/en/.mustflow/skills/quality-gaming-guard/SKILL.md +165 -0
- package/templates/default/locales/en/.mustflow/skills/router.toml +67 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +6 -0
- package/templates/default/locales/en/AGENTS.md +15 -7
- package/templates/default/locales/ko/.mustflow/context/PROJECT.md +4 -2
- package/templates/default/locales/ko/.mustflow/docs/agent-workflow.md +18 -12
- package/templates/default/locales/ko/AGENTS.md +8 -6
- package/templates/default/manifest.toml +9 -1
|
@@ -89,6 +89,12 @@ export const COMMAND_DEFINITIONS = [
|
|
|
89
89
|
summaryKey: 'command.lineEndings.summary',
|
|
90
90
|
loadRunner: async () => (await import('../commands/line-endings.js')).runLineEndings,
|
|
91
91
|
},
|
|
92
|
+
{
|
|
93
|
+
id: 'quality',
|
|
94
|
+
usage: 'mf quality',
|
|
95
|
+
summaryKey: 'command.quality.summary',
|
|
96
|
+
loadRunner: async () => (await import('../commands/quality.js')).runQuality,
|
|
97
|
+
},
|
|
92
98
|
{
|
|
93
99
|
id: 'run',
|
|
94
100
|
usage: 'mf run',
|
|
@@ -137,6 +143,12 @@ export const COMMAND_DEFINITIONS = [
|
|
|
137
143
|
summaryKey: 'command.search.summary',
|
|
138
144
|
loadRunner: async () => (await import('../commands/search.js')).runSearch,
|
|
139
145
|
},
|
|
146
|
+
{
|
|
147
|
+
id: 'skill',
|
|
148
|
+
usage: 'mf skill',
|
|
149
|
+
summaryKey: 'command.skill.summary',
|
|
150
|
+
loadRunner: async () => (await import('../commands/skill.js')).runSkill,
|
|
151
|
+
},
|
|
140
152
|
{
|
|
141
153
|
id: 'dashboard',
|
|
142
154
|
usage: 'mf dashboard',
|
|
@@ -32,14 +32,13 @@ export const MUSTFLOW_RELATIVE_PATH = '.mustflow/config/mustflow.toml';
|
|
|
32
32
|
export const INDEX_CONFIG_RELATIVE_PATH = '.mustflow/config/index.toml';
|
|
33
33
|
export const DEFAULT_PROMPT_CACHE_STABLE_READ = [
|
|
34
34
|
'AGENTS.md',
|
|
35
|
-
'.mustflow/
|
|
36
|
-
'.mustflow/config/mustflow.toml',
|
|
37
|
-
'.mustflow/config/commands.toml',
|
|
38
|
-
'.mustflow/skills/INDEX.md',
|
|
35
|
+
'.mustflow/skills/router.toml',
|
|
39
36
|
];
|
|
40
37
|
export const DEFAULT_PROMPT_CACHE_TASK_SOURCES = [
|
|
41
38
|
'.mustflow/context/INDEX.md',
|
|
42
|
-
'
|
|
39
|
+
'skill_route_candidates',
|
|
40
|
+
'route_metadata_fallback',
|
|
41
|
+
'repo_map_navigation',
|
|
43
42
|
'matching_skill',
|
|
44
43
|
'relevant_source_files',
|
|
45
44
|
];
|
|
@@ -5,8 +5,9 @@ import { collectDocuments } from './workflow-documents.js';
|
|
|
5
5
|
export function readStoredSchemaVersion(database) {
|
|
6
6
|
return readMetadataValue(database, 'schema_version');
|
|
7
7
|
}
|
|
8
|
-
export function getStalePaths(projectRoot, database) {
|
|
8
|
+
export function getStalePaths(projectRoot, database, options = {}) {
|
|
9
9
|
const schemaVersion = readStoredSchemaVersion(database);
|
|
10
|
+
const includeState = options.includeState ?? true;
|
|
10
11
|
if (schemaVersion !== LOCAL_INDEX_SCHEMA_VERSION) {
|
|
11
12
|
return ['.mustflow/cache/mustflow.sqlite'];
|
|
12
13
|
}
|
|
@@ -17,6 +18,9 @@ export function getStalePaths(projectRoot, database) {
|
|
|
17
18
|
for (const row of indexedRows) {
|
|
18
19
|
const indexedPath = toSearchString(row.path);
|
|
19
20
|
const sourceScope = normalizeIndexedFileSourceScope(toSearchString(row.source_scope));
|
|
21
|
+
if (!includeState && sourceScope === 'state') {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
20
24
|
try {
|
|
21
25
|
const current = readIndexedFileRecord(projectRoot, indexedPath, sourceScope);
|
|
22
26
|
if (current.contentHash !== toSearchString(row.content_hash)) {
|
|
@@ -383,7 +383,7 @@ export async function readLocalIndexPromptContext(projectRoot) {
|
|
|
383
383
|
const SQL = await loadSqlJs();
|
|
384
384
|
database = new SQL.Database(readFileSync(databasePath));
|
|
385
385
|
const capabilities = readStoredSearchCapabilities(database);
|
|
386
|
-
const stalePaths = getStalePaths(projectRoot, database);
|
|
386
|
+
const stalePaths = getStalePaths(projectRoot, database, { includeState: false });
|
|
387
387
|
if (stalePaths.length > 0) {
|
|
388
388
|
return createLocalIndexPromptContextStatus(databasePath, 'stale', stalePaths, capabilities);
|
|
389
389
|
}
|
package/dist/cli/lib/repo-map.js
CHANGED
|
@@ -28,6 +28,8 @@ const DEFAULT_PRIORITY_PATHS = [
|
|
|
28
28
|
'.mustflow/config/commands.toml',
|
|
29
29
|
'.mustflow/config/preferences.toml',
|
|
30
30
|
'.mustflow/context/INDEX.md',
|
|
31
|
+
'.mustflow/skills/router.toml',
|
|
32
|
+
'.mustflow/skills/routes.toml',
|
|
31
33
|
'.mustflow/skills/INDEX.md',
|
|
32
34
|
];
|
|
33
35
|
const ROOT_OPTIONAL_MARKDOWN_ANCHOR_FILES = [
|
|
@@ -81,6 +83,8 @@ const DEFAULT_NESTED_ANCHOR_FILES = [
|
|
|
81
83
|
'.mustflow/config/preferences.toml',
|
|
82
84
|
'.mustflow/context/INDEX.md',
|
|
83
85
|
'.mustflow/context/PROJECT.md',
|
|
86
|
+
'.mustflow/skills/router.toml',
|
|
87
|
+
'.mustflow/skills/routes.toml',
|
|
84
88
|
'.mustflow/skills/INDEX.md',
|
|
85
89
|
...ROOT_OPTIONAL_MARKDOWN_ANCHOR_FILES,
|
|
86
90
|
...MACHINE_CONTRACT_ANCHOR_FILES,
|
|
@@ -191,7 +195,9 @@ const EXACT_ANCHOR_DESCRIPTIONS = new Map([
|
|
|
191
195
|
['.mustflow/context/INDEX.md', 'Task-specific project context router. Read only when context is needed.'],
|
|
192
196
|
['.mustflow/context/PROJECT.md', 'Project goals, non-goals, terms, and repository-wide promises for agents.'],
|
|
193
197
|
['.mustflow/docs/agent-workflow.md', 'Shared workflow policy for agent work.'],
|
|
194
|
-
['.mustflow/skills/
|
|
198
|
+
['.mustflow/skills/router.toml', 'Stable compact skill-routing kernel for prompt-cache-friendly first-pass selection.'],
|
|
199
|
+
['.mustflow/skills/routes.toml', 'Full skill-routing metadata for detailed procedure selection.'],
|
|
200
|
+
['.mustflow/skills/INDEX.md', 'Expanded route table for detailed skill selection and route maintenance.'],
|
|
195
201
|
]);
|
|
196
202
|
function spawnGitLsFiles(command, args, options) {
|
|
197
203
|
const result = spawnSync(command, [...args], options);
|
|
@@ -21,6 +21,8 @@ export const REQUIRED_FILES = [
|
|
|
21
21
|
'AGENTS.md',
|
|
22
22
|
'.mustflow/config/mustflow.toml',
|
|
23
23
|
'.mustflow/config/commands.toml',
|
|
24
|
+
'.mustflow/skills/router.toml',
|
|
25
|
+
'.mustflow/skills/routes.toml',
|
|
24
26
|
'.mustflow/skills/INDEX.md',
|
|
25
27
|
];
|
|
26
28
|
export const ALLOWED_MAP_MODES = new Set(['anchors_only']);
|
|
@@ -181,6 +183,7 @@ export const SKILL_COMMAND_PERMISSION_CLAIM_PATTERNS = [
|
|
|
181
183
|
export const ROUTER_INDEX_PROCEDURE_SECTION_PATTERN = /^##\s+(?:Use When|Do Not Use When|Required Inputs|Preconditions|Allowed Edits|Procedure|Postconditions|Verification|Failure Handling|Output Format|사용 조건|사용하지 않는 경우|필요한 입력|사전 조건|허용 수정 범위|절차|사후 조건|검증|실패 대응|출력 형식)\s*$/imu;
|
|
182
184
|
export const ROUTER_INDEX_FILES = ['.mustflow/skills/INDEX.md', '.mustflow/context/INDEX.md'];
|
|
183
185
|
export const SKILL_INDEX_PATH = '.mustflow/skills/INDEX.md';
|
|
186
|
+
export const SKILL_ROUTER_PATH = '.mustflow/skills/router.toml';
|
|
184
187
|
export const SKILL_ROUTES_METADATA_PATH = '.mustflow/skills/routes.toml';
|
|
185
188
|
export const SUPPORTED_SKILL_SCHEMA_VERSION = '1';
|
|
186
189
|
export const SKILL_PACK_ID_PATTERN = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/u;
|
|
@@ -17,6 +17,8 @@ import { MUSTFLOW_JSON_MAX_BYTES } from '../mustflow-read.js';
|
|
|
17
17
|
import { getContractModelDefinitions, validateCandidateContractModelConfig, } from '../../../core/contract-models.js';
|
|
18
18
|
import { VERSIONING_CONFIG_PATH, detectVersionSourcePaths, readDeclaredVersionSources, releaseVersioningIsEnabled, } from '../../../core/version-sources.js';
|
|
19
19
|
import { normalizeTechnologyPreferencesTable, TECHNOLOGY_CONFIG_RELATIVE_PATH, } from '../../../core/technology-preferences.js';
|
|
20
|
+
import { isPromptCacheStableLeafSkillSurface, measurePromptCacheReferenceBlockBytes, } from '../../../core/prompt-cache-rendering.js';
|
|
21
|
+
import { validateSkillRouteFixtures } from '../../../core/skill-route-fixtures.js';
|
|
20
22
|
import { ALLOWED_APPROVAL_ACTIONS, ALLOWED_APPROVAL_GATES, ALLOWED_BUDGET_LIMIT_ACTIONS, ALLOWED_CAPABILITY_STATES, ALLOWED_COMMIT_MESSAGE_STYLES, ALLOWED_COMPACTION_CATEGORIES, ALLOWED_COMPACTION_LONG_LIMIT_ACTIONS, ALLOWED_COMPACTION_RAW_LIMIT_ACTIONS, ALLOWED_COMPACTION_STATE_STORES, ALLOWED_COMPACTION_STRATEGIES, ALLOWED_CONTEXT_AUTHORITIES, ALLOWED_CONTEXT_DOCUMENT_AUTHORITIES, ALLOWED_CONTEXT_READ_POLICIES, ALLOWED_HANDOFF_MODES, ALLOWED_HARNESS_FRESH_CONTEXT_MODES, ALLOWED_HARNESS_MODES, ALLOWED_HARNESS_PHASES, ALLOWED_ISOLATION_PREFERENCES, ALLOWED_MAP_MODES, ALLOWED_MAP_PRIVACY_LEVELS, ALLOWED_PROJECT_PROFILES, ALLOWED_REPO_MAP_DEGRADED_VALUES, ALLOWED_REPO_MAP_GIT_LS_FILES_STATUSES, ALLOWED_PROMPT_CACHE_STABLE_PREFIX_POLICIES, ALLOWED_PROMPT_CACHE_STRATEGIES, ALLOWED_PROMPT_CACHE_TASK_READ_POLICIES, ALLOWED_REFRESH_CHECKPOINTS, ALLOWED_REFRESH_METHODS, ALLOWED_REFRESH_MODES, ALLOWED_REFRESH_STATE_STORES, ALLOWED_SKILL_RESOURCE_TYPES, ALLOWED_SKILL_ROUTE_CATEGORIES, ALLOWED_SKILL_ROUTE_PROFILES, ALLOWED_SKILL_ROUTE_TYPES, ALLOWED_STALE_TEST_ACTIONS, ALLOWED_TEST_AUTHORING_POLICIES, ALLOWED_TEST_DELETION_REASONS, ALLOWED_TESTING_POLICIES, ALLOWED_TRANSLATION_POLICIES, ALLOWED_VERIFICATION_SELECTION_STRATEGIES, ALLOWED_VERSION_SOURCE_AUTHORITIES, ALLOWED_VERSION_SOURCE_KINDS, CAPABILITY_BOOLEAN_FIELDS, CAPABILITY_STATE_FIELDS, CONTEXT_AUTHORITY_DRIFT_PATTERNS, DESIGN_TOKEN_DEFINITION_PATTERNS, FORBIDDEN_RELEASE_VERSIONING_CONTRACT_FIELDS, FORBIDDEN_TEST_DELETION_REASONS, FORBIDDEN_VERIFICATION_SELECTION_AUTHORITY_FIELDS, LOCAL_ABSOLUTE_PATH_PATTERNS, LOCAL_TASK_STATE_ROOTS, RAW_COMMAND_FENCE_PATTERN, RELEASE_VERSIONING_BOOLEAN_FIELDS, REPO_MAP_DOC_ID, REPO_MAP_GENERATOR, REPO_MAP_LIFECYCLE, REPO_MAP_PRIVACY_MODE, REPO_MAP_RELATIVE_ROOT, REPO_MAP_REMOTE_OR_BRANCH_PATTERNS, REPO_MAP_SOURCE_FINGERPRINT_PATTERN, REPO_MAP_SOURCE_POLICY, REQUIRED_AGENT_LOOP_PHASES, REQUIRED_SKILL_SCRIPT_RUN_POLICY, REQUIRED_SKILL_SECTION_IDS, ROUTER_INDEX_FILES, ROUTER_INDEX_PROCEDURE_SECTION_PATTERN, SECRET_LIKE_CONTEXT_PATTERNS, SKILL_COMMAND_PERMISSION_CLAIM_PATTERNS, SKILL_INDEX_PATH, SKILL_PACK_ID_PATTERN, SKILL_RESOURCE_MANIFEST, SKILL_RESOURCE_ROOTS, SKILL_RESOURCE_TYPE_BY_ROOT, SKILL_ROUTE_CATEGORY_LABELS, SKILL_ROUTES_METADATA_PATH, SUPPORTED_SKILL_SCHEMA_VERSION, TEST_AUTHORING_BOOLEAN_FIELDS, VERIFICATION_SELECTION_BOOLEAN_FIELDS, VOLATILE_REPO_MAP_PATTERNS } from './constants.js';
|
|
21
23
|
import { hasOwn, isPositiveInteger, isSafeRelativePath, pushStrictIssue, pushStrictWarning, validateAllowedStringField, validateBooleanField, validateExactStringArrayField, validateNestedTable, validatePathArrayField, validatePathField, validatePositiveIntegerField, validateRequiredFiles, validateRequiredPathField, validateRequiredStringField, validateStringArrayField, validateStringArrayMembers, validateStringField, validateTable, validateToml, validateWorkspaceRoots } from './primitives.js';
|
|
22
24
|
import { isConfiguredCommandIntent, isDeclaredCommandIntent } from './command-intents.js';
|
|
@@ -63,6 +65,7 @@ function validateMustflowConfig(mustflowToml, issues) {
|
|
|
63
65
|
const stable = validateNestedTable(layers, 'stable', '[prompt_cache.layers.stable]', issues);
|
|
64
66
|
if (stable) {
|
|
65
67
|
validatePathArrayField(stable, 'read', '[prompt_cache.layers.stable].read', issues);
|
|
68
|
+
validatePositiveIntegerField(stable, 'target_kb', '[prompt_cache.layers.stable].target_kb', issues);
|
|
66
69
|
}
|
|
67
70
|
const task = validateNestedTable(layers, 'task', '[prompt_cache.layers.task]', issues);
|
|
68
71
|
if (task) {
|
|
@@ -511,7 +514,33 @@ function validateManifestLock(projectRoot, issues) {
|
|
|
511
514
|
issues.push({ message: issue });
|
|
512
515
|
}
|
|
513
516
|
}
|
|
514
|
-
function
|
|
517
|
+
function validateStrictStablePromptCacheBudget(projectRoot, promptCache, stableRead, issues) {
|
|
518
|
+
if (!isPositiveInteger(promptCache.max_stable_prefix_kb)) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const budgetBytes = Number(promptCache.max_stable_prefix_kb) * 1024;
|
|
522
|
+
let renderedBytes = 0;
|
|
523
|
+
for (const entry of stableRead) {
|
|
524
|
+
if (typeof entry !== 'string' || !isSafeRelativePath(entry)) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const normalizedPath = toPosixPath(entry);
|
|
528
|
+
const absolutePath = path.join(projectRoot, entry);
|
|
529
|
+
if (!existsSync(absolutePath)) {
|
|
530
|
+
pushStrictIssue(issues, `stable prefix document is missing: ${normalizedPath}`);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const content = readStrictMustflowText(projectRoot, entry, issues);
|
|
534
|
+
if (content === undefined) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
renderedBytes += measurePromptCacheReferenceBlockBytes(entry, content);
|
|
538
|
+
}
|
|
539
|
+
if (renderedBytes > budgetBytes) {
|
|
540
|
+
pushStrictIssue(issues, `stable prefix exceeds [prompt_cache].max_stable_prefix_kb: ${renderedBytes} rendered bytes > ${budgetBytes} budget bytes`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function validateStrictPromptCachePolicy(projectRoot, mustflowToml, issues) {
|
|
515
544
|
if (!mustflowToml || !isRecord(mustflowToml.prompt_cache)) {
|
|
516
545
|
pushStrictIssue(issues, '[prompt_cache] table is required');
|
|
517
546
|
return;
|
|
@@ -534,7 +563,11 @@ function validateStrictPromptCachePolicy(mustflowToml, issues) {
|
|
|
534
563
|
if (typeof entry === 'string' && volatileSources.has(entry)) {
|
|
535
564
|
pushStrictIssue(issues, `[prompt_cache.layers.stable].read must not include volatile path "${entry}"`);
|
|
536
565
|
}
|
|
566
|
+
if (typeof entry === 'string' && isPromptCacheStableLeafSkillSurface(entry)) {
|
|
567
|
+
pushStrictIssue(issues, `[prompt_cache.layers.stable].read must not include leaf skill or expanded route surface "${toPosixPath(entry)}"`);
|
|
568
|
+
}
|
|
537
569
|
}
|
|
570
|
+
validateStrictStablePromptCacheBudget(projectRoot, promptCache, stable.read, issues);
|
|
538
571
|
if (promptCache.exclude_volatile_state_from_prefix !== true) {
|
|
539
572
|
pushStrictIssue(issues, '[prompt_cache].exclude_volatile_state_from_prefix should be true');
|
|
540
573
|
}
|
|
@@ -590,6 +623,11 @@ function validateStrictRouterIndexes(projectRoot, issues) {
|
|
|
590
623
|
}
|
|
591
624
|
}
|
|
592
625
|
}
|
|
626
|
+
function validateStrictSkillRouteFixtures(projectRoot, issues) {
|
|
627
|
+
for (const issue of validateSkillRouteFixtures(projectRoot)) {
|
|
628
|
+
pushStrictIssue(issues, issue.message);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
593
631
|
function validateSkillIndexRouteShape(content, issues) {
|
|
594
632
|
for (const line of content.split(/\r?\n/u)) {
|
|
595
633
|
if (!line.trim().startsWith('|')) {
|
|
@@ -1536,7 +1574,7 @@ function validateStrictStorage(projectRoot, limits, issues) {
|
|
|
1536
1574
|
}
|
|
1537
1575
|
function validateStrict(projectRoot, parsed, issues) {
|
|
1538
1576
|
const retentionLimits = validateStrictRetentionPolicy(parsed.mustflowToml, issues);
|
|
1539
|
-
validateStrictPromptCachePolicy(parsed.mustflowToml, issues);
|
|
1577
|
+
validateStrictPromptCachePolicy(projectRoot, parsed.mustflowToml, issues);
|
|
1540
1578
|
validateStrictRefreshPolicy(parsed.mustflowToml, issues);
|
|
1541
1579
|
validateStrictHarnessPolicy(parsed.mustflowToml, issues);
|
|
1542
1580
|
validateStrictCommandDefaults(projectRoot, parsed.commandsToml, issues);
|
|
@@ -1548,6 +1586,7 @@ function validateStrict(projectRoot, parsed, issues) {
|
|
|
1548
1586
|
validateStrictTemplateVersionSync(projectRoot, parsed.preferencesToml, issues);
|
|
1549
1587
|
validateStrictManagedMarkdownIdentities(projectRoot, issues);
|
|
1550
1588
|
validateStrictRouterIndexes(projectRoot, issues);
|
|
1589
|
+
validateStrictSkillRouteFixtures(projectRoot, issues);
|
|
1551
1590
|
validateStrictSkills(projectRoot, parsed.commandsToml, issues);
|
|
1552
1591
|
validateStrictTemplateSkillProfiles(issues);
|
|
1553
1592
|
validateStrictRepoMap(projectRoot, issues);
|
|
@@ -30,6 +30,9 @@ const CHECK_ISSUE_ID_RULES = [
|
|
|
30
30
|
['mustflow.command_contract.project_local_bin_bare_executable', /^Strict warning: configured agent-runnable intent [^\s]+ uses bare executable "[^"]+" that matches project-local node_modules\/\.bin/u],
|
|
31
31
|
['mustflow.prompt_cache.required', /^Strict: \[prompt_cache\] table is required$/u],
|
|
32
32
|
['mustflow.prompt_cache.volatile_in_stable', /^Strict: \[prompt_cache\.layers\.stable\]\.read must not include volatile path /u],
|
|
33
|
+
['mustflow.prompt_cache.leaf_skill_in_stable', /^Strict: \[prompt_cache\.layers\.stable\]\.read must not include leaf skill or expanded route surface /u],
|
|
34
|
+
['mustflow.prompt_cache.stable_file_missing', /^Strict: stable prefix document is missing: /u],
|
|
35
|
+
['mustflow.prompt_cache.stable_prefix_over_budget', /^Strict: stable prefix exceeds \[prompt_cache\]\.max_stable_prefix_kb: \d+ rendered bytes > \d+ budget bytes$/u],
|
|
33
36
|
['mustflow.refresh.hash_method_required', /^Strict: \[refresh\]\.default_method should be "hash_check" for cache-friendly refresh$/u],
|
|
34
37
|
['mustflow.release.template_version_mismatch', /^Strict: templates\/default\/manifest\.toml version "[^"]+" must match package\.json version "[^"]+" when \[release\.versioning\]\.sync_template_version is true$/u],
|
|
35
38
|
['mustflow.release.template_version_ahead', /^Strict: templates\/default\/manifest\.toml version "[^"]+" must not be ahead of package\.json version "[^"]+"$/u],
|
|
@@ -57,6 +60,8 @@ const CHECK_ISSUE_ID_RULES = [
|
|
|
57
60
|
['mustflow.skill.route_metadata_category_mismatch', /^Strict: \.mustflow\/skills\/INDEX\.md route "[^"]+" must appear under the .+ category section from \.mustflow\/skills\/routes\.toml$/u],
|
|
58
61
|
['mustflow.skill.route_metadata_unknown_reference', /^Strict: \.mustflow\/skills\/routes\.toml route "[^"]+" references unknown mutually exclusive route "[^"]+"$/u],
|
|
59
62
|
['mustflow.skill.route_metadata_asymmetric_exclusion', /^Strict warning: \.mustflow\/skills\/routes\.toml route "[^"]+" lists "[^"]+" as mutually exclusive but the reverse route does not$/u],
|
|
63
|
+
['mustflow.skill.route_fixture_invalid', /^Strict: \.mustflow\/skills\/route-fixtures\.json (?:must|is not valid JSON|cases\[\d+\])/u],
|
|
64
|
+
['mustflow.skill.route_fixture_mismatch', /^Strict: Skill route fixture "[^"]+" (?:expected|forbids)/u],
|
|
60
65
|
['mustflow.skill.template_profile_empty_category', /^Strict: template profile "[^"]+" (?:skill index category ".+" has no route rows|route category gate references ".+" without route rows)$/u],
|
|
61
66
|
['mustflow.skill.template_profile_dead_route', /^Strict: template profile "[^"]+" (?:\.mustflow\/skills\/INDEX\.md route "[^"]+" points to a skill not installed by that profile|\.mustflow\/skills\/routes\.toml route "[^"]+" points to a skill not installed by that profile|skill "[^"]+" is installed but not listed in \.mustflow\/skills\/INDEX\.md)$/u],
|
|
62
67
|
['mustflow.skill.template_profile_missing_main_route', /^Strict: template profile "[^"]+" skill category ".+" must include at least one primary or authoring route$/u],
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function toPromptCachePosixPath(relativePath) {
|
|
2
|
+
return relativePath.replace(/\\/gu, '/');
|
|
3
|
+
}
|
|
4
|
+
export function isPromptCacheStableLeafSkillSurface(relativePath) {
|
|
5
|
+
const normalized = toPromptCachePosixPath(relativePath);
|
|
6
|
+
return normalized === '.mustflow/skills/INDEX.md'
|
|
7
|
+
|| normalized === '.mustflow/skills/routes.toml'
|
|
8
|
+
|| /^\.mustflow\/skills\/[^/]+\/.+/u.test(normalized);
|
|
9
|
+
}
|
|
10
|
+
export function normalizePromptCacheBlockContent(content) {
|
|
11
|
+
const normalized = content.replace(/\r\n?/gu, '\n').replace(/\n*$/u, '');
|
|
12
|
+
return `${normalized}\n`;
|
|
13
|
+
}
|
|
14
|
+
export function renderPromptCacheReferenceBlock(relativePath, content) {
|
|
15
|
+
return `--- mustflow-cache-block: ${toPromptCachePosixPath(relativePath)} ---\n${normalizePromptCacheBlockContent(content)}`;
|
|
16
|
+
}
|
|
17
|
+
export function measurePromptCacheReferenceBlockBytes(relativePath, content) {
|
|
18
|
+
return Buffer.byteLength(renderPromptCacheReferenceBlock(relativePath, content), 'utf8');
|
|
19
|
+
}
|
|
@@ -198,6 +198,41 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
198
198
|
installedCommand: ['mf', 'line-endings', 'check', '--json'],
|
|
199
199
|
expectedExitCodes: [0, 1],
|
|
200
200
|
},
|
|
201
|
+
{
|
|
202
|
+
id: 'quality-gaming-report',
|
|
203
|
+
schemaFile: 'quality-gaming-report.schema.json',
|
|
204
|
+
producer: 'mf quality check --json',
|
|
205
|
+
packaged: true,
|
|
206
|
+
documented: true,
|
|
207
|
+
installedCommand: ['mf', 'quality', 'check', '--json'],
|
|
208
|
+
expectedExitCodes: [0, 1],
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: 'skill-route-report',
|
|
212
|
+
schemaFile: 'skill-route-report.schema.json',
|
|
213
|
+
producer: 'mf skill route --json',
|
|
214
|
+
packaged: true,
|
|
215
|
+
documented: true,
|
|
216
|
+
installedCommand: [
|
|
217
|
+
'mf',
|
|
218
|
+
'skill',
|
|
219
|
+
'route',
|
|
220
|
+
'--task',
|
|
221
|
+
'Change TypeScript CLI output',
|
|
222
|
+
'--path',
|
|
223
|
+
'src/cli/index.ts',
|
|
224
|
+
'--reason',
|
|
225
|
+
'code_change',
|
|
226
|
+
'--json',
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 'route-fixture',
|
|
231
|
+
schemaFile: 'route-fixture.schema.json',
|
|
232
|
+
producer: 'parsed .mustflow/skills/route-fixtures.json',
|
|
233
|
+
packaged: true,
|
|
234
|
+
documented: true,
|
|
235
|
+
},
|
|
201
236
|
{
|
|
202
237
|
id: 'latest-run-pointer',
|
|
203
238
|
schemaFile: 'latest-run-pointer.schema.json',
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createCommandEnv } from './command-env.js';
|
|
5
|
+
import { readFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
6
|
+
const LONG_LINE_THRESHOLD = 160;
|
|
7
|
+
const MULTI_STATEMENT_SEMICOLON_THRESHOLD = 1;
|
|
8
|
+
const LARGE_HELPER_LINE_THRESHOLD = 120;
|
|
9
|
+
const TEXT_FILE_PATTERN = /\.(?:cjs|cts|css|go|h|hpp|html|java|js|jsx|json|kt|md|mjs|mts|php|ps1|py|rb|rs|scss|sh|sql|svelte|swift|toml|ts|tsx|txt|vue|xml|yaml|yml)$/iu;
|
|
10
|
+
const CODE_FILE_PATTERN = /\.(?:cjs|cts|go|java|js|jsx|kt|mjs|mts|php|ps1|py|rb|rs|svelte|swift|ts|tsx|vue)$/iu;
|
|
11
|
+
const GENERATED_OR_VENDOR_PATTERN = /(?:^|\/)(?:generated|vendor|third_party)(?:\/|$)/iu;
|
|
12
|
+
const HELPER_CONTAINER_PATTERN = /(?:^|\/)[^/]*(?:helper|helpers|util|utils|manager|common|misc)[^/]*\.(?:cjs|cts|go|java|js|jsx|kt|mjs|mts|php|ps1|py|rb|rs|svelte|swift|ts|tsx|vue)$/iu;
|
|
13
|
+
const SUPPRESSION_PATTERN = /(?:eslint-disable|ts-ignore|ts-expect-error|biome-ignore|rome-ignore|noqa|type:\s*ignore|pragma:\s*no cover|istanbul\s+ignore|c8\s+ignore)/iu;
|
|
14
|
+
const TYPE_ESCAPE_PATTERN = /\b(?:as\s+any|as\s+unknown\s+as|as\s+never|:\s*any\b|!\.|!\)|!\]|!\s*[,;]|\bObject\s+as\s+unknown)\b/u;
|
|
15
|
+
const TEST_BYPASS_PATTERN = /(?:\.skip\b|\.only\b|@Disabled\b|@Ignore\b|\bxfail\b|pytest\.mark\.skip|it\.todo\b|test\.todo\b)/u;
|
|
16
|
+
const PLACEHOLDER_PATTERN = /(?:TODO|FIXME|HACK|not\s+implemented|unimplemented|throw\s+new\s+Error\(["'`](?:not implemented|todo|unsupported)|\bpass\s*$)/iu;
|
|
17
|
+
const THROW_PLACEHOLDER_MESSAGE_PATTERN = /(?:not\s+implemented|unimplemented|todo|unsupported)/iu;
|
|
18
|
+
const EMPTY_CATCH_PATTERN = /\bcatch\s*(?:\([^)]*\))?\s*\{\s*\}/u;
|
|
19
|
+
function toPosixPath(value) {
|
|
20
|
+
return value.split(path.sep).join('/');
|
|
21
|
+
}
|
|
22
|
+
function normalizeGitPath(value) {
|
|
23
|
+
return value.replace(/\\/gu, '/');
|
|
24
|
+
}
|
|
25
|
+
function gitList(projectRoot, args) {
|
|
26
|
+
const result = spawnSync('git', [...args, '-z'], {
|
|
27
|
+
cwd: projectRoot,
|
|
28
|
+
encoding: 'buffer',
|
|
29
|
+
env: createCommandEnv(projectRoot, { policy: 'minimal', allowlist: [] }),
|
|
30
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
|
+
timeout: 30_000,
|
|
32
|
+
windowsHide: true,
|
|
33
|
+
});
|
|
34
|
+
if (result.status !== 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return result.stdout
|
|
38
|
+
.toString('utf8')
|
|
39
|
+
.split('\0')
|
|
40
|
+
.map((entry) => normalizeGitPath(entry.trim()))
|
|
41
|
+
.filter((entry) => entry.length > 0)
|
|
42
|
+
.sort((left, right) => left.localeCompare(right));
|
|
43
|
+
}
|
|
44
|
+
function gitText(projectRoot, args) {
|
|
45
|
+
const result = spawnSync('git', args, {
|
|
46
|
+
cwd: projectRoot,
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
env: createCommandEnv(projectRoot, { policy: 'minimal', allowlist: [] }),
|
|
49
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
|
+
timeout: 30_000,
|
|
51
|
+
windowsHide: true,
|
|
52
|
+
});
|
|
53
|
+
return result.status === 0 ? result.stdout : null;
|
|
54
|
+
}
|
|
55
|
+
function listGitTrackedFiles(projectRoot) {
|
|
56
|
+
return gitList(projectRoot, ['ls-files']);
|
|
57
|
+
}
|
|
58
|
+
function listGitChangedFiles(projectRoot) {
|
|
59
|
+
const changedLists = [
|
|
60
|
+
gitList(projectRoot, ['diff', '--name-only', '--diff-filter=ACMR']),
|
|
61
|
+
gitList(projectRoot, ['diff', '--cached', '--name-only', '--diff-filter=ACMR']),
|
|
62
|
+
gitList(projectRoot, ['ls-files', '--others', '--exclude-standard']),
|
|
63
|
+
];
|
|
64
|
+
if (changedLists.some((list) => list === null)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return [...new Set(changedLists.flatMap((list) => list ?? []))].sort((left, right) => left.localeCompare(right));
|
|
68
|
+
}
|
|
69
|
+
function isTextCandidate(relativePath) {
|
|
70
|
+
return TEXT_FILE_PATTERN.test(relativePath) && !relativePath.startsWith('.mustflow/state/');
|
|
71
|
+
}
|
|
72
|
+
function isCodeCandidate(relativePath) {
|
|
73
|
+
return CODE_FILE_PATTERN.test(relativePath);
|
|
74
|
+
}
|
|
75
|
+
function isMeaningfulCodeLine(text) {
|
|
76
|
+
const trimmed = text.trim();
|
|
77
|
+
return trimmed.length > 0 && !trimmed.startsWith('//') && !trimmed.startsWith('#') && !trimmed.startsWith('*');
|
|
78
|
+
}
|
|
79
|
+
function stripQuotedText(text) {
|
|
80
|
+
let stripped = '';
|
|
81
|
+
let quote = null;
|
|
82
|
+
let escaped = false;
|
|
83
|
+
for (const char of text) {
|
|
84
|
+
if (quote) {
|
|
85
|
+
stripped += ' ';
|
|
86
|
+
if (escaped) {
|
|
87
|
+
escaped = false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (char === '\\') {
|
|
91
|
+
escaped = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (char === quote) {
|
|
95
|
+
quote = null;
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
100
|
+
quote = char;
|
|
101
|
+
stripped += ' ';
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
stripped += char;
|
|
105
|
+
}
|
|
106
|
+
return stripped;
|
|
107
|
+
}
|
|
108
|
+
function stripRegexLiterals(text) {
|
|
109
|
+
const patternDeclaration = /^(\s*(?:const\s+[A-Z0-9_]+\s*=\s*)?)\/.*\/[a-z]*;?\s*$/u;
|
|
110
|
+
if (patternDeclaration.test(text)) {
|
|
111
|
+
return text.replace(/\/.*\/[a-z]*;?/u, 'REGEX;');
|
|
112
|
+
}
|
|
113
|
+
return text.replace(/([=(:,\[]\s*)\/.*\/[a-z]*/giu, '$1 REGEX');
|
|
114
|
+
}
|
|
115
|
+
function codeSurfaceForLine(text) {
|
|
116
|
+
const literalStripped = stripQuotedText(stripRegexLiterals(text));
|
|
117
|
+
return literalStripped.split('//', 1)[0] ?? literalStripped;
|
|
118
|
+
}
|
|
119
|
+
function isCommentOnlyLine(text) {
|
|
120
|
+
const trimmed = text.trim();
|
|
121
|
+
return trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*');
|
|
122
|
+
}
|
|
123
|
+
function countSemicolonsOutsideComment(text) {
|
|
124
|
+
return [...codeSurfaceForLine(text)].filter((char) => char === ';').length;
|
|
125
|
+
}
|
|
126
|
+
function readTextLines(projectRoot, relativePath, issues) {
|
|
127
|
+
const absolutePath = path.join(projectRoot, ...relativePath.split('/'));
|
|
128
|
+
if (!existsSync(absolutePath)) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const buffer = readFileInsideWithoutSymlinks(projectRoot, absolutePath);
|
|
133
|
+
if (buffer.includes(0)) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
return buffer
|
|
137
|
+
.toString('utf8')
|
|
138
|
+
.split(/\r?\n/u)
|
|
139
|
+
.map((text, index) => ({ line: index + 1, text }));
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function isGitTracked(projectRoot, relativePath) {
|
|
147
|
+
const result = spawnSync('git', ['ls-files', '--error-unmatch', '--', relativePath], {
|
|
148
|
+
cwd: projectRoot,
|
|
149
|
+
encoding: 'utf8',
|
|
150
|
+
env: createCommandEnv(projectRoot, { policy: 'minimal', allowlist: [] }),
|
|
151
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
152
|
+
timeout: 30_000,
|
|
153
|
+
windowsHide: true,
|
|
154
|
+
});
|
|
155
|
+
return result.status === 0;
|
|
156
|
+
}
|
|
157
|
+
function parseAddedLinesFromDiff(diffText) {
|
|
158
|
+
const addedLines = [];
|
|
159
|
+
let nextLine = 0;
|
|
160
|
+
for (const rawLine of diffText.split(/\r?\n/u)) {
|
|
161
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/u.exec(rawLine);
|
|
162
|
+
if (hunk) {
|
|
163
|
+
nextLine = Number.parseInt(hunk[1], 10);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (nextLine === 0) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (rawLine.startsWith('+++')) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (rawLine.startsWith('+')) {
|
|
173
|
+
addedLines.push({ line: nextLine, text: rawLine.slice(1) });
|
|
174
|
+
nextLine += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (rawLine.startsWith('-')) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
nextLine += 1;
|
|
181
|
+
}
|
|
182
|
+
return addedLines;
|
|
183
|
+
}
|
|
184
|
+
function readChangedLineCandidates(projectRoot, relativePath, issues) {
|
|
185
|
+
if (!isGitTracked(projectRoot, relativePath)) {
|
|
186
|
+
return readTextLines(projectRoot, relativePath, issues);
|
|
187
|
+
}
|
|
188
|
+
const diffText = gitText(projectRoot, ['diff', 'HEAD', '--no-ext-diff', '--unified=0', '--', relativePath]);
|
|
189
|
+
if (diffText === null) {
|
|
190
|
+
return readTextLines(projectRoot, relativePath, issues);
|
|
191
|
+
}
|
|
192
|
+
return parseAddedLinesFromDiff(diffText);
|
|
193
|
+
}
|
|
194
|
+
function addRisk(risks, code, severity, relativePath, line, detail, metric) {
|
|
195
|
+
risks.push({
|
|
196
|
+
code,
|
|
197
|
+
severity,
|
|
198
|
+
path: toPosixPath(relativePath),
|
|
199
|
+
line,
|
|
200
|
+
detail,
|
|
201
|
+
metric,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function inspectLine(relativePath, candidate, risks) {
|
|
205
|
+
const text = candidate.text;
|
|
206
|
+
const trimmed = text.trim();
|
|
207
|
+
const isCode = isCodeCandidate(relativePath);
|
|
208
|
+
const codeSurface = codeSurfaceForLine(text);
|
|
209
|
+
const markerSurface = isCommentOnlyLine(text) ? trimmed : codeSurface;
|
|
210
|
+
if (isCode && codeSurface.length > LONG_LINE_THRESHOLD) {
|
|
211
|
+
addRisk(risks, 'line_stuffing_added', 'high', relativePath, candidate.line, 'Added line exceeds the anti-stuffing length threshold.', {
|
|
212
|
+
name: 'line_length',
|
|
213
|
+
value: codeSurface.length,
|
|
214
|
+
threshold: LONG_LINE_THRESHOLD,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (isCode &&
|
|
218
|
+
isMeaningfulCodeLine(text) &&
|
|
219
|
+
countSemicolonsOutsideComment(text) > MULTI_STATEMENT_SEMICOLON_THRESHOLD) {
|
|
220
|
+
addRisk(risks, 'multiple_statements_per_line_added', 'high', relativePath, candidate.line, 'Added code line appears to contain multiple statements.', {
|
|
221
|
+
name: 'semicolons',
|
|
222
|
+
value: countSemicolonsOutsideComment(text),
|
|
223
|
+
threshold: MULTI_STATEMENT_SEMICOLON_THRESHOLD,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (isCode && SUPPRESSION_PATTERN.test(markerSurface)) {
|
|
227
|
+
addRisk(risks, 'suppression_added', 'critical', relativePath, candidate.line, 'Added line contains a validation suppression marker.', null);
|
|
228
|
+
}
|
|
229
|
+
if (isCode && TYPE_ESCAPE_PATTERN.test(codeSurface)) {
|
|
230
|
+
addRisk(risks, 'type_escape_added', 'high', relativePath, candidate.line, 'Added line contains a broad type escape or non-null assertion pattern.', null);
|
|
231
|
+
}
|
|
232
|
+
if (isCode && TEST_BYPASS_PATTERN.test(codeSurface)) {
|
|
233
|
+
addRisk(risks, 'test_bypass_marker_added', 'critical', relativePath, candidate.line, 'Added line contains a test bypass marker.', null);
|
|
234
|
+
}
|
|
235
|
+
if (isCode &&
|
|
236
|
+
(PLACEHOLDER_PATTERN.test(markerSurface) ||
|
|
237
|
+
(/\bthrow\s+new\s+Error\s*\(/u.test(codeSurface) && THROW_PLACEHOLDER_MESSAGE_PATTERN.test(text)))) {
|
|
238
|
+
addRisk(risks, 'placeholder_added', 'medium', relativePath, candidate.line, 'Added line looks like a placeholder or unfinished implementation.', null);
|
|
239
|
+
}
|
|
240
|
+
if (isCode && EMPTY_CATCH_PATTERN.test(codeSurface)) {
|
|
241
|
+
addRisk(risks, 'empty_catch_swallow_added', 'high', relativePath, candidate.line, 'Added line contains an empty catch block that swallows failures.', null);
|
|
242
|
+
}
|
|
243
|
+
if (GENERATED_OR_VENDOR_PATTERN.test(relativePath) && isCode && isMeaningfulCodeLine(text)) {
|
|
244
|
+
addRisk(risks, 'generated_or_vendor_logic_added', 'high', relativePath, candidate.line, 'Added executable-looking logic under generated, vendor, or third_party paths.', null);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function inspectFile(projectRoot, relativePath, scope, issues) {
|
|
248
|
+
const candidates = scope === 'tracked'
|
|
249
|
+
? readTextLines(projectRoot, relativePath, issues)
|
|
250
|
+
: readChangedLineCandidates(projectRoot, relativePath, issues);
|
|
251
|
+
const risks = [];
|
|
252
|
+
for (const candidate of candidates) {
|
|
253
|
+
inspectLine(relativePath, candidate, risks);
|
|
254
|
+
}
|
|
255
|
+
if (scope === 'tracked' && HELPER_CONTAINER_PATTERN.test(relativePath) && candidates.length > LARGE_HELPER_LINE_THRESHOLD) {
|
|
256
|
+
addRisk(risks, 'large_helper_container_added', 'medium', relativePath, null, 'Helper, util, manager, common, or misc container exceeds the size threshold.', {
|
|
257
|
+
name: 'file_lines',
|
|
258
|
+
value: candidates.length,
|
|
259
|
+
threshold: LARGE_HELPER_LINE_THRESHOLD,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (risks.length === 0) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
path: toPosixPath(relativePath),
|
|
267
|
+
checked_lines: candidates.length,
|
|
268
|
+
risk_count: risks.length,
|
|
269
|
+
risks,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export function inspectQualityGaming(projectRoot, options = {}) {
|
|
273
|
+
const root = path.resolve(projectRoot);
|
|
274
|
+
const scope = options.scope ?? 'changed';
|
|
275
|
+
const candidateFiles = scope === 'tracked' ? listGitTrackedFiles(root) : listGitChangedFiles(root);
|
|
276
|
+
const issues = [];
|
|
277
|
+
const riskyFiles = [];
|
|
278
|
+
if (!candidateFiles) {
|
|
279
|
+
issues.push('Git files could not be listed. Run this command inside a Git working tree.');
|
|
280
|
+
}
|
|
281
|
+
for (const relativePath of candidateFiles ?? []) {
|
|
282
|
+
if (!isTextCandidate(relativePath)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const fileReport = inspectFile(root, relativePath, scope, issues);
|
|
286
|
+
if (fileReport) {
|
|
287
|
+
riskyFiles.push(fileReport);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const riskCount = riskyFiles.reduce((sum, file) => sum + file.risk_count, 0);
|
|
291
|
+
return {
|
|
292
|
+
schema_version: '1',
|
|
293
|
+
command: 'quality',
|
|
294
|
+
mode: 'check',
|
|
295
|
+
scope,
|
|
296
|
+
ok: issues.length === 0 && riskCount === 0,
|
|
297
|
+
mustflow_root: root,
|
|
298
|
+
git_tracked: candidateFiles !== null,
|
|
299
|
+
checked_files: candidateFiles?.filter(isTextCandidate).length ?? 0,
|
|
300
|
+
risk_count: riskCount,
|
|
301
|
+
risky_files: riskyFiles,
|
|
302
|
+
issues,
|
|
303
|
+
};
|
|
304
|
+
}
|