mustflow 2.59.0 → 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 +1 -0
- package/dist/cli/commands/context.js +81 -6
- package/dist/cli/commands/skill.js +116 -0
- package/dist/cli/i18n/en.js +3 -1
- package/dist/cli/i18n/es.js +3 -1
- package/dist/cli/i18n/fr.js +3 -1
- package/dist/cli/i18n/hi.js +3 -1
- package/dist/cli/i18n/ko.js +3 -1
- package/dist/cli/i18n/zh.js +3 -1
- package/dist/cli/lib/agent-context.js +981 -8
- package/dist/cli/lib/command-registry.js +6 -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 +26 -0
- package/dist/core/quality-gaming.js +4 -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 +7 -2
- package/schemas/context-report.schema.json +442 -0
- package/schemas/quality-gaming-report.schema.json +1 -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 +18 -2
- package/templates/default/common/.mustflow/config/mustflow.toml +16 -10
- package/templates/default/i18n.toml +9 -9
- 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 +2 -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 +8 -6
- package/templates/default/locales/en/.mustflow/skills/router.toml +67 -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 +2 -1
|
@@ -6,28 +6,31 @@ import { readRetentionStore } from '../../core/retention-policy.js';
|
|
|
6
6
|
import { toPosixPath } from './filesystem.js';
|
|
7
7
|
import { readLocalIndexPromptContext } from './local-index.js';
|
|
8
8
|
import { inspectManifestLock } from './manifest-lock.js';
|
|
9
|
-
import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile, readMustflowTextFileIfExists, } from './mustflow-read.js';
|
|
9
|
+
import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile, readMustflowTextFileIfExists, readMustflowTextFileResult, } from './mustflow-read.js';
|
|
10
10
|
import { createRunPlan } from './run-plan.js';
|
|
11
11
|
import { readMustflowTomlFile } from './toml.js';
|
|
12
12
|
import { normalizeTechnologyPreferencesTable, TECHNOLOGY_CONFIG_RELATIVE_PATH, } from '../../core/technology-preferences.js';
|
|
13
|
+
import { isPromptCacheStableLeafSkillSurface, measurePromptCacheReferenceBlockBytes, renderPromptCacheReferenceBlock, } from '../../core/prompt-cache-rendering.js';
|
|
14
|
+
import { resolveSkillRoutes } from '../../core/skill-route-resolution.js';
|
|
13
15
|
const CONTEXT_SCHEMA_VERSION = '1';
|
|
14
16
|
const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
|
|
15
17
|
const MUSTFLOW_RELATIVE_PATH = '.mustflow/config/mustflow.toml';
|
|
16
18
|
const PREFERENCES_RELATIVE_PATH = '.mustflow/config/preferences.toml';
|
|
19
|
+
const SKILL_ROUTER_RELATIVE_PATH = '.mustflow/skills/router.toml';
|
|
20
|
+
const SKILL_ROUTE_CANDIDATES_VIRTUAL_PATH = '.mustflow/generated/skill-route-candidates.compact.json';
|
|
17
21
|
const TECHNOLOGY_RELATIVE_PATH = TECHNOLOGY_CONFIG_RELATIVE_PATH;
|
|
18
22
|
const LATEST_RUN_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
|
|
19
23
|
const CACHE_RELATIVE_PATH = '.mustflow/cache/';
|
|
20
24
|
const STATE_RELATIVE_PATH = '.mustflow/state/';
|
|
21
25
|
const DEFAULT_PROMPT_CACHE_STABLE_READ = [
|
|
22
26
|
'AGENTS.md',
|
|
23
|
-
'.mustflow/
|
|
24
|
-
'.mustflow/config/mustflow.toml',
|
|
25
|
-
'.mustflow/config/commands.toml',
|
|
26
|
-
'.mustflow/skills/INDEX.md',
|
|
27
|
+
'.mustflow/skills/router.toml',
|
|
27
28
|
];
|
|
28
29
|
const DEFAULT_PROMPT_CACHE_TASK_SOURCES = [
|
|
29
30
|
'.mustflow/context/INDEX.md',
|
|
30
|
-
'
|
|
31
|
+
'skill_route_candidates',
|
|
32
|
+
'route_metadata_fallback',
|
|
33
|
+
'repo_map_navigation',
|
|
31
34
|
'matching_skill',
|
|
32
35
|
'relevant_source_files',
|
|
33
36
|
];
|
|
@@ -108,6 +111,145 @@ function readScalarObject(table) {
|
|
|
108
111
|
function sha256(content) {
|
|
109
112
|
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
110
113
|
}
|
|
114
|
+
function estimateTokens(renderedBytes) {
|
|
115
|
+
return Math.ceil(renderedBytes / 4);
|
|
116
|
+
}
|
|
117
|
+
function budgetBytes(budgetKb) {
|
|
118
|
+
return budgetKb === null ? null : Math.round(budgetKb * 1024);
|
|
119
|
+
}
|
|
120
|
+
function budgetStatus(renderedBytes, budget) {
|
|
121
|
+
if (renderedBytes === null || budget === null) {
|
|
122
|
+
return 'unknown';
|
|
123
|
+
}
|
|
124
|
+
return renderedBytes <= budget ? 'within_budget' : 'over_budget';
|
|
125
|
+
}
|
|
126
|
+
function renderedDigest(relativePath, content) {
|
|
127
|
+
return sha256(renderPromptCacheReferenceBlock(relativePath, content));
|
|
128
|
+
}
|
|
129
|
+
function calculateBudgetShare(renderedBytes, budget) {
|
|
130
|
+
if (renderedBytes === null || budget === null || budget <= 0) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return Number((renderedBytes / budget).toFixed(6));
|
|
134
|
+
}
|
|
135
|
+
function isDirectPromptCacheSource(source) {
|
|
136
|
+
return (source.includes('/') ||
|
|
137
|
+
source.endsWith('.md') ||
|
|
138
|
+
source.endsWith('.toml') ||
|
|
139
|
+
source.endsWith('.json') ||
|
|
140
|
+
source.endsWith('.yaml') ||
|
|
141
|
+
source.endsWith('.yml'));
|
|
142
|
+
}
|
|
143
|
+
function taskSourceReferencePath(source) {
|
|
144
|
+
const normalized = toPosixPath(source);
|
|
145
|
+
if (normalized === 'route_metadata_fallback') {
|
|
146
|
+
return '.mustflow/skills/routes.toml';
|
|
147
|
+
}
|
|
148
|
+
if (normalized === 'expanded_skill_index_fallback') {
|
|
149
|
+
return '.mustflow/skills/INDEX.md';
|
|
150
|
+
}
|
|
151
|
+
if (isDirectPromptCacheSource(normalized)) {
|
|
152
|
+
return normalized;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function taskSourceSelectionPolicy(source) {
|
|
157
|
+
const normalized = toPosixPath(source);
|
|
158
|
+
if (normalized === '.mustflow/skills/routes.toml' ||
|
|
159
|
+
normalized === '.mustflow/skills/INDEX.md' ||
|
|
160
|
+
normalized === 'route_metadata_fallback' ||
|
|
161
|
+
normalized === 'expanded_skill_index_fallback') {
|
|
162
|
+
return 'fallback_when_needed';
|
|
163
|
+
}
|
|
164
|
+
if (isDirectPromptCacheSource(normalized)) {
|
|
165
|
+
return 'read_when_selected';
|
|
166
|
+
}
|
|
167
|
+
return 'selected_at_runtime';
|
|
168
|
+
}
|
|
169
|
+
function taskSourceKind(source) {
|
|
170
|
+
return taskSourceReferencePath(source) === null ? 'dynamic_selection' : 'file_reference';
|
|
171
|
+
}
|
|
172
|
+
function taskSourceCacheability(source, selectionPolicy) {
|
|
173
|
+
if (selectionPolicy === 'fallback_when_needed') {
|
|
174
|
+
return 'task_fallback';
|
|
175
|
+
}
|
|
176
|
+
return taskSourceKind(source) === 'file_reference' ? 'task_selective' : 'runtime_selection';
|
|
177
|
+
}
|
|
178
|
+
function taskSourceReloadOn(source, selectionPolicy) {
|
|
179
|
+
if (selectionPolicy === 'fallback_when_needed') {
|
|
180
|
+
return ['route_resolution_changed'];
|
|
181
|
+
}
|
|
182
|
+
if (source === 'repo_map_navigation') {
|
|
183
|
+
return ['navigation_scope_changed', 'source_anchor_selection_changed'];
|
|
184
|
+
}
|
|
185
|
+
if (taskSourceKind(source) === 'file_reference') {
|
|
186
|
+
return ['task_selection_changed', 'source_file_hash_changed'];
|
|
187
|
+
}
|
|
188
|
+
if (source === 'relevant_source_files') {
|
|
189
|
+
return ['source_selection_changed'];
|
|
190
|
+
}
|
|
191
|
+
return ['route_resolution_changed'];
|
|
192
|
+
}
|
|
193
|
+
function isDeferredTaskFallback(selectionPolicy) {
|
|
194
|
+
return selectionPolicy === 'fallback_when_needed';
|
|
195
|
+
}
|
|
196
|
+
function taskSourceIssue(source, measurementStatus) {
|
|
197
|
+
if (measurementStatus === 'hash_only_deferred') {
|
|
198
|
+
return `task source is selection-gated; hash is recorded, but content is measured only when selected: ${toPosixPath(source)}`;
|
|
199
|
+
}
|
|
200
|
+
return `task source is selected at runtime and cannot be measured before the current task is assembled: ${source}`;
|
|
201
|
+
}
|
|
202
|
+
function hasPromptCacheRouteInput(input) {
|
|
203
|
+
return Boolean(input?.taskText?.trim() || input?.paths.length || input?.reasons.length);
|
|
204
|
+
}
|
|
205
|
+
function resolvePromptCacheSkillRoutes(projectRoot, input) {
|
|
206
|
+
if (!hasPromptCacheRouteInput(input)) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return resolveSkillRoutes(projectRoot, input);
|
|
210
|
+
}
|
|
211
|
+
function selectedSkillRoutePaths(routeReport) {
|
|
212
|
+
return routeReport?.read_plan.selected_skill_paths ?? [];
|
|
213
|
+
}
|
|
214
|
+
function selectedTaskBlockOrder(sourceIndex, selectedIndex) {
|
|
215
|
+
return ((sourceIndex + 1) * 100) + selectedIndex + 1;
|
|
216
|
+
}
|
|
217
|
+
function compactSkillRouteCandidate(candidate) {
|
|
218
|
+
return {
|
|
219
|
+
skill: candidate.skill,
|
|
220
|
+
skill_path: candidate.skill_path,
|
|
221
|
+
category: candidate.category,
|
|
222
|
+
route_type: candidate.route_type,
|
|
223
|
+
score: Number(candidate.score.toFixed(3)),
|
|
224
|
+
selection_reasons: candidate.selection_reasons,
|
|
225
|
+
verification_intents: candidate.verification_intents,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function readCompactSkillRouteCandidateContent(routeReport) {
|
|
229
|
+
if (routeReport === null) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return `${JSON.stringify({
|
|
233
|
+
schema_version: '1',
|
|
234
|
+
kind: 'skill_route_candidates_compact',
|
|
235
|
+
input: routeReport.input,
|
|
236
|
+
signals: {
|
|
237
|
+
task_terms: routeReport.signals.task_terms,
|
|
238
|
+
path_terms: routeReport.signals.path_terms,
|
|
239
|
+
reasons: routeReport.signals.reasons,
|
|
240
|
+
},
|
|
241
|
+
selected: {
|
|
242
|
+
main: routeReport.selected.main === null ? null : compactSkillRouteCandidate(routeReport.selected.main),
|
|
243
|
+
adjuncts: routeReport.selected.adjuncts.map(compactSkillRouteCandidate),
|
|
244
|
+
},
|
|
245
|
+
candidates: routeReport.candidates.map(compactSkillRouteCandidate),
|
|
246
|
+
read_plan: {
|
|
247
|
+
selected_skill_paths: routeReport.read_plan.selected_skill_paths,
|
|
248
|
+
candidate_skill_paths: routeReport.read_plan.candidate_skill_paths,
|
|
249
|
+
avoid_by_default: routeReport.read_plan.avoid_by_default,
|
|
250
|
+
},
|
|
251
|
+
}, null, 2)}\n`;
|
|
252
|
+
}
|
|
111
253
|
function readCommandContractContext(projectRoot) {
|
|
112
254
|
const commands = readTomlTableIfExists(projectRoot, COMMANDS_RELATIVE_PATH);
|
|
113
255
|
if (!commands || !isRecord(commands.intents)) {
|
|
@@ -283,6 +425,66 @@ function mapLocalIndexPromptContext(context) {
|
|
|
283
425
|
refresh_hint: context.refreshHint,
|
|
284
426
|
};
|
|
285
427
|
}
|
|
428
|
+
function readSkillRouterTable(projectRoot) {
|
|
429
|
+
try {
|
|
430
|
+
const parsed = readMustflowTomlFile(projectRoot, SKILL_ROUTER_RELATIVE_PATH);
|
|
431
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function readTaskPromptCacheRouteReadPlan(projectRoot) {
|
|
438
|
+
const router = readSkillRouterTable(projectRoot);
|
|
439
|
+
const readWhen = readNestedTable(router, 'read_when');
|
|
440
|
+
const fullRoutes = readOptionalString(router, 'full_routes') ?? '.mustflow/skills/routes.toml';
|
|
441
|
+
const expandedIndex = readOptionalString(router, 'expanded_index') ?? '.mustflow/skills/INDEX.md';
|
|
442
|
+
const skillRoot = readOptionalString(router, 'skill_root') ?? '.mustflow/skills';
|
|
443
|
+
const candidates = readNumber(router, 'selection_limit') ?? 5;
|
|
444
|
+
const main = readNumber(router, 'main_limit') ?? 1;
|
|
445
|
+
const adjuncts = readNumber(router, 'adjunct_limit') ?? 2;
|
|
446
|
+
return {
|
|
447
|
+
resolver_command: ['mf', 'skill', 'route', '--json'],
|
|
448
|
+
stable_kernel: [toPosixPath(SKILL_ROUTER_RELATIVE_PATH)],
|
|
449
|
+
route_sources: [
|
|
450
|
+
toPosixPath(fullRoutes),
|
|
451
|
+
`${toPosixPath(skillRoot)}/*/SKILL.md frontmatter`,
|
|
452
|
+
],
|
|
453
|
+
selected_skill_paths_source: 'mf skill route --json read_plan.selected_skill_paths',
|
|
454
|
+
route_metadata_fallback: {
|
|
455
|
+
path: toPosixPath(fullRoutes),
|
|
456
|
+
read_when: readOptionalStringArray(readWhen, 'full_routes') ?? [],
|
|
457
|
+
avoid_by_default: true,
|
|
458
|
+
},
|
|
459
|
+
expanded_index_fallback: {
|
|
460
|
+
path: toPosixPath(expandedIndex),
|
|
461
|
+
read_when: readOptionalStringArray(readWhen, 'expanded_index') ?? [],
|
|
462
|
+
avoid_by_default: true,
|
|
463
|
+
},
|
|
464
|
+
selection_limits: { candidates, main, adjuncts },
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function readTaskPromptCacheRepoMapReadPlan() {
|
|
468
|
+
return {
|
|
469
|
+
source: 'repo_map_navigation',
|
|
470
|
+
strategy: 'select_anchors_or_spans_before_full_map',
|
|
471
|
+
anchor_sources: [
|
|
472
|
+
'local_index source anchors',
|
|
473
|
+
'REPO_MAP.md Priority Anchors',
|
|
474
|
+
'REPO_MAP.md Source Anchors',
|
|
475
|
+
],
|
|
476
|
+
fallback: {
|
|
477
|
+
path: 'REPO_MAP.md',
|
|
478
|
+
read_when: [
|
|
479
|
+
'broader repository navigation is needed',
|
|
480
|
+
'local index is missing, stale, or unavailable',
|
|
481
|
+
'selected anchors or spans do not identify the needed files',
|
|
482
|
+
'task edits the repository map generator or generated map contract',
|
|
483
|
+
],
|
|
484
|
+
avoid_by_default: true,
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
286
488
|
async function readTaskPromptCacheLayer(projectRoot, mustflow) {
|
|
287
489
|
const layer = readPromptCacheLayer(mustflow, 'task');
|
|
288
490
|
const localIndex = await readLocalIndexPromptContext(projectRoot);
|
|
@@ -290,6 +492,8 @@ async function readTaskPromptCacheLayer(projectRoot, mustflow) {
|
|
|
290
492
|
cache_layer: 'task',
|
|
291
493
|
read_policy: readOptionalString(layer, 'read_policy'),
|
|
292
494
|
sources: readOptionalStringArray(layer, 'sources') ?? [...DEFAULT_PROMPT_CACHE_TASK_SOURCES],
|
|
495
|
+
route_read_plan: readTaskPromptCacheRouteReadPlan(projectRoot),
|
|
496
|
+
repo_map_read_plan: readTaskPromptCacheRepoMapReadPlan(),
|
|
293
497
|
local_index: mapLocalIndexPromptContext(localIndex),
|
|
294
498
|
};
|
|
295
499
|
}
|
|
@@ -303,14 +507,783 @@ function readVolatilePromptCacheLayer(mustflow) {
|
|
|
303
507
|
include_latest_run: false,
|
|
304
508
|
};
|
|
305
509
|
}
|
|
306
|
-
|
|
510
|
+
function readStablePromptBundleLayer(projectRoot, mustflow) {
|
|
511
|
+
const layer = readPromptCacheLayer(mustflow, 'stable');
|
|
512
|
+
const read = readOptionalStringArray(layer, 'read') ?? [...DEFAULT_PROMPT_CACHE_STABLE_READ];
|
|
513
|
+
return {
|
|
514
|
+
cache_layer: 'stable',
|
|
515
|
+
blocks: read.map((relativePath, index) => {
|
|
516
|
+
const path = toPosixPath(relativePath);
|
|
517
|
+
const content = safeRead(projectRoot, relativePath);
|
|
518
|
+
const renderedBytes = content === null ? null : measurePromptCacheReferenceBlockBytes(relativePath, content);
|
|
519
|
+
const issue = content === null ? `stable prefix document is missing: ${path}` : null;
|
|
520
|
+
return {
|
|
521
|
+
id: `stable:${index + 1}:${path}`,
|
|
522
|
+
cache_layer: 'stable',
|
|
523
|
+
order: index + 1,
|
|
524
|
+
kind: content === null ? 'source_placeholder' : 'file',
|
|
525
|
+
path: content === null ? null : path,
|
|
526
|
+
source: null,
|
|
527
|
+
source_kind: 'file_reference',
|
|
528
|
+
selection_policy: 'always_rendered',
|
|
529
|
+
content_included: false,
|
|
530
|
+
content_hash: content === null ? null : sha256(content),
|
|
531
|
+
rendered_digest: content === null ? null : renderedDigest(relativePath, content),
|
|
532
|
+
rendered_bytes: renderedBytes,
|
|
533
|
+
estimated_tokens: renderedBytes === null ? null : estimateTokens(renderedBytes),
|
|
534
|
+
cacheability: 'provider_prefix_candidate',
|
|
535
|
+
reload_on: ['stable_file_hash_changed'],
|
|
536
|
+
issue,
|
|
537
|
+
};
|
|
538
|
+
}),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function readTaskPromptBundleLayer(projectRoot, mustflow, routeReport) {
|
|
542
|
+
const layer = readPromptCacheLayer(mustflow, 'task');
|
|
543
|
+
const sources = readOptionalStringArray(layer, 'sources') ?? [...DEFAULT_PROMPT_CACHE_TASK_SOURCES];
|
|
544
|
+
const selectedSkillPaths = selectedSkillRoutePaths(routeReport);
|
|
545
|
+
return {
|
|
546
|
+
cache_layer: 'task',
|
|
547
|
+
blocks: sources.flatMap((source, index) => {
|
|
548
|
+
const selectionPolicy = taskSourceSelectionPolicy(source);
|
|
549
|
+
const sourceKind = taskSourceKind(source);
|
|
550
|
+
const referencePath = taskSourceReferencePath(source);
|
|
551
|
+
const content = referencePath === null ? null : safeRead(projectRoot, referencePath);
|
|
552
|
+
const routeCandidateContent = source === 'skill_route_candidates'
|
|
553
|
+
? readCompactSkillRouteCandidateContent(routeReport)
|
|
554
|
+
: null;
|
|
555
|
+
const deferFallback = isDeferredTaskFallback(selectionPolicy);
|
|
556
|
+
let contentHash = null;
|
|
557
|
+
let renderedBytes = null;
|
|
558
|
+
let blockRenderedDigest = null;
|
|
559
|
+
if (routeCandidateContent !== null) {
|
|
560
|
+
contentHash = sha256(routeCandidateContent);
|
|
561
|
+
renderedBytes = measurePromptCacheReferenceBlockBytes(SKILL_ROUTE_CANDIDATES_VIRTUAL_PATH, routeCandidateContent);
|
|
562
|
+
blockRenderedDigest = renderedDigest(SKILL_ROUTE_CANDIDATES_VIRTUAL_PATH, routeCandidateContent);
|
|
563
|
+
}
|
|
564
|
+
else if (content !== null) {
|
|
565
|
+
contentHash = sha256(content);
|
|
566
|
+
if (referencePath !== null && !deferFallback) {
|
|
567
|
+
renderedBytes = measurePromptCacheReferenceBlockBytes(referencePath, content);
|
|
568
|
+
blockRenderedDigest = renderedDigest(referencePath, content);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const hasResolvedMatchingSkill = source === 'matching_skill' && selectedSkillPaths.length > 0;
|
|
572
|
+
const hasResolvedDynamicSource = hasResolvedMatchingSkill || routeCandidateContent !== null;
|
|
573
|
+
const issue = !hasResolvedDynamicSource && (content === null || deferFallback)
|
|
574
|
+
? taskSourceIssue(referencePath ?? source, sourceKind === 'file_reference' ? 'hash_only_deferred' : 'dynamic_unmeasured')
|
|
575
|
+
: null;
|
|
576
|
+
const baseBlock = {
|
|
577
|
+
id: `task:${index + 1}:${source}`,
|
|
578
|
+
cache_layer: 'task',
|
|
579
|
+
order: index + 1,
|
|
580
|
+
kind: sourceKind === 'file_reference' && content !== null && !deferFallback ? 'file' : 'source_placeholder',
|
|
581
|
+
path: sourceKind === 'file_reference' && content !== null ? referencePath : null,
|
|
582
|
+
source,
|
|
583
|
+
source_kind: sourceKind,
|
|
584
|
+
selection_policy: selectionPolicy,
|
|
585
|
+
content_included: false,
|
|
586
|
+
content_hash: contentHash,
|
|
587
|
+
rendered_digest: blockRenderedDigest,
|
|
588
|
+
rendered_bytes: renderedBytes,
|
|
589
|
+
estimated_tokens: renderedBytes === null ? null : estimateTokens(renderedBytes),
|
|
590
|
+
cacheability: taskSourceCacheability(source, selectionPolicy),
|
|
591
|
+
reload_on: taskSourceReloadOn(source, selectionPolicy),
|
|
592
|
+
issue,
|
|
593
|
+
};
|
|
594
|
+
if (source !== 'matching_skill') {
|
|
595
|
+
return [baseBlock];
|
|
596
|
+
}
|
|
597
|
+
const selectedBlocks = selectedSkillPaths.map((skillPath, selectedIndex) => {
|
|
598
|
+
const normalizedPath = toPosixPath(skillPath);
|
|
599
|
+
const skillContent = safeRead(projectRoot, normalizedPath);
|
|
600
|
+
const skillRenderedBytes = skillContent === null ? null : measurePromptCacheReferenceBlockBytes(normalizedPath, skillContent);
|
|
601
|
+
const skillIssue = skillContent === null
|
|
602
|
+
? taskSourceIssue(normalizedPath, 'hash_only_deferred')
|
|
603
|
+
: null;
|
|
604
|
+
return {
|
|
605
|
+
id: `task:${index + 1}:matching_skill:${selectedIndex + 1}:${normalizedPath}`,
|
|
606
|
+
cache_layer: 'task',
|
|
607
|
+
order: selectedTaskBlockOrder(index, selectedIndex),
|
|
608
|
+
kind: skillContent === null ? 'source_placeholder' : 'file',
|
|
609
|
+
path: skillContent === null ? null : normalizedPath,
|
|
610
|
+
source,
|
|
611
|
+
source_kind: skillContent === null ? 'dynamic_selection' : 'file_reference',
|
|
612
|
+
selection_policy: 'selected_at_runtime',
|
|
613
|
+
content_included: false,
|
|
614
|
+
content_hash: skillContent === null ? null : sha256(skillContent),
|
|
615
|
+
rendered_digest: skillContent === null ? null : renderedDigest(normalizedPath, skillContent),
|
|
616
|
+
rendered_bytes: skillRenderedBytes,
|
|
617
|
+
estimated_tokens: skillRenderedBytes === null ? null : estimateTokens(skillRenderedBytes),
|
|
618
|
+
cacheability: 'runtime_selection',
|
|
619
|
+
reload_on: ['route_resolution_changed', 'source_file_hash_changed'],
|
|
620
|
+
issue: skillIssue,
|
|
621
|
+
};
|
|
622
|
+
});
|
|
623
|
+
return [baseBlock, ...selectedBlocks];
|
|
624
|
+
}),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function readVolatilePromptBundleLayer(mustflow) {
|
|
628
|
+
const layer = readPromptCacheLayer(mustflow, 'volatile');
|
|
629
|
+
const sources = readOptionalStringArray(layer, 'sources') ?? [...DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES];
|
|
630
|
+
const issue = 'volatile suffix sources are runtime-only; current task, changed files, and command tails are not rendered here';
|
|
631
|
+
return {
|
|
632
|
+
cache_layer: 'volatile',
|
|
633
|
+
blocks: sources.map((source, index) => ({
|
|
634
|
+
id: `volatile:${index + 1}:${source}`,
|
|
635
|
+
cache_layer: 'volatile',
|
|
636
|
+
order: index + 1,
|
|
637
|
+
kind: 'source_placeholder',
|
|
638
|
+
path: null,
|
|
639
|
+
source,
|
|
640
|
+
source_kind: 'runtime_volatile',
|
|
641
|
+
selection_policy: 'volatile_runtime',
|
|
642
|
+
content_included: false,
|
|
643
|
+
content_hash: null,
|
|
644
|
+
rendered_digest: null,
|
|
645
|
+
rendered_bytes: null,
|
|
646
|
+
estimated_tokens: null,
|
|
647
|
+
cacheability: 'volatile_suffix',
|
|
648
|
+
reload_on: ['volatile_state_changed'],
|
|
649
|
+
issue,
|
|
650
|
+
})),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function readPromptBundle(projectRoot, mustflow, profile, routeReport = null) {
|
|
654
|
+
const layers = [];
|
|
655
|
+
if (profile === 'stable' || profile === 'all') {
|
|
656
|
+
layers.push(readStablePromptBundleLayer(projectRoot, mustflow));
|
|
657
|
+
}
|
|
658
|
+
if (profile === 'task' || profile === 'all') {
|
|
659
|
+
layers.push(readTaskPromptBundleLayer(projectRoot, mustflow, routeReport));
|
|
660
|
+
}
|
|
661
|
+
if (profile === 'volatile' || profile === 'all') {
|
|
662
|
+
layers.push(readVolatilePromptBundleLayer(mustflow));
|
|
663
|
+
}
|
|
664
|
+
const blocks = layers.flatMap((layer) => layer.blocks);
|
|
665
|
+
const shapeMaterial = blocks
|
|
666
|
+
.map((block) => [block.cache_layer, block.order, block.kind, block.path ?? '', block.source ?? '', block.selection_policy, block.cacheability].join('\0'))
|
|
667
|
+
.join('\n');
|
|
668
|
+
const bundleMaterial = blocks
|
|
669
|
+
.map((block) => [block.id, block.content_hash ?? '', block.rendered_digest ?? '', block.issue ?? ''].join('\0'))
|
|
670
|
+
.join('\n');
|
|
671
|
+
return {
|
|
672
|
+
schema_version: '1',
|
|
673
|
+
renderer: 'reference_bundle_utf8_lf_v1',
|
|
674
|
+
content_included: false,
|
|
675
|
+
request_shape_hash: sha256(shapeMaterial),
|
|
676
|
+
bundle_hash: sha256(bundleMaterial),
|
|
677
|
+
layers,
|
|
678
|
+
issues: [...new Set(blocks.flatMap((block) => (block.issue === null ? [] : [block.issue])))],
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function isPromptBundleBlock(value) {
|
|
682
|
+
return isRecord(value) && typeof value.id === 'string' && typeof value.cache_layer === 'string' && typeof value.order === 'number';
|
|
683
|
+
}
|
|
684
|
+
function isPromptBundleLayer(value) {
|
|
685
|
+
return isRecord(value) && typeof value.cache_layer === 'string' && Array.isArray(value.blocks) && value.blocks.every(isPromptBundleBlock);
|
|
686
|
+
}
|
|
687
|
+
function isPromptBundleContext(value) {
|
|
688
|
+
return (isRecord(value) &&
|
|
689
|
+
typeof value.request_shape_hash === 'string' &&
|
|
690
|
+
typeof value.bundle_hash === 'string' &&
|
|
691
|
+
Array.isArray(value.layers) &&
|
|
692
|
+
value.layers.every(isPromptBundleLayer));
|
|
693
|
+
}
|
|
694
|
+
function readBaselinePromptBundle(projectRoot, baselinePath) {
|
|
695
|
+
const normalizedPath = toPosixPath(baselinePath);
|
|
696
|
+
const readResult = readMustflowTextFileResult(projectRoot, normalizedPath, { maxBytes: MUSTFLOW_JSON_MAX_BYTES });
|
|
697
|
+
if (!readResult.ok) {
|
|
698
|
+
if (!readResult.exists) {
|
|
699
|
+
return {
|
|
700
|
+
status: 'baseline_missing',
|
|
701
|
+
bundle: null,
|
|
702
|
+
issues: [`baseline context report is missing: ${normalizedPath}`],
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
status: 'baseline_unreadable',
|
|
707
|
+
bundle: null,
|
|
708
|
+
issues: [`baseline context report is unreadable: ${readResult.error ?? normalizedPath}`],
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
let parsed;
|
|
712
|
+
try {
|
|
713
|
+
parsed = JSON.parse(readResult.content);
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
return {
|
|
717
|
+
status: 'baseline_invalid',
|
|
718
|
+
bundle: null,
|
|
719
|
+
issues: [`baseline context report is not valid JSON: ${error instanceof Error ? error.message : String(error)}`],
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
if (!isRecord(parsed) || !('prompt_bundle' in parsed)) {
|
|
723
|
+
return {
|
|
724
|
+
status: 'baseline_without_prompt_bundle',
|
|
725
|
+
bundle: null,
|
|
726
|
+
issues: [`baseline context report does not contain prompt_bundle: ${normalizedPath}`],
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
if (!isPromptBundleContext(parsed.prompt_bundle)) {
|
|
730
|
+
return {
|
|
731
|
+
status: 'baseline_invalid',
|
|
732
|
+
bundle: null,
|
|
733
|
+
issues: [`baseline prompt_bundle has an unsupported shape: ${normalizedPath}`],
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
return { status: 'unchanged', bundle: parsed.prompt_bundle, issues: [] };
|
|
737
|
+
}
|
|
738
|
+
function blockKey(block) {
|
|
739
|
+
return `${block.cache_layer}:${block.order}`;
|
|
740
|
+
}
|
|
741
|
+
function blockLocation(block) {
|
|
742
|
+
return [blockKey(block), block.path ?? block.source ?? block.id].join(':');
|
|
743
|
+
}
|
|
744
|
+
function compareStringArray(left, right) {
|
|
745
|
+
return left.length === right.length && left.every((entry, index) => entry === right[index]);
|
|
746
|
+
}
|
|
747
|
+
function changedBlockFields(current, baseline) {
|
|
748
|
+
const fields = [];
|
|
749
|
+
if (current.id !== baseline.id) {
|
|
750
|
+
fields.push('id');
|
|
751
|
+
}
|
|
752
|
+
if (current.kind !== baseline.kind) {
|
|
753
|
+
fields.push('kind');
|
|
754
|
+
}
|
|
755
|
+
if (current.path !== baseline.path) {
|
|
756
|
+
fields.push('path');
|
|
757
|
+
}
|
|
758
|
+
if (current.source !== baseline.source) {
|
|
759
|
+
fields.push('source');
|
|
760
|
+
}
|
|
761
|
+
if (current.source_kind !== baseline.source_kind) {
|
|
762
|
+
fields.push('source_kind');
|
|
763
|
+
}
|
|
764
|
+
if (current.selection_policy !== baseline.selection_policy) {
|
|
765
|
+
fields.push('selection_policy');
|
|
766
|
+
}
|
|
767
|
+
if (current.content_hash !== baseline.content_hash) {
|
|
768
|
+
fields.push('content_hash');
|
|
769
|
+
}
|
|
770
|
+
if (current.rendered_digest !== baseline.rendered_digest) {
|
|
771
|
+
fields.push('rendered_digest');
|
|
772
|
+
}
|
|
773
|
+
if (current.rendered_bytes !== baseline.rendered_bytes) {
|
|
774
|
+
fields.push('rendered_bytes');
|
|
775
|
+
}
|
|
776
|
+
if (current.estimated_tokens !== baseline.estimated_tokens) {
|
|
777
|
+
fields.push('estimated_tokens');
|
|
778
|
+
}
|
|
779
|
+
if (current.cacheability !== baseline.cacheability) {
|
|
780
|
+
fields.push('cacheability');
|
|
781
|
+
}
|
|
782
|
+
if (!compareStringArray(current.reload_on, baseline.reload_on)) {
|
|
783
|
+
fields.push('reload_on');
|
|
784
|
+
}
|
|
785
|
+
if (current.issue !== baseline.issue) {
|
|
786
|
+
fields.push('issue');
|
|
787
|
+
}
|
|
788
|
+
return fields;
|
|
789
|
+
}
|
|
790
|
+
function flattenBundleBlocks(bundle) {
|
|
791
|
+
return bundle.layers.flatMap((layer) => layer.blocks);
|
|
792
|
+
}
|
|
793
|
+
function stableBundleBlocks(bundle) {
|
|
794
|
+
return flattenBundleBlocks(bundle).filter((block) => block.cache_layer === 'stable');
|
|
795
|
+
}
|
|
796
|
+
function promptBundleBlocksMatch(current, baseline) {
|
|
797
|
+
return changedBlockFields(current, baseline).length === 0;
|
|
798
|
+
}
|
|
799
|
+
function comparePromptBundles(current, baseline) {
|
|
800
|
+
const currentBlocks = flattenBundleBlocks(current);
|
|
801
|
+
const baselineBlocks = flattenBundleBlocks(baseline);
|
|
802
|
+
const currentByKey = new Map(currentBlocks.map((block) => [blockKey(block), block]));
|
|
803
|
+
const baselineByKey = new Map(baselineBlocks.map((block) => [blockKey(block), block]));
|
|
804
|
+
const keys = [...new Set([...baselineBlocks.map(blockKey), ...currentBlocks.map(blockKey)])].sort();
|
|
805
|
+
const diffs = [];
|
|
806
|
+
for (const key of keys) {
|
|
807
|
+
const currentBlock = currentByKey.get(key);
|
|
808
|
+
const baselineBlock = baselineByKey.get(key);
|
|
809
|
+
if (currentBlock && !baselineBlock) {
|
|
810
|
+
diffs.push({
|
|
811
|
+
kind: 'added',
|
|
812
|
+
cache_layer: currentBlock.cache_layer,
|
|
813
|
+
order: currentBlock.order,
|
|
814
|
+
current_id: currentBlock.id,
|
|
815
|
+
baseline_id: null,
|
|
816
|
+
reason: `prompt bundle block added at ${key}`,
|
|
817
|
+
fields: [],
|
|
818
|
+
});
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (!currentBlock && baselineBlock) {
|
|
822
|
+
diffs.push({
|
|
823
|
+
kind: 'removed',
|
|
824
|
+
cache_layer: baselineBlock.cache_layer,
|
|
825
|
+
order: baselineBlock.order,
|
|
826
|
+
current_id: null,
|
|
827
|
+
baseline_id: baselineBlock.id,
|
|
828
|
+
reason: `prompt bundle block removed at ${key}`,
|
|
829
|
+
fields: [],
|
|
830
|
+
});
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (!currentBlock || !baselineBlock) {
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
const fields = changedBlockFields(currentBlock, baselineBlock);
|
|
837
|
+
if (fields.length > 0) {
|
|
838
|
+
diffs.push({
|
|
839
|
+
kind: 'changed',
|
|
840
|
+
cache_layer: currentBlock.cache_layer,
|
|
841
|
+
order: currentBlock.order,
|
|
842
|
+
current_id: currentBlock.id,
|
|
843
|
+
baseline_id: baselineBlock.id,
|
|
844
|
+
reason: `prompt bundle block changed at ${key}: ${fields.join(', ')}`,
|
|
845
|
+
fields,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return diffs;
|
|
850
|
+
}
|
|
851
|
+
function emptyStableDiff(currentBundle) {
|
|
852
|
+
return {
|
|
853
|
+
stable_block_count: stableBundleBlocks(currentBundle).length,
|
|
854
|
+
baseline_stable_block_count: null,
|
|
855
|
+
matching_prefix_blocks: 0,
|
|
856
|
+
matching_prefix_ratio: null,
|
|
857
|
+
stable_prefix_preserved: null,
|
|
858
|
+
first_stable_difference: null,
|
|
859
|
+
first_stable_invalidation_reason: null,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function readStablePrefixDifference(currentBlock, baselineBlock, index) {
|
|
863
|
+
const key = `stable prefix block ${index + 1}`;
|
|
864
|
+
if (currentBlock && !baselineBlock) {
|
|
865
|
+
return {
|
|
866
|
+
kind: 'added',
|
|
867
|
+
cache_layer: currentBlock.cache_layer,
|
|
868
|
+
order: currentBlock.order,
|
|
869
|
+
current_id: currentBlock.id,
|
|
870
|
+
baseline_id: null,
|
|
871
|
+
reason: `${key} added at ${blockLocation(currentBlock)}`,
|
|
872
|
+
fields: [],
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
if (!currentBlock && baselineBlock) {
|
|
876
|
+
return {
|
|
877
|
+
kind: 'removed',
|
|
878
|
+
cache_layer: baselineBlock.cache_layer,
|
|
879
|
+
order: baselineBlock.order,
|
|
880
|
+
current_id: null,
|
|
881
|
+
baseline_id: baselineBlock.id,
|
|
882
|
+
reason: `${key} removed at ${blockLocation(baselineBlock)}`,
|
|
883
|
+
fields: [],
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
if (!currentBlock || !baselineBlock) {
|
|
887
|
+
throw new Error('stable prefix comparison reached an impossible block state');
|
|
888
|
+
}
|
|
889
|
+
const fields = changedBlockFields(currentBlock, baselineBlock);
|
|
890
|
+
return {
|
|
891
|
+
kind: 'changed',
|
|
892
|
+
cache_layer: currentBlock.cache_layer,
|
|
893
|
+
order: currentBlock.order,
|
|
894
|
+
current_id: currentBlock.id,
|
|
895
|
+
baseline_id: baselineBlock.id,
|
|
896
|
+
reason: `${key} changed at ${blockLocation(currentBlock)}: ${fields.join(', ')}`,
|
|
897
|
+
fields,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function readPromptBundleStableDiff(currentBundle, baselineBundle) {
|
|
901
|
+
const currentStableBlocks = stableBundleBlocks(currentBundle);
|
|
902
|
+
const baselineStableBlocks = stableBundleBlocks(baselineBundle);
|
|
903
|
+
const comparedBlockCount = Math.max(currentStableBlocks.length, baselineStableBlocks.length);
|
|
904
|
+
let matchingPrefixBlocks = 0;
|
|
905
|
+
let firstStableDifference = null;
|
|
906
|
+
let index = 0;
|
|
907
|
+
while (index < comparedBlockCount) {
|
|
908
|
+
const currentBlock = currentStableBlocks[index];
|
|
909
|
+
const baselineBlock = baselineStableBlocks[index];
|
|
910
|
+
if (!currentBlock || !baselineBlock || !promptBundleBlocksMatch(currentBlock, baselineBlock)) {
|
|
911
|
+
firstStableDifference = readStablePrefixDifference(currentBlock, baselineBlock, index);
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
matchingPrefixBlocks += 1;
|
|
915
|
+
index += 1;
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
stable_block_count: currentStableBlocks.length,
|
|
919
|
+
baseline_stable_block_count: baselineStableBlocks.length,
|
|
920
|
+
matching_prefix_blocks: matchingPrefixBlocks,
|
|
921
|
+
matching_prefix_ratio: comparedBlockCount === 0
|
|
922
|
+
? null
|
|
923
|
+
: Number((matchingPrefixBlocks / comparedBlockCount).toFixed(6)),
|
|
924
|
+
stable_prefix_preserved: firstStableDifference === null && currentStableBlocks.length === baselineStableBlocks.length,
|
|
925
|
+
first_stable_difference: firstStableDifference,
|
|
926
|
+
first_stable_invalidation_reason: firstStableDifference?.reason ?? null,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function readPromptBundleDiff(projectRoot, currentBundle, baselinePath) {
|
|
930
|
+
const normalizedPath = toPosixPath(baselinePath);
|
|
931
|
+
const baseline = readBaselinePromptBundle(projectRoot, normalizedPath);
|
|
932
|
+
if (!baseline.bundle) {
|
|
933
|
+
return {
|
|
934
|
+
baseline_path: normalizedPath,
|
|
935
|
+
status: baseline.status,
|
|
936
|
+
baseline_request_shape_hash: null,
|
|
937
|
+
current_request_shape_hash: currentBundle.request_shape_hash,
|
|
938
|
+
request_shape_changed: null,
|
|
939
|
+
baseline_bundle_hash: null,
|
|
940
|
+
current_bundle_hash: currentBundle.bundle_hash,
|
|
941
|
+
bundle_changed: null,
|
|
942
|
+
first_difference: null,
|
|
943
|
+
stable_diff: emptyStableDiff(currentBundle),
|
|
944
|
+
changed_blocks: [],
|
|
945
|
+
issues: baseline.issues,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
const changedBlocks = comparePromptBundles(currentBundle, baseline.bundle);
|
|
949
|
+
const requestShapeChanged = baseline.bundle.request_shape_hash !== currentBundle.request_shape_hash;
|
|
950
|
+
const bundleChanged = baseline.bundle.bundle_hash !== currentBundle.bundle_hash;
|
|
951
|
+
return {
|
|
952
|
+
baseline_path: normalizedPath,
|
|
953
|
+
status: requestShapeChanged || bundleChanged || changedBlocks.length > 0 ? 'changed' : 'unchanged',
|
|
954
|
+
baseline_request_shape_hash: baseline.bundle.request_shape_hash,
|
|
955
|
+
current_request_shape_hash: currentBundle.request_shape_hash,
|
|
956
|
+
request_shape_changed: requestShapeChanged,
|
|
957
|
+
baseline_bundle_hash: baseline.bundle.bundle_hash,
|
|
958
|
+
current_bundle_hash: currentBundle.bundle_hash,
|
|
959
|
+
bundle_changed: bundleChanged,
|
|
960
|
+
first_difference: changedBlocks[0] ?? null,
|
|
961
|
+
stable_diff: readPromptBundleStableDiff(currentBundle, baseline.bundle),
|
|
962
|
+
changed_blocks: changedBlocks.slice(0, 20),
|
|
963
|
+
issues: [],
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function readStablePromptCacheAuditLayer(projectRoot, mustflow, settings) {
|
|
967
|
+
const layer = readPromptCacheLayer(mustflow, 'stable');
|
|
968
|
+
const read = readOptionalStringArray(layer, 'read') ?? [...DEFAULT_PROMPT_CACHE_STABLE_READ];
|
|
969
|
+
const budget = budgetBytes(settings.max_stable_prefix_kb);
|
|
970
|
+
const targetKb = readNumber(layer, 'target_kb');
|
|
971
|
+
const target = budgetBytes(targetKb);
|
|
972
|
+
const issues = [];
|
|
973
|
+
const blocks = read.map((relativePath, index) => {
|
|
974
|
+
const content = safeRead(projectRoot, relativePath);
|
|
975
|
+
const id = `stable:${index + 1}:${toPosixPath(relativePath)}`;
|
|
976
|
+
if (content === null) {
|
|
977
|
+
const issue = `stable prefix document is missing: ${toPosixPath(relativePath)}`;
|
|
978
|
+
issues.push(issue);
|
|
979
|
+
return {
|
|
980
|
+
id,
|
|
981
|
+
kind: 'file',
|
|
982
|
+
path: toPosixPath(relativePath),
|
|
983
|
+
source: null,
|
|
984
|
+
source_kind: 'file_reference',
|
|
985
|
+
selection_policy: 'always_rendered',
|
|
986
|
+
measurement_status: 'measured',
|
|
987
|
+
candidate_exists: false,
|
|
988
|
+
candidate_content_hash: null,
|
|
989
|
+
exists: false,
|
|
990
|
+
content_hash: null,
|
|
991
|
+
rendered_bytes: 0,
|
|
992
|
+
estimated_tokens: 0,
|
|
993
|
+
budget_share: calculateBudgetShare(0, budget),
|
|
994
|
+
issue,
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
const renderedBytes = measurePromptCacheReferenceBlockBytes(relativePath, content);
|
|
998
|
+
const contentHash = sha256(content);
|
|
999
|
+
return {
|
|
1000
|
+
id,
|
|
1001
|
+
kind: 'file',
|
|
1002
|
+
path: toPosixPath(relativePath),
|
|
1003
|
+
source: null,
|
|
1004
|
+
source_kind: 'file_reference',
|
|
1005
|
+
selection_policy: 'always_rendered',
|
|
1006
|
+
measurement_status: 'measured',
|
|
1007
|
+
candidate_exists: true,
|
|
1008
|
+
candidate_content_hash: contentHash,
|
|
1009
|
+
exists: true,
|
|
1010
|
+
content_hash: contentHash,
|
|
1011
|
+
rendered_bytes: renderedBytes,
|
|
1012
|
+
estimated_tokens: estimateTokens(renderedBytes),
|
|
1013
|
+
budget_share: calculateBudgetShare(renderedBytes, budget),
|
|
1014
|
+
issue: null,
|
|
1015
|
+
};
|
|
1016
|
+
});
|
|
1017
|
+
const renderedBytes = blocks.reduce((total, block) => total + (block.rendered_bytes ?? 0), 0);
|
|
1018
|
+
const estimatedTokens = estimateTokens(renderedBytes);
|
|
1019
|
+
const status = budgetStatus(renderedBytes, budget);
|
|
1020
|
+
const targetStatus = budgetStatus(renderedBytes, target);
|
|
1021
|
+
if (status === 'over_budget') {
|
|
1022
|
+
issues.push(`stable prefix exceeds max_stable_prefix_kb: ${renderedBytes} rendered bytes > ${budget} budget bytes`);
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
cache_layer: 'stable',
|
|
1026
|
+
budget_kb: settings.max_stable_prefix_kb,
|
|
1027
|
+
budget_bytes: budget,
|
|
1028
|
+
target_kb: targetKb,
|
|
1029
|
+
target_bytes: target,
|
|
1030
|
+
target_status: targetStatus,
|
|
1031
|
+
rendered_bytes: renderedBytes,
|
|
1032
|
+
estimated_tokens: estimatedTokens,
|
|
1033
|
+
budget_status: status,
|
|
1034
|
+
blocks,
|
|
1035
|
+
largest_blocks: [...blocks]
|
|
1036
|
+
.sort((left, right) => (right.rendered_bytes ?? 0) - (left.rendered_bytes ?? 0))
|
|
1037
|
+
.slice(0, 5),
|
|
1038
|
+
issues,
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function readTaskSourceAuditLayer(projectRoot, sources, budgetKb, routeReport) {
|
|
1042
|
+
const budget = budgetBytes(budgetKb);
|
|
1043
|
+
const issues = new Set();
|
|
1044
|
+
const selectedSkillPaths = selectedSkillRoutePaths(routeReport);
|
|
1045
|
+
const blocks = sources.flatMap((source, index) => {
|
|
1046
|
+
const selectionPolicy = taskSourceSelectionPolicy(source);
|
|
1047
|
+
const sourceKind = taskSourceKind(source);
|
|
1048
|
+
const referencePath = taskSourceReferencePath(source);
|
|
1049
|
+
const content = referencePath === null ? null : safeRead(projectRoot, referencePath);
|
|
1050
|
+
const routeCandidateContent = source === 'skill_route_candidates'
|
|
1051
|
+
? readCompactSkillRouteCandidateContent(routeReport)
|
|
1052
|
+
: null;
|
|
1053
|
+
const deferFallback = isDeferredTaskFallback(selectionPolicy);
|
|
1054
|
+
let contentHash = null;
|
|
1055
|
+
let renderedBytes = null;
|
|
1056
|
+
if (routeCandidateContent !== null) {
|
|
1057
|
+
contentHash = sha256(routeCandidateContent);
|
|
1058
|
+
renderedBytes = measurePromptCacheReferenceBlockBytes(SKILL_ROUTE_CANDIDATES_VIRTUAL_PATH, routeCandidateContent);
|
|
1059
|
+
}
|
|
1060
|
+
else if (content !== null) {
|
|
1061
|
+
contentHash = sha256(content);
|
|
1062
|
+
if (referencePath !== null && !deferFallback) {
|
|
1063
|
+
renderedBytes = measurePromptCacheReferenceBlockBytes(referencePath, content);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const measurementStatus = routeCandidateContent !== null
|
|
1067
|
+
? 'measured'
|
|
1068
|
+
: sourceKind === 'file_reference'
|
|
1069
|
+
? content === null || deferFallback
|
|
1070
|
+
? 'hash_only_deferred'
|
|
1071
|
+
: 'measured'
|
|
1072
|
+
: 'dynamic_unmeasured';
|
|
1073
|
+
const hasResolvedMatchingSkill = source === 'matching_skill' && selectedSkillPaths.length > 0;
|
|
1074
|
+
const hasResolvedDynamicSource = hasResolvedMatchingSkill || routeCandidateContent !== null;
|
|
1075
|
+
const issue = !hasResolvedDynamicSource && (content === null || deferFallback)
|
|
1076
|
+
? taskSourceIssue(referencePath ?? source, measurementStatus)
|
|
1077
|
+
: null;
|
|
1078
|
+
if (issue) {
|
|
1079
|
+
issues.add(issue);
|
|
1080
|
+
}
|
|
1081
|
+
if (!hasResolvedDynamicSource && sourceKind === 'dynamic_selection') {
|
|
1082
|
+
issues.add(taskSourceIssue(source, measurementStatus));
|
|
1083
|
+
}
|
|
1084
|
+
const baseBlock = {
|
|
1085
|
+
id: `task:${index + 1}:${source}`,
|
|
1086
|
+
kind: sourceKind === 'file_reference' && content !== null && !deferFallback ? 'file' : 'source_placeholder',
|
|
1087
|
+
path: sourceKind === 'file_reference' && content !== null ? referencePath : null,
|
|
1088
|
+
source,
|
|
1089
|
+
source_kind: sourceKind,
|
|
1090
|
+
selection_policy: selectionPolicy,
|
|
1091
|
+
measurement_status: measurementStatus,
|
|
1092
|
+
candidate_exists: sourceKind === 'file_reference' ? content !== null : null,
|
|
1093
|
+
candidate_content_hash: contentHash,
|
|
1094
|
+
exists: sourceKind === 'file_reference' ? content !== null : null,
|
|
1095
|
+
content_hash: contentHash,
|
|
1096
|
+
rendered_bytes: renderedBytes,
|
|
1097
|
+
estimated_tokens: renderedBytes === null ? null : estimateTokens(renderedBytes),
|
|
1098
|
+
budget_share: calculateBudgetShare(renderedBytes, budget),
|
|
1099
|
+
issue,
|
|
1100
|
+
};
|
|
1101
|
+
if (source !== 'matching_skill') {
|
|
1102
|
+
return [baseBlock];
|
|
1103
|
+
}
|
|
1104
|
+
const selectedBlocks = selectedSkillPaths.map((skillPath, selectedIndex) => {
|
|
1105
|
+
const normalizedPath = toPosixPath(skillPath);
|
|
1106
|
+
const skillContent = safeRead(projectRoot, normalizedPath);
|
|
1107
|
+
const skillRenderedBytes = skillContent === null ? null : measurePromptCacheReferenceBlockBytes(normalizedPath, skillContent);
|
|
1108
|
+
const skillMeasurementStatus = skillContent === null
|
|
1109
|
+
? 'hash_only_deferred'
|
|
1110
|
+
: 'measured';
|
|
1111
|
+
const skillIssue = skillContent === null ? taskSourceIssue(normalizedPath, skillMeasurementStatus) : null;
|
|
1112
|
+
if (skillIssue) {
|
|
1113
|
+
issues.add(skillIssue);
|
|
1114
|
+
}
|
|
1115
|
+
return {
|
|
1116
|
+
id: `task:${index + 1}:matching_skill:${selectedIndex + 1}:${normalizedPath}`,
|
|
1117
|
+
kind: skillContent === null ? 'source_placeholder' : 'file',
|
|
1118
|
+
path: skillContent === null ? null : normalizedPath,
|
|
1119
|
+
source,
|
|
1120
|
+
source_kind: skillContent === null ? 'dynamic_selection' : 'file_reference',
|
|
1121
|
+
selection_policy: 'selected_at_runtime',
|
|
1122
|
+
measurement_status: skillMeasurementStatus,
|
|
1123
|
+
candidate_exists: skillContent !== null,
|
|
1124
|
+
candidate_content_hash: skillContent === null ? null : sha256(skillContent),
|
|
1125
|
+
exists: skillContent !== null,
|
|
1126
|
+
content_hash: skillContent === null ? null : sha256(skillContent),
|
|
1127
|
+
rendered_bytes: skillRenderedBytes,
|
|
1128
|
+
estimated_tokens: skillRenderedBytes === null ? null : estimateTokens(skillRenderedBytes),
|
|
1129
|
+
budget_share: calculateBudgetShare(skillRenderedBytes, budget),
|
|
1130
|
+
issue: skillIssue,
|
|
1131
|
+
};
|
|
1132
|
+
});
|
|
1133
|
+
return [baseBlock, ...selectedBlocks];
|
|
1134
|
+
});
|
|
1135
|
+
const measuredBytes = blocks.reduce((total, block) => total + (block.rendered_bytes ?? 0), 0);
|
|
1136
|
+
const renderedBytes = measuredBytes > 0 ? measuredBytes : null;
|
|
1137
|
+
const hasDynamicSources = blocks.some((block) => block.measurement_status === 'dynamic_unmeasured');
|
|
1138
|
+
const measuredStatus = budgetStatus(renderedBytes, budget);
|
|
1139
|
+
const status = measuredStatus === 'over_budget' || !hasDynamicSources
|
|
1140
|
+
? measuredStatus
|
|
1141
|
+
: 'unknown';
|
|
1142
|
+
if (measuredStatus === 'over_budget' && renderedBytes !== null && budget !== null) {
|
|
1143
|
+
issues.add(`task context measured sources exceed max_task_context_kb: ${renderedBytes} rendered bytes > ${budget} budget bytes`);
|
|
1144
|
+
}
|
|
1145
|
+
return {
|
|
1146
|
+
cache_layer: 'task',
|
|
1147
|
+
budget_kb: budgetKb,
|
|
1148
|
+
budget_bytes: budget,
|
|
1149
|
+
target_kb: null,
|
|
1150
|
+
target_bytes: null,
|
|
1151
|
+
target_status: 'unknown',
|
|
1152
|
+
rendered_bytes: renderedBytes,
|
|
1153
|
+
estimated_tokens: renderedBytes === null ? null : estimateTokens(renderedBytes),
|
|
1154
|
+
budget_status: status,
|
|
1155
|
+
blocks,
|
|
1156
|
+
largest_blocks: [...blocks]
|
|
1157
|
+
.filter((block) => block.rendered_bytes !== null)
|
|
1158
|
+
.sort((left, right) => (right.rendered_bytes ?? 0) - (left.rendered_bytes ?? 0))
|
|
1159
|
+
.slice(0, 5),
|
|
1160
|
+
issues: [...issues],
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
function readVolatileSourceAuditLayer(sources, budgetKb) {
|
|
1164
|
+
const issue = 'volatile suffix sources are runtime-only; current task, changed files, and command tails are not rendered here';
|
|
1165
|
+
const blocks = sources.map((source, index) => ({
|
|
1166
|
+
id: `volatile:${index + 1}:${source}`,
|
|
1167
|
+
kind: 'source_placeholder',
|
|
1168
|
+
path: null,
|
|
1169
|
+
source,
|
|
1170
|
+
source_kind: 'runtime_volatile',
|
|
1171
|
+
selection_policy: 'volatile_runtime',
|
|
1172
|
+
measurement_status: 'dynamic_unmeasured',
|
|
1173
|
+
candidate_exists: null,
|
|
1174
|
+
candidate_content_hash: null,
|
|
1175
|
+
exists: null,
|
|
1176
|
+
content_hash: null,
|
|
1177
|
+
rendered_bytes: null,
|
|
1178
|
+
estimated_tokens: null,
|
|
1179
|
+
budget_share: null,
|
|
1180
|
+
issue,
|
|
1181
|
+
}));
|
|
1182
|
+
return {
|
|
1183
|
+
cache_layer: 'volatile',
|
|
1184
|
+
budget_kb: budgetKb,
|
|
1185
|
+
budget_bytes: budgetBytes(budgetKb),
|
|
1186
|
+
target_kb: null,
|
|
1187
|
+
target_bytes: null,
|
|
1188
|
+
target_status: 'unknown',
|
|
1189
|
+
rendered_bytes: null,
|
|
1190
|
+
estimated_tokens: null,
|
|
1191
|
+
budget_status: 'unknown',
|
|
1192
|
+
blocks,
|
|
1193
|
+
largest_blocks: [],
|
|
1194
|
+
issues: [issue],
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
function readPromptCacheAuditSummary(layers) {
|
|
1198
|
+
const blocks = layers.flatMap((layer) => layer.blocks);
|
|
1199
|
+
const measuredBytes = blocks.reduce((total, block) => total + (block.rendered_bytes ?? 0), 0);
|
|
1200
|
+
const measuredBlockCount = blocks.filter((block) => block.measurement_status === 'measured').length;
|
|
1201
|
+
const stableLayer = layers.find((layer) => layer.cache_layer === 'stable') ?? null;
|
|
1202
|
+
const taskLayer = layers.find((layer) => layer.cache_layer === 'task') ?? null;
|
|
1203
|
+
const volatileLayer = layers.find((layer) => layer.cache_layer === 'volatile') ?? null;
|
|
1204
|
+
const stableLayerIndex = layers.findIndex((layer) => layer.cache_layer === 'stable');
|
|
1205
|
+
const volatileBeforeStableCount = stableLayerIndex === -1
|
|
1206
|
+
? 0
|
|
1207
|
+
: layers.slice(0, stableLayerIndex).filter((layer) => layer.cache_layer === 'volatile').length;
|
|
1208
|
+
const stableLargestBlockBudgetShare = stableLayer?.largest_blocks
|
|
1209
|
+
.map((block) => block.budget_share)
|
|
1210
|
+
.filter((share) => share !== null)
|
|
1211
|
+
.sort((left, right) => right - left)[0] ?? null;
|
|
1212
|
+
const stableLeafSkillRiskPaths = stableLayer?.blocks
|
|
1213
|
+
.map((block) => block.path)
|
|
1214
|
+
.filter((blockPath) => blockPath !== null && isPromptCacheStableLeafSkillSurface(blockPath)) ?? [];
|
|
1215
|
+
return {
|
|
1216
|
+
rendered_bytes: measuredBlockCount === 0 ? null : measuredBytes,
|
|
1217
|
+
estimated_tokens: measuredBlockCount === 0 ? null : estimateTokens(measuredBytes),
|
|
1218
|
+
measured_block_count: measuredBlockCount,
|
|
1219
|
+
dynamic_source_count: blocks.filter((block) => block.source_kind !== 'file_reference').length,
|
|
1220
|
+
unresolved_reference_count: blocks.filter((block) => block.source_kind === 'file_reference' && block.measurement_status !== 'measured').length,
|
|
1221
|
+
volatile_before_stable_count: volatileBeforeStableCount,
|
|
1222
|
+
serialization_deterministic: true,
|
|
1223
|
+
stable_leaf_skill_isolated: stableLayer === null ? null : stableLeafSkillRiskPaths.length === 0,
|
|
1224
|
+
stable_leaf_skill_risk_paths: stableLeafSkillRiskPaths,
|
|
1225
|
+
leaf_skill_change_stable_hash_delta: stableLeafSkillRiskPaths.length === 0 ? 0 : null,
|
|
1226
|
+
stable_rendered_bytes: stableLayer?.rendered_bytes ?? null,
|
|
1227
|
+
stable_estimated_tokens: stableLayer?.estimated_tokens ?? null,
|
|
1228
|
+
stable_budget_status: stableLayer?.budget_status ?? null,
|
|
1229
|
+
stable_largest_block_budget_share: stableLargestBlockBudgetShare,
|
|
1230
|
+
task_budget_status: taskLayer?.budget_status ?? null,
|
|
1231
|
+
volatile_budget_status: volatileLayer?.budget_status ?? null,
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
function readPromptCacheAudit(projectRoot, mustflow, profile, settings, routeReport = null) {
|
|
1235
|
+
const layers = [];
|
|
1236
|
+
if (profile === 'stable' || profile === 'all') {
|
|
1237
|
+
layers.push(readStablePromptCacheAuditLayer(projectRoot, mustflow, settings));
|
|
1238
|
+
}
|
|
1239
|
+
if (profile === 'task' || profile === 'all') {
|
|
1240
|
+
const taskLayer = readPromptCacheLayer(mustflow, 'task');
|
|
1241
|
+
layers.push(readTaskSourceAuditLayer(projectRoot, readOptionalStringArray(taskLayer, 'sources') ?? [...DEFAULT_PROMPT_CACHE_TASK_SOURCES], settings.max_task_context_kb, routeReport));
|
|
1242
|
+
}
|
|
1243
|
+
if (profile === 'volatile' || profile === 'all') {
|
|
1244
|
+
const volatileLayer = readPromptCacheLayer(mustflow, 'volatile');
|
|
1245
|
+
layers.push(readVolatileSourceAuditLayer(readOptionalStringArray(volatileLayer, 'sources') ?? [...DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES], settings.max_volatile_suffix_kb));
|
|
1246
|
+
}
|
|
1247
|
+
return {
|
|
1248
|
+
measurement: 'reference_bundle',
|
|
1249
|
+
estimator: {
|
|
1250
|
+
name: 'reference_bundle_utf8_lf_v1',
|
|
1251
|
+
estimated_bytes_per_token: 4,
|
|
1252
|
+
caveat: 'Token counts are rough byte-based estimates, not provider billing or tokenizer output.',
|
|
1253
|
+
},
|
|
1254
|
+
canonicalization: [
|
|
1255
|
+
'UTF-8 byte length',
|
|
1256
|
+
'LF line endings',
|
|
1257
|
+
'trim trailing blank lines',
|
|
1258
|
+
'stable document path header before each block',
|
|
1259
|
+
],
|
|
1260
|
+
summary: readPromptCacheAuditSummary(layers),
|
|
1261
|
+
layers,
|
|
1262
|
+
issues: layers.flatMap((layer) => layer.issues),
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
export async function getPromptCacheProfileContext(projectRoot, profile, options = {}) {
|
|
307
1266
|
const mustflow = readTomlTableIfExists(projectRoot, MUSTFLOW_RELATIVE_PATH);
|
|
308
1267
|
const lockInspection = inspectManifestLock(projectRoot);
|
|
1268
|
+
const promptCacheSettings = readPromptCacheSettings(mustflow);
|
|
1269
|
+
const routeReport = resolvePromptCacheSkillRoutes(projectRoot, options.routeInput);
|
|
1270
|
+
const promptBundle = readPromptBundle(projectRoot, mustflow, profile, routeReport);
|
|
309
1271
|
const output = {
|
|
310
1272
|
schema_version: CONTEXT_SCHEMA_VERSION,
|
|
311
1273
|
command: 'context',
|
|
312
1274
|
cache_profile: profile,
|
|
313
|
-
prompt_cache:
|
|
1275
|
+
prompt_cache: promptCacheSettings,
|
|
1276
|
+
prompt_bundle: promptBundle,
|
|
1277
|
+
...(options.comparePath
|
|
1278
|
+
? {
|
|
1279
|
+
prompt_bundle_diff: readPromptBundleDiff(projectRoot, promptBundle, options.comparePath),
|
|
1280
|
+
}
|
|
1281
|
+
: {}),
|
|
1282
|
+
...(options.includeAudit
|
|
1283
|
+
? {
|
|
1284
|
+
cache_audit: readPromptCacheAudit(projectRoot, mustflow, profile, promptCacheSettings, routeReport),
|
|
1285
|
+
}
|
|
1286
|
+
: {}),
|
|
314
1287
|
issues: lockInspection.issues,
|
|
315
1288
|
};
|
|
316
1289
|
if (profile === 'stable' || profile === 'all') {
|