@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
|
@@ -24,8 +24,49 @@
|
|
|
24
24
|
* self_review_gate --findings-out) into the ledger with an empty
|
|
25
25
|
* disposition, so validation is red until a human fills it.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* ABSENCE IS NOT EVIDENCE OF ZERO. An absent ledger used to exit 0 for any
|
|
28
|
+
* version, so a RELEASED version whose findings were never ingested read as
|
|
29
|
+
* "no findings" — the failure this paragraph exists to stop. When the ledger
|
|
30
|
+
* file is missing the gate now asks whether the version has SHIPPED:
|
|
31
|
+
*
|
|
32
|
+
* released a tag for the version exists locally, or the remote says
|
|
33
|
+
* the release exists -> an absent ledger is a FAILURE (1)
|
|
34
|
+
* unreleased the local tag list is COMPLETE and lacks it, or the remote
|
|
35
|
+
* says so -> normal in-flight state, absent ledger is fine
|
|
36
|
+
* undeterminable the tag list cannot settle it and no remote answered ->
|
|
37
|
+
* exit 2. Refusing beats passing: a silent green here is the
|
|
38
|
+
* very inversion this split exists to remove
|
|
39
|
+
*
|
|
40
|
+
* THE SOURCE IS THE TAG / RELEASE, AND DELIBERATELY NOT `CHANGELOG.md`. The
|
|
41
|
+
* changelog heading is written by the release PR itself — gh pr diff 1836 shows
|
|
42
|
+
* "+## [14.16.0](...)" inside the release commit — so on branch release/X the
|
|
43
|
+
* changelog claims X is released before X exists. Reading it would redden every
|
|
44
|
+
* in-flight release branch over a file nobody could yet have written, which is
|
|
45
|
+
* the exact behaviour this split is here to preserve. The tag is created at
|
|
46
|
+
* publish, after merge, so it cannot make that mistake.
|
|
47
|
+
*
|
|
48
|
+
* A local tag MISS is not authoritative on its own: actions/checkout fetches
|
|
49
|
+
* depth 1 and no tags by default, and the one workflow that runs this gate does
|
|
50
|
+
* not override either, so in CI the local tag list is EMPTY. See releaseStatus
|
|
51
|
+
* for the asymmetry and the remote fallback.
|
|
52
|
+
*
|
|
53
|
+
* AN EMPTY LEDGER MUST SAY WHY IT IS EMPTY. The released/unreleased split above
|
|
54
|
+
* closed the missing-file half; it left the other half open, because
|
|
55
|
+
* `findings: []` still passed while asserting nothing. A writer could satisfy
|
|
56
|
+
* the gate by creating an empty file, which is the same silence wearing the
|
|
57
|
+
* shape of a record. `no_findings_reason` is now required whenever the file
|
|
58
|
+
* exists and the array is empty — see empty_ledger_problem for what the reason
|
|
59
|
+
* has to distinguish.
|
|
60
|
+
*
|
|
61
|
+
* A future reader changing allowEmpty below is the person this is addressed to:
|
|
62
|
+
* the EMPTY_VALID waiver now covers only an EXPLAINED empty ledger and an
|
|
63
|
+
* unreleased version. It covers neither a released version with no ledger nor an
|
|
64
|
+
* empty ledger with no reason.
|
|
65
|
+
*
|
|
66
|
+
* Exit codes: 0 complete · 1 missing/incomplete dispositions (an absent ledger
|
|
67
|
+
* for a RELEASED version and an unexplained empty ledger are both among them) ·
|
|
68
|
+
* 2 usage error, schema error, or an
|
|
69
|
+
* undeterminable release status. Deliberately NOT fail-open: this script never calls a model;
|
|
29
70
|
* a broken ledger file is a hard error, not a neutral skip.
|
|
30
71
|
*/
|
|
31
72
|
import { spawnSync } from 'node:child_process';
|
|
@@ -34,7 +75,7 @@ import * as path from 'node:path';
|
|
|
34
75
|
import process from 'node:process';
|
|
35
76
|
import { fileURLToPath } from 'node:url';
|
|
36
77
|
|
|
37
|
-
import {
|
|
78
|
+
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
38
79
|
|
|
39
80
|
const _HERE = fileURLToPath(import.meta.url);
|
|
40
81
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -60,6 +101,8 @@ export interface Ledger {
|
|
|
60
101
|
schema_version: number;
|
|
61
102
|
release: string;
|
|
62
103
|
findings: LedgerFinding[];
|
|
104
|
+
/** Why the finding set is empty. Required when it is; see empty_ledger_problem. */
|
|
105
|
+
no_findings_reason?: string;
|
|
63
106
|
}
|
|
64
107
|
|
|
65
108
|
/** Mirrors self_review_gate.classifyBlocking — security/claim × critical/high. */
|
|
@@ -105,6 +148,154 @@ export function missing_dispositions(findings: readonly LedgerFinding[]): string
|
|
|
105
148
|
return problems;
|
|
106
149
|
}
|
|
107
150
|
|
|
151
|
+
/**
|
|
152
|
+
* An empty finding set must say why it is empty.
|
|
153
|
+
*
|
|
154
|
+
* `findings: []` and a missing file made the same claim to every reader — "nothing
|
|
155
|
+
* to disposition" — and only one of them is evidence. Worse, they collapse two
|
|
156
|
+
* states that differ entirely: a review that ran and reported nothing, and a review
|
|
157
|
+
* that never ran at all. The reason field is what forces the writer to pick one and
|
|
158
|
+
* makes the pick falsifiable, so a fabricated empty ledger has to assert something
|
|
159
|
+
* a reader can check rather than merely occupy the path the gate looks at.
|
|
160
|
+
*
|
|
161
|
+
* Applies ONLY to a ledger file that exists. An absent ledger on an unreleased
|
|
162
|
+
* version is the normal in-flight state and has nothing to carry a reason.
|
|
163
|
+
*/
|
|
164
|
+
export function empty_ledger_problem(
|
|
165
|
+
ledger: Pick<Ledger, 'findings' | 'no_findings_reason'>,
|
|
166
|
+
): string | null {
|
|
167
|
+
if (ledger.findings.length > 0) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if ((ledger.no_findings_reason ?? '').trim() !== '') {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return (
|
|
174
|
+
'the ledger exists with an empty `findings` array and no `no_findings_reason`. '
|
|
175
|
+
+ 'An unexplained empty ledger is indistinguishable from an absent one — state '
|
|
176
|
+
+ 'whether the review ran and reported nothing, or did not run, and how a reader '
|
|
177
|
+
+ 'can check that.'
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type ReleaseStatus = 'released' | 'unreleased' | 'undeterminable';
|
|
182
|
+
export type RemoteReleaseAnswer = 'released' | 'unreleased' | 'unavailable';
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Pure core of the released-vs-unreleased discriminator. Every signal is
|
|
186
|
+
* INJECTED, so both directions AND the degradation are testable offline. A
|
|
187
|
+
* red-only test cannot catch the day this starts answering 'unreleased' for
|
|
188
|
+
* everything, which silently restores the fail-open it exists to replace.
|
|
189
|
+
*
|
|
190
|
+
* The asymmetry is the design, and it is the council correction to a first
|
|
191
|
+
* implementation that read local tags only:
|
|
192
|
+
*
|
|
193
|
+
* a local tag HIT is authoritative — the tag exists, the version shipped
|
|
194
|
+
* a local tag MISS is NOT authoritative — unless the tag list is COMPLETE
|
|
195
|
+
*
|
|
196
|
+
* A miss means "not in THIS checkout", which is not "never shipped".
|
|
197
|
+
* tagsComplete is the condition under which a miss IS authoritative: the
|
|
198
|
+
* repository is not shallow and carries at least one tag. That keeps a normal
|
|
199
|
+
* full clone offline and deterministic; only an incomplete checkout pays for a
|
|
200
|
+
* remote lookup.
|
|
201
|
+
*/
|
|
202
|
+
export function releaseStatus(signals: {
|
|
203
|
+
localTagHit: boolean;
|
|
204
|
+
tagsComplete: boolean;
|
|
205
|
+
remote: RemoteReleaseAnswer;
|
|
206
|
+
}): ReleaseStatus {
|
|
207
|
+
if (signals.localTagHit) {
|
|
208
|
+
return 'released';
|
|
209
|
+
}
|
|
210
|
+
if (signals.tagsComplete) {
|
|
211
|
+
return 'unreleased';
|
|
212
|
+
}
|
|
213
|
+
if (signals.remote === 'released') {
|
|
214
|
+
return 'released';
|
|
215
|
+
}
|
|
216
|
+
if (signals.remote === 'unreleased') {
|
|
217
|
+
return 'unreleased';
|
|
218
|
+
}
|
|
219
|
+
return 'undeterminable';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Tag equality, tolerating this repo bare tags and a v prefix. */
|
|
223
|
+
export function localTagHit(release: string, tags: readonly string[]): boolean {
|
|
224
|
+
const v = release.trim();
|
|
225
|
+
if (!v) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return tags.some((t) => {
|
|
229
|
+
const tt = t.trim();
|
|
230
|
+
return tt === v || tt === `v${v}`;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function _git(args: readonly string[]): { ok: boolean; out: string } {
|
|
235
|
+
const r = spawnSync('git', args as string[], {
|
|
236
|
+
encoding: 'utf-8',
|
|
237
|
+
cwd: REPO_ROOT,
|
|
238
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
239
|
+
});
|
|
240
|
+
return { ok: r.status === 0, out: (r.stdout ?? '').trim() };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** This repository tags. A git failure is an empty list, i.e. not complete. */
|
|
244
|
+
export function git_tags(): string[] {
|
|
245
|
+
const r = _git(['tag', '--list']);
|
|
246
|
+
return r.ok ? r.out.split('\n').map((l) => l.trim()).filter(Boolean) : [];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** True when a local tag MISS may be trusted: full clone, and tags present. */
|
|
250
|
+
export function tags_complete(tags: readonly string[]): boolean {
|
|
251
|
+
if (tags.length === 0) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
const r = _git(['rev-parse', '--is-shallow-repository']);
|
|
255
|
+
return r.ok && r.out === 'false';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Remote answer, used ONLY when the local tag list cannot settle it. gh first —
|
|
260
|
+
* this repo publishes tag and GitHub Release together, the Release object is
|
|
261
|
+
* publication-specific rather than a bare ref push, and --pr mode already shells
|
|
262
|
+
* out to gh. git ls-remote is the fallback when gh is absent.
|
|
263
|
+
*/
|
|
264
|
+
export function remote_release_status(release: string): RemoteReleaseAnswer {
|
|
265
|
+
const gh = spawnSync('gh', ['release', 'view', release, '--json', 'tagName'], {
|
|
266
|
+
encoding: 'utf-8',
|
|
267
|
+
cwd: REPO_ROOT,
|
|
268
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
269
|
+
});
|
|
270
|
+
if (gh.status === 0) {
|
|
271
|
+
return 'released';
|
|
272
|
+
}
|
|
273
|
+
if (gh.status !== null && /release not found|could not find|not found/i.test(gh.stderr ?? '')) {
|
|
274
|
+
return 'unreleased';
|
|
275
|
+
}
|
|
276
|
+
const ls = spawnSync(
|
|
277
|
+
'git',
|
|
278
|
+
['ls-remote', '--tags', 'origin', `refs/tags/${release}`, `refs/tags/v${release}`],
|
|
279
|
+
{ encoding: 'utf-8', cwd: REPO_ROOT, maxBuffer: 8 * 1024 * 1024 },
|
|
280
|
+
);
|
|
281
|
+
if (ls.status === 0) {
|
|
282
|
+
return (ls.stdout ?? '').trim() ? 'released' : 'unreleased';
|
|
283
|
+
}
|
|
284
|
+
return 'unavailable';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Resolve every signal and decide. Called only when the ledger file is absent. */
|
|
288
|
+
export function resolve_release_status(release: string): ReleaseStatus {
|
|
289
|
+
const tags = git_tags();
|
|
290
|
+
const hit = localTagHit(release, tags);
|
|
291
|
+
const complete = tags_complete(tags);
|
|
292
|
+
return releaseStatus({
|
|
293
|
+
localTagHit: hit,
|
|
294
|
+
tagsComplete: complete,
|
|
295
|
+
remote: hit || complete ? 'unavailable' : remote_release_status(release),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
108
299
|
/** Findings the gate comment reported but the ledger never ingested. */
|
|
109
300
|
export function unrecorded_findings(
|
|
110
301
|
reported: ReadonlyArray<Pick<LedgerFinding, 'finding_id' | 'severity' | 'kind' | 'title'>>,
|
|
@@ -227,6 +418,30 @@ function main(argv: readonly string[]): number {
|
|
|
227
418
|
return 0;
|
|
228
419
|
}
|
|
229
420
|
|
|
421
|
+
if (!fs.existsSync(ledgerPath)) {
|
|
422
|
+
const status = resolve_release_status(release);
|
|
423
|
+
if (status === 'released') {
|
|
424
|
+
process.stderr.write(
|
|
425
|
+
`❌ ${release} has shipped and carries no findings ledger at `
|
|
426
|
+
+ `${path.relative(REPO_ROOT, ledgerPath)}.\n`
|
|
427
|
+
+ ' Absence is not evidence of zero — the release PR\'s self-review may have\n'
|
|
428
|
+
+ ' reported findings that were never ingested. Recover the machine block and:\n'
|
|
429
|
+
+ ` ./scripts-run src/scripts/check_finding_dispositions --ingest <findings.json> --release ${release}\n`,
|
|
430
|
+
);
|
|
431
|
+
return 1;
|
|
432
|
+
}
|
|
433
|
+
if (status === 'undeterminable') {
|
|
434
|
+
process.stderr.write(
|
|
435
|
+
`❌ cannot determine whether ${release} has shipped: this checkout tag list is`
|
|
436
|
+
+ ' incomplete (shallow or tagless) and no remote answered.\n'
|
|
437
|
+
+ ' Refusing to pass on an absent ledger without knowing — that silent green is\n'
|
|
438
|
+
+ ' the defect this check replaces. Fetch tags (actions/checkout fetch-tags: true)\n'
|
|
439
|
+
+ ' or make the gh CLI available.\n',
|
|
440
|
+
);
|
|
441
|
+
return 2;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
230
445
|
let ledger: Ledger = { schema_version: 1, release, findings: [] };
|
|
231
446
|
if (fs.existsSync(ledgerPath)) {
|
|
232
447
|
try {
|
|
@@ -237,6 +452,19 @@ function main(argv: readonly string[]): number {
|
|
|
237
452
|
}
|
|
238
453
|
}
|
|
239
454
|
|
|
455
|
+
// An existing ledger that records nothing must say why. Checked here rather
|
|
456
|
+
// than in the schema because the requirement is conditional on the finding
|
|
457
|
+
// count, which the repo's Draft-07 subset validator cannot express.
|
|
458
|
+
if (fs.existsSync(ledgerPath)) {
|
|
459
|
+
const emptyProblem = empty_ledger_problem(ledger);
|
|
460
|
+
if (emptyProblem !== null) {
|
|
461
|
+
process.stderr.write(
|
|
462
|
+
`❌ ${path.relative(REPO_ROOT, ledgerPath)}: ${emptyProblem}\n`,
|
|
463
|
+
);
|
|
464
|
+
return 1;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
240
468
|
// Scope declaration, not a scope guard. The ledger is written per release by
|
|
241
469
|
// `--ingest`, so "no <release>.json" is the normal state of a release whose
|
|
242
470
|
// self-review reported nothing — the gate's own success line says so. That
|
|
@@ -245,7 +473,7 @@ function main(argv: readonly string[]): number {
|
|
|
245
473
|
// durable trigger for an un-ingested finding is `--pr` mode, which compares
|
|
246
474
|
// the ledger against what the self-review actually reported.
|
|
247
475
|
try {
|
|
248
|
-
|
|
476
|
+
reportScanned({
|
|
249
477
|
gate: 'check_finding_dispositions',
|
|
250
478
|
scanned: ledger.findings.length,
|
|
251
479
|
units: 'recorded finding(s)',
|
|
@@ -253,8 +481,13 @@ function main(argv: readonly string[]): number {
|
|
|
253
481
|
allowEmpty:
|
|
254
482
|
'EMPTY_VALID: zero recorded findings IS the pass state for a release whose '
|
|
255
483
|
+ 'self-review reported none — the ledger file is created on first --ingest, so a '
|
|
256
|
-
+ 'clean release legitimately has no <version>.json.
|
|
257
|
-
+ '
|
|
484
|
+
+ 'clean release legitimately has no <version>.json. NARROWED TWICE: it no longer '
|
|
485
|
+
+ 'covers a RELEASED version with no ledger, which returns 1 above on the '
|
|
486
|
+
+ 'released/unreleased split, and it no longer covers an empty ledger that does '
|
|
487
|
+
+ 'not say why it is empty, which returns 1 above via empty_ledger_problem. What '
|
|
488
|
+
+ 'remains is an EXPLAINED empty ledger and an unreleased version. Un-ingested '
|
|
489
|
+
+ 'findings on an in-flight release are caught by --pr (unrecorded_findings), '
|
|
490
|
+
+ 'never by this count.',
|
|
258
491
|
});
|
|
259
492
|
} catch (exc) {
|
|
260
493
|
if (exc instanceof DeadScopeError) {
|
|
@@ -113,6 +113,21 @@ export interface GateSpec {
|
|
|
113
113
|
* `pending` is reported instead of hidden.
|
|
114
114
|
*/
|
|
115
115
|
unavailable_exit?: number;
|
|
116
|
+
/**
|
|
117
|
+
* Repo-relative workflow file this row's `argv` is pinned to.
|
|
118
|
+
*
|
|
119
|
+
* Rule 2 of this manifest — CI-IDENTICAL INVOCATION — was an instruction to
|
|
120
|
+
* the author and nothing else: the guard runs whatever `argv` says and never
|
|
121
|
+
* looked at how CI calls the gate, so a row could drift from its workflow, or
|
|
122
|
+
* the workflow step could be deleted outright, and this file would keep
|
|
123
|
+
* reporting healthy coverage for a gate CI no longer runs.
|
|
124
|
+
*
|
|
125
|
+
* Opt-in per row, because most gates here have several CI invocations with
|
|
126
|
+
* different arguments and one `argv` cannot be identical to all of them. Where
|
|
127
|
+
* a row DOES name a workflow, that workflow must contain an invocation of the
|
|
128
|
+
* gate whose argument list equals `argv` exactly.
|
|
129
|
+
*/
|
|
130
|
+
ci_invocation?: string;
|
|
116
131
|
note?: string;
|
|
117
132
|
}
|
|
118
133
|
|
|
@@ -191,6 +206,9 @@ export function load_manifest(file = MANIFEST): GateSpec[] {
|
|
|
191
206
|
...(e['unavailable_exit'] === undefined
|
|
192
207
|
? {}
|
|
193
208
|
: { unavailable_exit: _require_int(e['unavailable_exit'], id, i) }),
|
|
209
|
+
...(e['ci_invocation'] === undefined
|
|
210
|
+
? {}
|
|
211
|
+
: { ci_invocation: _require_str(e['ci_invocation'], 'ci_invocation', id, i) }),
|
|
194
212
|
...(e['note'] === undefined ? {} : { note: String(e['note']) }),
|
|
195
213
|
...(e['canary'] === undefined ? {} : { canary: _require_canary(e['canary'], id, i) }),
|
|
196
214
|
...(e['no_canary_reason'] === undefined
|
|
@@ -405,6 +423,73 @@ function report_negative_control_inventory(specs: readonly GateSpec[]): void {
|
|
|
405
423
|
}
|
|
406
424
|
}
|
|
407
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Invocations of `src/scripts/<id>` in a workflow, each as its argument list.
|
|
428
|
+
*
|
|
429
|
+
* Deliberately a text scan rather than a YAML walk: an invocation lives inside a
|
|
430
|
+
* `run:` block scalar, so parsing the workflow buys nothing and costs the ability
|
|
431
|
+
* to see a call split across a line continuation, which is how the release job
|
|
432
|
+
* writes this very gate. Backslash-newline joins are folded before matching for
|
|
433
|
+
* that reason.
|
|
434
|
+
*/
|
|
435
|
+
export function ci_invocations(workflowText: string, id: string): string[][] {
|
|
436
|
+
const folded = workflowText.replace(/\\\n\s*/gu, ' ');
|
|
437
|
+
const out: string[][] = [];
|
|
438
|
+
const re = new RegExp(String.raw`src/scripts/${id}(?![A-Za-z0-9_])([^\n]*)`, 'gu');
|
|
439
|
+
for (const m of folded.matchAll(re)) {
|
|
440
|
+
out.push(
|
|
441
|
+
(m[1] ?? '')
|
|
442
|
+
.trim()
|
|
443
|
+
.split(/\s+/u)
|
|
444
|
+
.filter((t) => t !== ''),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The row's `argv` must be reproducible from the workflow it pins.
|
|
452
|
+
*
|
|
453
|
+
* Both directions are failures and they are different defects: no matching
|
|
454
|
+
* invocation means the manifest probes the gate in a way CI does not (or CI
|
|
455
|
+
* stopped calling it at all), which is the drift rule 2 asks for and nothing
|
|
456
|
+
* enforced.
|
|
457
|
+
*/
|
|
458
|
+
export function ci_invocation_problem(
|
|
459
|
+
workflowText: string,
|
|
460
|
+
id: string,
|
|
461
|
+
argv: readonly string[],
|
|
462
|
+
): string | null {
|
|
463
|
+
const found = ci_invocations(workflowText, id);
|
|
464
|
+
if (found.length === 0) {
|
|
465
|
+
return `no invocation of src/scripts/${id} — the workflow does not call this gate`;
|
|
466
|
+
}
|
|
467
|
+
const want = argv.join(' ');
|
|
468
|
+
if (found.some((a) => a.join(' ') === want)) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
return (
|
|
472
|
+
`argv [${want}] matches no invocation there; the workflow calls it as ` +
|
|
473
|
+
found.map((a) => `[${a.join(' ')}]`).join(', ')
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Rows whose `ci_invocation` no longer describes their workflow. */
|
|
478
|
+
export function ci_invocation_drift(specs: readonly GateSpec[], repoRoot = REPO_ROOT): string[] {
|
|
479
|
+
const problems: string[] = [];
|
|
480
|
+
for (const s of specs) {
|
|
481
|
+
if (s.ci_invocation === undefined) continue;
|
|
482
|
+
const file = path.join(repoRoot, s.ci_invocation);
|
|
483
|
+
if (!fs.existsSync(file)) {
|
|
484
|
+
problems.push(`${s.id}: ci_invocation ${s.ci_invocation} does not exist`);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const problem = ci_invocation_problem(fs.readFileSync(file, 'utf-8'), s.id, s.argv);
|
|
488
|
+
if (problem !== null) problems.push(`${s.id}: ${s.ci_invocation} — ${problem}`);
|
|
489
|
+
}
|
|
490
|
+
return problems;
|
|
491
|
+
}
|
|
492
|
+
|
|
408
493
|
export function main(argv: readonly string[]): number {
|
|
409
494
|
const wantJson = argv.includes('--format') && argv[argv.indexOf('--format') + 1] === 'json';
|
|
410
495
|
const listOnly = argv.includes('--list');
|
|
@@ -523,12 +608,23 @@ export function main(argv: readonly string[]): number {
|
|
|
523
608
|
);
|
|
524
609
|
}
|
|
525
610
|
report_negative_control_inventory(specs);
|
|
611
|
+
const drift = ci_invocation_drift(specs);
|
|
612
|
+
for (const d of drift) {
|
|
613
|
+
process.stdout.write(` ❌ ${d}\n`);
|
|
614
|
+
}
|
|
526
615
|
const hardening = report_hardening_ratchet();
|
|
527
616
|
const selfTest = report_self_test_ratchet();
|
|
528
617
|
if (failed.length > 0) {
|
|
529
618
|
process.stdout.write(`❌ ${String(failed.length)} gate(s) failed the coverage floor.\n`);
|
|
530
619
|
return 1;
|
|
531
620
|
}
|
|
621
|
+
if (drift.length > 0) {
|
|
622
|
+
process.stdout.write(
|
|
623
|
+
`❌ ${String(drift.length)} row(s) declare a ci_invocation their workflow no longer matches — ` +
|
|
624
|
+
`the manifest probes the gate differently from CI, or CI stopped calling it.\n`,
|
|
625
|
+
);
|
|
626
|
+
return 1;
|
|
627
|
+
}
|
|
532
628
|
if (hardening !== 0) {
|
|
533
629
|
return hardening;
|
|
534
630
|
}
|