@event4u/agent-config 14.13.0 → 14.14.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +116 -387
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
- package/dist/agent-src/scripts/stubs_due.ts +118 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/cmd_conformance.js +38 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +4 -4
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1438 -711
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +112 -17
- package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
- package/docs/guidelines/code-clarity.md +18 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/src/agent-src/scripts/guarded_baseline.ts +22 -7
- package/src/agent-src/scripts/stubs_due.ts +118 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/src/config/estate-count-budget.json +20 -1
- package/src/config/gate-coverage.yml +150 -0
- package/src/config/gate-violation-baselines.json +8 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/scripts/_dispatch.bash +0 -4
- package/src/scripts/_lib/activation_ladder.ts +47 -3
- package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
- package/src/scripts/_lib/adr_frontmatter.ts +26 -0
- package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
- package/src/scripts/_lib/carrier_status.ts +118 -0
- package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
- package/src/scripts/_lib/corpus_manifest.ts +726 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
- package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
- package/src/scripts/_lib/evaluation_cascade.ts +452 -0
- package/src/scripts/_lib/evolution_roi.ts +562 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
- package/src/scripts/_lib/lexical_shortlist.ts +215 -0
- package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
- package/src/scripts/_lib/pathology_archive.ts +393 -0
- package/src/scripts/_lib/promotion_capability.ts +315 -0
- package/src/scripts/_lib/promotion_evidence.ts +414 -0
- package/src/scripts/_lib/promotion_review.ts +294 -0
- package/src/scripts/_lib/prompt_shape.ts +92 -0
- package/src/scripts/_lib/release_highlights.ts +70 -1
- package/src/scripts/_lib/release_material.ts +125 -2
- package/src/scripts/_lib/routing_corpus.ts +307 -0
- package/src/scripts/_lib/routing_index_input.ts +104 -0
- package/src/scripts/_lib/rule_injection.ts +22 -1
- package/src/scripts/_lib/semantic_noop.ts +167 -0
- package/src/scripts/activation_receipt.ts +258 -0
- package/src/scripts/ai_council/cross_exam.ts +128 -0
- package/src/scripts/ai_council/explain_route.ts +157 -0
- package/src/scripts/ai_council/information_gain.ts +293 -0
- package/src/scripts/ai_council/leakage_corpus.ts +385 -0
- package/src/scripts/ai_council/leakage_patterns.ts +453 -0
- package/src/scripts/ai_council/minority_retention.ts +171 -0
- package/src/scripts/ai_council/recouncil_savings.ts +299 -0
- package/src/scripts/ai_council/replay_route.ts +223 -0
- package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
- package/src/scripts/ai_council/routing_training_row.ts +197 -0
- package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
- package/src/scripts/check_adr_frontmatter.ts +4 -9
- package/src/scripts/check_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +96 -13
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +181 -8
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +28 -6
- package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
- package/src/scripts/language_mirror_hook.ts +14 -2
- package/src/scripts/lint_carrier_integrity.ts +820 -0
- package/src/scripts/lint_code_comments.ts +643 -0
- package/src/scripts/lint_plan_risk_register.ts +39 -19
- package/src/scripts/lint_promotion_paths.ts +779 -0
- package/src/scripts/llm_propose.ts +256 -0
- package/src/scripts/measure_delivery_sets.ts +361 -0
- package/src/scripts/measure_routing_signal.ts +307 -0
- package/src/scripts/model_rule_injection.ts +19 -3
- package/src/scripts/probe_council_retention.ts +495 -0
- package/src/scripts/release.ts +21 -37
- package/src/scripts/release_drill.ts +143 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +84 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lifecycle AFTER promotion — `road-to-harness-promotion-bridge` steps 7.6
|
|
3
|
+
* and 7.7.
|
|
4
|
+
*
|
|
5
|
+
* ## 7.6 — a promoted artefact is not immortal
|
|
6
|
+
*
|
|
7
|
+
* > *post-promotion re-evaluation with `KEEP / REVISE / MERGE / SPLIT / RETIRE`.
|
|
8
|
+
* > The master's promotion phase ends at the evidence package plus a cooldown,
|
|
9
|
+
* > so nothing reopens a promoted artefact and the lifecycle is manual-only at
|
|
10
|
+
* > exactly the point where growth accumulates.*
|
|
11
|
+
* > verify: **a promoted artefact reaching its review trigger produces one of
|
|
12
|
+
* > the five verdicts, and at least one `RETIRE` path is exercised in a
|
|
13
|
+
* > fixture.**
|
|
14
|
+
*
|
|
15
|
+
* Two halves were already in the tree and one was missing. Four of the five
|
|
16
|
+
* verdicts are E6 curator ops (`_lib/curator_ops.ts`) — `REVISE` is not — and
|
|
17
|
+
* `promoted -> retired` is already the one legal retirement edge
|
|
18
|
+
* (`_lib/candidate_record.ts:210-219`). What did not exist was the REVIEW
|
|
19
|
+
* TRIGGER: nothing decided that a promoted artefact was due, and nothing turned
|
|
20
|
+
* that into a verdict. {@link reviewPromoted} is that.
|
|
21
|
+
*
|
|
22
|
+
* **The verdict set is NOT a subset of `CURATOR_OPS`, and finding that out is
|
|
23
|
+
* part of what this step buys.** E6 argued for seven curator ops on the ground
|
|
24
|
+
* that 7.6 specifies five verdicts a four-op curator could not execute — and the
|
|
25
|
+
* shipped seven still cannot execute one of them, because `REVISE` is not among
|
|
26
|
+
* them. The relationship is pinned by a test in both directions rather than
|
|
27
|
+
* papered over with a mapping nobody chose; see {@link POST_PROMOTION_VERDICTS}.
|
|
28
|
+
*
|
|
29
|
+
* ## 7.7 — best-known-state reference on regression
|
|
30
|
+
*
|
|
31
|
+
* > *Roll back to the recorded best-known state; lineage, not endless append.*
|
|
32
|
+
* > verify: **an injected regression triggers the rollback path in a fixture.**
|
|
33
|
+
*
|
|
34
|
+
* {@link BestKnownState} is the record, {@link planRollback} is the decision.
|
|
35
|
+
* "Lineage, not endless append" is carried in the type: a rollback names the
|
|
36
|
+
* state it returns to and the state it leaves, so the history is a chain rather
|
|
37
|
+
* than a pile of snapshots nobody can order.
|
|
38
|
+
*
|
|
39
|
+
* ## This module promotes nothing
|
|
40
|
+
*
|
|
41
|
+
* It reads promoted artefacts and it can RETIRE one — the only transition it
|
|
42
|
+
* expresses is `promoted -> retired`, which `assertTransition` accepts without
|
|
43
|
+
* an approver because retirement is the direction that shrinks the estate.
|
|
44
|
+
* There is no path here that moves anything INTO `promoted`; that stays behind
|
|
45
|
+
* `_lib/promotion_capability.ts`, and `lint_promotion_paths.ts` is what proves
|
|
46
|
+
* this file did not grow one.
|
|
47
|
+
*/
|
|
48
|
+
import { ACCEPTED_STATE, type LifecycleState, assertTransition } from './candidate_record.js';
|
|
49
|
+
import { CURATOR_OPS, type CuratorOp } from './curator_ops.js';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The five verdicts step 7.6 names, verbatim.
|
|
53
|
+
*
|
|
54
|
+
* **Four of the five are E6 curator ops. `REVISE` is not, and that is a finding
|
|
55
|
+
* rather than a detail.** `_lib/curator_ops.ts:48-56` is `KEEP · ADD · MERGE ·
|
|
56
|
+
* REPLACE · SPLIT · RETIRE · SKIP`, and its own header argues for seven ops on
|
|
57
|
+
* the ground that *"step 7.6 of the same roadmap already specifies the verdict
|
|
58
|
+
* set KEEP / REVISE / MERGE / SPLIT / RETIRE, so a four-op curator would emit
|
|
59
|
+
* verdicts it cannot execute"*. Measured against the shipped set: `REVISE` is
|
|
60
|
+
* still a verdict the curator cannot execute — `REPLACE` is the nearest op and
|
|
61
|
+
* it is not the same thing, since `REPLACE` is 1→1 with a new artefact while
|
|
62
|
+
* `REVISE` edits the one in place and keeps its identity.
|
|
63
|
+
*
|
|
64
|
+
* The list is therefore written out rather than derived, because deriving it
|
|
65
|
+
* would require inventing the mapping the two sets do not have. What IS pinned,
|
|
66
|
+
* by `tests/scripts/promotion_review.test.ts`, is the relationship in both
|
|
67
|
+
* directions: the four shared verdicts really are curator ops, and `REVISE`
|
|
68
|
+
* really is not — so a later change that adds `REVISE` to `CURATOR_OPS`, or
|
|
69
|
+
* drops one of the four, fails a test instead of drifting.
|
|
70
|
+
*/
|
|
71
|
+
export const POST_PROMOTION_VERDICTS = ['KEEP', 'REVISE', 'MERGE', 'SPLIT', 'RETIRE'] as const;
|
|
72
|
+
|
|
73
|
+
/** The four verdicts the curator can actually execute today. */
|
|
74
|
+
export const VERDICTS_EXECUTABLE_BY_CURATOR: readonly CuratorOp[] = CURATOR_OPS.filter(
|
|
75
|
+
(op) => (POST_PROMOTION_VERDICTS as readonly string[]).includes(op),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export type PostPromotionVerdict = (typeof POST_PROMOTION_VERDICTS)[number];
|
|
79
|
+
|
|
80
|
+
/** Why a promoted artefact came up for review. Absent means it is not due. */
|
|
81
|
+
export const REVIEW_TRIGGERS = [
|
|
82
|
+
'regression',
|
|
83
|
+
'usage-floor',
|
|
84
|
+
'duplicate-detected',
|
|
85
|
+
'scope-overgrown',
|
|
86
|
+
'age',
|
|
87
|
+
] as const;
|
|
88
|
+
export type ReviewTrigger = (typeof REVIEW_TRIGGERS)[number];
|
|
89
|
+
|
|
90
|
+
/** What a review reads. Every field is an observation, not a judgement. */
|
|
91
|
+
export interface PromotedArtefactState {
|
|
92
|
+
readonly id: string;
|
|
93
|
+
readonly lifecycle: LifecycleState;
|
|
94
|
+
/** Measured effect now, in the same units as the promotion evidence's delta. */
|
|
95
|
+
readonly currentDeltaPercent: number;
|
|
96
|
+
/** The delta recorded at promotion. The regression test is `current < promoted`. */
|
|
97
|
+
readonly promotedDeltaPercent: number;
|
|
98
|
+
/** How many times the artefact was actually reached since promotion. */
|
|
99
|
+
readonly invocations: number;
|
|
100
|
+
/** Ids of promoted artefacts whose text this one now duplicates. */
|
|
101
|
+
readonly duplicates: readonly string[];
|
|
102
|
+
/** Distinct responsibilities the artefact has accumulated. */
|
|
103
|
+
readonly responsibilities: number;
|
|
104
|
+
readonly daysSincePromotion: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class NotPromotedError extends Error {
|
|
108
|
+
constructor(id: string, lifecycle: LifecycleState) {
|
|
109
|
+
super(
|
|
110
|
+
`'${id}' is '${lifecycle}', not '${ACCEPTED_STATE}' — post-promotion review acts on a ` +
|
|
111
|
+
'PROMOTED artefact. A candidate that never landed is rejected, not retired, and ' +
|
|
112
|
+
'reviewing one here would report a lifecycle event that did not happen.',
|
|
113
|
+
);
|
|
114
|
+
this.name = 'NotPromotedError';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ReviewOutcome {
|
|
119
|
+
readonly id: string;
|
|
120
|
+
readonly trigger: ReviewTrigger;
|
|
121
|
+
readonly verdict: PostPromotionVerdict;
|
|
122
|
+
readonly reason: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The floors the review reads. Stated defaults; each carries its own revisit-if. */
|
|
126
|
+
export const REVIEW_THRESHOLDS = {
|
|
127
|
+
/** Below this many invocations the artefact is not earning its place. `revisit-if` a
|
|
128
|
+
* legitimately rare artefact is retired for being rare. */
|
|
129
|
+
usageFloor: 1,
|
|
130
|
+
/** Distinct responsibilities above which the artefact should be SPLIT. `revisit-if` a
|
|
131
|
+
* cohesive artefact with several facets is split for having them. */
|
|
132
|
+
responsibilityCeiling: 2,
|
|
133
|
+
/** Days after which an unreviewed artefact is due. `revisit-if` the cadence is noise. */
|
|
134
|
+
ageDays: 180,
|
|
135
|
+
} as const;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Is this artefact due for review, and why? `null` means not due.
|
|
139
|
+
*
|
|
140
|
+
* Order is precedence: a regression outranks a usage question, which outranks a
|
|
141
|
+
* duplicate, which outranks overgrowth, which outranks mere age. Without a fixed
|
|
142
|
+
* order two triggers firing at once would make the verdict depend on evaluation
|
|
143
|
+
* order, which is how a review becomes irreproducible.
|
|
144
|
+
*/
|
|
145
|
+
export function reviewTriggerFor(state: PromotedArtefactState): ReviewTrigger | null {
|
|
146
|
+
if (state.currentDeltaPercent < state.promotedDeltaPercent) return 'regression';
|
|
147
|
+
if (state.invocations < REVIEW_THRESHOLDS.usageFloor) return 'usage-floor';
|
|
148
|
+
if (state.duplicates.length > 0) return 'duplicate-detected';
|
|
149
|
+
if (state.responsibilities > REVIEW_THRESHOLDS.responsibilityCeiling) return 'scope-overgrown';
|
|
150
|
+
if (state.daysSincePromotion >= REVIEW_THRESHOLDS.ageDays) return 'age';
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Produce exactly ONE of the five verdicts for a promoted artefact that reached
|
|
156
|
+
* its review trigger.
|
|
157
|
+
*
|
|
158
|
+
* @throws {NotPromotedError} when the artefact is not promoted.
|
|
159
|
+
*/
|
|
160
|
+
export function reviewPromoted(state: PromotedArtefactState, trigger: ReviewTrigger): ReviewOutcome {
|
|
161
|
+
if (state.lifecycle !== ACCEPTED_STATE) {
|
|
162
|
+
throw new NotPromotedError(state.id, state.lifecycle);
|
|
163
|
+
}
|
|
164
|
+
const outcome = (verdict: PostPromotionVerdict, reason: string): ReviewOutcome => ({
|
|
165
|
+
id: state.id,
|
|
166
|
+
trigger,
|
|
167
|
+
verdict,
|
|
168
|
+
reason,
|
|
169
|
+
});
|
|
170
|
+
switch (trigger) {
|
|
171
|
+
case 'regression':
|
|
172
|
+
// A measured effect that fell BELOW zero is not worth revising; it is
|
|
173
|
+
// now costing what it was promoted to save.
|
|
174
|
+
return state.currentDeltaPercent <= 0
|
|
175
|
+
? outcome('RETIRE', `effect fell to ${String(state.currentDeltaPercent)} pp — it no longer earns its place`)
|
|
176
|
+
: outcome(
|
|
177
|
+
'REVISE',
|
|
178
|
+
`effect fell ${String(state.promotedDeltaPercent)} -> ${String(state.currentDeltaPercent)} pp ` +
|
|
179
|
+
'but is still positive',
|
|
180
|
+
);
|
|
181
|
+
case 'usage-floor':
|
|
182
|
+
return outcome('RETIRE', `reached ${String(state.invocations)} time(s) since promotion`);
|
|
183
|
+
case 'duplicate-detected':
|
|
184
|
+
return outcome('MERGE', `now duplicates ${state.duplicates.join(', ')}`);
|
|
185
|
+
case 'scope-overgrown':
|
|
186
|
+
return outcome('SPLIT', `carries ${String(state.responsibilities)} distinct responsibilities`);
|
|
187
|
+
case 'age':
|
|
188
|
+
return outcome('KEEP', `${String(state.daysSincePromotion)} days since promotion, effect held`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Execute the RETIRE verdict.
|
|
194
|
+
*
|
|
195
|
+
* Routes through `assertTransition`, so the ONE legal retirement edge is the one
|
|
196
|
+
* this takes and a caller cannot retire something that never landed. Returns the
|
|
197
|
+
* next lifecycle state rather than writing anything — nothing in this module
|
|
198
|
+
* touches the filesystem.
|
|
199
|
+
*
|
|
200
|
+
* @throws {LifecycleTransitionError} from `assertTransition`.
|
|
201
|
+
*/
|
|
202
|
+
export function retirePromoted(state: PromotedArtefactState): LifecycleState {
|
|
203
|
+
assertTransition(state.lifecycle, 'retired');
|
|
204
|
+
return 'retired';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- 7.7 — best-known state and rollback ------------------------------------
|
|
208
|
+
|
|
209
|
+
/** A recorded good state, and the state it came from. Lineage, not endless append. */
|
|
210
|
+
export interface BestKnownState {
|
|
211
|
+
readonly artefactId: string;
|
|
212
|
+
/** The revision or bundle identifier this state is recoverable from. */
|
|
213
|
+
readonly reference: string;
|
|
214
|
+
readonly deltaPercent: number;
|
|
215
|
+
/** The best-known state this one replaced. `null` only for the first. */
|
|
216
|
+
readonly supersedes: string | null;
|
|
217
|
+
readonly recordedAt: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface RollbackPlan {
|
|
221
|
+
readonly artefactId: string;
|
|
222
|
+
readonly from: string;
|
|
223
|
+
readonly to: string;
|
|
224
|
+
readonly reason: string;
|
|
225
|
+
/** Oldest first. The chain that makes this a lineage rather than a pile. */
|
|
226
|
+
readonly lineage: readonly string[];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export class NoBestKnownStateError extends Error {
|
|
230
|
+
constructor(artefactId: string) {
|
|
231
|
+
super(
|
|
232
|
+
`'${artefactId}' regressed and has no recorded best-known state to return to. A rollback ` +
|
|
233
|
+
'target is recorded at promotion time, not reconstructed after the regression — by ' +
|
|
234
|
+
'then the state that worked is exactly what is missing.',
|
|
235
|
+
);
|
|
236
|
+
this.name = 'NoBestKnownStateError';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Walk the `supersedes` chain, oldest first.
|
|
242
|
+
*
|
|
243
|
+
* Cycle-guarded: a chain that points at itself is a corrupted lineage, and
|
|
244
|
+
* following it forever would hang the caller rather than report the corruption.
|
|
245
|
+
*/
|
|
246
|
+
export function lineageOf(head: BestKnownState, all: readonly BestKnownState[]): string[] {
|
|
247
|
+
const byRef = new Map(all.map((s) => [s.reference, s]));
|
|
248
|
+
const chain: string[] = [];
|
|
249
|
+
const seen = new Set<string>();
|
|
250
|
+
let cursor: BestKnownState | undefined = head;
|
|
251
|
+
while (cursor !== undefined && !seen.has(cursor.reference)) {
|
|
252
|
+
seen.add(cursor.reference);
|
|
253
|
+
chain.push(cursor.reference);
|
|
254
|
+
cursor = cursor.supersedes === null ? undefined : byRef.get(cursor.supersedes);
|
|
255
|
+
}
|
|
256
|
+
return chain.reverse();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Plan a rollback when the current state regressed against the best known one.
|
|
261
|
+
*
|
|
262
|
+
* `null` when there is no regression — the caller does not have to know the
|
|
263
|
+
* comparison, and a rollback that fires on an improvement is worse than none.
|
|
264
|
+
*
|
|
265
|
+
* @throws {NoBestKnownStateError} when a regression has nowhere to return to.
|
|
266
|
+
*/
|
|
267
|
+
export function planRollback(
|
|
268
|
+
artefactId: string,
|
|
269
|
+
currentReference: string,
|
|
270
|
+
currentDeltaPercent: number,
|
|
271
|
+
history: readonly BestKnownState[],
|
|
272
|
+
): RollbackPlan | null {
|
|
273
|
+
const mine = history.filter((s) => s.artefactId === artefactId);
|
|
274
|
+
const best = mine.reduce<BestKnownState | null>(
|
|
275
|
+
(acc, s) => (acc === null || s.deltaPercent > acc.deltaPercent ? s : acc),
|
|
276
|
+
null,
|
|
277
|
+
);
|
|
278
|
+
if (best === null) {
|
|
279
|
+
if (currentDeltaPercent < 0) throw new NoBestKnownStateError(artefactId);
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
if (currentDeltaPercent >= best.deltaPercent) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
artefactId,
|
|
287
|
+
from: currentReference,
|
|
288
|
+
to: best.reference,
|
|
289
|
+
reason:
|
|
290
|
+
`regressed ${String(best.deltaPercent)} -> ${String(currentDeltaPercent)} pp against the ` +
|
|
291
|
+
`best-known state '${best.reference}'`,
|
|
292
|
+
lineage: lineageOf(best, mine),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
@@ -86,3 +86,95 @@ export function humanAuthoredLead(text: string): string {
|
|
|
86
86
|
}
|
|
87
87
|
return kept.join("\n");
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
/** A line that is nothing but an opening tag, with the tag name captured. */
|
|
91
|
+
const REGION_OPEN = /^\s*<([A-Za-z][\w-]*)>\s*$/;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The host's own framing of an injected region — the sentence it writes ABOUT
|
|
95
|
+
* the block, not a sentence the user typed.
|
|
96
|
+
*
|
|
97
|
+
* Named literally, for the same reason `isSyntheticPrompt` names wrapper tags:
|
|
98
|
+
* this is host furniture, and furniture is identified by what it says about
|
|
99
|
+
* itself. It is deliberately NOT a language rule — the clauses describe the
|
|
100
|
+
* region's provenance and its data status, and a user who types one is quoting
|
|
101
|
+
* the host, which is exactly the case that should be dropped too.
|
|
102
|
+
*/
|
|
103
|
+
const REGION_NOTE =
|
|
104
|
+
/\b(content (above|below) is from|treat (it|this|that|the (content|text|above|following)) as data)\b/i;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Remove host-injected wrapper regions — `<launch-selected-element>` and
|
|
108
|
+
* friends — leaving the text the human actually typed around them.
|
|
109
|
+
*
|
|
110
|
+
* MEASURED, from a 14.13.0 field report (2026-09-01) reproduced against this
|
|
111
|
+
* source. `DOCUMENT_HEAD` matches a bare `<tag>` line, and Claude Code PREPENDS
|
|
112
|
+
* exactly that when the user picks a DOM element in the browser pane. So
|
|
113
|
+
* `humanAuthoredLead` broke at line zero and returned `""` for every such
|
|
114
|
+
* prompt — the lead-first isolation was not degraded, it was OFF — and the
|
|
115
|
+
* whole-text fallback then scored ~4 KB of class-heavy markup plus the host's
|
|
116
|
+
* own advisory line. Four consecutive German turns pinned English, and the
|
|
117
|
+
* turn-end gate refused each one.
|
|
118
|
+
*
|
|
119
|
+
* Two things had to move, and both are visible in the reproduction:
|
|
120
|
+
*
|
|
121
|
+
* 1. A bare `<tag>` line that HAS a matching close is the START of a region
|
|
122
|
+
* to skip, never the end of the human lead. That is this function.
|
|
123
|
+
* 2. It runs BEFORE `instructionText`. The block's indented lines arm that
|
|
124
|
+
* filter's paste-state machine, and where no blank line separates the
|
|
125
|
+
* block from the user's sentence, the sentence itself was deleted — the
|
|
126
|
+
* German markers did not merely lose the count, they reached zero.
|
|
127
|
+
*
|
|
128
|
+
* Conservative by construction, in three ways. An UNBALANCED bare tag is left
|
|
129
|
+
* in place, so a pasted document opening with one still terminates the lead
|
|
130
|
+
* exactly as before. A prompt with no balanced region is returned untouched,
|
|
131
|
+
* so nothing outside this shape reclassifies. And the framing sentence is
|
|
132
|
+
* dropped LINE BY LINE against `REGION_NOTE` rather than as a paragraph: in the
|
|
133
|
+
* reported shape the advisory and the user's sentence share one unbroken run,
|
|
134
|
+
* so dropping the run would have taken the sentence with it.
|
|
135
|
+
*
|
|
136
|
+
* Not `isSyntheticPrompt`: that answers "is this whole turn machine-written",
|
|
137
|
+
* and these turns are not — a human sentence follows the block. The wrapper is
|
|
138
|
+
* removed; the turn is kept.
|
|
139
|
+
*
|
|
140
|
+
* Nested same-name regions resolve to the FIRST matching close tag. Host
|
|
141
|
+
* wrappers do not nest by name, and stopping early keeps more human text than
|
|
142
|
+
* scanning past the wrong close would.
|
|
143
|
+
*/
|
|
144
|
+
export function stripInjectedRegions(text: string): string {
|
|
145
|
+
const lines = text.split("\n");
|
|
146
|
+
const kept: string[] = [];
|
|
147
|
+
for (let i = 0; i < lines.length; i++) {
|
|
148
|
+
const line = lines[i] ?? "";
|
|
149
|
+
const open = REGION_OPEN.exec(line);
|
|
150
|
+
if (open === null) {
|
|
151
|
+
kept.push(line);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// The tag name comes from `[A-Za-z][\w-]*`, so it carries no regex
|
|
155
|
+
// metacharacter and needs no escaping.
|
|
156
|
+
const close = new RegExp(`^\\s*</${open[1] ?? ""}>\\s*$`);
|
|
157
|
+
let end = -1;
|
|
158
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
159
|
+
if (close.test(lines[j] ?? "")) {
|
|
160
|
+
end = j;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (end === -1) {
|
|
165
|
+
kept.push(line);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (kept.length > 0 && REGION_NOTE.test(kept[kept.length - 1] ?? "")) {
|
|
169
|
+
kept.pop();
|
|
170
|
+
}
|
|
171
|
+
i = end;
|
|
172
|
+
while (i + 1 < lines.length && (lines[i + 1] ?? "").trim() !== "") {
|
|
173
|
+
if (!REGION_NOTE.test(lines[i + 1] ?? "")) {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
i++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return kept.join("\n");
|
|
180
|
+
}
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { spawnSync } from 'node:child_process';
|
|
27
27
|
|
|
28
|
+
import { CURATED_HEAD_INSTRUCTION } from './release_material.js';
|
|
29
|
+
|
|
28
30
|
/** The five curated labels, in the order an operator reads them. */
|
|
29
31
|
export const HEAD_LABELS: readonly string[] = [
|
|
30
32
|
'Behaviour changes',
|
|
@@ -39,11 +41,27 @@ export const HEAD_NONE = '_none_';
|
|
|
39
41
|
/**
|
|
40
42
|
* Prefix marking a head line the generator derived rather than a human wrote.
|
|
41
43
|
*
|
|
42
|
-
* Machine-checkable so the gate can
|
|
44
|
+
* Machine-checkable so the gate can REFUSE when one survives to merge, and
|
|
43
45
|
* readable so the line still says something true to anyone who ships it
|
|
44
46
|
* unedited. It is not a placeholder token: the line it introduces carries real
|
|
45
47
|
* evidence (the reason plus the citing SHAs), so nothing here is
|
|
46
48
|
* wrong-if-shipped — only unpolished-if-unedited.
|
|
49
|
+
*
|
|
50
|
+
* **REVERSED 2026-09-01 — "only unpolished-if-unedited" no longer implies
|
|
51
|
+
* "therefore advisory".** The clause above is kept because it is TRUE, and the
|
|
52
|
+
* conclusion that used to follow from it is the part that was falsified: the
|
|
53
|
+
* gate warned, and the marker shipped anyway in five consecutive released
|
|
54
|
+
* sections (14.9.0 through 14.13.0, eighteen lines, re-derived at
|
|
55
|
+
* `b50b27281`). `check_release_highlights` now exits non-zero on a marker in
|
|
56
|
+
* the section under release, and the sibling annotation in that file's header
|
|
57
|
+
* carries the full reversal.
|
|
58
|
+
*
|
|
59
|
+
* **Instructing authority:** the maintainer's closing instruction in the
|
|
60
|
+
* 2026-09-a inbox round, carried into
|
|
61
|
+
* `agents/roadmaps/archive/road-to-publication-integrity-hard-fail.md` § Phase 1.
|
|
62
|
+
*
|
|
63
|
+
* Nothing about the marker's own shape changes here — it is still one
|
|
64
|
+
* definition, still shared with the generator that writes it.
|
|
47
65
|
*/
|
|
48
66
|
export const DERIVED_MARKER = '_auto-derived, rewrite before merge:_';
|
|
49
67
|
|
|
@@ -283,6 +301,57 @@ export function render_derived_head_values(
|
|
|
283
301
|
return out;
|
|
284
302
|
}
|
|
285
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Reasons the section under release must NOT be published, in the order a
|
|
306
|
+
* reader should act on them. Empty means nothing here blocks publication.
|
|
307
|
+
*
|
|
308
|
+
* Added 2026-09-01 for roadmap § 3.1. It exists because the two IRREVERSIBLE
|
|
309
|
+
* transitions — the annotated tag and the GitHub Release body — happen inside
|
|
310
|
+
* `release.ts` after the PR gate has already passed, and nothing re-read the
|
|
311
|
+
* section between them. `check_release_highlights` guards the PR; this guards
|
|
312
|
+
* the publish.
|
|
313
|
+
*
|
|
314
|
+
* **It takes a section body, never a path and never a repository.** A guard
|
|
315
|
+
* that reads a file decides for itself what it is guarding, and the two
|
|
316
|
+
* refused earlier attempts both died there: one placed the check inside a pure
|
|
317
|
+
* formatter, which has no idea whether it is publishing, and one read the live
|
|
318
|
+
* changelog from a drill fixture. The caller cuts the section and owns the
|
|
319
|
+
* decision to publish; this function only answers whether that text is
|
|
320
|
+
* publishable.
|
|
321
|
+
*
|
|
322
|
+
* **Two markers since 2026-09-01, and the second one had to wait for a writer
|
|
323
|
+
* change.** This function used to check `DERIVED_MARKER` only, and said so:
|
|
324
|
+
* the generator still wrote its authoring comment into every section it cut,
|
|
325
|
+
* so refusing on that comment would have redded every release on its first run
|
|
326
|
+
* — Risk 1 of the roadmap, reproduced exactly. Roadmap § Phase 2 Option A
|
|
327
|
+
* removed the emission (`render_release_head` in `release.ts`), which is what
|
|
328
|
+
* makes refusing on it safe. The order was forced and it is the reason the
|
|
329
|
+
* check arrives second rather than late.
|
|
330
|
+
*
|
|
331
|
+
* `CURATED_HEAD_INSTRUCTION` is a named sentinel imported from the module that
|
|
332
|
+
* defines it — never a shape match over comment prose, which would reject
|
|
333
|
+
* unrelated legitimate comments and miss this one after a reword.
|
|
334
|
+
*/
|
|
335
|
+
export function publication_blockers(sectionBody: string, version: string): string[] {
|
|
336
|
+
const out: string[] = [];
|
|
337
|
+
if (sectionBody.includes(DERIVED_MARKER)) {
|
|
338
|
+
out.push(
|
|
339
|
+
`the ${version} section still carries \`${DERIVED_MARKER}\` — the generator's ` +
|
|
340
|
+
'draft head, not a curated claim. Rewrite those line(s) on ' +
|
|
341
|
+
`${'`main`'} and re-run.`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (sectionBody.includes(CURATED_HEAD_INSTRUCTION)) {
|
|
345
|
+
out.push(
|
|
346
|
+
`the ${version} section still carries the authoring instruction ` +
|
|
347
|
+
`(\`${CURATED_HEAD_INSTRUCTION}\`) — a reminder to the releaser, not ` +
|
|
348
|
+
'release content, and `CHANGELOG.md` is published to npm. Delete that ' +
|
|
349
|
+
'comment line from the section and re-run.',
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
|
|
286
355
|
/** Labels whose curated value is still the generator's unedited draft. */
|
|
287
356
|
export function stale_draft_labels(
|
|
288
357
|
curated: Readonly<Record<string, string>>,
|
|
@@ -61,6 +61,116 @@ export function extract_changelog_section(text: string, version: string): Change
|
|
|
61
61
|
return { heading: m[0], body };
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The generator's authoring instruction, identified by ONE named sentinel.
|
|
66
|
+
*
|
|
67
|
+
* Roadmap `road-to-publication-integrity-hard-fail` § Phase 2, Option A. Until
|
|
68
|
+
* 2026-09-01 `render_release_head` wrote this comment into every changelog
|
|
69
|
+
* section it cut, and nothing removed it at release time — so it shipped. At
|
|
70
|
+
* that commit `grep -c 'Curated head: fill before merge' CHANGELOG.md` read 2,
|
|
71
|
+
* and `package.json` `files` carries the bare `"CHANGELOG.md"`, which is the
|
|
72
|
+
* npm-published artifact. The instruction now lives in the release-PR body,
|
|
73
|
+
* which is never published, and the writer emits none.
|
|
74
|
+
*
|
|
75
|
+
* **A sentinel, deliberately not a shape match.** A council seat warned that a
|
|
76
|
+
* pattern such as `/<!-- .* should(?: only)? be .* -->/` is overbroad and
|
|
77
|
+
* under-specified: it rejects unrelated legitimate comments while missing the
|
|
78
|
+
* prohibited instruction after harmless rewording. This follows the discipline
|
|
79
|
+
* `DERIVED_MARKER` already follows — one definition, imported by every
|
|
80
|
+
* consumer, including the gates that refuse on it.
|
|
81
|
+
*/
|
|
82
|
+
export const CURATED_HEAD_INSTRUCTION = 'Curated head: fill before merge';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The full authoring instruction as the release-PR body carries it.
|
|
86
|
+
*
|
|
87
|
+
* Parameterized rather than importing `RELEASE_HEAD_CAP_LINES` / `HEAD_NONE`,
|
|
88
|
+
* because this module has no imports and that is load-bearing: it is the one
|
|
89
|
+
* place the equality gate, the drill fixtures and the unit tests all reach, so
|
|
90
|
+
* a dependency here is a dependency in all three.
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* Cap on curated-head lines, and the placeholder a label carries when the
|
|
94
|
+
* answer is genuinely nothing.
|
|
95
|
+
*
|
|
96
|
+
* Relocated here from `release.ts` on 2026-09-01 with the instruction text:
|
|
97
|
+
* this module builds the release-PR body, so the instruction and the two
|
|
98
|
+
* values it interpolates belong on the same side of the import boundary. The
|
|
99
|
+
* generator re-exports `RELEASE_HEAD_CAP_LINES` under its original name, so
|
|
100
|
+
* nothing that referenced it there has to change.
|
|
101
|
+
*/
|
|
102
|
+
export const RELEASE_HEAD_CAP_LINES = 10;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The fallback value of a head line the span does not substantiate.
|
|
106
|
+
*
|
|
107
|
+
* `_none_` and not a placeholder token: where nothing was derived it is the
|
|
108
|
+
* **true** answer, and a release that genuinely changed no defaults should say
|
|
109
|
+
* so rather than carry an unfilled marker.
|
|
110
|
+
*
|
|
111
|
+
* It is a fallback and no longer a blanket default. Writing `_none_` into
|
|
112
|
+
* every field made the generator assert five things it had not checked, and
|
|
113
|
+
* `check_release_highlights` rejected exactly that assertion the moment the
|
|
114
|
+
* span contradicted it — which for this package is every release, because every
|
|
115
|
+
* release touches `src/rules/` or `src/scripts/schemas/`. Substantiated labels
|
|
116
|
+
* are now pre-filled from the span (`_derive_head_prefill`), so the tool states
|
|
117
|
+
* only what it can support and `_none_` means what it says.
|
|
118
|
+
*/
|
|
119
|
+
export const RELEASE_HEAD_DEFAULT = '_none_';
|
|
120
|
+
|
|
121
|
+
export function render_curated_head_instruction(capLines: number, headDefault: string): string {
|
|
122
|
+
return (
|
|
123
|
+
`<!-- ${CURATED_HEAD_INSTRUCTION}, keep it under ${capLines} lines, and leave ` +
|
|
124
|
+
`\`${headDefault}\` where it is genuinely the answer. The generated log below is ` +
|
|
125
|
+
'unchanged. -->'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The instruction exactly as the release-PR body carries it. */
|
|
130
|
+
export const CURATED_HEAD_INSTRUCTION_COMMENT = render_curated_head_instruction(
|
|
131
|
+
RELEASE_HEAD_CAP_LINES,
|
|
132
|
+
RELEASE_HEAD_DEFAULT,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Delimiters bounding the release-PR-only region.
|
|
137
|
+
*
|
|
138
|
+
* The region exists so the PR body can carry something the changelog must not,
|
|
139
|
+
* WITHOUT relaxing surface equality anywhere else. `strip_pr_wrapper` removes
|
|
140
|
+
* exactly what sits between these two markers and nothing else — never a prose
|
|
141
|
+
* match, never a whole-body relaxation. Relaxing equality wholesale would
|
|
142
|
+
* remove the release-truth guarantee that the PR body and the changelog say
|
|
143
|
+
* the same thing, which is a larger floor than the one this change
|
|
144
|
+
* strengthens.
|
|
145
|
+
*
|
|
146
|
+
* An UNTERMINATED region is deliberately left in place: it then surfaces as an
|
|
147
|
+
* ordinary divergence instead of silently swallowing the rest of the body.
|
|
148
|
+
*/
|
|
149
|
+
export const PR_ONLY_START = '<!-- release-pr-only:start -->';
|
|
150
|
+
export const PR_ONLY_END = '<!-- release-pr-only:end -->';
|
|
151
|
+
|
|
152
|
+
/** Remove every complete PR-only region. Delimiter-bounded, never a prose match. */
|
|
153
|
+
export function strip_pr_only_regions(s: string): string {
|
|
154
|
+
const out: string[] = [];
|
|
155
|
+
let rest = s;
|
|
156
|
+
for (;;) {
|
|
157
|
+
const i = rest.indexOf(PR_ONLY_START);
|
|
158
|
+
if (i === -1) {
|
|
159
|
+
out.push(rest);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
const j = rest.indexOf(PR_ONLY_END, i + PR_ONLY_START.length);
|
|
163
|
+
if (j === -1) {
|
|
164
|
+
// Unterminated — leave it standing so equality reports it.
|
|
165
|
+
out.push(rest);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
out.push(rest.slice(0, i));
|
|
169
|
+
rest = rest.slice(j + PR_ONLY_END.length);
|
|
170
|
+
}
|
|
171
|
+
return out.join('');
|
|
172
|
+
}
|
|
173
|
+
|
|
64
174
|
/** Footer appended to the release-PR body; the strip helper accepts legacy variants. */
|
|
65
175
|
export const PR_BODY_FOOTER = 'Created by `src/scripts/release.ts`.';
|
|
66
176
|
const _LEGACY_PR_FOOTERS = ['Created by `scripts/release.py`.', PR_BODY_FOOTER];
|
|
@@ -68,9 +178,18 @@ const _LEGACY_PR_FOOTERS = ['Created by `scripts/release.py`.', PR_BODY_FOOTER];
|
|
|
68
178
|
/** Marker `_cap_body` embeds when a surface was truncated to a GitHub limit. */
|
|
69
179
|
export const TRUNCATION_MARKER = '_Changelog truncated to fit GitHub';
|
|
70
180
|
|
|
71
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* Release-PR body derived from the (possibly capped) changelog-section body.
|
|
183
|
+
*
|
|
184
|
+
* Carries the authoring instruction inside the PR-only region. The region is
|
|
185
|
+
* emitted unconditionally rather than passed in by each caller: there are two
|
|
186
|
+
* PR-creation paths (`release.ts` and `release_publication.ts`), and a caller
|
|
187
|
+
* that forgot the argument would silently ship a PR with no reminder — the
|
|
188
|
+
* failure this change exists to make impossible, reintroduced one level up.
|
|
189
|
+
*/
|
|
72
190
|
export function pr_body_from_section(cappedBody: string, version: string): string {
|
|
73
|
-
|
|
191
|
+
const prOnly = `${PR_ONLY_START}\n${CURATED_HEAD_INSTRUCTION_COMMENT}\n${PR_ONLY_END}`;
|
|
192
|
+
return `Release ${version}.\n\n${prOnly}\n\n${cappedBody}\n\n${PR_BODY_FOOTER}`;
|
|
74
193
|
}
|
|
75
194
|
|
|
76
195
|
/** GitHub-release notes derived from the (possibly capped) changelog-section body. */
|
|
@@ -96,6 +215,10 @@ export function strip_pr_wrapper(prBody: string, version: string): string | null
|
|
|
96
215
|
return null;
|
|
97
216
|
}
|
|
98
217
|
s = s.slice(prefix.length);
|
|
218
|
+
// Delimiter-bounded, before the footer walk: the region is PR-only material
|
|
219
|
+
// by construction, so it is removed from the comparison rather than
|
|
220
|
+
// compared. Everything outside it still has to match the changelog.
|
|
221
|
+
s = strip_pr_only_regions(s);
|
|
99
222
|
for (const footer of _LEGACY_PR_FOOTERS) {
|
|
100
223
|
if (s.trimEnd().endsWith(footer)) {
|
|
101
224
|
s = s.trimEnd();
|