@event4u/agent-config 9.20.0 → 9.22.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +86 -0
- package/dist/agent-src/commands/worktree/cleanup.md +17 -0
- package/dist/agent-src/commands/worktree/status.md +6 -0
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +22 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +5 -5
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.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 +58 -2
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +47 -29
- package/package.json +1 -1
- package/src/config/gate-coverage.yml +22 -4
- package/src/config/gate-violation-baselines.json +3 -8
- package/src/scripts/_lib/bench_ab_activation.ts +368 -0
- package/src/scripts/_lib/gate_population.ts +70 -0
- package/src/scripts/_lib/roadmap_granularity.ts +157 -0
- package/src/scripts/_lib/scan_scope.ts +44 -0
- package/src/scripts/_lib/stdin.ts +82 -0
- package/src/scripts/audit_adr_coverage.ts +38 -0
- package/src/scripts/audit_auto_rules.ts +17 -3
- package/src/scripts/audit_command_surface.ts +51 -0
- package/src/scripts/audit_initial_context.ts +39 -3
- package/src/scripts/audit_mcp_tools.ts +25 -0
- package/src/scripts/audit_overlap.ts +24 -1
- package/src/scripts/audit_skill_descriptions.ts +19 -0
- package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
- package/src/scripts/bench_ab_task_runner.ts +18 -0
- package/src/scripts/bench_ab_v2_run.ts +147 -6
- package/src/scripts/bench_ab_v2_stats.ts +74 -2
- package/src/scripts/check_adr_frontmatter.ts +23 -3
- package/src/scripts/check_always_budget.ts +24 -0
- package/src/scripts/check_artefact_checksums.ts +24 -1
- package/src/scripts/check_artefact_count_messaging.ts +20 -0
- package/src/scripts/check_augmentignore.ts +22 -15
- package/src/scripts/check_backstop_debt.ts +22 -0
- package/src/scripts/check_beta_review_markers.ts +22 -1
- package/src/scripts/check_bridge_derivation.ts +22 -1
- package/src/scripts/check_bundle_path_leakage.ts +26 -0
- package/src/scripts/check_ci_strict_superset.ts +21 -3
- package/src/scripts/check_claims.ts +22 -0
- package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
- package/src/scripts/check_cluster_patterns.ts +26 -4
- package/src/scripts/check_command_count_messaging.ts +20 -4
- package/src/scripts/check_comparison.ts +18 -0
- package/src/scripts/check_condensed_paths.ts +23 -2
- package/src/scripts/check_consumer_scope_flip.ts +21 -2
- package/src/scripts/check_council_config_location.ts +20 -0
- package/src/scripts/check_council_layout.ts +22 -2
- package/src/scripts/check_council_references.ts +23 -1
- package/src/scripts/check_dependency_floors.ts +19 -3
- package/src/scripts/check_discovery_determinism.ts +26 -0
- package/src/scripts/check_enforcement_coverage.ts +20 -0
- package/src/scripts/check_evaluator_budgets.ts +23 -0
- package/src/scripts/check_finding_dispositions.ts +29 -0
- package/src/scripts/check_gate_coverage.ts +63 -22
- package/src/scripts/check_gate_paths.ts +30 -5
- package/src/scripts/check_generated_artefact_headers.ts +21 -0
- package/src/scripts/check_generator_output_coverage.ts +23 -0
- package/src/scripts/check_gitignore_freshness.ts +23 -0
- package/src/scripts/check_host_loadability.ts +34 -8
- package/src/scripts/check_installer_import_purity.ts +22 -8
- package/src/scripts/check_kernel_prefix_stability.ts +19 -1
- package/src/scripts/check_kernel_rule_bundle.ts +23 -0
- package/src/scripts/check_knowledge_cards.ts +13 -0
- package/src/scripts/check_knowledge_pages.ts +23 -0
- package/src/scripts/check_knowledge_sharing.ts +27 -0
- package/src/scripts/check_md_language.ts +24 -0
- package/src/scripts/check_media_deps.ts +23 -0
- package/src/scripts/check_memory.ts +26 -1
- package/src/scripts/check_memory_contradiction.ts +16 -1
- package/src/scripts/check_memory_proposal.ts +15 -0
- package/src/scripts/check_memory_similarity.ts +16 -0
- package/src/scripts/check_module_management_neutral.ts +19 -11
- package/src/scripts/check_no_conflict_markers.ts +22 -1
- package/src/scripts/check_no_external_sources.ts +26 -1
- package/src/scripts/check_no_local_settings_committed.ts +37 -10
- package/src/scripts/check_no_new_legacy_path.ts +30 -1
- package/src/scripts/check_one_off_location.ts +43 -0
- package/src/scripts/check_originality_freshness.ts +25 -1
- package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
- package/src/scripts/check_pack_size.ts +22 -0
- package/src/scripts/check_preamble_payload_budget.ts +35 -5
- package/src/scripts/check_proposal.ts +24 -1
- package/src/scripts/check_public_catalog_links.ts +27 -0
- package/src/scripts/check_public_links.ts +19 -0
- package/src/scripts/check_quality_regression.ts +19 -0
- package/src/scripts/check_reach_channels.ts +44 -0
- package/src/scripts/check_reach_staleness.ts +27 -0
- package/src/scripts/check_release_adjacent_health.ts +28 -0
- package/src/scripts/check_release_highlights.ts +34 -0
- package/src/scripts/check_release_includes_discovery.ts +25 -6
- package/src/scripts/check_release_pr_shape.ts +18 -1
- package/src/scripts/check_release_published.ts +21 -0
- package/src/scripts/check_release_surface_equality.ts +22 -0
- package/src/scripts/check_release_trunk_sync.ts +19 -0
- package/src/scripts/check_reply_consistency.ts +22 -0
- package/src/scripts/check_roadmap_trackable.ts +20 -0
- package/src/scripts/check_role_doc_links.ts +23 -0
- package/src/scripts/check_rule_invariants.ts +14 -0
- package/src/scripts/check_secret_leak.ts +47 -0
- package/src/scripts/check_skill_gaps.ts +32 -0
- package/src/scripts/check_skill_requires.ts +19 -0
- package/src/scripts/check_source_pointer_freshness.ts +20 -0
- package/src/scripts/check_structural_breaking.ts +29 -2
- package/src/scripts/check_surface_tiers.ts +34 -0
- package/src/scripts/check_template_pin_drift.ts +23 -1
- package/src/scripts/check_test_coverage_diff.ts +17 -1
- package/src/scripts/check_token_optimizer_freshness.ts +18 -3
- package/src/scripts/check_token_quality_golden.ts +21 -0
- package/src/scripts/check_token_regression.ts +25 -0
- package/src/scripts/check_tracked_but_ignored.ts +26 -0
- package/src/scripts/check_trigger_eval_presence.ts +23 -1
- package/src/scripts/check_trigger_evals.ts +36 -0
- package/src/scripts/check_trunk_drift.ts +23 -0
- package/src/scripts/check_update_banner.ts +24 -0
- package/src/scripts/gate_scope_classify.ts +513 -0
- package/src/scripts/hook_manifest.yaml +1 -1
- package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/lint_agent_security.ts +19 -0
- package/src/scripts/lint_agent_skill_names.ts +30 -0
- package/src/scripts/lint_agents_layout.ts +20 -0
- package/src/scripts/lint_agents_md.ts +20 -0
- package/src/scripts/lint_archived_skills.ts +24 -3
- package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
- package/src/scripts/lint_bench_ab.ts +25 -0
- package/src/scripts/lint_bench_corpus.ts +20 -4
- package/src/scripts/lint_breaking_changes_index.ts +24 -0
- package/src/scripts/lint_budget_ownership.ts +20 -3
- package/src/scripts/lint_changelog_rollback.ts +24 -0
- package/src/scripts/lint_command_flow_coverage.ts +22 -0
- package/src/scripts/lint_command_tiers.ts +29 -5
- package/src/scripts/lint_commit_subjects.ts +15 -0
- package/src/scripts/lint_confusables.ts +24 -0
- package/src/scripts/lint_consumer_internal_refs.ts +24 -0
- package/src/scripts/lint_design_quality.ts +22 -0
- package/src/scripts/lint_design_slop.ts +23 -0
- package/src/scripts/lint_discovery_manifest.ts +23 -0
- package/src/scripts/lint_discovery_vocabulary.ts +19 -0
- package/src/scripts/lint_documented_commands.ts +23 -0
- package/src/scripts/lint_empty_roadmaps.ts +23 -0
- package/src/scripts/lint_eval_fixture_citations.ts +19 -0
- package/src/scripts/lint_eval_freshness.ts +35 -0
- package/src/scripts/lint_examples.ts +18 -3
- package/src/scripts/lint_explain_trace.ts +23 -0
- package/src/scripts/lint_featured_skills.ts +26 -1
- package/src/scripts/lint_flows.ts +19 -0
- package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
- package/src/scripts/lint_ghostwriter_source.ts +26 -6
- package/src/scripts/lint_glama_drift.ts +24 -0
- package/src/scripts/lint_global_paths.ts +24 -0
- package/src/scripts/lint_governed_writes.ts +20 -0
- package/src/scripts/lint_hidden_unicode.ts +25 -0
- package/src/scripts/lint_hook_concern_budget.ts +39 -6
- package/src/scripts/lint_hook_manifest.ts +22 -0
- package/src/scripts/lint_instruction_smuggling.ts +23 -0
- package/src/scripts/lint_knowledge_scale.ts +32 -2
- package/src/scripts/lint_legal_pack.ts +31 -0
- package/src/scripts/lint_marketplace.ts +26 -0
- package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
- package/src/scripts/lint_mcp_config_security.ts +24 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
- package/src/scripts/lint_memory_tripwire.ts +35 -0
- package/src/scripts/lint_missions.ts +23 -7
- package/src/scripts/lint_model_tier_coverage.ts +18 -0
- package/src/scripts/lint_namespace_collisions.ts +19 -0
- package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
- package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
- package/src/scripts/lint_one_off_age.ts +38 -2
- package/src/scripts/lint_orchestration_dsl.ts +23 -3
- package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
- package/src/scripts/lint_originality.ts +22 -4
- package/src/scripts/lint_originality_shingles.ts +28 -3
- package/src/scripts/lint_output_slop.ts +19 -0
- package/src/scripts/lint_override_kernel_guard.ts +30 -0
- package/src/scripts/lint_pack_first_win.ts +23 -0
- package/src/scripts/lint_pack_risk_class.ts +19 -3
- package/src/scripts/lint_persistence.ts +19 -0
- package/src/scripts/lint_persona_governance.ts +18 -3
- package/src/scripts/lint_positioning.ts +23 -0
- package/src/scripts/lint_pre_migration_refs.ts +24 -0
- package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
- package/src/scripts/lint_provenance.ts +21 -0
- package/src/scripts/lint_provenance_vocabulary.ts +24 -1
- package/src/scripts/lint_readme_jargon.ts +21 -3
- package/src/scripts/lint_readme_serial_comma.ts +21 -3
- package/src/scripts/lint_readme_size.ts +21 -3
- package/src/scripts/lint_regression.ts +28 -0
- package/src/scripts/lint_roadmap_blockers.ts +43 -0
- package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
- package/src/scripts/lint_roadmap_complexity.ts +47 -0
- package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
- package/src/scripts/lint_rule_interactions.ts +16 -2
- package/src/scripts/lint_rule_tiers.ts +19 -6
- package/src/scripts/lint_showcase_sessions.ts +19 -3
- package/src/scripts/lint_skill_descriptions.ts +19 -0
- package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
- package/src/scripts/lint_skill_originality.ts +18 -3
- package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
- package/src/scripts/lint_skill_tools.ts +84 -145
- package/src/scripts/lint_spawn_payload.ts +27 -1
- package/src/scripts/lint_store_boundary.ts +19 -0
- package/src/scripts/lint_subagent_determinism.ts +23 -1
- package/src/scripts/lint_supported_tools_matrix.ts +18 -0
- package/src/scripts/lint_surface_matrix.ts +19 -3
- package/src/scripts/lint_ticket_buildable.ts +23 -0
- package/src/scripts/lint_topics_yaml.ts +19 -12
- package/src/scripts/lint_trigger_precision.ts +27 -0
- package/src/scripts/lint_trust_coherence.ts +21 -0
- package/src/scripts/lint_ui_stack_bundles.ts +17 -3
- package/src/scripts/lint_value_dashboard.ts +19 -3
- package/src/scripts/lint_versioned_cache.ts +19 -1
- package/src/scripts/lint_workflow_security.ts +22 -14
- package/src/scripts/pr_url_reminder_hook.ts +1 -1
- package/src/scripts/skill_collision_clusters.ts +37 -3
- package/src/scripts/skill_discovery.ts +20 -0
- package/src/scripts/skill_eval_coverage.ts +22 -0
- package/src/scripts/skill_overlap.ts +32 -6
- package/src/scripts/skill_preview.ts +31 -0
- package/src/scripts/skill_trigger_eval.ts +18 -0
- package/src/scripts/skill_usage_collect.ts +21 -0
- package/src/scripts/skill_usage_report.ts +21 -0
- package/src/scripts/sweep_dead_scan_roots.ts +11 -3
- package/src/scripts/verify_physical_move.ts +24 -0
- package/src/scripts/worktree_cleanup_check.ts +319 -4
- package/src/scripts/check_bite_sized_granularity.ts +0 -116
- package/src/scripts/lint_workspace_boundary.ts +0 -267
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import * as fs from 'node:fs';
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
|
|
22
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
22
23
|
import { main as runOriginality } from './lint_originality.js';
|
|
23
24
|
|
|
24
25
|
const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
|
|
@@ -136,9 +137,32 @@ export function main(): number {
|
|
|
136
137
|
);
|
|
137
138
|
return 0;
|
|
138
139
|
}
|
|
139
|
-
const { drifted, missing } = checkFreshness();
|
|
140
140
|
const rel = (f: string): string => path.relative(REPO_ROOT, f);
|
|
141
141
|
|
|
142
|
+
// `REPORTS` is the entire scope. One of the two absent is a real state the
|
|
143
|
+
// `missing` branch below reports; BOTH absent is the reports directory
|
|
144
|
+
// having moved, and the sweep would then WRITE two files, call them missing,
|
|
145
|
+
// and delete them again — a warning about the wrong thing. Exit stays 0:
|
|
146
|
+
// this surface is an advisory by design and has no failure code.
|
|
147
|
+
try {
|
|
148
|
+
assertWatchlistResolves({
|
|
149
|
+
gate: 'check_originality_freshness',
|
|
150
|
+
candidates: REPORTS.map(rel),
|
|
151
|
+
repoRoot: REPO_ROOT,
|
|
152
|
+
});
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (err instanceof DeadScopeError) {
|
|
155
|
+
process.stderr.write(
|
|
156
|
+
`⚠️ ${err.message}\n` +
|
|
157
|
+
' Run `./scripts-run src/scripts/lint_originality` and commit the result.\n',
|
|
158
|
+
);
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { drifted, missing } = checkFreshness();
|
|
165
|
+
|
|
142
166
|
if (missing.length > 0) {
|
|
143
167
|
process.stdout.write(
|
|
144
168
|
`⚠️ originality report is not committed yet: ${missing.map(rel).join(', ')}\n` +
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
CASCADE_ELIGIBLE_KINDS,
|
|
31
31
|
USER_GLOBAL_OVERLAY_KINDS,
|
|
32
32
|
} from './_lib/agents_overlay.js';
|
|
33
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
33
34
|
|
|
34
35
|
const _HERE = fileURLToPath(import.meta.url);
|
|
35
36
|
// src/scripts/check_overlay_cascade_subdirs.ts → two dirs up is the repo root.
|
|
@@ -96,6 +97,26 @@ export function main(): number {
|
|
|
96
97
|
}
|
|
97
98
|
const text = fs.readFileSync(DOCS_PATH, 'utf-8');
|
|
98
99
|
const parsed = _parse_doc_table(text);
|
|
100
|
+
// The doc side of this comparison is a table PARSE, so a restructured,
|
|
101
|
+
// renamed, or reformatted table yields zero rows while the file still
|
|
102
|
+
// exists — and the set comparison then reports drift against phantoms, or
|
|
103
|
+
// nothing at all once both lists are empty. Exit 3, not 1: an unreadable
|
|
104
|
+
// table means the gate could not run, which is the same class as the
|
|
105
|
+
// `not found` path above; 1 is reserved for real code↔docs drift.
|
|
106
|
+
try {
|
|
107
|
+
assertScanned({
|
|
108
|
+
gate: 'check_overlay_cascade_subdirs',
|
|
109
|
+
scanned: parsed.all_kinds.size,
|
|
110
|
+
units: 'overlay table row(s)',
|
|
111
|
+
roots: ['docs/customization.md'],
|
|
112
|
+
});
|
|
113
|
+
} catch (exc) {
|
|
114
|
+
if (exc instanceof DeadScopeError) {
|
|
115
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
116
|
+
return 3;
|
|
117
|
+
}
|
|
118
|
+
throw exc;
|
|
119
|
+
}
|
|
99
120
|
const doc_cascade = parsed.cascade_yes;
|
|
100
121
|
const doc_user_global = parsed.user_global_yes;
|
|
101
122
|
|
|
@@ -27,6 +27,8 @@ import * as fs from 'node:fs';
|
|
|
27
27
|
import * as path from 'node:path';
|
|
28
28
|
import { fileURLToPath } from 'node:url';
|
|
29
29
|
|
|
30
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
31
|
+
|
|
30
32
|
const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
|
|
31
33
|
const BUDGET_PATH = path.join(REPO_ROOT, 'src', 'config', 'pack-size-budget.json');
|
|
32
34
|
const SKILL_PREFIX = 'dist/agent-src/skills/';
|
|
@@ -176,6 +178,26 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
176
178
|
process.stderr.write(`❌ pack size: npm pack failed: ${String(err)}\n`);
|
|
177
179
|
return 2;
|
|
178
180
|
}
|
|
181
|
+
// The pack manifest IS the corpus. `evaluate` refuses a vacuous per-skill
|
|
182
|
+
// share, but the packed-size arm has no such floor: an empty payload — a
|
|
183
|
+
// botched `files[]`, a pack that resolved nothing — measures 0 MB and sits
|
|
184
|
+
// comfortably under every budget.
|
|
185
|
+
try {
|
|
186
|
+
assertScanned({
|
|
187
|
+
gate: 'check_pack_size',
|
|
188
|
+
scanned: pack.files.length,
|
|
189
|
+
units: 'packed file(s)',
|
|
190
|
+
roots: ['npm pack --dry-run payload (package.json files[])'],
|
|
191
|
+
});
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err instanceof DeadScopeError) {
|
|
194
|
+
// 2, not 1: the documented meaning of 1 is "over budget", and an
|
|
195
|
+
// empty manifest is unreadable input, not a budget violation.
|
|
196
|
+
process.stderr.write(`❌ pack size: ${err.message}\n`);
|
|
197
|
+
return 2;
|
|
198
|
+
}
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
179
201
|
|
|
180
202
|
const errors = evaluate(budget, pack);
|
|
181
203
|
const { perSkill, total } = skillBytes(pack.files);
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import * as fs from 'node:fs';
|
|
24
24
|
import * as path from 'node:path';
|
|
25
25
|
|
|
26
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
26
27
|
import { censusClaudeMdHierarchy, censusRuleDir, censusSkillsCatalog } from './preamble_byte_census.js';
|
|
27
28
|
|
|
28
29
|
const HERE = path.dirname(new URL(import.meta.url).pathname);
|
|
@@ -40,7 +41,7 @@ export interface BudgetVerdict {
|
|
|
40
41
|
baseline: number;
|
|
41
42
|
ceiling: number;
|
|
42
43
|
withinBudget: boolean;
|
|
43
|
-
buckets: Array<{ name: string; tokens: number }>;
|
|
44
|
+
buckets: Array<{ name: string; tokens: number; files: number }>;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
function tokens(chars: number): number {
|
|
@@ -65,7 +66,7 @@ export function readBudget(file: string = BUDGET_FILE): Budget {
|
|
|
65
66
|
/** Measure only what the repo tree determines — no `~`, no transcripts, no network. */
|
|
66
67
|
export function measureDeterministicPayload(
|
|
67
68
|
repoRoot: string = REPO_ROOT,
|
|
68
|
-
): Array<{ name: string; tokens: number }> {
|
|
69
|
+
): Array<{ name: string; tokens: number; files: number }> {
|
|
69
70
|
const projectRules = censusRuleDir(path.join(repoRoot, 'dist', 'agent-src', 'rules'));
|
|
70
71
|
const skills = censusSkillsCatalog(path.join(repoRoot, 'dist', 'agent-src', 'skills'));
|
|
71
72
|
// Only the PROJECT half of the CLAUDE.md hierarchy is deterministic — the
|
|
@@ -73,10 +74,20 @@ export function measureDeterministicPayload(
|
|
|
73
74
|
const claudeMd = censusClaudeMdHierarchy(repoRoot, path.join(repoRoot, '.no-such-home'));
|
|
74
75
|
const projectClaudeMdChars =
|
|
75
76
|
claudeMd.project_claude_md_chars + claudeMd.project_claude_local_md_chars;
|
|
77
|
+
// `files` carries the count of sources each census actually read. A token
|
|
78
|
+
// total cannot tell "the root moved" from "the payload is genuinely tiny",
|
|
79
|
+
// so the scope assertion in `main` needs the count, not the bytes.
|
|
80
|
+
const claudeMdFiles =
|
|
81
|
+
(claudeMd.project_claude_md_present ? 1 : 0) +
|
|
82
|
+
(claudeMd.project_claude_local_md_present ? 1 : 0);
|
|
76
83
|
return [
|
|
77
|
-
{ name: 'project-scope rules', tokens: tokens(projectRules.chars) },
|
|
78
|
-
{ name: 'preloaded skills catalog', tokens: tokens(skills.chars) },
|
|
79
|
-
{
|
|
84
|
+
{ name: 'project-scope rules', tokens: tokens(projectRules.chars), files: projectRules.files },
|
|
85
|
+
{ name: 'preloaded skills catalog', tokens: tokens(skills.chars), files: skills.skills },
|
|
86
|
+
{
|
|
87
|
+
name: 'CLAUDE.md hierarchy (project only)',
|
|
88
|
+
tokens: tokens(projectClaudeMdChars),
|
|
89
|
+
files: claudeMdFiles,
|
|
90
|
+
},
|
|
80
91
|
];
|
|
81
92
|
}
|
|
82
93
|
|
|
@@ -98,6 +109,25 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
98
109
|
return 2;
|
|
99
110
|
}
|
|
100
111
|
|
|
112
|
+
// A ratchet over a measurement of nothing always passes: move
|
|
113
|
+
// `dist/agent-src/` and every census returns zero, which is trivially under
|
|
114
|
+
// any ceiling. Exit 2 (misuse / unreadable budget — the could-not-run
|
|
115
|
+
// code), never 1, which asserts the payload actually grew.
|
|
116
|
+
try {
|
|
117
|
+
assertScanned({
|
|
118
|
+
gate: 'check_preamble_payload_budget',
|
|
119
|
+
scanned: verdict.buckets.reduce((n, b) => n + b.files, 0),
|
|
120
|
+
units: 'payload source file(s)',
|
|
121
|
+
roots: ['dist/agent-src/rules', 'dist/agent-src/skills', 'CLAUDE.md'],
|
|
122
|
+
});
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err instanceof DeadScopeError) {
|
|
125
|
+
process.stderr.write(`❌ preamble-payload budget: ${err.message}\n`);
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
|
|
101
131
|
if (json) {
|
|
102
132
|
process.stdout.write(JSON.stringify(verdict, null, 2) + '\n');
|
|
103
133
|
return verdict.withinBudget ? 0 : 1;
|
|
@@ -37,6 +37,8 @@ import process from 'node:process';
|
|
|
37
37
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
38
|
import { parse as parseYaml } from 'yaml';
|
|
39
39
|
|
|
40
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
41
|
+
|
|
40
42
|
type Severity = 'error' | 'warning';
|
|
41
43
|
|
|
42
44
|
const REQUIRED_FRONTMATTER: ReadonlySet<string> = new Set([
|
|
@@ -456,7 +458,28 @@ function main(): number {
|
|
|
456
458
|
process.stderr.write(`error: ${p} not found\n`);
|
|
457
459
|
return 3;
|
|
458
460
|
}
|
|
459
|
-
const
|
|
461
|
+
const text = fs.readFileSync(p, 'utf-8');
|
|
462
|
+
// No corpus walk: this gate's scope is the single document named on argv.
|
|
463
|
+
// The unit is therefore the content lines actually read — every check below
|
|
464
|
+
// is a search over this text, so a zero-byte target makes the negative ones
|
|
465
|
+
// ("no TODO/TBD markers") vacuously true against nothing.
|
|
466
|
+
try {
|
|
467
|
+
assertScanned({
|
|
468
|
+
gate: 'check_proposal',
|
|
469
|
+
scanned: text.split('\n').filter((l) => l.trim() !== '').length,
|
|
470
|
+
units: 'content line(s)',
|
|
471
|
+
roots: [p],
|
|
472
|
+
});
|
|
473
|
+
} catch (exc) {
|
|
474
|
+
if (exc instanceof DeadScopeError) {
|
|
475
|
+
// 1 = gate failure, which is what an unusable proposal already
|
|
476
|
+
// returns today (3 is reserved for the not-found path above).
|
|
477
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
478
|
+
return 1;
|
|
479
|
+
}
|
|
480
|
+
throw exc;
|
|
481
|
+
}
|
|
482
|
+
const findings = _runChecks(text, p);
|
|
460
483
|
const errors = findings.filter((f) => f.severity === 'error');
|
|
461
484
|
if (args.format === 'json') {
|
|
462
485
|
process.stdout.write(
|
|
@@ -26,6 +26,8 @@ import * as fs from 'node:fs';
|
|
|
26
26
|
import * as path from 'node:path';
|
|
27
27
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
28
|
|
|
29
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
30
|
+
|
|
29
31
|
const QUIET = process.argv.slice(2).includes('--quiet');
|
|
30
32
|
|
|
31
33
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -117,6 +119,10 @@ function main(): number {
|
|
|
117
119
|
const missing: Array<[number, string]> = [];
|
|
118
120
|
const unshipped: Array<[number, string]> = [];
|
|
119
121
|
|
|
122
|
+
// Counted BEFORE any filtering — external hrefs and anchors included — so
|
|
123
|
+
// the number answers "did the catalog still carry links", not "how many
|
|
124
|
+
// survived the checks".
|
|
125
|
+
let links_seen = 0;
|
|
120
126
|
const lines = _splitlines(text);
|
|
121
127
|
for (let i = 0; i < lines.length; i++) {
|
|
122
128
|
const lineno = i + 1;
|
|
@@ -124,6 +130,7 @@ function main(): number {
|
|
|
124
130
|
LINK_RE.lastIndex = 0;
|
|
125
131
|
let m: RegExpExecArray | null;
|
|
126
132
|
while ((m = LINK_RE.exec(line)) !== null) {
|
|
133
|
+
links_seen += 1;
|
|
127
134
|
const href = m[1]!;
|
|
128
135
|
if (href.includes(FORBIDDEN_PREFIX)) {
|
|
129
136
|
forbidden.push([lineno, href]);
|
|
@@ -143,6 +150,26 @@ function main(): number {
|
|
|
143
150
|
}
|
|
144
151
|
}
|
|
145
152
|
|
|
153
|
+
// The catalog is generated. A renderer that stops emitting links leaves a
|
|
154
|
+
// file that still exists and still parses, so the three violation buckets
|
|
155
|
+
// stay empty and this reports "all links resolve" over no links at all.
|
|
156
|
+
// Exit 1 is the only failure code here, and it already carries the
|
|
157
|
+
// could-not-run meaning (the missing-catalog path above returns it too).
|
|
158
|
+
try {
|
|
159
|
+
assertScanned({
|
|
160
|
+
gate: 'check_public_catalog_links',
|
|
161
|
+
scanned: links_seen,
|
|
162
|
+
units: 'markdown link(s)',
|
|
163
|
+
roots: ['docs/catalog.md'],
|
|
164
|
+
});
|
|
165
|
+
} catch (exc) {
|
|
166
|
+
if (exc instanceof DeadScopeError) {
|
|
167
|
+
process.stdout.write(`❌ ${exc.message}\n`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
throw exc;
|
|
171
|
+
}
|
|
172
|
+
|
|
146
173
|
const total_violations = forbidden.length + missing.length + unshipped.length;
|
|
147
174
|
if (total_violations === 0) {
|
|
148
175
|
if (!QUIET) {
|
|
@@ -22,6 +22,8 @@ import * as fs from 'node:fs';
|
|
|
22
22
|
import * as path from 'node:path';
|
|
23
23
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
24
|
|
|
25
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
26
|
+
|
|
25
27
|
const _HERE = fileURLToPath(import.meta.url);
|
|
26
28
|
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
27
29
|
const PUBLIC_FILES = ['README.md', 'AGENTS.md', 'docs/architecture.md'] as const;
|
|
@@ -269,6 +271,23 @@ function main(): number {
|
|
|
269
271
|
const args = parse_args(process.argv.slice(2));
|
|
270
272
|
|
|
271
273
|
const contracts = collect_contracts();
|
|
274
|
+
// Every verdict below is decided against this map: an empty contracts dir
|
|
275
|
+
// turns "no stability violations" into "nothing was ever compared".
|
|
276
|
+
try {
|
|
277
|
+
assertScanned({
|
|
278
|
+
gate: 'check_public_links',
|
|
279
|
+
scanned: contracts.size,
|
|
280
|
+
units: 'contract file(s)',
|
|
281
|
+
roots: [CONTRACTS_DIR],
|
|
282
|
+
});
|
|
283
|
+
} catch (exc) {
|
|
284
|
+
if (exc instanceof DeadScopeError) {
|
|
285
|
+
// 1 = violations found (2 is argparse, 3 internal error).
|
|
286
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
throw exc;
|
|
290
|
+
}
|
|
272
291
|
if (args.list) {
|
|
273
292
|
for (const [p, lvl] of contracts) {
|
|
274
293
|
const label = lvl ?? '(no frontmatter)';
|
|
@@ -51,6 +51,7 @@ import process from 'node:process';
|
|
|
51
51
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
52
52
|
|
|
53
53
|
import { wilcoxon } from './bench_ab_v2_stats.js';
|
|
54
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
54
55
|
|
|
55
56
|
const _HERE = fileURLToPath(import.meta.url);
|
|
56
57
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -299,6 +300,24 @@ function main(argv: string[]): number {
|
|
|
299
300
|
}
|
|
300
301
|
|
|
301
302
|
const results = Array.isArray(report.results) ? report.results : [];
|
|
303
|
+
// A MISSING report is the documented inert path and already returned above. A
|
|
304
|
+
// report that exists and carries no pairs is a different thing entirely — a
|
|
305
|
+
// truncated or half-written judge run — yet it aggregates to `no-data` and
|
|
306
|
+
// exits 0 outside flip-gate mode, i.e. reads as "quality held" on zero data.
|
|
307
|
+
try {
|
|
308
|
+
assertScanned({
|
|
309
|
+
gate: 'check_quality_regression',
|
|
310
|
+
scanned: results.length,
|
|
311
|
+
units: 'judged pair(s)',
|
|
312
|
+
roots: [path.relative(REPO_ROOT, reportPath)],
|
|
313
|
+
});
|
|
314
|
+
} catch (e) {
|
|
315
|
+
if (e instanceof DeadScopeError) {
|
|
316
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
317
|
+
return 1; // the gate's file-error code — the report is present but unusable
|
|
318
|
+
}
|
|
319
|
+
throw e;
|
|
320
|
+
}
|
|
302
321
|
const thr = typeof report.threshold === 'number' ? report.threshold : threshold;
|
|
303
322
|
const agg = aggregate(results, thr);
|
|
304
323
|
|
|
@@ -53,6 +53,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
53
53
|
|
|
54
54
|
import { parse as parseYaml } from 'yaml';
|
|
55
55
|
|
|
56
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
56
57
|
import { SchemaError, validate, type YamlValue } from './validate_frontmatter.js';
|
|
57
58
|
|
|
58
59
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -305,6 +306,27 @@ export function check_probe_cmd_binding(registry: unknown): SchemaError[] {
|
|
|
305
306
|
return findings;
|
|
306
307
|
}
|
|
307
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Backends declared across every channel — the units the schema rules and the
|
|
311
|
+
* `probe_cmd-binding` rule actually judge. Same deliberate shape-tolerance as
|
|
312
|
+
* `check_probe_cmd_binding`: anything unreadable is skipped, so the number is
|
|
313
|
+
* "entries a rule could be applied to", never "entries that passed".
|
|
314
|
+
*/
|
|
315
|
+
function count_backends(registry: unknown): number {
|
|
316
|
+
const root =
|
|
317
|
+
registry !== null && typeof registry === 'object' && !Array.isArray(registry)
|
|
318
|
+
? (registry as Record<string, unknown>)
|
|
319
|
+
: null;
|
|
320
|
+
const channels = Array.isArray(root?.['channels']) ? (root['channels'] as unknown[]) : [];
|
|
321
|
+
return channels.reduce<number>((total, rawChannel) => {
|
|
322
|
+
if (rawChannel === null || typeof rawChannel !== 'object' || Array.isArray(rawChannel)) {
|
|
323
|
+
return total;
|
|
324
|
+
}
|
|
325
|
+
const backends = (rawChannel as Record<string, unknown>)['backends'];
|
|
326
|
+
return total + (Array.isArray(backends) ? backends.length : 0);
|
|
327
|
+
}, 0);
|
|
328
|
+
}
|
|
329
|
+
|
|
308
330
|
/**
|
|
309
331
|
* Validate one registry file against the schema.
|
|
310
332
|
*
|
|
@@ -376,6 +398,28 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
376
398
|
return 1;
|
|
377
399
|
}
|
|
378
400
|
|
|
401
|
+
// Reaching a CLEAN verdict over zero backends means the SCHEMA stopped
|
|
402
|
+
// constraining, not that the registry is empty: `minItems: 1` sits on both
|
|
403
|
+
// `channels` and `backends`, so an empty registry is an error-severity
|
|
404
|
+
// finding and returned above. A gutted or repointed schema is therefore
|
|
405
|
+
// the only way to arrive here with nothing scanned — and it would print ✅
|
|
406
|
+
// over a file no rule was applied to. Exit 3 is the could-not-run class
|
|
407
|
+
// `RegistryLoadError` already uses; 1 asserts real violations were found.
|
|
408
|
+
try {
|
|
409
|
+
assertScanned({
|
|
410
|
+
gate: 'check_reach_channels',
|
|
411
|
+
scanned: count_backends(load_registry(targetPath)),
|
|
412
|
+
units: 'backend(s)',
|
|
413
|
+
roots: [relTarget],
|
|
414
|
+
});
|
|
415
|
+
} catch (err) {
|
|
416
|
+
if (err instanceof DeadScopeError || err instanceof RegistryLoadError) {
|
|
417
|
+
process.stderr.write(`❌ check_reach_channels: ${err.message}\n`);
|
|
418
|
+
return 3;
|
|
419
|
+
}
|
|
420
|
+
throw err;
|
|
421
|
+
}
|
|
422
|
+
|
|
379
423
|
if (!quiet) {
|
|
380
424
|
process.stdout.write(`✅ ${relTarget}: schema-valid (reach channel registry).\n`);
|
|
381
425
|
}
|
|
@@ -51,6 +51,7 @@ import * as path from 'node:path';
|
|
|
51
51
|
import process from 'node:process';
|
|
52
52
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
53
53
|
|
|
54
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
54
55
|
import { RegistryLoadError, load_registry } from './check_reach_channels.js';
|
|
55
56
|
|
|
56
57
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -313,8 +314,16 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
313
314
|
const relTarget = path.relative(ROOT, registryPath) || registryPath;
|
|
314
315
|
|
|
315
316
|
let findings: StalenessFinding[];
|
|
317
|
+
let channelCount: number;
|
|
316
318
|
try {
|
|
317
319
|
findings = run_checks({ registryPath, today });
|
|
320
|
+
// `load_registry` already refuses a missing / unparseable file (exit 3),
|
|
321
|
+
// but a registry that PARSES with no `channels:` list is the blind case:
|
|
322
|
+
// `extract_channels` is defensive by contract and returns [], so every
|
|
323
|
+
// per-channel check iterates nothing and the gate prints the ✅ line.
|
|
324
|
+
// Re-reading the (small) YAML keeps run_checks / extract_channels — both
|
|
325
|
+
// exported and pinned by fixtures — untouched.
|
|
326
|
+
channelCount = extract_channels(load_registry(registryPath)).length;
|
|
318
327
|
} catch (err) {
|
|
319
328
|
if (err instanceof RegistryLoadError) {
|
|
320
329
|
process.stderr.write(`❌ check_reach_staleness: ${err.message}\n`);
|
|
@@ -323,6 +332,24 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
323
332
|
throw err;
|
|
324
333
|
}
|
|
325
334
|
|
|
335
|
+
// No `allowEmpty`: a registry with zero channels is a registry this gate
|
|
336
|
+
// cannot judge, and it reads exactly like a clean one. Exit 3 (unusable
|
|
337
|
+
// input) over 1 (violations) — a dead scope means the gate could not run.
|
|
338
|
+
try {
|
|
339
|
+
assertScanned({
|
|
340
|
+
gate: 'check_reach_staleness',
|
|
341
|
+
scanned: channelCount,
|
|
342
|
+
units: 'channel(s)',
|
|
343
|
+
roots: [`${path.relative(ROOT, registryPath) || registryPath} → channels[]`],
|
|
344
|
+
});
|
|
345
|
+
} catch (err) {
|
|
346
|
+
if (err instanceof DeadScopeError) {
|
|
347
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
348
|
+
return 3;
|
|
349
|
+
}
|
|
350
|
+
throw err;
|
|
351
|
+
}
|
|
352
|
+
|
|
326
353
|
if (findings.length > 0) {
|
|
327
354
|
process.stdout.write(`❌ ${relTarget}: ${findings.length} staleness violation(s):\n`);
|
|
328
355
|
for (const finding of findings) {
|
|
@@ -18,7 +18,14 @@
|
|
|
18
18
|
* Exit codes: 0 all green/absent · 1 durably-red workflow(s) · 2 usage/env.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import * as path from 'node:path';
|
|
21
22
|
import { argv, env, exit } from 'node:process';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
26
|
+
|
|
27
|
+
// src/scripts/check_release_adjacent_health.ts → two levels up is the repo root.
|
|
28
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
22
29
|
|
|
23
30
|
const WATCHED = [
|
|
24
31
|
'publish-npm.yml',
|
|
@@ -71,6 +78,27 @@ async function main(): Promise<number> {
|
|
|
71
78
|
return 2;
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
// The scope is the watch list itself. A renamed or deleted workflow answers
|
|
82
|
+
// 404 → the loop logs "no completed runs (or workflow absent) — skipping"
|
|
83
|
+
// and continues, so a fully stale WATCHED list still prints "✅ healthy" —
|
|
84
|
+
// the same silent-tripwire failure this gate exists to detect. Checking the
|
|
85
|
+
// names against `.github/workflows/` catches the rename locally, before the
|
|
86
|
+
// API can launder it into a skip. Exit 2 (usage/env) over 1 (durably red):
|
|
87
|
+
// a phantom watch list means the tripwire could not run.
|
|
88
|
+
try {
|
|
89
|
+
assertWatchlistResolves({
|
|
90
|
+
gate: 'check_release_adjacent_health',
|
|
91
|
+
candidates: WATCHED.map((wf) => `.github/workflows/${wf}`),
|
|
92
|
+
repoRoot: REPO,
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (err instanceof DeadScopeError) {
|
|
96
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
97
|
+
return 2;
|
|
98
|
+
}
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
|
|
74
102
|
// Tag/release-triggered workflows have no branch association — query
|
|
75
103
|
// without the branch filter for those; branch-scoped for PR/push ones.
|
|
76
104
|
const branchless = new Set(['publish-npm.yml', 'deploy-mcp-worker.yml', 'release.yml']);
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
stale_draft_labels,
|
|
48
48
|
} from './_lib/release_highlights.js';
|
|
49
49
|
import { extract_changelog_section } from './_lib/release_material.js';
|
|
50
|
+
import { assertScanned, assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
50
51
|
|
|
51
52
|
const _HERE = fileURLToPath(import.meta.url);
|
|
52
53
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -142,6 +143,23 @@ function main(argv: readonly string[]): number {
|
|
|
142
143
|
return 2;
|
|
143
144
|
}
|
|
144
145
|
}
|
|
146
|
+
// The changelog is the curated half of the comparison and it is read
|
|
147
|
+
// unconditionally below; a moved or renamed file would otherwise surface as
|
|
148
|
+
// an ENOENT stack trace rather than a gate verdict. Exit 2 is this gate's
|
|
149
|
+
// could-not-run code (same as a missing section); 1 means a contradiction.
|
|
150
|
+
try {
|
|
151
|
+
assertWatchlistResolves({
|
|
152
|
+
gate: 'check_release_highlights',
|
|
153
|
+
candidates: [path.relative(REPO_ROOT, path.resolve(changelogPath))],
|
|
154
|
+
repoRoot: REPO_ROOT,
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err instanceof DeadScopeError) {
|
|
158
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
159
|
+
return 2;
|
|
160
|
+
}
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
145
163
|
const section = extract_changelog_section(fs.readFileSync(changelogPath, 'utf-8'), version);
|
|
146
164
|
if (!section) {
|
|
147
165
|
process.stderr.write(`CHANGELOG carries no section for ${version}\n`);
|
|
@@ -159,6 +177,22 @@ function main(argv: readonly string[]): number {
|
|
|
159
177
|
process.stderr.write(`${(err as Error).message}\n`);
|
|
160
178
|
return 2;
|
|
161
179
|
}
|
|
180
|
+
// Every derived category comes from this span, so an empty one makes the
|
|
181
|
+
// contradiction check pass over nothing.
|
|
182
|
+
assertScanned({
|
|
183
|
+
gate: 'check_release_highlights',
|
|
184
|
+
scanned: span.length,
|
|
185
|
+
units: 'span commit(s)',
|
|
186
|
+
roots: [`git log ${from}..${to}`],
|
|
187
|
+
allowEmpty:
|
|
188
|
+
'EMPTY_VALID: git already answered for the range — a from..to it could not read ' +
|
|
189
|
+
'throws in collect_span_commits above and exits 2, so reaching here with zero ' +
|
|
190
|
+
'commits means git read the range successfully and it is genuinely empty. ' +
|
|
191
|
+
'"Empty span with _none_ everywhere is green" is a pre-registered fixture of this ' +
|
|
192
|
+
'gate, not a scope failure: with no commits there is no evidence a curated `_none_` ' +
|
|
193
|
+
'could contradict. Deleting the scan root is not the same state — that is the ' +
|
|
194
|
+
'throwing branch, not this one.',
|
|
195
|
+
});
|
|
162
196
|
// A generator-derived line that nobody rewrote is a prose gap, never a
|
|
163
197
|
// contradiction: the line's evidence is true, it is just unpolished. Warn
|
|
164
198
|
// so the omission is visible, and keep the exit code owned solely by the
|
|
@@ -20,6 +20,8 @@ import * as fs from 'node:fs';
|
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
22
22
|
|
|
23
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
24
|
+
|
|
23
25
|
const _HERE = fileURLToPath(import.meta.url);
|
|
24
26
|
// src/scripts/check_release_includes_discovery.ts → two dirs up is repo root.
|
|
25
27
|
// Mirrors Python `Path(__file__).resolve().parents[2]`.
|
|
@@ -68,17 +70,34 @@ function main(): number {
|
|
|
68
70
|
data !== null && typeof data === 'object'
|
|
69
71
|
? (data as Record<string, unknown>)['artefacts']
|
|
70
72
|
: undefined;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
// The empty-manifest check IS this gate's scan-scope assertion: a manifest
|
|
74
|
+
// listing nothing means the discovery scanner walked a dead root, and
|
|
75
|
+
// shipping it would tell every consumer the package has no artefacts.
|
|
76
|
+
const artefact_count = Array.isArray(artefacts) ? artefacts.length : 0;
|
|
77
|
+
try {
|
|
78
|
+
assertScanned({
|
|
79
|
+
gate: 'check_release_includes_discovery',
|
|
80
|
+
scanned: artefact_count,
|
|
81
|
+
units: 'artefact(s)',
|
|
82
|
+
roots: [_rel(MANIFEST)],
|
|
83
|
+
});
|
|
84
|
+
} catch (exc) {
|
|
85
|
+
if (exc instanceof DeadScopeError) {
|
|
86
|
+
// 1 is this gate's only failure code, and the wording below is the
|
|
87
|
+
// pinned py2ts message — so the DeadScopeError text is not
|
|
88
|
+
// substituted for it.
|
|
89
|
+
return _die(
|
|
90
|
+
`${_rel(MANIFEST)} carries no artefacts — discovery` +
|
|
91
|
+
' scanner produced an empty manifest.',
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
throw exc;
|
|
76
95
|
}
|
|
77
96
|
if (!_isFile(SUMMARY)) {
|
|
78
97
|
return _die(`${_rel(SUMMARY)} is missing.`);
|
|
79
98
|
}
|
|
80
99
|
process.stdout.write(
|
|
81
|
-
`check-release-discovery: OK (${
|
|
100
|
+
`check-release-discovery: OK (${artefact_count} artefacts in` +
|
|
82
101
|
` ${_rel(MANIFEST)})\n`,
|
|
83
102
|
);
|
|
84
103
|
return 0;
|
|
@@ -20,6 +20,8 @@ import * as fs from 'node:fs';
|
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
22
22
|
|
|
23
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
24
|
+
|
|
23
25
|
const _HERE = fileURLToPath(import.meta.url);
|
|
24
26
|
|
|
25
27
|
const ALLOWLIST_GLOBS = [
|
|
@@ -127,7 +129,22 @@ function _gh_diff_files(pr: string): string[] {
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
function check(files: readonly string[]): number {
|
|
130
|
-
|
|
132
|
+
// This gate already refused an empty diff by hand; routing that refusal
|
|
133
|
+
// through the shared primitive keeps the behaviour (pinned stdout line,
|
|
134
|
+
// exit 1) while making the scope assertion explicit. No `allowEmpty`: a
|
|
135
|
+
// release PR that touches nothing is the failure, not a clean state — and
|
|
136
|
+
// an empty `gh pr diff` (bad PR number, auth failure) reads identically.
|
|
137
|
+
try {
|
|
138
|
+
assertScanned({
|
|
139
|
+
gate: 'check_release_pr_shape',
|
|
140
|
+
scanned: files.length,
|
|
141
|
+
units: 'changed file(s)',
|
|
142
|
+
roots: ['gh pr diff --name-only <pr> | --files <list>'],
|
|
143
|
+
});
|
|
144
|
+
} catch (exc) {
|
|
145
|
+
if (!(exc instanceof DeadScopeError)) {
|
|
146
|
+
throw exc;
|
|
147
|
+
}
|
|
131
148
|
process.stdout.write(
|
|
132
149
|
'OUT-OF-SHAPE: empty diff — release PR must touch at least one file.\n',
|
|
133
150
|
);
|