@event4u/agent-config 14.13.0 → 14.14.1
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 +138 -376
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/commands/pr/create.md +16 -1
- package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- 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/registry.js +1 -0
- package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
- package/dist/cli-delegate/cmd_conformance.js +39 -2
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- 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 +1696 -812
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +297 -115
- 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/assurance-capability-registry.json +1 -1
- package/src/config/estate-count-budget.json +20 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +211 -1
- package/src/config/gate-reachability-exemptions.json +1 -0
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/config/web-launch-readiness.json +4 -2
- package/src/scripts/_cli/_doctor_emit.ts +71 -0
- package/src/scripts/_cli/_doctor_strict.ts +163 -0
- package/src/scripts/_cli/cmd_doctor.ts +16 -37
- package/src/scripts/_dispatch.bash +11 -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/branch_convergence.ts +186 -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/context_observation.ts +132 -0
- package/src/scripts/_lib/continuation_ladder.ts +188 -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/experiment_binding.ts +95 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
- package/src/scripts/_lib/ignored_blocker.ts +8 -1
- 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/outcome_envelope.ts +6 -1
- package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
- 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 +79 -1
- package/src/scripts/_lib/release_material.ts +125 -2
- package/src/scripts/_lib/repeated_failure.ts +19 -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/runtime_journal.ts +11 -4
- 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_agent_artifact_location.ts +337 -0
- package/src/scripts/check_artefact_count_messaging.ts +74 -8
- package/src/scripts/check_enforcement_coverage.ts +40 -1
- package/src/scripts/check_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +113 -14
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/check_web_launch_readiness.ts +310 -20
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +234 -9
- package/src/scripts/generate_capability_matrix.ts +16 -9
- package/src/scripts/grade_target_readiness.ts +221 -2
- package/src/scripts/hook_effect_doctor.ts +47 -4
- 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/run_continuation_hook.ts +78 -88
- package/src/scripts/hooks/session_eol_hook.ts +12 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
- package/src/scripts/install-hooks.sh +18 -0
- 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 +748 -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 +199 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +142 -0
- package/src/scripts/roadmap_context.ts +15 -0
- package/src/scripts/skill_scout.ts +633 -0
- package/src/scripts/sync_pr_branch.ts +521 -42
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* or merges one in the same change — or the added file carries an explicit
|
|
48
48
|
* `estate_offset_exempt:` reason in its frontmatter, which costs a visible line
|
|
49
49
|
* in the diff of the very commit that claims it. This half is diff-scoped: it
|
|
50
|
-
* reads `<base>...HEAD` over `agents/roadmaps/`, so on a branch that adds no
|
|
50
|
+
* reads `<base>...HEAD` over `agents/roadmaps/`, so on a branch that adds no code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
|
|
51
51
|
* roadmap it legitimately finds nothing to weigh.
|
|
52
52
|
*
|
|
53
53
|
* THE THREE WAYS THE ESTATE MAY LEGITIMATELY GROW
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
* estate rather than creating it, so it needs no authorisation; what the
|
|
66
66
|
* allowance does NOT cover is a `later/` file appearing from nowhere.
|
|
67
67
|
* 3. **Anything else** — `estate_growth_exempt: <reason>` added, in this change,
|
|
68
|
-
* to the frontmatter of any roadmap under `agents/roadmaps/`. The canonical
|
|
68
|
+
* to the frontmatter of any roadmap under `agents/roadmaps/`. The canonical code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
|
|
69
69
|
* case is a blocker discovered while doing the work: `open_blockers` rises,
|
|
70
70
|
* nothing was archived, and the reason belongs next to the blocker.
|
|
71
71
|
*
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
* which one lied.
|
|
118
118
|
*
|
|
119
119
|
* The same function measures the BASE side, over a scratch copy of the base
|
|
120
|
-
* ref's `agents/roadmaps/` (`_lib/base_tree.ts`). Re-deriving the floor with a
|
|
120
|
+
* ref's `agents/roadmaps/` (`_lib/base_tree.ts`). Re-deriving the floor with a code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
|
|
121
121
|
* second implementation would let the two sides disagree about one tree, which
|
|
122
122
|
* is the failure this section already argues against, one level up.
|
|
123
123
|
*
|
|
@@ -143,7 +143,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
143
143
|
|
|
144
144
|
import {
|
|
145
145
|
collect,
|
|
146
|
-
|
|
146
|
+
is_unscheduled as isUnscheduled,
|
|
147
147
|
is_roadmap_candidate as isRoadmapCandidate,
|
|
148
148
|
parse_frontmatter as parseFrontmatter,
|
|
149
149
|
parse_roadmap as parseRoadmap,
|
|
@@ -155,6 +155,8 @@ import { materialiseSubtree } from './_lib/base_tree.js';
|
|
|
155
155
|
import { SKILLS_POSIX, measureSkillEstate } from './_lib/skill_estate.js';
|
|
156
156
|
import { CONCERN_MANIFEST_POSIX, countConcerns } from './_lib/concern_estate.js';
|
|
157
157
|
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
158
|
+
import { countDeclaredCarriers, declaresCarrier } from './_lib/carrier_status.js';
|
|
159
|
+
import { carrierEstateCases } from './_lib/estate_carrier_cases.js';
|
|
158
160
|
|
|
159
161
|
const GATE = 'check_estate_count';
|
|
160
162
|
const BUDGET_REL = path.join('src', 'config', 'estate-count-budget.json');
|
|
@@ -232,6 +234,63 @@ export interface EstateBudget {
|
|
|
232
234
|
* number here is how it gets a threshold.
|
|
233
235
|
*/
|
|
234
236
|
one_in_one_out: { applies_above_active: number | null };
|
|
237
|
+
/**
|
|
238
|
+
* The capped provisional-promotion path — specified, and DECLINED.
|
|
239
|
+
*
|
|
240
|
+
* READ ITS NULLS THE OTHER WAY ROUND FROM THE KEY ABOVE. There `null` means
|
|
241
|
+
* UNCONDITIONAL; here `max_live: null` and `expires_after_days: null` mean
|
|
242
|
+
* NOT REGISTERED, PATH INACTIVE. The two keys sit a few lines apart in one
|
|
243
|
+
* file and their nulls mean opposite things, so a reader who transfers the
|
|
244
|
+
* sibling's semantics reads a declined path as an uncapped one.
|
|
245
|
+
*/
|
|
246
|
+
provisional_promotion: ProvisionalPromotion;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The `provisional_promotion` object as the budget file carries it.
|
|
251
|
+
*
|
|
252
|
+
* `status` is optional so that a file which registered the two integers without
|
|
253
|
+
* restating a status is still readable — the integers are what activate the
|
|
254
|
+
* path, and a declination that dropped its own marker would fall to
|
|
255
|
+
* `unregistered`, which is the safe direction.
|
|
256
|
+
*/
|
|
257
|
+
export interface ProvisionalPromotion {
|
|
258
|
+
status?: string;
|
|
259
|
+
max_live: number | null;
|
|
260
|
+
expires_after_days: number | null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Three states, and the reason there are three rather than two.
|
|
265
|
+
*
|
|
266
|
+
* The 2026-09-01 council that declined the path attached one obligation to the
|
|
267
|
+
* decline: the checker must distinguish an intentional declination from missing
|
|
268
|
+
* configuration. Two states cannot — `null` would read the same either way, and
|
|
269
|
+
* "nobody has written this yet" would be indistinguishable from "this was
|
|
270
|
+
* specified in full and refused on 2026-09-01".
|
|
271
|
+
*/
|
|
272
|
+
export type ProvisionalPromotionState = 'declined' | 'unregistered' | 'registered';
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Classify the path's state. NEVER activates it, in any state.
|
|
276
|
+
*
|
|
277
|
+
* A half-registered object — one integer, one null — throws rather than
|
|
278
|
+
* resolving to either neighbour: a cap with no expiry and an expiry with no cap
|
|
279
|
+
* are different mechanisms, and guessing which half was meant is how a bounded
|
|
280
|
+
* path becomes an unbounded one by omission.
|
|
281
|
+
*/
|
|
282
|
+
export function provisionalPromotionState(pp: ProvisionalPromotion): ProvisionalPromotionState {
|
|
283
|
+
const live = pp.max_live;
|
|
284
|
+
const days = pp.expires_after_days;
|
|
285
|
+
if (typeof live === 'number' && typeof days === 'number') return 'registered';
|
|
286
|
+
if ((live === null) !== (days === null)) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`${BUDGET_REL} "provisional_promotion" is half-registered ` +
|
|
289
|
+
`(max_live=${JSON.stringify(live)}, expires_after_days=${JSON.stringify(days)}). ` +
|
|
290
|
+
'Both integers activate the path; both null leave it inactive. One of each is neither.',
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return pp.status === 'declined' ? 'declined' : 'unregistered';
|
|
235
294
|
}
|
|
236
295
|
|
|
237
296
|
/** One gated count that rose above the floor measured at the base ref. */
|
|
@@ -305,6 +364,15 @@ export interface EstateVerdict {
|
|
|
305
364
|
offsets: OffsetLedger | null;
|
|
306
365
|
offsetSkipReason: string | null;
|
|
307
366
|
unpaid: number;
|
|
367
|
+
/**
|
|
368
|
+
* The provisional-promotion path's state, reported and never acted on.
|
|
369
|
+
*
|
|
370
|
+
* Carried in the verdict so a `--json` consumer can tell a declined path
|
|
371
|
+
* from an unwritten one without re-reading the budget file, which is the
|
|
372
|
+
* distinction the 2026-09-01 decline was conditioned on. It contributes
|
|
373
|
+
* nothing to `withinBudget` in any state: reading is not activation.
|
|
374
|
+
*/
|
|
375
|
+
provisionalPromotion: ProvisionalPromotionState;
|
|
308
376
|
withinBudget: boolean;
|
|
309
377
|
}
|
|
310
378
|
|
|
@@ -370,7 +438,39 @@ function countIn(roadmapRoot: string, sub: string): number {
|
|
|
370
438
|
}
|
|
371
439
|
}
|
|
372
440
|
|
|
373
|
-
/**
|
|
441
|
+
/**
|
|
442
|
+
* Top-level roadmaps `collect()` drops for declaring `status: carrier`, added
|
|
443
|
+
* back so that FLIPPING a status is count-neutral.
|
|
444
|
+
*
|
|
445
|
+
* Measured before this: flipping one file to `carrier` moved `active_roadmaps
|
|
446
|
+
* 3 → 2` and the gate printed "estate within its ratchet", because a count
|
|
447
|
+
* below the floor is a drawdown and free. The floor is measured at the base
|
|
448
|
+
* ref, so once such a flip merges the lowered number becomes the next floor —
|
|
449
|
+
* the file cannot be flipped back without paying, and any roadmap can be
|
|
450
|
+
* laundered out of the count by adding one word.
|
|
451
|
+
*
|
|
452
|
+
* A reclassification is not a disposal, so the metric counts the file on both
|
|
453
|
+
* sides. Same argument `open_blockers` makes below for spanning `later/`.
|
|
454
|
+
* Deleting a carrier is still a shrink and still earns no offset
|
|
455
|
+
* (`classifyDiff`); adding one is growth that takes the claim path.
|
|
456
|
+
*
|
|
457
|
+
* This is therefore the one roadmap metric that does NOT equal the dashboard
|
|
458
|
+
* header, stated here rather than left to be tripped over.
|
|
459
|
+
*/
|
|
460
|
+
function countActiveCarriers(roadmapRoot: string): number {
|
|
461
|
+
return countDeclaredCarriers(roadmapRoot, isRoadmapCandidate, fs, path.join);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Roadmaps parked in `later/` whose blockers count, parsed for them.
|
|
466
|
+
*
|
|
467
|
+
* Skips a draft AND a carrier, which is the same pair `collect()` skips on the
|
|
468
|
+
* active side. It used to skip only the draft, so parking a carrier moved its
|
|
469
|
+
* blockers INTO the gated count while the identical file at the top level kept
|
|
470
|
+
* them out — a ratchet verdict that moved on a relocation, for no stated
|
|
471
|
+
* reason. Three of the four status readers learned `carrier` when the status
|
|
472
|
+
* shipped; this is the fourth.
|
|
473
|
+
*/
|
|
374
474
|
function laterRoadmaps(roadmapRoot: string): Array<{ open_blockers: readonly unknown[] }> {
|
|
375
475
|
const dir = path.join(roadmapRoot, 'later');
|
|
376
476
|
let names: string[];
|
|
@@ -386,7 +486,7 @@ function laterRoadmaps(roadmapRoot: string): Array<{ open_blockers: readonly unk
|
|
|
386
486
|
// `collect()` cannot be reused here: it filters through the same
|
|
387
487
|
// path-based predicate and would reject its own root.
|
|
388
488
|
const text = fs.readFileSync(abs, 'utf-8');
|
|
389
|
-
if (
|
|
489
|
+
if (isUnscheduled(parseFrontmatter(text))) continue;
|
|
390
490
|
const stats = parseRoadmap(abs, dir);
|
|
391
491
|
if (stats !== null) out.push(stats as unknown as { open_blockers: readonly unknown[] });
|
|
392
492
|
}
|
|
@@ -396,8 +496,11 @@ function laterRoadmaps(roadmapRoot: string): Array<{ open_blockers: readonly unk
|
|
|
396
496
|
/**
|
|
397
497
|
* Count the estate, three ways.
|
|
398
498
|
*
|
|
399
|
-
* `active_roadmaps`
|
|
400
|
-
*
|
|
499
|
+
* `active_roadmaps` is `collect()` PLUS the top-level carriers `collect()`
|
|
500
|
+
* drops, so the parser is shared with the dashboard while a status flip stays
|
|
501
|
+
* count-neutral — see `countActiveCarriers` for why that beats parity here, and
|
|
502
|
+
* for the one number on this gate that the dashboard header does not print.
|
|
503
|
+
* `later_roadmaps` is a directory listing because parked files are
|
|
401
504
|
* outside that corpus by design — filtered through the same `is_roadmap_candidate`
|
|
402
505
|
* predicate as the active side, so `later/README.md` is not a roadmap here either.
|
|
403
506
|
*
|
|
@@ -421,7 +524,7 @@ export function countEstate(repoRoot: string): EstateCounts {
|
|
|
421
524
|
rows.reduce((n, r) => n + r.open_blockers.length, 0);
|
|
422
525
|
const skills = measureSkillEstate(repoRoot);
|
|
423
526
|
return {
|
|
424
|
-
active_roadmaps: stats.length,
|
|
527
|
+
active_roadmaps: stats.length + countActiveCarriers(roadmapRoot),
|
|
425
528
|
later_roadmaps: countIn(roadmapRoot, 'later'),
|
|
426
529
|
open_blockers: openOf(stats) + openOf(laterRoadmaps(roadmapRoot)),
|
|
427
530
|
skill_count: skills.skill_count,
|
|
@@ -440,7 +543,7 @@ export function skillTokensExact(root: string): boolean {
|
|
|
440
543
|
return measureSkillEstate(root).skill_description_tokens !== null;
|
|
441
544
|
}
|
|
442
545
|
|
|
443
|
-
/** `agents/roadmaps/<name>.md` — the active top level,
|
|
546
|
+
/** `agents/roadmaps/<name>.md` — the active top level. code-comment-allow provenance-comment -- operand, not provenance */
|
|
444
547
|
function isActiveTopLevel(rel: string): boolean {
|
|
445
548
|
const norm = rel.split(path.sep).join('/');
|
|
446
549
|
if (!norm.startsWith('agents/roadmaps/') || !norm.endsWith('.md')) {
|
|
@@ -487,6 +590,9 @@ export function exemptionReason(text: string): string | null {
|
|
|
487
590
|
return reason === '' ? null : reason;
|
|
488
591
|
}
|
|
489
592
|
|
|
593
|
+
/** Re-export; `_lib/carrier_status.ts` holds the one implementation. */
|
|
594
|
+
export const isCarrierText = declaresCarrier;
|
|
595
|
+
|
|
490
596
|
/**
|
|
491
597
|
* Classify the change's effect on the active roadmap tree.
|
|
492
598
|
*
|
|
@@ -495,7 +601,17 @@ export function exemptionReason(text: string): string | null {
|
|
|
495
601
|
* `later/road-to-x.md` → `road-to-x.md` is an un-parking and counts as an
|
|
496
602
|
* addition. A top-level-to-top-level rename is neither.
|
|
497
603
|
*/
|
|
498
|
-
export function classifyDiff(
|
|
604
|
+
export function classifyDiff(
|
|
605
|
+
nameStatus: string,
|
|
606
|
+
readFile: (rel: string) => string | null,
|
|
607
|
+
/**
|
|
608
|
+
* The file's content at the BASE ref, for paths this change removed — a
|
|
609
|
+
* deleted carrier cannot be read from the working tree and its status
|
|
610
|
+
* decides whether the deletion earns an offset. Defaults to null, which
|
|
611
|
+
* reproduces the previous scoring for any caller without a pre-image.
|
|
612
|
+
*/
|
|
613
|
+
readBase: (rel: string) => string | null = () => null,
|
|
614
|
+
): OffsetLedger {
|
|
499
615
|
const added: string[] = [];
|
|
500
616
|
const offsets: string[] = [];
|
|
501
617
|
const exempt: Array<{ file: string; reason: string }> = [];
|
|
@@ -508,8 +624,10 @@ export function classifyDiff(nameStatus: string, readFile: (rel: string) => stri
|
|
|
508
624
|
const from = cols[1] ?? '';
|
|
509
625
|
const to = cols[2] ?? '';
|
|
510
626
|
if (isActiveTopLevel(from) && isDisposed(to)) {
|
|
511
|
-
|
|
512
|
-
|
|
627
|
+
if (!isCarrierText(readBase(from))) {
|
|
628
|
+
offsets.push(from);
|
|
629
|
+
if (isParked(to)) parked.push(to);
|
|
630
|
+
}
|
|
513
631
|
} else if (isDisposed(from) && isActiveTopLevel(to)) {
|
|
514
632
|
added.push(to);
|
|
515
633
|
}
|
|
@@ -520,7 +638,10 @@ export function classifyDiff(nameStatus: string, readFile: (rel: string) => stri
|
|
|
520
638
|
if (status === 'A') {
|
|
521
639
|
added.push(file);
|
|
522
640
|
} else if (status === 'D') {
|
|
523
|
-
|
|
641
|
+
// A carrier's removal is never an offset. See `readBase`.
|
|
642
|
+
if (!isCarrierText(readBase(file))) {
|
|
643
|
+
offsets.push(file);
|
|
644
|
+
}
|
|
524
645
|
}
|
|
525
646
|
}
|
|
526
647
|
for (const file of added) {
|
|
@@ -587,6 +708,18 @@ export function evaluate(
|
|
|
587
708
|
if (budget.one_in_one_out === undefined) {
|
|
588
709
|
throw new Error(`${BUDGET_REL} carries no "one_in_one_out" object.`);
|
|
589
710
|
}
|
|
711
|
+
// Same shape, same reason: an absent policy object is a could-not-run, not a
|
|
712
|
+
// pass. Here it also carries a second job — with the key required, "the path
|
|
713
|
+
// was declined on 2026-09-01" and "nobody has configured this" cannot
|
|
714
|
+
// collapse into the same reading, which is the condition the council
|
|
715
|
+
// attached to the decline.
|
|
716
|
+
if (budget.provisional_promotion === undefined) {
|
|
717
|
+
throw new Error(
|
|
718
|
+
`${BUDGET_REL} carries no "provisional_promotion" object. ` +
|
|
719
|
+
'Absent is misconfiguration, never an inactive path — a declination is recorded, not implied.',
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
const provisionalPromotion = provisionalPromotionState(budget.provisional_promotion);
|
|
590
723
|
|
|
591
724
|
const counts = countEstate(repoRoot);
|
|
592
725
|
const baseRef = opts.baseRef ?? resolveBaseRef(repoRoot);
|
|
@@ -699,13 +832,20 @@ export function evaluate(
|
|
|
699
832
|
if (!diff.ok) {
|
|
700
833
|
offsetSkipReason = `git diff against ${baseRef} failed — one-in-one-out not evaluated`;
|
|
701
834
|
} else {
|
|
702
|
-
offsets = classifyDiff(
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
835
|
+
offsets = classifyDiff(
|
|
836
|
+
diff.stdout,
|
|
837
|
+
(rel) => {
|
|
838
|
+
try {
|
|
839
|
+
return fs.readFileSync(path.join(repoRoot, rel), 'utf-8');
|
|
840
|
+
} catch {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
(rel) => {
|
|
845
|
+
const show = git(['show', `${baseRef}:${rel}`], repoRoot);
|
|
846
|
+
return show.ok ? show.stdout : null;
|
|
847
|
+
},
|
|
848
|
+
);
|
|
709
849
|
}
|
|
710
850
|
const patch = git(
|
|
711
851
|
['diff', '--unified=0', '--find-renames', `${baseRef}...HEAD`, '--', ROADMAPS_REL],
|
|
@@ -790,6 +930,7 @@ export function evaluate(
|
|
|
790
930
|
offsets,
|
|
791
931
|
offsetSkipReason,
|
|
792
932
|
unpaid,
|
|
933
|
+
provisionalPromotion,
|
|
793
934
|
// No floor is a FAILURE, not a skip, and it is the one place this gate
|
|
794
935
|
// convicts on a missing input. The other halves can report "unproven" and
|
|
795
936
|
// still leave a meaningful verdict behind; the count half IS the verdict,
|
|
@@ -800,7 +941,7 @@ export function evaluate(
|
|
|
800
941
|
}
|
|
801
942
|
|
|
802
943
|
/** Floors for `--self-test`, declared here so a truncation is a visible diff. */
|
|
803
|
-
const SELF_TEST_MIN_CASES =
|
|
944
|
+
const SELF_TEST_MIN_CASES = 13;
|
|
804
945
|
const SELF_TEST_MIN_REJECT = 8;
|
|
805
946
|
|
|
806
947
|
/**
|
|
@@ -825,11 +966,32 @@ function selfTest(): number {
|
|
|
825
966
|
* the baseline with no reason" is not a state a change can reach.
|
|
826
967
|
*/
|
|
827
968
|
const budgetJson = (): string =>
|
|
828
|
-
`${JSON.stringify(
|
|
969
|
+
`${JSON.stringify(
|
|
970
|
+
{
|
|
971
|
+
one_in_one_out: { applies_above_active: null },
|
|
972
|
+
// Required since the path was specified and declined: the gate
|
|
973
|
+
// refuses a budget that carries no `provisional_promotion`, so a
|
|
974
|
+
// fixture without it exits 2 on every case rather than on the one
|
|
975
|
+
// case testing that refusal.
|
|
976
|
+
provisional_promotion: { status: 'declined', max_live: null, expires_after_days: null },
|
|
977
|
+
},
|
|
978
|
+
null,
|
|
979
|
+
4,
|
|
980
|
+
)}\n`;
|
|
829
981
|
|
|
830
982
|
const roadmap = (name: string, extra = ''): string =>
|
|
831
983
|
`# Roadmap: ${name}\n\n## Phase 1\n\n- [ ] **1.1** s\n${extra}`;
|
|
832
984
|
|
|
985
|
+
/** A `status: carrier` roadmap — counted as estate, and never an offset. */
|
|
986
|
+
const CARRIER = `---\nstatus: carrier\n---\n# Roadmap: carrier\n\n## Phase 1\n\n- [~] **1.1** s\n`;
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* A `status: draft` roadmap — invisible to `collect()`, so adding one cannot
|
|
990
|
+
* move `active_roadmaps`. That is what makes it the fixture for a case that
|
|
991
|
+
* must be decided by the offset scoring and by nothing else.
|
|
992
|
+
*/
|
|
993
|
+
const DRAFT = `---\nstatus: draft\n---\n# Roadmap: draft\n\n## Phase 1\n\n- [ ] **1.1** s\n`;
|
|
994
|
+
|
|
833
995
|
/** An open blocker — no `Status: resolved` prefix, so it counts as open. */
|
|
834
996
|
const BLOCKER = '\n## Blockers\n\n### Blocker: fixture\n\n- **Status:** open\n';
|
|
835
997
|
|
|
@@ -1035,6 +1197,14 @@ function selfTest(): number {
|
|
|
1035
1197
|
after: (dir) => write(dir, 'agents/roadmaps/later/road-to-parked.md', roadmap('P')),
|
|
1036
1198
|
}),
|
|
1037
1199
|
},
|
|
1200
|
+
...carrierEstateCases({
|
|
1201
|
+
fixture,
|
|
1202
|
+
write,
|
|
1203
|
+
remove: (dir, rel) => fs.rmSync(path.join(dir, rel)),
|
|
1204
|
+
roadmap,
|
|
1205
|
+
CARRIER,
|
|
1206
|
+
DRAFT,
|
|
1207
|
+
}),
|
|
1038
1208
|
{
|
|
1039
1209
|
name: 'a new open blocker with no claim → reject',
|
|
1040
1210
|
expect: 'reject',
|
|
@@ -1160,7 +1330,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
1160
1330
|
return 2;
|
|
1161
1331
|
}
|
|
1162
1332
|
|
|
1163
|
-
// A ratchet over an empty estate always passes. Move `agents/roadmaps/` and
|
|
1333
|
+
// A ratchet over an empty estate always passes. Move `agents/roadmaps/` and code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
|
|
1164
1334
|
// every count is 0, which is trivially under any baseline — exit 2 (could
|
|
1165
1335
|
// not run), never 1, which would assert the estate actually grew.
|
|
1166
1336
|
try {
|
|
@@ -1237,6 +1407,17 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
1237
1407
|
} else if (verdict.offsetSkipReason !== null) {
|
|
1238
1408
|
process.stdout.write(` ⚠️ ${verdict.offsetSkipReason}\n`);
|
|
1239
1409
|
}
|
|
1410
|
+
// Printed unconditionally so the state is visible without `--json`. It never
|
|
1411
|
+
// changes the exit code — a declined path and a registered one both leave
|
|
1412
|
+
// the ratchet exactly as it was, and the allowance a registered path would
|
|
1413
|
+
// grant is deliberately unwired (the owner registers the numbers; wiring
|
|
1414
|
+
// what they buy is a separate, separately authorised change).
|
|
1415
|
+
process.stdout.write(
|
|
1416
|
+
` ${'provisional'.padEnd(18)} ${verdict.provisionalPromotion.padStart(5)}` +
|
|
1417
|
+
`${verdict.provisionalPromotion === 'declined' ? ' (specified and declined 2026-09-01 — path inactive by decision)' : ''}` +
|
|
1418
|
+
`${verdict.provisionalPromotion === 'unregistered' ? ' (no numbers registered — path inactive)' : ''}` +
|
|
1419
|
+
`${verdict.provisionalPromotion === 'registered' ? ' (numbers registered by the owner; this gate grants no allowance for them)' : ''}\n`,
|
|
1420
|
+
);
|
|
1240
1421
|
for (const c of verdict.claims) {
|
|
1241
1422
|
process.stdout.write(` ↑ growth claimed in ${c.file}: ${c.reason.slice(0, 120)}\n`);
|
|
1242
1423
|
}
|
|
@@ -18,7 +18,33 @@
|
|
|
18
18
|
* `_none_` everywhere. That removes the guaranteed first-run red (9.17.0,
|
|
19
19
|
* 9.18.0) without blunting this gate: a machine-written `_none_` was never the
|
|
20
20
|
* failure it was built for — a HUMAN writing one was (9.13.0, 9.14.0), and that
|
|
21
|
-
* still fails here.
|
|
21
|
+
* still fails here.
|
|
22
|
+
*
|
|
23
|
+
* ## REVERSED 2026-09-01 — an unrewritten derived line now BLOCKS
|
|
24
|
+
*
|
|
25
|
+
* This header used to end the paragraph above with *"An unrewritten derived
|
|
26
|
+
* line warns; it never blocks."* That is no longer the behaviour, and the
|
|
27
|
+
* sentence is replaced rather than deleted so the reversal is legible.
|
|
28
|
+
*
|
|
29
|
+
* **The premise was never wrong; the conclusion drawn from it was.** A derived
|
|
30
|
+
* line does carry true evidence — the deriving reason plus citing SHAs — so it
|
|
31
|
+
* is not false-if-shipped. What the advisory conclusion assumed is that
|
|
32
|
+
* "unpolished-if-unedited" would in fact get edited. Measured at `b50b27281`,
|
|
33
|
+
* it did not: `_auto-derived, rewrite before merge:_` shipped in FIVE
|
|
34
|
+
* consecutive released sections — 14.9.0 (4 lines), 14.10.0 (2), 14.11.0 (4),
|
|
35
|
+
* 14.12.0 (4), 14.13.0 (4), eighteen lines in total. A warning that has been
|
|
36
|
+
* ignored eighteen times is not a warning; it is a comment.
|
|
37
|
+
*
|
|
38
|
+
* **Instructing authority:** the maintainer, in the closing instruction of the
|
|
39
|
+
* 2026-09-a inbox round, requiring that the release-placeholder defect be taken
|
|
40
|
+
* into a roadmap and fixed. Carried into
|
|
41
|
+
* `agents/roadmaps/archive/road-to-publication-integrity-hard-fail.md` § Phase 1, which
|
|
42
|
+
* is the record of this change.
|
|
43
|
+
*
|
|
44
|
+
* **What did NOT change.** The read stays scoped to the section under release
|
|
45
|
+
* (`extract_changelog_section` below), so the eighteen historical lines cannot
|
|
46
|
+
* red a future release; a filled field is still never judged for prose quality;
|
|
47
|
+
* and the `_none_` contradiction check is untouched.
|
|
22
48
|
*
|
|
23
49
|
* Derivation is deliberately conservative (documented per-label below):
|
|
24
50
|
* a false red makes every release annoying; a miss only returns the head to
|
|
@@ -29,7 +55,8 @@
|
|
|
29
55
|
* [--changelog <path>]
|
|
30
56
|
*
|
|
31
57
|
* `--from` defaults to the latest reachable release tag before `--to`
|
|
32
|
-
* (default HEAD). Exit codes: 0 plausible · 1
|
|
58
|
+
* (default HEAD). Exit codes: 0 plausible · 1 refusal (an unrewritten
|
|
59
|
+
* auto-derived head line, or a `_none_` contradiction) · 2 usage error.
|
|
33
60
|
*/
|
|
34
61
|
import * as fs from 'node:fs';
|
|
35
62
|
import * as path from 'node:path';
|
|
@@ -37,6 +64,7 @@ import process from 'node:process';
|
|
|
37
64
|
import { fileURLToPath } from 'node:url';
|
|
38
65
|
|
|
39
66
|
import {
|
|
67
|
+
DERIVED_MARKER,
|
|
40
68
|
HEAD_LABELS,
|
|
41
69
|
HEAD_NONE,
|
|
42
70
|
type SpanCommit,
|
|
@@ -46,7 +74,10 @@ import {
|
|
|
46
74
|
previous_release_tag,
|
|
47
75
|
stale_draft_labels,
|
|
48
76
|
} from './_lib/release_highlights.js';
|
|
49
|
-
import {
|
|
77
|
+
import {
|
|
78
|
+
CURATED_HEAD_INSTRUCTION,
|
|
79
|
+
extract_changelog_section,
|
|
80
|
+
} from './_lib/release_material.js';
|
|
50
81
|
import { assertScanned, assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
51
82
|
|
|
52
83
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -116,9 +147,17 @@ export function highlight_contradictions(
|
|
|
116
147
|
|
|
117
148
|
/**
|
|
118
149
|
* Exported so the advisory-vs-blocking decision is a fixture, not a claim.
|
|
150
|
+
*
|
|
151
|
+
* CORRECTED 2026-09-01 with the reversal. This comment used to say that
|
|
119
152
|
* `stale_draft_labels` firing while the exit code stays 0 is the behaviour the
|
|
120
|
-
* cadence blocker adjudicates
|
|
121
|
-
*
|
|
153
|
+
* cadence blocker adjudicates. That is no longer what happens — the branch at
|
|
154
|
+
* the call site returns 1 — and the sentence is replaced rather than deleted
|
|
155
|
+
* because it was the load-bearing claim on the other side.
|
|
156
|
+
*
|
|
157
|
+
* What it got RIGHT is worth keeping: the decision was a one-line diff with a
|
|
158
|
+
* test that notices, and the reversal proved it. The two fixtures that pinned
|
|
159
|
+
* the advisory branch were updated in place, not deleted, so the change of
|
|
160
|
+
* contract reads off the diff.
|
|
122
161
|
*/
|
|
123
162
|
export function main(argv: readonly string[]): number {
|
|
124
163
|
let version: string | null = null;
|
|
@@ -171,6 +210,33 @@ export function main(argv: readonly string[]): number {
|
|
|
171
210
|
process.stderr.write(`CHANGELOG carries no section for ${version}\n`);
|
|
172
211
|
return 2;
|
|
173
212
|
}
|
|
213
|
+
// A LEAKED AUTHORING INSTRUCTION REFUSES THE RELEASE (roadmap § 2.2).
|
|
214
|
+
//
|
|
215
|
+
// A different mechanism from the marker check below: that one catches an
|
|
216
|
+
// unpolished claim, this one catches a reminder to the releaser that was
|
|
217
|
+
// never meant to be release content at all. `CHANGELOG.md` is the bare
|
|
218
|
+
// entry in `package.json` `files`, so a comment surviving here is
|
|
219
|
+
// published to npm — it did, twice, before Option A stopped the writer
|
|
220
|
+
// emitting it.
|
|
221
|
+
//
|
|
222
|
+
// SAME SCOPE as the marker check, and for the same reason: `section` is
|
|
223
|
+
// the ONE section `extract_changelog_section` cut for `--version`. A
|
|
224
|
+
// historical section carrying the identical comment is not read here and
|
|
225
|
+
// does not block this release. Detection is the named sentinel, never a
|
|
226
|
+
// shape match over comment prose.
|
|
227
|
+
if (section.body.includes(CURATED_HEAD_INSTRUCTION)) {
|
|
228
|
+
process.stderr.write(
|
|
229
|
+
`❌ the ${version} section still carries the generator's authoring instruction ` +
|
|
230
|
+
`(\`${CURATED_HEAD_INSTRUCTION}\`).\n` +
|
|
231
|
+
' It is a reminder to the releaser, not release content, and CHANGELOG.md ' +
|
|
232
|
+
'is published to npm.\n' +
|
|
233
|
+
` Delete that comment line from the ${version} section. Only that section is ` +
|
|
234
|
+
'read; historical\n' +
|
|
235
|
+
' sections carrying the same comment are out of scope and do not block this ' +
|
|
236
|
+
'release.\n',
|
|
237
|
+
);
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
174
240
|
const curated = parse_curated_head(section.body);
|
|
175
241
|
if (!curated) {
|
|
176
242
|
process.stdout.write(`ℹ️ no curated head in the ${version} section — nothing to check\n`);
|
|
@@ -199,19 +265,52 @@ export function main(argv: readonly string[]): number {
|
|
|
199
265
|
'could contradict. Deleting the scan root is not the same state — that is the ' +
|
|
200
266
|
'throwing branch, not this one.',
|
|
201
267
|
});
|
|
202
|
-
// A generator-derived line that nobody rewrote
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
// `_none_` check
|
|
206
|
-
//
|
|
268
|
+
// A generator-derived line that nobody rewrote REFUSES the release.
|
|
269
|
+
//
|
|
270
|
+
// REVERSED 2026-09-01. This block used to warn and return the exit code to
|
|
271
|
+
// the `_none_` check alone, on the reasoning that a derived line is true
|
|
272
|
+
// and merely unpolished. The reasoning still holds and the conclusion does
|
|
273
|
+
// not: the warning was emitted and ignored through five consecutive
|
|
274
|
+
// released sections. See this file's header for the full record and the
|
|
275
|
+
// instructing authority.
|
|
276
|
+
//
|
|
277
|
+
// SCOPE, and it is the load-bearing half (roadmap § 1.3): `curated` comes
|
|
278
|
+
// from `parse_curated_head(section.body)` above, and `section` is the ONE
|
|
279
|
+
// section `extract_changelog_section` cut for `--version`. No read here
|
|
280
|
+
// reaches another section, another era file, or `docs/archive/`. That is
|
|
281
|
+
// what keeps eighteen historical marker lines from turning this into a
|
|
282
|
+
// permanent red on every future release.
|
|
283
|
+
const derived = derive_categories(span);
|
|
207
284
|
const drafts = stale_draft_labels(curated);
|
|
208
285
|
if (drafts.length > 0) {
|
|
209
|
-
process.
|
|
210
|
-
|
|
211
|
-
`${drafts.join(', ')}
|
|
286
|
+
process.stderr.write(
|
|
287
|
+
`❌ unrewritten auto-derived head line(s) in the ${version} section: ` +
|
|
288
|
+
`${drafts.join(', ')}\n` +
|
|
289
|
+
` Each still carries \`${DERIVED_MARKER}\` — the generator's draft, not a ` +
|
|
290
|
+
'curated claim.\n' +
|
|
291
|
+
` Rewrite those line(s) in the ${version} section of the changelog. Only that ` +
|
|
292
|
+
'section is read;\n' +
|
|
293
|
+
' historical sections carrying the same marker are out of scope and do not ' +
|
|
294
|
+
'block this release.\n',
|
|
212
295
|
);
|
|
296
|
+
// The evidence, not just the verdict. The draft line cites SHAs and a
|
|
297
|
+
// deriving reason; what a releaser needs in order to write the real
|
|
298
|
+
// claim is the SUBJECT behind each SHA, and looking eleven of them up
|
|
299
|
+
// by hand is the friction that made this refusal cheaper to ignore
|
|
300
|
+
// than to satisfy across five releases. The `_none_` branch below has
|
|
301
|
+
// printed its evidence since it was built; this one now matches it.
|
|
302
|
+
for (const label of drafts) {
|
|
303
|
+
const hits = derived[label] ?? [];
|
|
304
|
+
if (hits.length === 0) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
process.stderr.write(` - **${label}:** derived from\n`);
|
|
308
|
+
for (const e of hits) {
|
|
309
|
+
process.stderr.write(` ${e}\n`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return 1;
|
|
213
313
|
}
|
|
214
|
-
const derived = derive_categories(span);
|
|
215
314
|
const contradictions = highlight_contradictions(curated, derived);
|
|
216
315
|
if (contradictions.length === 0) {
|
|
217
316
|
process.stdout.write(`✅ curated head plausible for ${version} (span ${from}..${to})\n`);
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* (Python-only); these pure regex/parse helpers are replicated here
|
|
16
16
|
* byte-for-byte from that module. Keep in lock-step with it.
|
|
17
17
|
*
|
|
18
|
-
* Every non-draft file under `agents/roadmaps/` (excluding `archive/`,
|
|
18
|
+
* Every non-draft file under `agents/roadmaps/` (excluding `archive/`, code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
|
|
19
19
|
* `skipped/`, template/README/open-questions) MUST:
|
|
20
20
|
* 1. Be parseable by the dashboard's `PHASE_RE` (>= one `## Phase <id>`).
|
|
21
21
|
* 2. Have at least one trackable checkbox under every parsed phase.
|
|
@@ -49,6 +49,15 @@ const PHASE_RE =
|
|
|
49
49
|
// FRONTMATTER_RE = re.compile(r"\A---\n(.*?)\n---\s*\n", re.DOTALL)
|
|
50
50
|
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\s*\n/;
|
|
51
51
|
const DRAFT_VALUES: ReadonlySet<string> = new Set(['draft']);
|
|
52
|
+
/**
|
|
53
|
+
* A carrier holds obligations deferred out of an archived parent. It is not
|
|
54
|
+
* schedulable work — every item in it has an unmet resumption trigger — and it
|
|
55
|
+
* has no `## Phase` headings by construction, so it is skipped exactly where a
|
|
56
|
+
* draft is. It differs from a draft in what protects it: a draft may be deleted
|
|
57
|
+
* freely, a carrier may not, and `lint_carrier_integrity` reds when one loses
|
|
58
|
+
* its receiver.
|
|
59
|
+
*/
|
|
60
|
+
const CARRIER_VALUES: ReadonlySet<string> = new Set(['carrier']);
|
|
52
61
|
|
|
53
62
|
const EXCLUDE_NAMES: ReadonlySet<string> = new Set([
|
|
54
63
|
'template.md',
|
|
@@ -92,6 +101,11 @@ function is_draft(fm: Map<string, string>): boolean {
|
|
|
92
101
|
return DRAFT_VALUES.has((fm.get('status') ?? '').toLowerCase());
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
/** True for a `status: carrier` roadmap — see CARRIER_VALUES. */
|
|
105
|
+
function is_carrier(fm: Map<string, string>): boolean {
|
|
106
|
+
return CARRIER_VALUES.has((fm.get('status') ?? '').toLowerCase());
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
/** Mirror update_roadmap_progress.is_roadmap_candidate (POSIX path parts). */
|
|
96
110
|
function is_roadmap_candidate(p: string): boolean {
|
|
97
111
|
const name = path.basename(p);
|
|
@@ -168,7 +182,7 @@ function find_active_roadmaps(root: string): string[] {
|
|
|
168
182
|
continue;
|
|
169
183
|
}
|
|
170
184
|
const text = fs.readFileSync(p, 'utf-8');
|
|
171
|
-
if (is_draft(parse_frontmatter(text))) {
|
|
185
|
+
if (is_draft(parse_frontmatter(text)) || is_carrier(parse_frontmatter(text))) {
|
|
172
186
|
continue;
|
|
173
187
|
}
|
|
174
188
|
out.push(p);
|
|
@@ -345,6 +359,7 @@ export {
|
|
|
345
359
|
CHECKBOX_RE,
|
|
346
360
|
PHASE_RE,
|
|
347
361
|
is_draft,
|
|
362
|
+
is_carrier,
|
|
348
363
|
is_roadmap_candidate,
|
|
349
364
|
parse_frontmatter,
|
|
350
365
|
find_active_roadmaps,
|