@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.
Files changed (223) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +131 -34
  5. package/agents/gsd-debugger.md +12 -246
  6. package/agents/gsd-executor.md +7 -5
  7. package/agents/gsd-integration-checker.md +3 -0
  8. package/agents/gsd-plan-checker.md +9 -0
  9. package/agents/gsd-planner.md +5 -8
  10. package/agents/gsd-roadmapper.md +21 -3
  11. package/agents/gsd-verifier.md +14 -70
  12. package/bin/install.js +503 -341
  13. package/commands/gsd/mempalace-capture.md +1 -1
  14. package/commands/gsd/new-milestone.md +1 -1
  15. package/commands/gsd/plan-phase.md +1 -1
  16. package/gsd-core/bin/gsd-tools.cjs +607 -63
  17. package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
  18. package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
  19. package/gsd-core/bin/lib/api-coverage.cjs +120 -0
  20. package/gsd-core/bin/lib/audit.cjs +89 -1
  21. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  22. package/gsd-core/bin/lib/capability-registry.cjs +96 -110
  23. package/gsd-core/bin/lib/capability-validator.cjs +12 -2
  24. package/gsd-core/bin/lib/check-command-router.cjs +43 -1
  25. package/gsd-core/bin/lib/command-aliases.cjs +72 -0
  26. package/gsd-core/bin/lib/commands.cjs +26 -25
  27. package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
  28. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  29. package/gsd-core/bin/lib/config.cjs +12 -1
  30. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  31. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  32. package/gsd-core/bin/lib/core-utils.cjs +91 -12
  33. package/gsd-core/bin/lib/docs.cjs +3 -2
  34. package/gsd-core/bin/lib/external-job.cjs +19 -4
  35. package/gsd-core/bin/lib/frontmatter.cjs +84 -12
  36. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  37. package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
  38. package/gsd-core/bin/lib/graphify.cjs +142 -27
  39. package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
  40. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  41. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  42. package/gsd-core/bin/lib/init.cjs +1021 -57
  43. package/gsd-core/bin/lib/install-engine.cjs +64 -10
  44. package/gsd-core/bin/lib/install-profiles.cjs +27 -1
  45. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  47. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  48. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  49. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  50. package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
  51. package/gsd-core/bin/lib/io.cjs +28 -3
  52. package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
  53. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  54. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  55. package/gsd-core/bin/lib/milestone.cjs +106 -51
  56. package/gsd-core/bin/lib/phase-id.cjs +63 -0
  57. package/gsd-core/bin/lib/phase-locator.cjs +138 -45
  58. package/gsd-core/bin/lib/phase.cjs +260 -25
  59. package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
  60. package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
  61. package/gsd-core/bin/lib/project-root.cjs +48 -0
  62. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  63. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
  64. package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
  65. package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
  66. package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
  67. package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
  68. package/gsd-core/bin/lib/roadmap.cjs +37 -7
  69. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
  70. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
  71. package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
  72. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
  73. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  74. package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
  75. package/gsd-core/bin/lib/smart-entry.cjs +12 -0
  76. package/gsd-core/bin/lib/state-transition.cjs +73 -8
  77. package/gsd-core/bin/lib/state.cjs +151 -62
  78. package/gsd-core/bin/lib/surface.cjs +12 -1
  79. package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
  80. package/gsd-core/bin/lib/uat.cjs +320 -21
  81. package/gsd-core/bin/lib/unusable-input.cjs +9 -0
  82. package/gsd-core/bin/lib/verification.cjs +29 -12
  83. package/gsd-core/bin/lib/verify.cjs +29 -5
  84. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  85. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
  86. package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
  87. package/gsd-core/bin/lib/workstream.cjs +6 -0
  88. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  89. package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
  90. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  91. package/gsd-core/references/artifact-types.md +10 -3
  92. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  93. package/gsd-core/references/debugger-techniques.md +255 -0
  94. package/gsd-core/references/research-documentation-lookup.md +5 -3
  95. package/gsd-core/references/specless-probe-fallback.md +7 -6
  96. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  97. package/gsd-core/references/worktree-branch-check.md +2 -2
  98. package/gsd-core/templates/summary-complex.md +2 -0
  99. package/gsd-core/templates/summary-minimal.md +2 -0
  100. package/gsd-core/templates/summary-standard.md +2 -0
  101. package/gsd-core/templates/summary.md +2 -0
  102. package/gsd-core/workflows/audit-milestone.md +3 -0
  103. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  104. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  105. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  106. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  107. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  108. package/gsd-core/workflows/autonomous.md +32 -69
  109. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  110. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
  111. package/gsd-core/workflows/code-review.md +42 -145
  112. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  113. package/gsd-core/workflows/complete-milestone.md +23 -81
  114. package/gsd-core/workflows/debug.md +9 -12
  115. package/gsd-core/workflows/diagnose-issues.md +22 -0
  116. package/gsd-core/workflows/discovery-phase.md +4 -4
  117. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  118. package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
  119. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  120. package/gsd-core/workflows/docs-update.md +8 -51
  121. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
  122. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  123. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  124. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  126. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  127. package/gsd-core/workflows/execute-phase.md +65 -137
  128. package/gsd-core/workflows/execute-plan.md +1 -1
  129. package/gsd-core/workflows/help/modes/full.md +6 -1
  130. package/gsd-core/workflows/ingest-docs.md +2 -1
  131. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  132. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  133. package/gsd-core/workflows/new-milestone.md +21 -38
  134. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  135. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  136. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  137. package/gsd-core/workflows/new-project.md +13 -226
  138. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  139. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  140. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  141. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  142. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  143. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  144. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  145. package/gsd-core/workflows/plan-phase.md +49 -193
  146. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  147. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  148. package/gsd-core/workflows/progress.md +11 -153
  149. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  150. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  151. package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
  152. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  153. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  154. package/gsd-core/workflows/quick.md +20 -390
  155. package/gsd-core/workflows/resume-project.md +3 -0
  156. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  157. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  158. package/gsd-core/workflows/review.md +15 -8
  159. package/gsd-core/workflows/section-manifest.json +219 -0
  160. package/gsd-core/workflows/sketch.md +1 -1
  161. package/gsd-core/workflows/spec-phase.md +17 -14
  162. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  163. package/gsd-core/workflows/spike.md +50 -16
  164. package/gsd-core/workflows/sync-skills.md +49 -11
  165. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  166. package/gsd-core/workflows/transition.md +8 -21
  167. package/gsd-core/workflows/ui-phase.md +8 -7
  168. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  169. package/gsd-core/workflows/update.md +18 -7
  170. package/gsd-core/workflows/verify-phase.md +4 -7
  171. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  172. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  173. package/gsd-core/workflows/verify-work.md +8 -58
  174. package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
  175. package/hooks/dist/gsd-check-update-worker.js +14 -5
  176. package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
  177. package/hooks/dist/gsd-read-injection-scanner.js +7 -0
  178. package/hooks/dist/gsd-statusline.js +72 -6
  179. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  180. package/hooks/dist/gsd-write-guard.js +359 -0
  181. package/hooks/dist/lib/isolation-sentinel.js +268 -0
  182. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  183. package/hooks/gsd-agent-isolation-guard.js +428 -0
  184. package/hooks/gsd-check-update-worker.js +14 -5
  185. package/hooks/gsd-cursor-subagent-start.js +532 -26
  186. package/hooks/gsd-read-injection-scanner.js +7 -0
  187. package/hooks/gsd-statusline.js +72 -6
  188. package/hooks/gsd-worktree-path-guard.js +2 -1
  189. package/hooks/gsd-write-guard.js +359 -0
  190. package/hooks/hooks.json +12 -0
  191. package/hooks/lib/isolation-sentinel.js +268 -0
  192. package/hooks/managed-hooks-registry.cjs +2 -0
  193. package/package.json +14 -5
  194. package/pi/gsd.cjs +57 -12
  195. package/scripts/build-hooks.js +9 -0
  196. package/scripts/changeset/lint.cjs +9 -2
  197. package/scripts/changeset/serialize.cjs +5 -1
  198. package/scripts/gen-capability-matrix.cjs +1 -1
  199. package/scripts/gen-context-index.cjs +448 -0
  200. package/scripts/gen-inventory-manifest.cjs +101 -1
  201. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  202. package/scripts/gen-registry.cjs +39 -15
  203. package/scripts/gen-section-manifest.cjs +638 -0
  204. package/scripts/generate-package-identity.cjs +4 -2
  205. package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
  206. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  207. package/scripts/lint-docs-command-form.cjs +195 -0
  208. package/scripts/lint-docs-required.cjs +9 -1
  209. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  210. package/scripts/lint-example-parser-parity.cjs +395 -0
  211. package/scripts/lint-test-file-count.allowlist.json +27 -1
  212. package/scripts/mutation-matrix.cjs +13 -0
  213. package/scripts/prompt-injection-scan.sh +27 -6
  214. package/scripts/registry-schema.cjs +323 -94
  215. package/scripts/run-tests.cjs +3 -2
  216. package/scripts/validate-registry.cjs +10 -6
  217. package/skills/gsd-autonomous/SKILL.md +1 -1
  218. package/skills/gsd-execute-phase/SKILL.md +1 -1
  219. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  220. package/skills/gsd-new-milestone/SKILL.md +1 -1
  221. package/skills/gsd-plan-phase/SKILL.md +2 -2
  222. package/vscode/package.json +1 -1
  223. 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
+ }