@event4u/agent-config 14.17.0 → 14.19.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 +207 -77
- package/README.md +72 -150
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/pr/merge.md +35 -44
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
- package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +3 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
- package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +749 -669
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +39 -38
- 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 +2764 -2439
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1381 -1138
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/shared/settingsConsent.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +4 -4
- package/src/config/release-gate-locality.yml +141 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +265 -1
- package/src/scripts/_lib/release_material.ts +129 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/_lib/unified_diff.ts +121 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +239 -6
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +99 -14
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/code_graph/build.ts +230 -33
- package/src/scripts/code_graph/extract.ts +168 -33
- package/src/scripts/code_graph/types.ts +37 -2
- package/src/scripts/code_graph/validate.ts +12 -1
- package/src/scripts/conformance_scan.ts +45 -4
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +228 -18
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +47 -30
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/language_mirror_hook.ts +27 -5
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +31 -143
- package/src/scripts/release_drill.ts +71 -1
- package/src/scripts/release_publication.ts +243 -3
- package/src/scripts/release_verify.ts +220 -0
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +6 -1
- package/src/scripts/schemas/skill.schema.json +12 -2
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/src/shared/settingsConsent.ts +1 -1
|
@@ -16,12 +16,20 @@
|
|
|
16
16
|
* import path changes.
|
|
17
17
|
*/
|
|
18
18
|
import { spawnSync } from 'node:child_process';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
DERIVED_MARKER,
|
|
21
|
+
type MixObligation,
|
|
22
|
+
previous_release_tag,
|
|
23
|
+
publication_blockers,
|
|
24
|
+
section_publication_blockers,
|
|
25
|
+
} from './_lib/release_highlights.js';
|
|
26
|
+
import { loadTaxonomy, measureRange } from './measure_release_mix.js';
|
|
20
27
|
import * as fs from 'node:fs';
|
|
21
28
|
import process from 'node:process';
|
|
22
29
|
|
|
23
30
|
import { gh_argv_label, gh_retry } from './_lib/gh_transient.js';
|
|
24
31
|
import {
|
|
32
|
+
NEXT_SECTION_RE,
|
|
25
33
|
extract_changelog_section,
|
|
26
34
|
tag_message_from_section,
|
|
27
35
|
pr_body_from_section,
|
|
@@ -37,6 +45,7 @@ import {
|
|
|
37
45
|
SystemExitError,
|
|
38
46
|
_cap_body,
|
|
39
47
|
read_changelog_text,
|
|
48
|
+
reEscape,
|
|
40
49
|
} from './release_env.js';
|
|
41
50
|
|
|
42
51
|
function die(msg: string, code = 2): never {
|
|
@@ -422,6 +431,71 @@ export function _is_non_fast_forward(stderr: string, stdout: string): boolean {
|
|
|
422
431
|
return /(non-fast-forward|fetch first|behind its remote counterpart)/i.test(text);
|
|
423
432
|
}
|
|
424
433
|
|
|
434
|
+
/**
|
|
435
|
+
* The pre-push local gate run, as an argv the caller hands to `run()`.
|
|
436
|
+
*
|
|
437
|
+
* Every release-PR assertion that can run locally, run BEFORE the branch leaves
|
|
438
|
+
* the machine. `guard_release_branch_push` below covers the changelog-section
|
|
439
|
+
* obligations; this covers the rest of `release-validation.yml` — PR shape,
|
|
440
|
+
* template pin — from `src/config/release-gate-locality.yml`, the registry
|
|
441
|
+
* `tests/scripts/release_gate_locality.test.ts` keeps in step with the
|
|
442
|
+
* workflow. `--cheap` drops the one row that leaves the machine, so a registry
|
|
443
|
+
* outage cannot read as a release defect.
|
|
444
|
+
*
|
|
445
|
+
* An argv rather than a call, so the pipeline issues it through `run()` and
|
|
446
|
+
* `release_drill` intercepts it exactly as it intercepts `task
|
|
447
|
+
* release-prepare`. A real subprocess inside the simulated world would run
|
|
448
|
+
* gates against the live tree.
|
|
449
|
+
*
|
|
450
|
+
* Lives here rather than in `release.ts` for the reason
|
|
451
|
+
* `_refuse_unpublishable` already states: that file is 2000+ lines and every
|
|
452
|
+
* line above 1500 is charged by the source-size growth ratchet.
|
|
453
|
+
*/
|
|
454
|
+
export function local_release_gate_argv(): string[] {
|
|
455
|
+
return ['task', 'release:verify', '--', '--cheap'];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Measure the governance-versus-product level for the span under release.
|
|
460
|
+
*
|
|
461
|
+
* The push guard's fourth site reads the SAME obligation the release PR reads,
|
|
462
|
+
* which until 2026-09-05 it did not: `publication_blockers` knew two sentinels
|
|
463
|
+
* and not this one, so the earliest refusal was the pull request. That gap was
|
|
464
|
+
* documented in `docs/contracts/CHANGELOG-conventions.md` and it is what turned
|
|
465
|
+
* 14.17.0 red after a branch, a PR and a CI run had already been spent.
|
|
466
|
+
*
|
|
467
|
+
* Returns `null` on any measurement failure — a shallow clone or a missing tag
|
|
468
|
+
* is an environment fact, and the contract is explicit that it degrades to a
|
|
469
|
+
* warning rather than to a refusal. `null` produces no blocker downstream.
|
|
470
|
+
*/
|
|
471
|
+
export function measure_mix_obligation(
|
|
472
|
+
version: string,
|
|
473
|
+
fromRef?: string | null,
|
|
474
|
+
): MixObligation | null {
|
|
475
|
+
try {
|
|
476
|
+
const from = fromRef ?? previous_release_tag('HEAD', REPO_ROOT);
|
|
477
|
+
if (!from) return null;
|
|
478
|
+
const reading = measureRange(from, 'HEAD', loadTaxonomy(), version, REPO_ROOT);
|
|
479
|
+
const o = reading.response_obligation;
|
|
480
|
+
return {
|
|
481
|
+
triggered: o.triggered,
|
|
482
|
+
level:
|
|
483
|
+
`governance-only ${String(o.governance_only)} vs consumer-only ` +
|
|
484
|
+
`${String(o.consumer_only)} (taxonomy ${reading.taxonomy_version})`,
|
|
485
|
+
};
|
|
486
|
+
} catch (err) {
|
|
487
|
+
// The CI side prints `⚠️ governance mix not measured`; swallowing it
|
|
488
|
+
// here made a broken taxonomy file disable BOTH local guards in
|
|
489
|
+
// silence while CI kept refusing — the exact asymmetry this change
|
|
490
|
+
// exists to remove, restored one layer down.
|
|
491
|
+
process.stderr.write(
|
|
492
|
+
`⚠️ governance mix not measured for ${version}: ${(err as Error).message}\n` +
|
|
493
|
+
' Both local guards are inert for this obligation; the release PR still refuses.\n',
|
|
494
|
+
);
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
425
499
|
/**
|
|
426
500
|
* Refuse the FIRST remote state of a release whose section is not publishable.
|
|
427
501
|
*
|
|
@@ -465,7 +539,12 @@ export function guard_release_branch_push(branch: string): void {
|
|
|
465
539
|
if (!section) {
|
|
466
540
|
return;
|
|
467
541
|
}
|
|
468
|
-
const blockers =
|
|
542
|
+
const blockers = section_publication_blockers(
|
|
543
|
+
section.body,
|
|
544
|
+
version,
|
|
545
|
+
`\`${branch}\``,
|
|
546
|
+
measure_mix_obligation(version),
|
|
547
|
+
);
|
|
469
548
|
if (blockers.length === 0) {
|
|
470
549
|
return;
|
|
471
550
|
}
|
|
@@ -614,7 +693,28 @@ export function guard_release_curation(version: string, prMerged = false): void
|
|
|
614
693
|
if (!section) {
|
|
615
694
|
return;
|
|
616
695
|
}
|
|
617
|
-
|
|
696
|
+
// The head-level blockers plus the mix obligation — deliberately NOT the
|
|
697
|
+
// tests footer.
|
|
698
|
+
//
|
|
699
|
+
// This site runs BEFORE `prepend_changelog`'s output is committed, and
|
|
700
|
+
// `--resume` skips the whole bump block once `package.json` already carries
|
|
701
|
+
// the target version. So a refusal here for a missing footer is a trap: the
|
|
702
|
+
// writer never gets a second chance to render it, the message forbids
|
|
703
|
+
// hand-writing it, and the operator is left rolling `package.json` back by
|
|
704
|
+
// hand. The footer is a MEASUREMENT the writer produces, not prose the
|
|
705
|
+
// operator can supply, which is exactly why it does not belong at the site
|
|
706
|
+
// that fires before the artefact exists in git.
|
|
707
|
+
//
|
|
708
|
+
// `guard_release_branch_push` carries it instead: by then the section is
|
|
709
|
+
// committed and editable on the branch, and nothing has left the machine.
|
|
710
|
+
const blockers = [
|
|
711
|
+
...publication_blockers(
|
|
712
|
+
section.body,
|
|
713
|
+
version,
|
|
714
|
+
`\`release/${version}\``,
|
|
715
|
+
measure_mix_obligation(version),
|
|
716
|
+
),
|
|
717
|
+
];
|
|
618
718
|
if (blockers.length === 0) {
|
|
619
719
|
return;
|
|
620
720
|
}
|
|
@@ -1017,3 +1117,143 @@ export function create_and_push_annotated_tag(version: string): void {
|
|
|
1017
1117
|
run(['git', 'tag', '-a', version, '-m', tag_message_from_section(merged.body, version)]);
|
|
1018
1118
|
_push_tag(version);
|
|
1019
1119
|
}
|
|
1120
|
+
|
|
1121
|
+
// Relocated from `release.ts` on 2026-09-05, for the reason
|
|
1122
|
+
// `_refuse_unpublishable` above already states: that file is 2000+ lines and
|
|
1123
|
+
// every line above 1500 is charged by the source-size growth ratchet. The move
|
|
1124
|
+
// is not incidental to this change — the `Tests: N` footer became a
|
|
1125
|
+
// section-level publication obligation in the same diff
|
|
1126
|
+
// (`section_publication_blockers`), so the writer that emits it and the guard
|
|
1127
|
+
// that refuses its absence now sit in one module. `release.ts` re-exports all
|
|
1128
|
+
// four names, so every existing importer is unaffected.
|
|
1129
|
+
|
|
1130
|
+
const _TEST_COUNT_LINE_RE = /^Tests:\s+(\d+)/m;
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Buffer ceiling for the `vitest list` probe, in bytes.
|
|
1134
|
+
*
|
|
1135
|
+
* `spawnSync` buffers the child's whole stdout in memory and fails with
|
|
1136
|
+
* ENOBUFS past `maxBuffer`, whose default is 1 MiB. The probe emits one line
|
|
1137
|
+
* per test case, so its output grows with the suite: at 9470 cases the listing
|
|
1138
|
+
* is ~1.25 MB, i.e. already past the default. That is what silently dropped
|
|
1139
|
+
* the footer from the 9.10.0 notes and turned the `changelog-entry` gate red.
|
|
1140
|
+
* 64 MiB is ~50× the current listing — headroom for a suite many times this
|
|
1141
|
+
* size, at no cost when the output is small.
|
|
1142
|
+
*/
|
|
1143
|
+
export const _TEST_LIST_MAX_BUFFER = 64 * 1024 * 1024;
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Count test cases from a finished `vitest list` spawn result, or return null
|
|
1147
|
+
* when the probe did not produce a usable listing.
|
|
1148
|
+
*
|
|
1149
|
+
* Split out from `_count_tests_current` so the failure modes are testable
|
|
1150
|
+
* without spawning the real (~14s, >1 MB) collection. `warn` is injected for
|
|
1151
|
+
* the same reason; it defaults to stderr.
|
|
1152
|
+
*/
|
|
1153
|
+
export function _count_from_list_result(
|
|
1154
|
+
res: {
|
|
1155
|
+
error?: (Error & { code?: string }) | undefined;
|
|
1156
|
+
status: number | null;
|
|
1157
|
+
stdout: string | null;
|
|
1158
|
+
},
|
|
1159
|
+
warn: (msg: string) => void = (msg) => void process.stderr.write(msg),
|
|
1160
|
+
): number | null {
|
|
1161
|
+
// The trend line is informational and never blocks a release — but a
|
|
1162
|
+
// SILENT drop is what shipped 9.8.0 and 9.10.0 without a footer, so every
|
|
1163
|
+
// degradation says why. The `changelog-entry` CI gate treats a missing
|
|
1164
|
+
// footer as fatal, so this warning is the difference between fixing the
|
|
1165
|
+
// probe now and finding out from a red release PR.
|
|
1166
|
+
if (res.error) {
|
|
1167
|
+
// ENOENT (no npx) · ETIMEDOUT · ENOBUFS (listing past maxBuffer).
|
|
1168
|
+
const code = res.error.code ?? res.error.message;
|
|
1169
|
+
warn(
|
|
1170
|
+
`⚠️ test-count probe failed (${code}) — the \`Tests:\` footer will ` +
|
|
1171
|
+
`be omitted from the release notes, which fails the ` +
|
|
1172
|
+
`\`CHANGELOG entry exists for head version\` gate.\n`,
|
|
1173
|
+
);
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
if ((res.status ?? 1) !== 0) {
|
|
1177
|
+
warn(
|
|
1178
|
+
`⚠️ test-count probe exited ${res.status ?? '(null)'} — the ` +
|
|
1179
|
+
`\`Tests:\` footer will be omitted from the release notes.\n`,
|
|
1180
|
+
);
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
const lines = (res.stdout ?? '').split('\n').filter((l) => l.trim().length > 0);
|
|
1184
|
+
if (lines.length === 0) {
|
|
1185
|
+
warn('⚠️ test-count probe listed 0 cases — the `Tests:` footer will be omitted.\n');
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
return lines.length;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Return the collected vitest test-case count on the current tree
|
|
1193
|
+
* (`npx vitest list`, one line per case; ~14s wall). Returns null when
|
|
1194
|
+
* collection fails — the trend line is informational, never a release
|
|
1195
|
+
* blocker, but every failure warns (see `_count_from_list_result`).
|
|
1196
|
+
* (Replaced the dead `pytest --collect-only` probe on 2026-07-08,
|
|
1197
|
+
* road-to-truth-and-reference-hygiene Phase 3: the Python suite was retired
|
|
1198
|
+
* with ADR-200, so the old probe always degraded to null and the `Tests:`
|
|
1199
|
+
* footer silently vanished from release notes.)
|
|
1200
|
+
*/
|
|
1201
|
+
export function _count_tests_current(): number | null {
|
|
1202
|
+
// Recursion/cost guard: `npx vitest list` inside a vitest-driven release
|
|
1203
|
+
// test would collect the whole suite from within the suite (the child
|
|
1204
|
+
// inherits VITEST=… from the runner) — the exact recursion the caller
|
|
1205
|
+
// comment warns about. Degrade to null there, same as any other
|
|
1206
|
+
// collection failure.
|
|
1207
|
+
if (process.env['VITEST'] !== undefined) {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
const res = spawnSync('npx', ['vitest', 'list'], {
|
|
1211
|
+
cwd: REPO_ROOT,
|
|
1212
|
+
encoding: 'utf-8',
|
|
1213
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1214
|
+
timeout: 180_000,
|
|
1215
|
+
maxBuffer: _TEST_LIST_MAX_BUFFER,
|
|
1216
|
+
});
|
|
1217
|
+
return _count_from_list_result(res);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Read CHANGELOG.md and return the most recent `Tests: N` footer under the
|
|
1222
|
+
* `prev_tag` heading, or null when not found.
|
|
1223
|
+
*/
|
|
1224
|
+
export function _previous_test_count_from_changelog(prev_tag: string | null): number | null {
|
|
1225
|
+
if (!prev_tag || !fs.existsSync(CHANGELOG)) {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
const text = fs.readFileSync(CHANGELOG, 'utf-8');
|
|
1229
|
+
const heading_re = new RegExp(`^##\\s+\\[?${reEscape(prev_tag)}\\b`, 'm');
|
|
1230
|
+
const m = heading_re.exec(text);
|
|
1231
|
+
if (!m) {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
const headEnd = m.index + m[0].length;
|
|
1235
|
+
const rest = text.slice(headEnd);
|
|
1236
|
+
const next_heading = NEXT_SECTION_RE.exec(rest);
|
|
1237
|
+
const sectionEnd = headEnd + (next_heading ? next_heading.index : rest.length);
|
|
1238
|
+
const section = text.slice(headEnd, sectionEnd);
|
|
1239
|
+
const count_match = _TEST_COUNT_LINE_RE.exec(section);
|
|
1240
|
+
return count_match ? Number.parseInt(count_match[1] as string, 10) : null;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Return the `Tests: N (+M since X.Y.Z)` footer line, or null when the current
|
|
1245
|
+
* count cannot be determined. Silent on collection errors.
|
|
1246
|
+
*/
|
|
1247
|
+
export function _render_test_trend_line(prev_tag: string | null): string | null {
|
|
1248
|
+
const current = _count_tests_current();
|
|
1249
|
+
if (current === null) {
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
const previous = _previous_test_count_from_changelog(prev_tag);
|
|
1253
|
+
if (previous === null || !prev_tag) {
|
|
1254
|
+
return `Tests: ${current}`;
|
|
1255
|
+
}
|
|
1256
|
+
const delta = current - previous;
|
|
1257
|
+
const sign = delta >= 0 ? '+' : '';
|
|
1258
|
+
return `Tests: ${current} (${sign}${delta} since ${prev_tag})`;
|
|
1259
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Run every release-PR assertion that CAN run locally, before anything is
|
|
4
|
+
* pushed.
|
|
5
|
+
*
|
|
6
|
+
* `release-validation.yml` is the last gate a release passes and the most
|
|
7
|
+
* expensive one to fail: it runs after `gh pr create`, so a branch, a pull
|
|
8
|
+
* request and a CI run are already spent by the time it speaks. Twice in three
|
|
9
|
+
* releases it refused something reproducible locally in under two seconds —
|
|
10
|
+
* 14.14.0 (PR #1812) on the curated head, 14.17.0 (PR #1856) on the
|
|
11
|
+
* governance-mix response. In both cases the check existed, was runnable, and
|
|
12
|
+
* nothing ran it.
|
|
13
|
+
*
|
|
14
|
+
* This is the command that runs it. It is DERIVED from
|
|
15
|
+
* `src/config/release-gate-locality.yml` rather than from a hand-kept list, so
|
|
16
|
+
* a new release-validation job cannot be added without either giving this
|
|
17
|
+
* runner a command or declaring, with a classified reason, that it has none —
|
|
18
|
+
* `tests/scripts/release_gate_locality.test.ts` enforces that half.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT a gate script: it runs commands rather than inspecting a
|
|
21
|
+
* corpus, so it has no `scanned:` count to report and no place in
|
|
22
|
+
* `gate-coverage.yml`. Same shape as `release_drill`, which is also a runner
|
|
23
|
+
* with a task and a test and no coverage row.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* task release:verify # version from the branch, else package.json
|
|
27
|
+
* task release:verify -- --version 14.17.0
|
|
28
|
+
* task release:verify -- --list # print the plan, run nothing
|
|
29
|
+
*/
|
|
30
|
+
import { spawnSync } from 'node:child_process';
|
|
31
|
+
import * as fs from 'node:fs';
|
|
32
|
+
import * as path from 'node:path';
|
|
33
|
+
import process from 'node:process';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
|
|
36
|
+
import { parse as parseYaml } from 'yaml';
|
|
37
|
+
|
|
38
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
39
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
40
|
+
const REGISTRY = path.join(REPO_ROOT, 'src', 'config', 'release-gate-locality.yml');
|
|
41
|
+
|
|
42
|
+
interface Row {
|
|
43
|
+
local: string | null;
|
|
44
|
+
reason?: string;
|
|
45
|
+
verify: boolean;
|
|
46
|
+
network?: boolean;
|
|
47
|
+
partial?: string;
|
|
48
|
+
release_branch_only?: boolean;
|
|
49
|
+
script: string | null;
|
|
50
|
+
guard?: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Collapse rows that reproduce to the SAME command.
|
|
55
|
+
*
|
|
56
|
+
* Two jobs legitimately share one local reproduction — `changelog-entry` and
|
|
57
|
+
* `highlight-plausibility` are both answered by `check_release_highlights` —
|
|
58
|
+
* and running it twice would double the cost while reporting a coverage the
|
|
59
|
+
* second run did not add. The ids stay joined so the output still names every
|
|
60
|
+
* job the command answers for.
|
|
61
|
+
*/
|
|
62
|
+
function plan(rows: ReadonlyArray<readonly [string, Row]>, version: string): Array<[string, string]> {
|
|
63
|
+
const byCommand = new Map<string, string[]>();
|
|
64
|
+
for (const [id, r] of rows) {
|
|
65
|
+
const cmd = r.local!.replace('${VERSION}', version);
|
|
66
|
+
const label = r.partial ? `${id} (partial: ${r.partial})` : id;
|
|
67
|
+
byCommand.set(cmd, [...(byCommand.get(cmd) ?? []), label]);
|
|
68
|
+
}
|
|
69
|
+
return [...byCommand.entries()].map(([cmd, ids]) => [ids.join(' + '), cmd]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The version under release.
|
|
74
|
+
*
|
|
75
|
+
* The branch name first, because that is what `release-validation.yml` itself
|
|
76
|
+
* reads (`head_ref`) — running the gates against a different version than CI
|
|
77
|
+
* will read would be a green run that proves nothing. `package.json` is the
|
|
78
|
+
* fallback for a maintainer probing before the branch exists.
|
|
79
|
+
*/
|
|
80
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
81
|
+
|
|
82
|
+
export function resolve_version(argv: readonly string[], cwd = REPO_ROOT): string | null {
|
|
83
|
+
const v = resolve_version_raw(argv, cwd);
|
|
84
|
+
// Every row's command is interpolated into `bash -c`, and two of the three
|
|
85
|
+
// sources here are strings git and npm hand back rather than values this
|
|
86
|
+
// file chose. `on_release_branch` already applies exactly this shape check;
|
|
87
|
+
// not applying it on the path that reaches a shell was the asymmetry.
|
|
88
|
+
return v !== null && SEMVER_RE.test(v) ? v : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolve_version_raw(argv: readonly string[], cwd = REPO_ROOT): string | null {
|
|
92
|
+
const i = argv.indexOf('--version');
|
|
93
|
+
if (i !== -1 && argv[i + 1]) return argv[i + 1]!;
|
|
94
|
+
const branch = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
95
|
+
cwd,
|
|
96
|
+
encoding: 'utf-8',
|
|
97
|
+
});
|
|
98
|
+
const name = (branch.stdout ?? '').trim();
|
|
99
|
+
if (name.startsWith('release/')) return name.slice('release/'.length);
|
|
100
|
+
try {
|
|
101
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8')) as {
|
|
102
|
+
version?: string;
|
|
103
|
+
};
|
|
104
|
+
return pkg.version ?? null;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Whether HEAD is a `release/X.Y.Z` branch — decides the `release_branch_only` rows. */
|
|
111
|
+
export function on_release_branch(cwd = REPO_ROOT): boolean {
|
|
112
|
+
const r = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, encoding: 'utf-8' });
|
|
113
|
+
return /^release\/\d+\.\d+\.\d+$/u.test((r.stdout ?? '').trim());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
117
|
+
const version = resolve_version(argv);
|
|
118
|
+
if (!version) {
|
|
119
|
+
process.stderr.write('release_verify: could not resolve a version to check.\n');
|
|
120
|
+
return 2;
|
|
121
|
+
}
|
|
122
|
+
const reg = parseYaml(fs.readFileSync(REGISTRY, 'utf-8')) as { jobs: Record<string, Row> };
|
|
123
|
+
const cheap = argv.includes('--cheap');
|
|
124
|
+
const releaseHead = on_release_branch();
|
|
125
|
+
// Every job lands in exactly one bucket, and the buckets are built from one
|
|
126
|
+
// pass so they cannot fail to partition. The first version filtered `rows`
|
|
127
|
+
// and then derived `uncovered` from a DIFFERENT predicate, so `audit-gate`
|
|
128
|
+
// under `--cheap` satisfied neither and vanished from the report: the run
|
|
129
|
+
// printed "3 pass" and "4 not covered" over eight jobs and read as a
|
|
130
|
+
// clearance for a job nobody ran. `--cheap` is the variant `task release`
|
|
131
|
+
// fires unattended, so that was the report nobody would be reading.
|
|
132
|
+
const runnable: Array<[string, Row]> = [];
|
|
133
|
+
const skipped: Array<[string, string]> = [];
|
|
134
|
+
for (const [id, r] of Object.entries(reg.jobs)) {
|
|
135
|
+
if (!r.verify || !r.local) {
|
|
136
|
+
skipped.push([id, r.reason ?? 'not part of the verify set']);
|
|
137
|
+
} else if (cheap && r.network) {
|
|
138
|
+
skipped.push([id, 'skipped by --cheap — leaves the machine']);
|
|
139
|
+
} else if (r.release_branch_only && !releaseHead) {
|
|
140
|
+
skipped.push([id, 'asserts a release-PR diff, and HEAD is not a release/X.Y.Z branch']);
|
|
141
|
+
} else {
|
|
142
|
+
runnable.push([id, r]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (runnable.length + skipped.length !== Object.keys(reg.jobs).length) {
|
|
146
|
+
process.stderr.write('release_verify: internal accounting error — jobs lost.\n');
|
|
147
|
+
return 2;
|
|
148
|
+
}
|
|
149
|
+
const rows = runnable;
|
|
150
|
+
const steps = plan(rows, version);
|
|
151
|
+
|
|
152
|
+
// Where the version came from, because off a release branch it falls back
|
|
153
|
+
// to `package.json` — which is the version already RELEASED, not the one
|
|
154
|
+
// being cut. A green run against it says nothing about the next release,
|
|
155
|
+
// and `task release:verify` with no arguments is exactly the pre-probe the
|
|
156
|
+
// task description invites.
|
|
157
|
+
const provenance = argv.includes('--version')
|
|
158
|
+
? 'from --version'
|
|
159
|
+
: releaseHead
|
|
160
|
+
? 'from the release branch'
|
|
161
|
+
: 'from package.json — NOT a release branch, so this is the version already released';
|
|
162
|
+
process.stdout.write(
|
|
163
|
+
`release_verify: ${version} (${provenance}) — ${String(steps.length)} local gate command(s)` +
|
|
164
|
+
`${cheap ? ' (cheap set — network rows skipped)' : ''}\n\n`,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (argv.includes('--list')) {
|
|
168
|
+
for (const [id, cmd] of steps) {
|
|
169
|
+
process.stdout.write(` ${id}\n ${cmd}\n`);
|
|
170
|
+
}
|
|
171
|
+
for (const [id, why] of skipped) {
|
|
172
|
+
process.stdout.write(` ${id} — not run\n ${why}\n`);
|
|
173
|
+
}
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const failed: string[] = [];
|
|
178
|
+
for (const [id, cmd] of steps) {
|
|
179
|
+
process.stdout.write(`─── ${id}\n ${cmd}\n`);
|
|
180
|
+
const res = spawnSync('bash', ['-c', cmd], { cwd: REPO_ROOT, stdio: 'inherit' });
|
|
181
|
+
if (res.status !== 0) {
|
|
182
|
+
failed.push(id);
|
|
183
|
+
process.stdout.write(` ❌ ${id} (exit ${String(res.status ?? -1)})\n\n`);
|
|
184
|
+
} else {
|
|
185
|
+
process.stdout.write(` ✅ ${id}\n\n`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (failed.length > 0) {
|
|
190
|
+
process.stderr.write(
|
|
191
|
+
`❌ ${String(failed.length)} release gate(s) would fail the release PR: ${failed.join(', ')}.\n` +
|
|
192
|
+
' Nothing has left this machine yet. Fix them, then re-run.\n',
|
|
193
|
+
);
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
// The runner names what it did NOT cover — a green run that silently
|
|
197
|
+
// omitted a job would read as a clearance it is not.
|
|
198
|
+
process.stdout.write(
|
|
199
|
+
`✅ ${String(rows.length)} of ${String(Object.keys(reg.jobs).length)} ` +
|
|
200
|
+
`release-validation job(s) pass for ${version} (${String(steps.length)} command(s)).\n` +
|
|
201
|
+
` Not covered by this run (${String(skipped.length)}):\n` +
|
|
202
|
+
skipped.map(([id, why]) => ` ${id} — ${why}\n`).join('') +
|
|
203
|
+
' See src/config/release-gate-locality.yml.\n',
|
|
204
|
+
);
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const _isMain = (() => {
|
|
209
|
+
const entry = process.argv[1];
|
|
210
|
+
if (!entry) return false;
|
|
211
|
+
try {
|
|
212
|
+
return fs.realpathSync(entry) === fs.realpathSync(_HERE);
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
|
|
218
|
+
if (_isMain) {
|
|
219
|
+
process.exit(main());
|
|
220
|
+
}
|