@event4u/agent-config 14.17.0 → 14.18.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 +170 -79
- package/README.md +1 -1
- package/dist/agent-src/commands/pr/merge.md +35 -44
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -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-review/SKILL.md +2 -0
- 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/testing-anti-patterns/SKILL.md +40 -0
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +12 -0
- 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-delegate/cmd_conformance.js +335 -659
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +15 -15
- 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 +959 -1304
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +671 -1002
- package/dist/shared/settingsConsent.js +1 -1
- package/package.json +1 -1
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/release-gate-locality.yml +129 -0
- package/src/scripts/_lib/release_highlights.ts +172 -1
- package/src/scripts/_lib/release_material.ts +66 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/unified_diff.ts +121 -0
- package/src/scripts/check_finding_dispositions.ts +179 -3
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +25 -13
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- 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 +2 -2
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/git_authorization_hook.ts +153 -17
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +33 -19
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +25 -369
- package/src/scripts/hooks/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/language_mirror_hook.ts +27 -5
- 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/schemas/review-findings.schema.json +1 -1
- package/src/scripts/schemas/skill.schema.json +11 -1
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/shared/settingsConsent.ts +1 -1
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* would mean the agent could arrive at its own permission by observing the
|
|
16
16
|
* world, which is the confused-deputy shape in miniature.
|
|
17
17
|
*
|
|
18
|
-
* Modelled on `src/scripts/hooks/
|
|
18
|
+
* Modelled on `src/scripts/hooks/git_command_classifier.ts`: answer from a
|
|
19
19
|
* recorded fact rather than from the model's recollection, and treat a missing
|
|
20
20
|
* record as NOT granted rather than as probably-fine.
|
|
21
21
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
3
|
"mcpName": "io.github.event4u-app/agent-config",
|
|
4
|
-
"version": "14.
|
|
4
|
+
"version": "14.18.0",
|
|
5
5
|
"description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including the counts in these badges.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Pre-existing standard-conformance claims in src/skills/**/SKILL.md and src/rules/*.md that carry NO `enforced_by:` declaration. The list may only SHRINK — an entry whose artefact gained a declaration must leave it in the same change, and a new undeclared claim is never added. Written by road-to-defect-population-sweeps 3.2; enforced by tests/contracts/conformance_claim_declaration.test.ts.",
|
|
3
|
+
"_why_empty": "The measured population was TWO artefacts, not the large legacy set 3.2 anticipated, and both were declarable honestly in the same change: accessibility-auditor names its real resolver, design-review declares the gap with its reason. An empty baseline is the strongest state this ratchet can be in, not a missing one — the file exists so the mechanism is wired and testable, and so a future undeclared claim has somewhere it is conspicuously NOT allowed to land.",
|
|
4
|
+
"_known_out_of_scope": [
|
|
5
|
+
"src/skills/design-intelligence/references/design-rules-checklist.md — carries `WCAG 2.2 AA` in a dedup NOTE that points at accessibility-auditor rather than claiming conformance itself. Out of scope because roadmap step 3.1 scopes the bar to `a skill or rule`, and the scanned set is the artefacts that carry frontmatter: `SKILL.md` and `src/rules/*.md`. Recorded here rather than silently excluded."
|
|
6
|
+
],
|
|
7
|
+
"count": 0,
|
|
8
|
+
"undeclared": []
|
|
9
|
+
}
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"note": "The nine active non-draft roadmaps that predate the `relates:` requirement (road-to-agentic-engineering-assurance, road-to-component-library-lifecycle, road-to-council-evidence-integrity, road-to-frontend-fidelity-calibration, road-to-monorepo-scope-and-detection, road-to-observability-plate, road-to-role-scoped-spawn-profiles, road-to-skill-delivery-over-mcp, road-to-standing-payload-diet). This is a NEW obligation rather than a repaired scan root, and the ratchet is the point: `depends:` shipped in the template with nothing requiring it and measured 0 adoption, so requiring `relates:` retroactively across the estate would be an owner-reserved write with no analysis behind the edges it would have to invent (held as step 4.3, deferred). Clearing an entry here is one roadmap at a time: run `agent-config roadmap:context` on it, write the rows the probe surfaces, and lower the count."
|
|
35
35
|
},
|
|
36
36
|
"check_source_size_budget": {
|
|
37
|
-
"count":
|
|
38
|
-
"landed": "2026-09-
|
|
37
|
+
"count": 18068,
|
|
38
|
+
"landed": "2026-09-05",
|
|
39
39
|
"note": "Lowered 18,245 -> 18,230 on 2026-09-02 by drain/publication-integrity-option-a. Phase 2 Option A moved the authoring instruction out of the generator: release.ts, which sits ~530 lines PAST the 1500 cap so every line there costs one excess, loses the comment emission, a 16-line HEAD_DEFAULT doc block and its alias const, and gains a shorter writer docstring. The doc block moved to _lib/release_material.ts, which is 289 lines and therefore UNDER the cap, so the same prose now costs nothing. Net -15. The branch first came in at +8 and paid its own way down rather than raising the baseline.",
|
|
40
|
-
"reason": "Lowered 18,230 -> 18,209 on 2026-09-03 by drain/artifact-location. `doctor --strict` needed 16 lines of integration wiring in cmd_doctor.ts, which sits ~2,100 lines PAST the 1500 cap so every line there costs one excess. The branch first came in at +137 and paid its own way down rather than raising the baseline: the strict semantics, threshold, kill switch, payload helper and argv fragments moved to _cli/_doctor_strict.ts (~160 lines, under the cap, costs nothing), taking it to +15; then the 37-line drift renderer moved to _cli/_doctor_emit.ts. Net -21. The renderer was chosen deliberately: pure rendering, two dependencies, no argparse involvement and therefore none of the byte-parity risk the parser surface carries against the retired Python implementation; nothing outside cmd_doctor.ts imported it. An AI council on the residual split (a: raise by 15 / d: descope the feature); neither was taken, because a parallel branch demonstrated the same day that paying down is achievable in this codebase, which is the evidence the split lacked. This gate's own test comment prescribes it: a commit that splits a god-file lowers the excess and must carry the lowered baseline. MERGE RECONCILIATION 2026-09-03, 18,209 -> 18,205: drain/wired-instruments and drain/artifact-location each lowered from the SAME 18,230 base in parallel -- the branch to 18,226 (net -4, run_continuation_hook.ts's termination ladder moved to _lib/continuation_ladder.ts and runtime_journal.ts's prose to _lib/outcome_vocabularies.ts, both under the cap) and main to 18,209 (net -21, the doctor --strict split above). Taking either side alone is wrong in a different direction: main's value silently discards the branch's earned -4, and the branch's value RAISES the baseline against main and weakens the gate. The committed number is neither -- it is the gate's own output on the merged tree, where both reductions apply. THIS LOCAL READING IS ADMISSIBLE HERE, and the discriminator matters because this file's ci-parity:local-only entry records a 165 -> 164 lowering that was reverted for exactly the opposite case: that count depends on how the environment resolves the projection surface, whereas this gate walks only SOURCE_ROOTS = ['src'] (check_source_size_budget.ts:88) counting excess lines over the 1500 ceiling, and `git status --porcelain -uall -- src/` reported ZERO untracked files, so a clean CI checkout of this commit measures the same 18,205. If CI reads anything else, CI's number is the correct one and this note is the record of why. LOWERED 18,205 -> 18,193 on 2026-09-03 by fix/release-curation-ask. The change adds a fifth publication guard, and its own additions landed at +10 excess: release.ts 2,008 -> 2,018, every line of which is past the 1500 cap. Paid down rather than baselined, in the two ways this gate's doctrine prescribes. The guard's rationale (~13 lines of docstring, including why prMerged is a parameter rather than a call-site if) lives in release_publication.ts, 915 lines and therefore UNDER the cap, so the prose costs nothing; and the two pure text helpers _splitlines and _rstrip moved out of release.ts into release_env.ts (333 lines, also under the cap), taking release.ts to 1,996 -- 12 BELOW where it started. The branch came in at +10 and leaves at -12. Recorded while moving them: _splitlines is duplicated a third and fourth time at adoption_report.ts:106 and readme_linter.ts:418; collapsing all three onto one definition touches three modules and was left out of this change rather than done silently. LOWERED 18,193 -> 18,180 on 2026-09-03 by fix/release-branch-reuse. The step-1 branch decision -- twenty lines of if/else in release.ts -- became one call to checkout_release_branch in release_publication.ts, which is 1,019 lines and therefore UNDER the 1500 cap, so the twenty lines plus a ~35-line rationale now cost nothing. release.ts 1,996 -> 1,983, every one of those lines being past the cap. The drill grew 765 -> 918 for two new scenarios and the snapshot/restore that keeps them from writing the real CHANGELOG, and it is under the cap too. A change that adds a feature and lowers the excess is the shape this gate's doctrine asks for; the alternative was a +13 raise for logic that had no business in the god-file."
|
|
40
|
+
"reason": "Lowered 18,230 -> 18,209 on 2026-09-03 by drain/artifact-location. `doctor --strict` needed 16 lines of integration wiring in cmd_doctor.ts, which sits ~2,100 lines PAST the 1500 cap so every line there costs one excess. The branch first came in at +137 and paid its own way down rather than raising the baseline: the strict semantics, threshold, kill switch, payload helper and argv fragments moved to _cli/_doctor_strict.ts (~160 lines, under the cap, costs nothing), taking it to +15; then the 37-line drift renderer moved to _cli/_doctor_emit.ts. Net -21. The renderer was chosen deliberately: pure rendering, two dependencies, no argparse involvement and therefore none of the byte-parity risk the parser surface carries against the retired Python implementation; nothing outside cmd_doctor.ts imported it. An AI council on the residual split (a: raise by 15 / d: descope the feature); neither was taken, because a parallel branch demonstrated the same day that paying down is achievable in this codebase, which is the evidence the split lacked. This gate's own test comment prescribes it: a commit that splits a god-file lowers the excess and must carry the lowered baseline. MERGE RECONCILIATION 2026-09-03, 18,209 -> 18,205: drain/wired-instruments and drain/artifact-location each lowered from the SAME 18,230 base in parallel -- the branch to 18,226 (net -4, run_continuation_hook.ts's termination ladder moved to _lib/continuation_ladder.ts and runtime_journal.ts's prose to _lib/outcome_vocabularies.ts, both under the cap) and main to 18,209 (net -21, the doctor --strict split above). Taking either side alone is wrong in a different direction: main's value silently discards the branch's earned -4, and the branch's value RAISES the baseline against main and weakens the gate. The committed number is neither -- it is the gate's own output on the merged tree, where both reductions apply. THIS LOCAL READING IS ADMISSIBLE HERE, and the discriminator matters because this file's ci-parity:local-only entry records a 165 -> 164 lowering that was reverted for exactly the opposite case: that count depends on how the environment resolves the projection surface, whereas this gate walks only SOURCE_ROOTS = ['src'] (check_source_size_budget.ts:88) counting excess lines over the 1500 ceiling, and `git status --porcelain -uall -- src/` reported ZERO untracked files, so a clean CI checkout of this commit measures the same 18,205. If CI reads anything else, CI's number is the correct one and this note is the record of why. LOWERED 18,205 -> 18,193 on 2026-09-03 by fix/release-curation-ask. The change adds a fifth publication guard, and its own additions landed at +10 excess: release.ts 2,008 -> 2,018, every line of which is past the 1500 cap. Paid down rather than baselined, in the two ways this gate's doctrine prescribes. The guard's rationale (~13 lines of docstring, including why prMerged is a parameter rather than a call-site if) lives in release_publication.ts, 915 lines and therefore UNDER the cap, so the prose costs nothing; and the two pure text helpers _splitlines and _rstrip moved out of release.ts into release_env.ts (333 lines, also under the cap), taking release.ts to 1,996 -- 12 BELOW where it started. The branch came in at +10 and leaves at -12. Recorded while moving them: _splitlines is duplicated a third and fourth time at adoption_report.ts:106 and readme_linter.ts:418; collapsing all three onto one definition touches three modules and was left out of this change rather than done silently. LOWERED 18,193 -> 18,180 on 2026-09-03 by fix/release-branch-reuse. The step-1 branch decision -- twenty lines of if/else in release.ts -- became one call to checkout_release_branch in release_publication.ts, which is 1,019 lines and therefore UNDER the 1500 cap, so the twenty lines plus a ~35-line rationale now cost nothing. release.ts 1,996 -> 1,983, every one of those lines being past the cap. The drill grew 765 -> 918 for two new scenarios and the snapshot/restore that keeps them from writing the real CHANGELOG, and it is under the cap too. A change that adds a feature and lowers the excess is the shape this gate's doctrine asks for; the alternative was a +13 raise for logic that had no business in the god-file. LOWERED 18,180 -> 18,068 on 2026-09-05 by fix/release-gate-producer-parity. The change wires the governance-mix obligation into the writer and the two local guards and adds a release-gate locality registry; its own additions to release.ts landed at +19 excess, every one of those lines past the 1500 cap. Paid down rather than baselined, in the way this entry's doctrine prescribes: the 132-line test-count-trend unit (_TEST_COUNT_LINE_RE, _TEST_LIST_MAX_BUFFER, _count_from_list_result, _count_tests_current, _previous_test_count_from_changelog, _render_test_trend_line) moved from release.ts into release_publication.ts, which is under the cap and where it now belongs on merit rather than only on arithmetic -- the same diff makes the missing `Tests: N` footer a section-level publication blocker, so the writer that emits the footer and the guard that refuses its absence sit in one module. release.ts 1,983 -> 1,871. All four names are re-exported from release.ts, so tests/scripts/release.test.ts is unchanged. LOCAL READING, ADMISSIBLE ON THIS ENTRY'S OWN DISCRIMINATOR: the gate walks only SOURCE_ROOTS=['src'] counting excess over the ceiling, and `git status --porcelain -uall -- src/` reports ZERO untracked files with the two new src/ files staged, so a clean CI checkout of this commit measures the same 18,068. If CI reads anything else, CI's number is the correct one and this note is the record of why."
|
|
41
41
|
},
|
|
42
42
|
"ci-parity:local-only": {
|
|
43
43
|
"count": 165,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Release-gate locality registry
|
|
2
|
+
#
|
|
3
|
+
# WHY THIS FILE EXISTS
|
|
4
|
+
#
|
|
5
|
+
# `release-validation.yml` is the last gate a release passes and the first one
|
|
6
|
+
# that costs something to fail: by the time it runs, a branch, a pull request
|
|
7
|
+
# and a CI run already exist. Until 2026-09-05 nothing related its jobs to the
|
|
8
|
+
# commands a maintainer can run BEFORE any of that leaves the machine, and the
|
|
9
|
+
# result was a recurring failure shape rather than a one-off — 14.14.0 (PR
|
|
10
|
+
# #1812) on the curated head, 14.17.0 (PR #1856) on the governance-mix
|
|
11
|
+
# response. Both were discoverable locally in under two seconds. Neither was
|
|
12
|
+
# discovered locally, because nothing asked.
|
|
13
|
+
#
|
|
14
|
+
# The registry is the relation the tree was missing. It is read by two
|
|
15
|
+
# consumers, which is what keeps it from rotting into a comment:
|
|
16
|
+
#
|
|
17
|
+
# 1. `tests/scripts/release_gate_locality.test.ts` — every job in the
|
|
18
|
+
# workflow has a row, every row names a live job, every declared script
|
|
19
|
+
# exists, and every `local: ~` carries a reason with an approved prefix.
|
|
20
|
+
# A new release-validation job therefore cannot land without answering
|
|
21
|
+
# "and how does a maintainer run this before pushing?".
|
|
22
|
+
# 2. `task release:verify` — runs every row marked `verify: true` against the
|
|
23
|
+
# working tree. That is the command the release pipeline points at, and
|
|
24
|
+
# the one to run before `task release`.
|
|
25
|
+
#
|
|
26
|
+
# WHAT A ROW MEANS
|
|
27
|
+
#
|
|
28
|
+
# local: the shell command that reproduces the job's assertion locally.
|
|
29
|
+
# `~` (null) means it genuinely cannot run before the PR exists.
|
|
30
|
+
# reason: REQUIRED when `local` is null. Must start with one of
|
|
31
|
+
# NEEDS_PR: / NEEDS_ARTIFACT: / NEEDS_NETWORK: / NEEDS_TOKEN:
|
|
32
|
+
# so the cause is classified rather than merely narrated.
|
|
33
|
+
# verify: whether `task release:verify` runs it. Cheap and deterministic
|
|
34
|
+
# rows are true; anything that packs or installs is false and stays
|
|
35
|
+
# opt-in.
|
|
36
|
+
# network: marks a row that leaves the machine. Excluded from `--cheap`,
|
|
37
|
+
# which is the set the release pipeline runs on its own, so a
|
|
38
|
+
# registry outage cannot read as a release defect.
|
|
39
|
+
# release_branch_only:
|
|
40
|
+
# the assertion only means anything on a `release/X.Y.Z` head.
|
|
41
|
+
# Skipped with a printed reason elsewhere — never silently, and
|
|
42
|
+
# never reported as a pass.
|
|
43
|
+
# script: the implementing script, when the assertion lives in one. `~`
|
|
44
|
+
# marks an assertion that exists only as inline workflow YAML —
|
|
45
|
+
# which is itself the defect this column makes countable.
|
|
46
|
+
# guard: the local guard site that ALSO refuses this, when one exists.
|
|
47
|
+
# A row with a guard fails before the push, not merely before CI.
|
|
48
|
+
# partial: REQUIRED when the `local` command reproduces only part of what the
|
|
49
|
+
# job asserts. Names the uncovered part. A row silently claiming
|
|
50
|
+
# full coverage it does not have is the defect this file exists to
|
|
51
|
+
# end, so the gap is written down rather than inferred.
|
|
52
|
+
|
|
53
|
+
version: 1
|
|
54
|
+
workflow: .github/workflows/release-validation.yml
|
|
55
|
+
|
|
56
|
+
jobs:
|
|
57
|
+
release-shape:
|
|
58
|
+
local: 'git diff --name-only origin/main...HEAD | ./scripts-run src/scripts/check_release_pr_shape --files -'
|
|
59
|
+
verify: true
|
|
60
|
+
# The assertion is about a RELEASE PR's diff — every ordinary branch fails
|
|
61
|
+
# it by construction, so running it off a release branch would teach a
|
|
62
|
+
# maintainer to ignore this command's red. Skipped with a printed reason
|
|
63
|
+
# instead, which is the difference between a gate that is not applicable
|
|
64
|
+
# and one that passed.
|
|
65
|
+
release_branch_only: true
|
|
66
|
+
script: src/scripts/check_release_pr_shape.ts
|
|
67
|
+
guard: ~
|
|
68
|
+
|
|
69
|
+
changelog-entry:
|
|
70
|
+
# The job asserts three things: a heading for the head version, a non-empty
|
|
71
|
+
# body, and a `Tests: N (+M since PREV)` footer.
|
|
72
|
+
#
|
|
73
|
+
# `check_release_highlights` covers the first two — it exits 2 with
|
|
74
|
+
# "CHANGELOG carries no section for <v>". It does NOT read the footer, and
|
|
75
|
+
# saying otherwise here would be the exact defect this registry exists to
|
|
76
|
+
# end: a row asserting a coverage it does not have. The footer half is
|
|
77
|
+
# covered locally by the guard named below, not by the `local` command, so
|
|
78
|
+
# `partial` states that rather than leaving a reader to grep for it.
|
|
79
|
+
local: './scripts-run src/scripts/check_release_highlights --version ${VERSION}'
|
|
80
|
+
verify: true
|
|
81
|
+
partial: 'heading + non-empty body only; the `Tests: N` footer half is covered by the guard, not by this command'
|
|
82
|
+
script: ~
|
|
83
|
+
guard: guard_release_curation
|
|
84
|
+
|
|
85
|
+
surface-equality:
|
|
86
|
+
local: ~
|
|
87
|
+
reason: 'NEEDS_PR: compares the live PR body against the CHANGELOG section; there is no PR body before `gh pr create`. The body is DERIVED from that same section (`pr_body_from_section`), so the local half is the section check above.'
|
|
88
|
+
verify: false
|
|
89
|
+
script: src/scripts/check_release_surface_equality.ts
|
|
90
|
+
guard: ~
|
|
91
|
+
|
|
92
|
+
highlight-plausibility:
|
|
93
|
+
# The job that failed 14.17.0. All four sub-checks (authoring instruction,
|
|
94
|
+
# unrewritten derived marker, `_none_` contradiction, governance-mix
|
|
95
|
+
# response) run locally from local tags alone — no origin, no HEAD_REF.
|
|
96
|
+
local: './scripts-run src/scripts/check_release_highlights --version ${VERSION}'
|
|
97
|
+
verify: true
|
|
98
|
+
script: src/scripts/check_release_highlights.ts
|
|
99
|
+
guard: guard_release_branch_push
|
|
100
|
+
|
|
101
|
+
version-consistency:
|
|
102
|
+
local: './scripts-run src/scripts/check_template_pin_drift'
|
|
103
|
+
verify: true
|
|
104
|
+
script: src/scripts/check_template_pin_drift.ts
|
|
105
|
+
guard: ~
|
|
106
|
+
|
|
107
|
+
finding-dispositions:
|
|
108
|
+
local: './scripts-run src/scripts/check_finding_dispositions --release ${VERSION}'
|
|
109
|
+
verify: false
|
|
110
|
+
reason: 'NEEDS_PR: the unrecorded-findings half reads live PR review comments. The missing-dispositions half does run locally and is what the `local` command covers.'
|
|
111
|
+
script: src/scripts/check_finding_dispositions.ts
|
|
112
|
+
guard: ~
|
|
113
|
+
|
|
114
|
+
audit-gate:
|
|
115
|
+
local: 'npm audit --omit=dev --audit-level=high'
|
|
116
|
+
verify: true
|
|
117
|
+
# Reaches the registry, so it is excluded from the `--cheap` set the
|
|
118
|
+
# release pipeline runs unattended: a registry outage must not read as a
|
|
119
|
+
# release defect. `task release:verify` (no flag) still runs it.
|
|
120
|
+
network: true
|
|
121
|
+
script: ~
|
|
122
|
+
guard: ~
|
|
123
|
+
|
|
124
|
+
release-install-e2e:
|
|
125
|
+
local: ~
|
|
126
|
+
reason: 'NEEDS_ARTIFACT: needs a packed tarball plus a BASELINE_TARBALL of the upgrade-from version; runnable by hand as `bash tests/test_release_install_e2e.sh`, too slow for a pre-push gate.'
|
|
127
|
+
verify: false
|
|
128
|
+
script: ~
|
|
129
|
+
guard: ~
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { spawnSync } from 'node:child_process';
|
|
27
27
|
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
CURATED_HEAD_INSTRUCTION,
|
|
30
|
+
MIX_RESPONSE_MARKER,
|
|
31
|
+
MIX_RESPONSE_PLACEHOLDERS,
|
|
32
|
+
} from './release_material.js';
|
|
29
33
|
|
|
30
34
|
/** The five curated labels, in the order an operator reads them. */
|
|
31
35
|
export const HEAD_LABELS: readonly string[] = [
|
|
@@ -398,6 +402,7 @@ export function publication_blockers(
|
|
|
398
402
|
sectionBody: string,
|
|
399
403
|
version: string,
|
|
400
404
|
where = '`main`',
|
|
405
|
+
mix: MixObligation | null = null,
|
|
401
406
|
): string[] {
|
|
402
407
|
const out: string[] = [];
|
|
403
408
|
if (sectionBody.includes(DERIVED_MARKER)) {
|
|
@@ -414,9 +419,175 @@ export function publication_blockers(
|
|
|
414
419
|
'comment line from the section and re-run.',
|
|
415
420
|
);
|
|
416
421
|
}
|
|
422
|
+
out.push(...mix_response_blockers(sectionBody, version, where, mix));
|
|
417
423
|
return out;
|
|
418
424
|
}
|
|
419
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Everything that makes a whole SECTION unpublishable — the head-level
|
|
428
|
+
* blockers above plus the two obligations that only exist for a full section.
|
|
429
|
+
*
|
|
430
|
+
* Two levels and not one, because `publication_blockers` is legitimately called
|
|
431
|
+
* with a bare curated head (the prefill tests do exactly that), and a head
|
|
432
|
+
* fragment has no tests footer by construction. Folding a section obligation
|
|
433
|
+
* into the head predicate would make those callers red on a rule that does not
|
|
434
|
+
* apply to what they passed.
|
|
435
|
+
*
|
|
436
|
+
* The TWO cheapest guard sites call this one — `guard_release_curation` (before
|
|
437
|
+
* any commit) and `guard_release_branch_push` (before any remote state). The
|
|
438
|
+
* two publication sites reached through `_refuse_unpublishable` (the annotated
|
|
439
|
+
* tag and the GitHub Release) stay on the head-level predicate: by then the
|
|
440
|
+
* section has already shipped in a merged PR, and adding a refusal there would
|
|
441
|
+
* block a resumed publication over an obligation the release cannot still act
|
|
442
|
+
* on. Stated rather than implied, because "the guard sites call this one" read
|
|
443
|
+
* as all four and is not true of any of them but the two named here.
|
|
444
|
+
*/
|
|
445
|
+
export function section_publication_blockers(
|
|
446
|
+
sectionBody: string,
|
|
447
|
+
version: string,
|
|
448
|
+
where = '`main`',
|
|
449
|
+
mix: MixObligation | null = null,
|
|
450
|
+
): string[] {
|
|
451
|
+
const out = publication_blockers(sectionBody, version, where, mix);
|
|
452
|
+
if (!TEST_FOOTER_RE.test(sectionBody)) {
|
|
453
|
+
out.push(
|
|
454
|
+
`the ${version} section carries no \`Tests: N (+M since PREV)\` footer. ` +
|
|
455
|
+
'`_render_test_trend_line` degrades to `null` when the vitest collection ' +
|
|
456
|
+
'fails, so a missing line means the collection failed during `task release`. ' +
|
|
457
|
+
'Fix the collection, then render the number and put it on the section — ' +
|
|
458
|
+
`the release commit already exists on ${where}, so amend it there. Do not ` +
|
|
459
|
+
'invent the count: `npx vitest list | wc -l` is the same probe the writer runs.',
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* The tests-footer convention, in force for every release since 9.1.0.
|
|
467
|
+
*
|
|
468
|
+
* Duplicated from `release-validation.yml`'s inline check until 2026-09-05,
|
|
469
|
+
* where it was the ONLY copy — so the absence was red at release-PR time and
|
|
470
|
+
* invisible before it. 9.8.0 shipped without the line. The regex is the same
|
|
471
|
+
* one the workflow uses; the workflow's own comment already recorded that the
|
|
472
|
+
* writer "degrades SILENTLY to null on collection failure", which is exactly a
|
|
473
|
+
* defect a local guard should see first.
|
|
474
|
+
*/
|
|
475
|
+
const TEST_FOOTER_RE = /^Tests: \d+/mu;
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The measured governance-versus-product level, as the writer and the guards
|
|
479
|
+
* both receive it.
|
|
480
|
+
*
|
|
481
|
+
* A plain record and not the measurement itself: this module is reached by the
|
|
482
|
+
* push guard, the pre-commit ask and the CI gate, and only the callers have git
|
|
483
|
+
* to measure with. Passing the reading in keeps the predicate pure and keeps
|
|
484
|
+
* `measure_release_mix`'s git access out of a module three guards import.
|
|
485
|
+
*/
|
|
486
|
+
export interface MixObligation {
|
|
487
|
+
triggered: boolean;
|
|
488
|
+
level: string;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Minimum written characters after the marker — a bare marker is not an answer. */
|
|
492
|
+
export const MIX_RESPONSE_MIN_CHARS = 40;
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The ONE predicate for the governance-versus-product obligation.
|
|
496
|
+
*
|
|
497
|
+
* `check_release_highlights` refuses this from the CI side and
|
|
498
|
+
* `guard_release_branch_push` from the local side, and until 2026-09-05 only
|
|
499
|
+
* the first of the two knew the obligation existed. That asymmetry is what the
|
|
500
|
+
* contract itself recorded — *"the earliest refusal for this one obligation is
|
|
501
|
+
* the PR, not the push"* (`docs/contracts/CHANGELOG-conventions.md`) — and what
|
|
502
|
+
* cost 14.17.0 a red release PR. Both sides now read this function, so a change
|
|
503
|
+
* to the obligation cannot reach one guard and miss the other.
|
|
504
|
+
*
|
|
505
|
+
* `null` means the measurement did not run (shallow clone, missing tag). That
|
|
506
|
+
* degrades to no blocker, matching the gate's documented stance: this is a
|
|
507
|
+
* governance signal, not a correctness control, and an environment fact must
|
|
508
|
+
* not block a release.
|
|
509
|
+
*/
|
|
510
|
+
export function mix_response_blockers(
|
|
511
|
+
sectionBody: string,
|
|
512
|
+
version: string,
|
|
513
|
+
where = '`main`',
|
|
514
|
+
mix: MixObligation | null = null,
|
|
515
|
+
): string[] {
|
|
516
|
+
if (!mix || !mix.triggered) {
|
|
517
|
+
return [];
|
|
518
|
+
}
|
|
519
|
+
const idx = sectionBody.indexOf(MIX_RESPONSE_MARKER);
|
|
520
|
+
if (idx === -1) {
|
|
521
|
+
return [
|
|
522
|
+
`the ${version} section owes a governance-versus-product response ` +
|
|
523
|
+
`(${mix.level}) and carries none. Add one \`> ${MIX_RESPONSE_MARKER}\` line ` +
|
|
524
|
+
`immediately under the curated head on ${where}, naming the next cycle's ` +
|
|
525
|
+
'consumer work or a maintainer justification.',
|
|
526
|
+
];
|
|
527
|
+
}
|
|
528
|
+
const block = mix_response_block(sectionBody, idx);
|
|
529
|
+
const left = MIX_RESPONSE_PLACEHOLDERS.filter((t) => block.includes(t));
|
|
530
|
+
if (left.length > 0) {
|
|
531
|
+
return [
|
|
532
|
+
`the ${version} governance-versus-product response still carries the writer's ` +
|
|
533
|
+
`placeholder(s) (${left.map((t) => `\`${t}\``).join(', ')}) — the measured level ` +
|
|
534
|
+
'is the generator\'s, the answer is not, and `CHANGELOG.md` is published to npm. ' +
|
|
535
|
+
`Name the next cycle's consumer work (or the justification) on ${where} and re-run.`,
|
|
536
|
+
];
|
|
537
|
+
}
|
|
538
|
+
if (human_answer(block, mix.level).length < MIX_RESPONSE_MIN_CHARS) {
|
|
539
|
+
return [
|
|
540
|
+
`the ${version} governance-versus-product response carries no written answer ` +
|
|
541
|
+
`beyond the measured level (${mix.level}). The level is the generator's; the ` +
|
|
542
|
+
`response has to name the next cycle's consumer work or a maintainer ` +
|
|
543
|
+
`justification. Write it on ${where}, in the \`> \` block under the marker.`,
|
|
544
|
+
];
|
|
545
|
+
}
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* The whole response block: the marker's line plus every blockquote line under it.
|
|
551
|
+
*
|
|
552
|
+
* Reading only the FIRST line after the marker was wrong in both directions, and
|
|
553
|
+
* both were reproduced before this was written. It **accepted** a section whose
|
|
554
|
+
* placeholder line had simply been deleted, because the machine-written level
|
|
555
|
+
* alone is 54 characters and cleared the floor — the smuggled auto-approval the
|
|
556
|
+
* placeholder exists to prevent. And it **refused** the writer's own two-line
|
|
557
|
+
* template, where the level sits on line 1 and the answer on line 2.
|
|
558
|
+
*
|
|
559
|
+
* Scoping to the block also fixes the placeholder scan, which searched the
|
|
560
|
+
* entire section body: a legitimate changelog line containing
|
|
561
|
+
* `<roadmap or issue>` blocked the release.
|
|
562
|
+
*/
|
|
563
|
+
export function mix_response_block(sectionBody: string, markerIdx: number): string {
|
|
564
|
+
const lines = sectionBody.slice(markerIdx).split('\n');
|
|
565
|
+
const out: string[] = [lines[0] ?? ''];
|
|
566
|
+
for (const line of lines.slice(1)) {
|
|
567
|
+
if (!line.trimStart().startsWith('>')) break;
|
|
568
|
+
out.push(line);
|
|
569
|
+
}
|
|
570
|
+
return out.join('\n');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* What is left of the response block once the generator's own contribution is
|
|
575
|
+
* removed — i.e. the part a human had to write.
|
|
576
|
+
*
|
|
577
|
+
* The measured level is stripped because the writer emitted it; a floor that
|
|
578
|
+
* counted it would be satisfied by the tool on the author's behalf, which is
|
|
579
|
+
* the whole failure this obligation guards against.
|
|
580
|
+
*/
|
|
581
|
+
export function human_answer(block: string, level: string): string {
|
|
582
|
+
return block
|
|
583
|
+
.replace(MIX_RESPONSE_MARKER, '')
|
|
584
|
+
.split(level)
|
|
585
|
+
.join('')
|
|
586
|
+
.replace(/^[>\s.]+|[>\s.]+$/gu, '')
|
|
587
|
+
.replace(/[>\s]+/gu, ' ')
|
|
588
|
+
.trim();
|
|
589
|
+
}
|
|
590
|
+
|
|
420
591
|
/** Labels whose curated value is still the generator's unedited draft. */
|
|
421
592
|
export function stale_draft_labels(
|
|
422
593
|
curated: Readonly<Record<string, string>>,
|
|
@@ -288,3 +288,69 @@ function _first_diff(a: string, b: string): string {
|
|
|
288
288
|
}
|
|
289
289
|
return 'surfaces differ only in normalization-invisible content';
|
|
290
290
|
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The marker the governance-versus-product response line carries.
|
|
294
|
+
*
|
|
295
|
+
* Relocated here on 2026-09-05 from `check_release_highlights.ts`, where it was
|
|
296
|
+
* defined next to the refusal that reads it. That placement is why 14.17.0
|
|
297
|
+
* failed: the gate owned the marker, so the **generator** could not write the
|
|
298
|
+
* line without importing a gate, and therefore did not write it at all. The
|
|
299
|
+
* obligation was then discoverable only after `gh pr create` had already spent
|
|
300
|
+
* a branch, a PR and a CI run — the exact cost `guard_release_branch_push` was
|
|
301
|
+
* added to avoid for the sibling obligation two releases earlier.
|
|
302
|
+
*
|
|
303
|
+
* This module has no imports on purpose (see `CURATED_HEAD_INSTRUCTION`), which
|
|
304
|
+
* is what lets the writer, the local push guard and the CI gate all reach one
|
|
305
|
+
* definition instead of three copies.
|
|
306
|
+
*/
|
|
307
|
+
export const MIX_RESPONSE_MARKER = '**Governance mix:**';
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The sentinel that marks an EMITTED-BUT-UNANSWERED response.
|
|
311
|
+
*
|
|
312
|
+
* A generator that emits the measured level alone would satisfy the gate's
|
|
313
|
+
* length floor (`MIX_RESPONSE_MIN_CHARS`, 40) with a number the machine
|
|
314
|
+
* produced, turning a written-answer obligation into a formality the tool
|
|
315
|
+
* discharges for itself. That is a smuggled auto-approval, not a fix.
|
|
316
|
+
*
|
|
317
|
+
* So the writer emits the level AND this sentinel, and every gate refuses the
|
|
318
|
+
* sentinel — the same discipline `CURATED_HEAD_INSTRUCTION` and
|
|
319
|
+
* `DERIVED_MARKER` already follow. The net effect is only that the obligation
|
|
320
|
+
* moves from CI to the moment the section is written, where the fix is an edit
|
|
321
|
+
* to a file already open in the working tree.
|
|
322
|
+
*/
|
|
323
|
+
export const MIX_RESPONSE_PLACEHOLDER = '<the consumer work>';
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Every placeholder token the writer emits, so none of them can ship.
|
|
327
|
+
*
|
|
328
|
+
* The block carries two, and refusing only the first would leave
|
|
329
|
+
* `<roadmap or issue>` publishable — `CHANGELOG.md` is in `package.json`
|
|
330
|
+
* `files`, so that is the same "the generator's own scaffolding reached npm"
|
|
331
|
+
* failure `CURATED_HEAD_INSTRUCTION` exists for, one token to the right.
|
|
332
|
+
*
|
|
333
|
+
* A named list rather than a shape match over angle brackets, for the reason
|
|
334
|
+
* that constant already states: a pattern such as `/<[a-z ]+>/` rejects
|
|
335
|
+
* unrelated legitimate prose and misses a reworded placeholder. Adding a token
|
|
336
|
+
* to `render_mix_response` means adding it here.
|
|
337
|
+
*/
|
|
338
|
+
export const MIX_RESPONSE_PLACEHOLDERS: readonly string[] = [
|
|
339
|
+
MIX_RESPONSE_PLACEHOLDER,
|
|
340
|
+
'<roadmap or issue>',
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Render the response block the writer emits when the obligation triggers.
|
|
345
|
+
*
|
|
346
|
+
* Two blockquote lines, immediately under the curated head and outside it, per
|
|
347
|
+
* `docs/contracts/CHANGELOG-conventions.md` § Governance-versus-product
|
|
348
|
+
* response — the head's ten-line cap is for product lines, and a sixth label
|
|
349
|
+
* would make every historical section retroactively incomplete.
|
|
350
|
+
*/
|
|
351
|
+
export function render_mix_response(level: string): string[] {
|
|
352
|
+
return [
|
|
353
|
+
`> ${MIX_RESPONSE_MARKER} ${level}.`,
|
|
354
|
+
`> Next cycle ships ${MIX_RESPONSE_PLACEHOLDER}, tracked in <roadmap or issue>.`,
|
|
355
|
+
];
|
|
356
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repeated-run evidence mode and the `flaky` outcome.
|
|
3
|
+
*
|
|
4
|
+
* `docs/contracts/evidence-artifact-types.md` § The second axis is the
|
|
5
|
+
* contract; this is the executable half, so "a run that passes 4 of 5 times is
|
|
6
|
+
* recorded as flaky rather than as passing" is a thing a test can assert rather
|
|
7
|
+
* than a sentence in a document.
|
|
8
|
+
*
|
|
9
|
+
* Two rules carry the whole design, both from the AI council of 2026-09-04
|
|
10
|
+
* (2/2 convergent):
|
|
11
|
+
*
|
|
12
|
+
* 1. **`n >= 2`.** `repeated:1` is `single-run` wearing extra syntax, and
|
|
13
|
+
* admitting it would let one run be presented as a repeat.
|
|
14
|
+
* 2. **Unanimity, no threshold.** All pass → pass. All fail → fail. Anything
|
|
15
|
+
* else → flaky. A 4-of-5 pass is precisely the case this exists to stop
|
|
16
|
+
* being called a pass, so a majority rule would defeat the purpose.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type ExecutionMode = { kind: 'single-run' } | { kind: 'repeated'; n: number };
|
|
20
|
+
|
|
21
|
+
export type RepeatedOutcome = 'pass' | 'fail' | 'flaky';
|
|
22
|
+
|
|
23
|
+
/** The marker, when an artifact declares one. `single-run` is never written. */
|
|
24
|
+
export const EVIDENCE_MODE_RE = /<!--\s*evidence-mode:\s*(single-run|repeated:(\d+))\s*-->/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read the mode an artifact declares.
|
|
28
|
+
*
|
|
29
|
+
* Absent → `single-run`, which is the default that keeps this axis from being
|
|
30
|
+
* a migration. A malformed `repeated:<n>` with `n < 2` is rejected rather than
|
|
31
|
+
* rounded to `single-run`: a declaration that does not parse must not read to
|
|
32
|
+
* its author as accepted.
|
|
33
|
+
*/
|
|
34
|
+
export function parseExecutionMode(text: string): ExecutionMode | { kind: 'invalid'; reason: string } {
|
|
35
|
+
const m = EVIDENCE_MODE_RE.exec(text);
|
|
36
|
+
if (m === null) return { kind: 'single-run' };
|
|
37
|
+
if (m[1] === 'single-run') return { kind: 'single-run' };
|
|
38
|
+
const n = Number(m[2]);
|
|
39
|
+
if (!Number.isInteger(n) || n < 2) {
|
|
40
|
+
return {
|
|
41
|
+
kind: 'invalid',
|
|
42
|
+
reason: `repeated:${String(n)} declares fewer than 2 runs; repeated:1 is single-run with extra syntax`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { kind: 'repeated', n };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Aggregate `n` retained per-run verdicts.
|
|
50
|
+
*
|
|
51
|
+
* Takes the RUNS, never a summary count — an artifact that kept only
|
|
52
|
+
* "3 of 5 passed" has recorded a number, not evidence, and this signature is
|
|
53
|
+
* where that requirement is enforced rather than requested.
|
|
54
|
+
*/
|
|
55
|
+
export function aggregateRepeatedRuns(runs: readonly boolean[]): RepeatedOutcome {
|
|
56
|
+
if (runs.length < 2) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`aggregateRepeatedRuns needs at least 2 retained runs, got ${String(runs.length)}; ` +
|
|
59
|
+
'a single run is `single-run`, not a repeat',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (runs.every((r) => r)) return 'pass';
|
|
63
|
+
if (runs.every((r) => !r)) return 'fail';
|
|
64
|
+
return 'flaky';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One line for the artifact, so `flaky` is reported as itself. */
|
|
68
|
+
export function describeRepeatedRun(runs: readonly boolean[]): string {
|
|
69
|
+
const outcome = aggregateRepeatedRuns(runs);
|
|
70
|
+
const passed = runs.filter((r) => r).length;
|
|
71
|
+
const line = `evidence-mode: repeated:${String(runs.length)} · outcome: ${outcome} · ${String(passed)}/${String(runs.length)} passed`;
|
|
72
|
+
if (outcome !== 'flaky') return line;
|
|
73
|
+
return (
|
|
74
|
+
`${line} — the runs did not agree, so this is neither a pass nor a fail. ` +
|
|
75
|
+
'Recording it as either would be the silent green or the silent red this outcome exists to prevent.'
|
|
76
|
+
);
|
|
77
|
+
}
|