@opengsd/gsd-core 1.9.0 → 1.10.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +131 -34
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +503 -341
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +607 -63
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +106 -51
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -25
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +29 -5
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -145
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/registry-schema.cjs +323 -94
- package/scripts/run-tests.cjs +3 -2
- package/scripts/validate-registry.cjs +10 -6
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Context Composer — shared budget-trim seam (ADR-1671 Decision item 2).
|
|
4
|
+
*
|
|
5
|
+
* Extracted from the five-step trim ladder in `prompt-budget.cts` by issue
|
|
6
|
+
* #2929 (epic #1671 Phase 2) so a second consumer — per-runtime artifact
|
|
7
|
+
* emission, in a later phase — can reuse the exact same trimming behavior
|
|
8
|
+
* without re-deriving it. `prompt-budget.applyBudget` is routed through this
|
|
9
|
+
* module in a follow-up commit, which asserts byte-identical output against
|
|
10
|
+
* a committed corpus; the algorithm here is a characterization of that
|
|
11
|
+
* existing behavior, not a redesign.
|
|
12
|
+
*
|
|
13
|
+
* THE COMPOSER DECIDES, THE CALLER RENDERS. `composeWithinBudget` never
|
|
14
|
+
* joins fragments and never returns a rendered string — it returns a plan of
|
|
15
|
+
* surviving fragments with their (possibly trimmed) content, plus metadata
|
|
16
|
+
* describing what happened. Rendering is prompt-specific: the review-prompt
|
|
17
|
+
* consumer assembles Markdown sections with blank-line separators; the
|
|
18
|
+
* per-runtime emission consumer renders differently. Keeping the decision
|
|
19
|
+
* (what survives, in what shape) separate from the render (how it is joined)
|
|
20
|
+
* is what lets both consumers share this module.
|
|
21
|
+
*
|
|
22
|
+
* The budget unit is injected via `measure(text) => number` rather than
|
|
23
|
+
* hardcoded to a token estimator: `prompt-budget` passes its chars/4
|
|
24
|
+
* estimator, per-runtime emission passes a byte counter. `charsPerUnit` in
|
|
25
|
+
* `ComposeOptions` is the inverse of `measure` (4 for chars/4, 1 for a byte
|
|
26
|
+
* counter) and is what lets `proportional-truncate`'s char-budget math stay
|
|
27
|
+
* unit-agnostic.
|
|
28
|
+
*
|
|
29
|
+
* ADR-1671's literal contract describes "priority + binary-search cutoff"
|
|
30
|
+
* trimming. That description cannot express this module's actual shape:
|
|
31
|
+
* head-shrink (structural, line-bounded) and proportional-truncate-with-floor
|
|
32
|
+
* (per-fragment guaranteed minimum, not a single global cutoff) are not
|
|
33
|
+
* binary-search cutoffs. `ShrinkStrategy` — not a numeric priority field — is
|
|
34
|
+
* the core abstraction here, and a `cutoff` strategy is expected to join this
|
|
35
|
+
* closed set once per-runtime emission needs binary-search behavior. Relatedly,
|
|
36
|
+
* `verbatim` and `drop` fragments are never reordered by importance: survival
|
|
37
|
+
* and drop order both follow DECLARATION order (the order fragments appear in
|
|
38
|
+
* `ComposeInput.fragments`), not a numeric priority.
|
|
39
|
+
*/
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.headShrink = headShrink;
|
|
42
|
+
exports.tailTruncate = tailTruncate;
|
|
43
|
+
exports.composeWithinBudget = composeWithinBudget;
|
|
44
|
+
/**
|
|
45
|
+
* Head-shrink a string to at most `maxLines` lines.
|
|
46
|
+
*
|
|
47
|
+
* Copied verbatim from `prompt-budget.cts` (formerly private) — do not
|
|
48
|
+
* "improve" it; a committed corpus depends on this exact behavior.
|
|
49
|
+
*/
|
|
50
|
+
function headShrink(text, maxLines) {
|
|
51
|
+
if (maxLines <= 0)
|
|
52
|
+
return '';
|
|
53
|
+
let idx = -1;
|
|
54
|
+
let seen = 0;
|
|
55
|
+
while (seen < maxLines) {
|
|
56
|
+
idx = text.indexOf('\n', idx + 1);
|
|
57
|
+
if (idx === -1)
|
|
58
|
+
return text;
|
|
59
|
+
seen += 1;
|
|
60
|
+
}
|
|
61
|
+
return text.slice(0, idx);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Tail-truncate a string to at most `maxChars` characters.
|
|
65
|
+
*
|
|
66
|
+
* Copied verbatim from `prompt-budget.cts` (formerly private) — do not
|
|
67
|
+
* "improve" it; a committed corpus depends on this exact behavior.
|
|
68
|
+
*/
|
|
69
|
+
function tailTruncate(text, maxChars) {
|
|
70
|
+
if (text.length <= maxChars)
|
|
71
|
+
return text;
|
|
72
|
+
return text.slice(0, maxChars);
|
|
73
|
+
}
|
|
74
|
+
const isPresent = (w) => w.required || Boolean(w.content);
|
|
75
|
+
const costOf = (w, measure) => isPresent(w) ? measure(w.wrapper) + measure(w.content) : 0;
|
|
76
|
+
const totalOf = (working, measure) => working.reduce((sum, w) => sum + costOf(w, measure), 0);
|
|
77
|
+
/**
|
|
78
|
+
* Trim a set of fragments to fit within `budget`, following the exact
|
|
79
|
+
* five-step ladder characterized from `prompt-budget.applyBudget`:
|
|
80
|
+
* head-shrink, then group proportional-truncate, then declaration-order
|
|
81
|
+
* drops — each pass individually guarded by the running total against the
|
|
82
|
+
* content budget, walked once in declaration order.
|
|
83
|
+
*
|
|
84
|
+
* Returns a plan (surviving fragment content + metadata); it never renders.
|
|
85
|
+
*/
|
|
86
|
+
function composeWithinBudget(input) {
|
|
87
|
+
const { fragments, budget, measure, options = {} } = input;
|
|
88
|
+
const { safetyMarginPct = 0, reserve = 0, charsPerUnit = 1, minimumFor } = options;
|
|
89
|
+
const effectiveBudget = Math.floor(budget * (1 - safetyMarginPct / 100));
|
|
90
|
+
const seenIds = new Set();
|
|
91
|
+
const working = [];
|
|
92
|
+
let sawNonIsolate = false;
|
|
93
|
+
let isolatePrefix = '';
|
|
94
|
+
for (const f of fragments) {
|
|
95
|
+
if (seenIds.has(f.id)) {
|
|
96
|
+
throw new TypeError(`composeWithinBudget: duplicate fragment id "${f.id}"`);
|
|
97
|
+
}
|
|
98
|
+
seenIds.add(f.id);
|
|
99
|
+
const isolate = f.isolate === true;
|
|
100
|
+
if (isolate) {
|
|
101
|
+
if (sawNonIsolate) {
|
|
102
|
+
throw new TypeError(`composeWithinBudget: isolate fragment "${f.id}" declared after a non-isolate fragment; ` +
|
|
103
|
+
'isolate fragments must form a leading, contiguous prefix of ComposeInput.fragments');
|
|
104
|
+
}
|
|
105
|
+
isolatePrefix += (f.wrapper ?? '') + f.content;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
sawNonIsolate = true;
|
|
109
|
+
}
|
|
110
|
+
working.push({
|
|
111
|
+
id: f.id,
|
|
112
|
+
wrapper: f.wrapper ?? '',
|
|
113
|
+
strategy: f.strategy,
|
|
114
|
+
required: f.required === true,
|
|
115
|
+
group: f.group,
|
|
116
|
+
originalLength: f.content.length,
|
|
117
|
+
flexReserve: f.flexReserve ?? 0,
|
|
118
|
+
isolate,
|
|
119
|
+
content: f.content,
|
|
120
|
+
shrunk: false,
|
|
121
|
+
truncated: false,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// Minimum-set pre-check. Wrappers are deliberately NOT counted here, and
|
|
125
|
+
// the reserve is deliberately EXCLUDED — this checks the floor, not the
|
|
126
|
+
// steady-state budget.
|
|
127
|
+
if (minimumFor) {
|
|
128
|
+
const minTotal = fragments.reduce((sum, f) => sum + measure(minimumFor(f) ?? ''), 0);
|
|
129
|
+
if (minTotal > effectiveBudget) {
|
|
130
|
+
return {
|
|
131
|
+
fragments: [],
|
|
132
|
+
metadata: {
|
|
133
|
+
budget,
|
|
134
|
+
effectiveBudget,
|
|
135
|
+
contentBudget: effectiveBudget,
|
|
136
|
+
underPressure: false,
|
|
137
|
+
omitted: [],
|
|
138
|
+
shrunk: [],
|
|
139
|
+
floored: [],
|
|
140
|
+
truncationPct: 0,
|
|
141
|
+
hardFailed: true,
|
|
142
|
+
hardFailReason: 'minimum-set',
|
|
143
|
+
isolatePrefix,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const total = () => totalOf(working, measure);
|
|
149
|
+
const baseline = total();
|
|
150
|
+
const underPressure = baseline > effectiveBudget;
|
|
151
|
+
// The reserve is deducted ONLY when under pressure — deducting it
|
|
152
|
+
// unconditionally drops sections `reserve` units early
|
|
153
|
+
// (CONTEXT.md LEARNING.prompt-budget.boundary-gap, PR #3708).
|
|
154
|
+
const contentBudget = underPressure ? effectiveBudget - reserve : effectiveBudget;
|
|
155
|
+
const omitted = [];
|
|
156
|
+
const shrunk = [];
|
|
157
|
+
const floored = [];
|
|
158
|
+
const truncation = { pct: 0 };
|
|
159
|
+
const processedGroups = new Set();
|
|
160
|
+
for (const w of working) {
|
|
161
|
+
// Isolate fragments are the byte-stable canonical prefix (ADR-1671):
|
|
162
|
+
// never trimmed, shrunk, or dropped, regardless of declared strategy.
|
|
163
|
+
// They still count toward the budget via total()/costOf above.
|
|
164
|
+
if (w.isolate) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
switch (w.strategy.kind) {
|
|
168
|
+
case 'verbatim': {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
case 'head-shrink': {
|
|
172
|
+
if (total() > contentBudget && isPresent(w)) {
|
|
173
|
+
const next = headShrink(w.content, w.strategy.maxLines);
|
|
174
|
+
if (next !== w.content) {
|
|
175
|
+
if (w.flexReserve > 0 && measure(next) < w.flexReserve) {
|
|
176
|
+
// Shrinking would undershoot the floor — leave untouched.
|
|
177
|
+
floored.push(w.id);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
w.content = next;
|
|
181
|
+
w.shrunk = true;
|
|
182
|
+
shrunk.push(w.id);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
case 'proportional-truncate': {
|
|
189
|
+
const groupKey = w.group ?? w.id;
|
|
190
|
+
if (processedGroups.has(groupKey))
|
|
191
|
+
continue;
|
|
192
|
+
processedGroups.add(groupKey);
|
|
193
|
+
if (total() > contentBudget) {
|
|
194
|
+
const members = working.filter((m) => m.strategy.kind === 'proportional-truncate' && (m.group ?? m.id) === groupKey && !m.isolate);
|
|
195
|
+
const memberContentTotal = members.reduce((sum, m) => sum + measure(m.content), 0);
|
|
196
|
+
const overhead = total() - memberContentTotal;
|
|
197
|
+
const groupBudget = contentBudget - overhead;
|
|
198
|
+
const groupContentTotal = memberContentTotal;
|
|
199
|
+
if (groupBudget > 0 && groupBudget < groupContentTotal) {
|
|
200
|
+
const originalChars = members.reduce((sum, m) => sum + m.originalLength, 0);
|
|
201
|
+
const charsBudget = groupBudget * charsPerUnit;
|
|
202
|
+
for (const m of members) {
|
|
203
|
+
const floorChars = m.strategy.kind === 'proportional-truncate' ? m.strategy.floorChars : 0;
|
|
204
|
+
const share = originalChars > 0 ? Math.floor((m.originalLength / originalChars) * charsBudget) : 0;
|
|
205
|
+
const charsForReserve = m.flexReserve * charsPerUnit;
|
|
206
|
+
const capWithoutReserve = Math.max(share, floorChars);
|
|
207
|
+
const maxChars = Math.max(capWithoutReserve, charsForReserve);
|
|
208
|
+
const beforeTruncate = m.content;
|
|
209
|
+
const next = tailTruncate(m.content, maxChars);
|
|
210
|
+
if (next !== beforeTruncate) {
|
|
211
|
+
m.content = next;
|
|
212
|
+
m.truncated = true;
|
|
213
|
+
}
|
|
214
|
+
// flexReserve raised the cap above what the unreserved cap
|
|
215
|
+
// would have allowed, and that raised cap actually mattered
|
|
216
|
+
// (the unreserved cap would have cut into the content) —
|
|
217
|
+
// this holds whether the reserve prevented a trim outright
|
|
218
|
+
// (next === beforeTruncate) or merely shrank the trim below
|
|
219
|
+
// what the unreserved cap would have produced. Either way a
|
|
220
|
+
// cut was prevented, so it belongs in `floored`.
|
|
221
|
+
if (m.flexReserve > 0 &&
|
|
222
|
+
maxChars > capWithoutReserve &&
|
|
223
|
+
capWithoutReserve < beforeTruncate.length &&
|
|
224
|
+
!floored.includes(m.id)) {
|
|
225
|
+
floored.push(m.id);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const newChars = members.reduce((sum, m) => sum + m.content.length, 0);
|
|
229
|
+
if (originalChars > 0) {
|
|
230
|
+
truncation.pct = ((originalChars - newChars) / originalChars) * 100;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
case 'drop': {
|
|
237
|
+
if (total() > contentBudget && isPresent(w) && Boolean(w.content)) {
|
|
238
|
+
// Dropping always takes a fragment to 0, and 0 is below any
|
|
239
|
+
// positive floor — so ANY flexReserve > 0 disables drop entirely
|
|
240
|
+
// for this fragment, not just when its current size already sits
|
|
241
|
+
// at/under the floor.
|
|
242
|
+
if (w.flexReserve > 0) {
|
|
243
|
+
floored.push(w.id);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
w.content = '';
|
|
247
|
+
omitted.push(w.id);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const composedFragments = working.map((w) => ({
|
|
255
|
+
id: w.id,
|
|
256
|
+
content: w.content,
|
|
257
|
+
wrapper: w.wrapper,
|
|
258
|
+
present: isPresent(w),
|
|
259
|
+
shrunk: w.shrunk,
|
|
260
|
+
truncated: w.truncated,
|
|
261
|
+
}));
|
|
262
|
+
return {
|
|
263
|
+
fragments: composedFragments,
|
|
264
|
+
metadata: {
|
|
265
|
+
budget,
|
|
266
|
+
effectiveBudget,
|
|
267
|
+
contentBudget,
|
|
268
|
+
underPressure,
|
|
269
|
+
omitted,
|
|
270
|
+
shrunk,
|
|
271
|
+
floored,
|
|
272
|
+
truncationPct: truncation.pct,
|
|
273
|
+
hardFailed: false,
|
|
274
|
+
hardFailReason: null,
|
|
275
|
+
isolatePrefix,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|