@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
|
@@ -39,6 +39,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
39
39
|
// byte-faithful for behaviour (the only observable effect is the --global path).
|
|
40
40
|
import * as _kg from './_lib/knowledge_global.js';
|
|
41
41
|
import * as _kgr from './_lib/knowledge_global_redaction.js';
|
|
42
|
+
import { assertScanned } from './_lib/scan_scope.js';
|
|
42
43
|
|
|
43
44
|
// ROOT = Path(__file__).resolve().parent.parent.parent — src/scripts/ → repo root.
|
|
44
45
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
@@ -373,6 +374,18 @@ function main(): number {
|
|
|
373
374
|
}
|
|
374
375
|
|
|
375
376
|
const cards = _sortedGlobMd(card_dir).filter((p) => path.basename(p).toLowerCase() !== 'readme.md');
|
|
377
|
+
assertScanned({
|
|
378
|
+
gate: 'check_knowledge_cards',
|
|
379
|
+
scanned: cards.length,
|
|
380
|
+
units: 'knowledge card(s)',
|
|
381
|
+
roots: [path.relative(ROOT, card_dir) || card_dir],
|
|
382
|
+
allowEmpty:
|
|
383
|
+
'OPTIONAL_INPUT: cards are project-authored runtime content under agents/knowledge/ '
|
|
384
|
+
+ '(or a --dir / --global store), never shipped package source. Delete the root and '
|
|
385
|
+
+ 'zero still reads as "this project has captured no cards yet" — which is why both '
|
|
386
|
+
+ 'the missing-dir and empty-dir paths are pinned as exit 0 by the CLI contract '
|
|
387
|
+
+ 'below. A stale root cannot hide here: --dir is caller-supplied per invocation.',
|
|
388
|
+
});
|
|
376
389
|
if (cards.length === 0) {
|
|
377
390
|
process.stdout.write('No knowledge cards found — nothing to check.\n');
|
|
378
391
|
return 0;
|
|
@@ -29,6 +29,8 @@ import process from 'node:process';
|
|
|
29
29
|
import { fileURLToPath } from 'node:url';
|
|
30
30
|
import YAML from 'yaml';
|
|
31
31
|
|
|
32
|
+
import { assertScanned } from './_lib/scan_scope.js';
|
|
33
|
+
|
|
32
34
|
const PROG = 'check_knowledge_pages.ts';
|
|
33
35
|
const PAGE_LINE_BUDGET = 200;
|
|
34
36
|
const CONTESTED_WARN_COUNT = 2;
|
|
@@ -135,6 +137,14 @@ function listMarkdownFiles(dirPath: string): string[] {
|
|
|
135
137
|
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
/** Pages `lintAll` reads — the corpus, independent of how many warn. */
|
|
141
|
+
function countPages(knowledgeRoot: string): number {
|
|
142
|
+
return TYPED_DIRS.reduce(
|
|
143
|
+
(n, dir) => n + listMarkdownFiles(path.join(knowledgeRoot, dir)).length,
|
|
144
|
+
0,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
138
148
|
export function lintAll(knowledgeRoot: string, today: Date = new Date()): Warning[] {
|
|
139
149
|
const warnings: Warning[] = [];
|
|
140
150
|
for (const dir of TYPED_DIRS) {
|
|
@@ -187,6 +197,19 @@ export function main(argv: string[]): number {
|
|
|
187
197
|
|
|
188
198
|
const knowledgeRoot = path.join(dir, 'agents', 'knowledge');
|
|
189
199
|
const warnings = lintAll(knowledgeRoot);
|
|
200
|
+
// Pages read, not warnings emitted — this lint never fails the build, so a
|
|
201
|
+
// moved root would print "no warnings" indefinitely with nothing behind it.
|
|
202
|
+
assertScanned({
|
|
203
|
+
gate: 'check_knowledge_pages',
|
|
204
|
+
scanned: countPages(knowledgeRoot),
|
|
205
|
+
units: 'knowledge page(s)',
|
|
206
|
+
roots: TYPED_DIRS.map((d) => path.join('agents', 'knowledge', d)),
|
|
207
|
+
allowEmpty:
|
|
208
|
+
'OPTIONAL_INPUT: the lifecycle-typed pages are project-authored runtime content, '
|
|
209
|
+
+ 'not shipped package source, and the four typed subdirs are created on first '
|
|
210
|
+
+ 'capture. Delete them and zero still reads as "this project has written no '
|
|
211
|
+
+ 'knowledge pages yet" — the state every consumer starts in.',
|
|
212
|
+
});
|
|
190
213
|
|
|
191
214
|
if (format === 'json') {
|
|
192
215
|
process.stdout.write(JSON.stringify(warnings, null, 2) + '\n');
|
|
@@ -29,6 +29,8 @@ import process from 'node:process';
|
|
|
29
29
|
import { fileURLToPath } from 'node:url';
|
|
30
30
|
import YAML from 'yaml';
|
|
31
31
|
|
|
32
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
33
|
+
|
|
32
34
|
const PROG = 'check_knowledge_sharing.ts';
|
|
33
35
|
const CREATION_BUDGET_WARN = 5;
|
|
34
36
|
const FRONTMATTER_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/;
|
|
@@ -150,6 +152,31 @@ export function main(argv: string[]): number {
|
|
|
150
152
|
return 3;
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
// Diff-driven: the corpus is whatever is staged, and an empty index is the
|
|
156
|
+
// normal state of a clean tree — so zero is allowed here. The dangerous
|
|
157
|
+
// empty (git itself failing) is NOT swallowed: it exits 3 above rather than
|
|
158
|
+
// reaching this point with an empty list. Exit 3 (internal error) is the
|
|
159
|
+
// code a dead scope would share, for the same "could not run" reason.
|
|
160
|
+
try {
|
|
161
|
+
assertScanned({
|
|
162
|
+
gate: 'check_knowledge_sharing',
|
|
163
|
+
scanned: staged.length,
|
|
164
|
+
units: 'staged path(s)',
|
|
165
|
+
roots: ['git diff --cached --name-status'],
|
|
166
|
+
allowEmpty:
|
|
167
|
+
'EMPTY_VALID: nothing staged means there is no commit to judge — an absent ' +
|
|
168
|
+
'question, not an empty corpus. Deletion test: deleting agents/knowledge/ ' +
|
|
169
|
+
'would not produce this zero (the staged deletions would themselves show up ' +
|
|
170
|
+
'in the diff); only an empty index does, and that is a clean tree.',
|
|
171
|
+
});
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (err instanceof DeadScopeError) {
|
|
174
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
175
|
+
return 3;
|
|
176
|
+
}
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
|
|
153
180
|
const { blocked, warnings } = checkSharing(staged, gitStagedContent);
|
|
154
181
|
|
|
155
182
|
for (const w of warnings) {
|
|
@@ -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
|
// Umlauts and German-only characters.
|
|
24
26
|
const UMLAUT_RE = /[äöüÄÖÜß]/g;
|
|
25
27
|
|
|
@@ -247,6 +249,7 @@ export function main(argv?: readonly string[]): number {
|
|
|
247
249
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
248
250
|
|
|
249
251
|
const all_violations: Violation[] = [];
|
|
252
|
+
let scanned = 0;
|
|
250
253
|
for (const raw_path of args.paths) {
|
|
251
254
|
if (!_exists(raw_path)) {
|
|
252
255
|
process.stderr.write(`⚠️ Not found: ${raw_path}\n`);
|
|
@@ -256,9 +259,30 @@ export function main(argv?: readonly string[]): number {
|
|
|
256
259
|
process.stderr.write(`⚠️ Skipping non-.md: ${raw_path}\n`);
|
|
257
260
|
continue;
|
|
258
261
|
}
|
|
262
|
+
scanned += 1;
|
|
259
263
|
all_violations.push(...scan_file(raw_path));
|
|
260
264
|
}
|
|
261
265
|
|
|
266
|
+
// The caller expands the path list (`find docs -name '*.md' …`), so a moved
|
|
267
|
+
// docs tree hands this gate paths that every skip above swallows into a
|
|
268
|
+
// stderr warning while stdout still reports "No German content detected".
|
|
269
|
+
// Exit 3 is the documented "internal error" code — the gate could not run;
|
|
270
|
+
// 1 means it read files and found German in them.
|
|
271
|
+
try {
|
|
272
|
+
assertScanned({
|
|
273
|
+
gate: 'check_md_language',
|
|
274
|
+
scanned,
|
|
275
|
+
units: '.md file(s)',
|
|
276
|
+
roots: [`${String(args.paths.length)} caller-supplied path(s)`],
|
|
277
|
+
});
|
|
278
|
+
} catch (exc) {
|
|
279
|
+
if (exc instanceof DeadScopeError) {
|
|
280
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
281
|
+
return 3;
|
|
282
|
+
}
|
|
283
|
+
throw exc;
|
|
284
|
+
}
|
|
285
|
+
|
|
262
286
|
if (args.format === 'json') {
|
|
263
287
|
process.stdout.write(JSON.stringify(all_violations, null, 2) + '\n');
|
|
264
288
|
} else if (all_violations.length === 0) {
|
|
@@ -21,6 +21,8 @@ import * as path from 'node:path';
|
|
|
21
21
|
import * as fs from 'node:fs';
|
|
22
22
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
23
|
|
|
24
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
25
|
+
|
|
24
26
|
const _FILE = fileURLToPath(import.meta.url);
|
|
25
27
|
|
|
26
28
|
export interface MediaTool {
|
|
@@ -71,6 +73,27 @@ export function missingTools(tools: MediaTool[] = MEDIA_TOOLS): MediaTool[] {
|
|
|
71
73
|
|
|
72
74
|
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
73
75
|
const quiet = argv.includes('--quiet');
|
|
76
|
+
|
|
77
|
+
// The scan scope is the declared tool list, not a directory: an empty
|
|
78
|
+
// MEDIA_TOOLS probes nothing and still prints the ✅ "all present" line.
|
|
79
|
+
// Deletion test: emptying the declaration would legitimately yield 0 only
|
|
80
|
+
// if "no tooling is required", which is never true for a prereq check.
|
|
81
|
+
// Exit 1 is the only failure code this CLI has, so a dead scope shares it.
|
|
82
|
+
try {
|
|
83
|
+
assertScanned({
|
|
84
|
+
gate: 'check_media_deps',
|
|
85
|
+
scanned: MEDIA_TOOLS.length,
|
|
86
|
+
units: 'declared media tool(s)',
|
|
87
|
+
roots: ['MEDIA_TOOLS (declared in src/scripts/check_media_deps.ts)'],
|
|
88
|
+
});
|
|
89
|
+
} catch (exc) {
|
|
90
|
+
if (exc instanceof DeadScopeError) {
|
|
91
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
throw exc;
|
|
95
|
+
}
|
|
96
|
+
|
|
74
97
|
const missing = missingTools();
|
|
75
98
|
if (missing.length === 0) {
|
|
76
99
|
if (!quiet) {
|
|
@@ -53,6 +53,8 @@ import process from 'node:process';
|
|
|
53
53
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
54
54
|
import YAML, { parseDocument } from 'yaml';
|
|
55
55
|
|
|
56
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
57
|
+
|
|
56
58
|
type Severity = 'error' | 'warning' | 'info';
|
|
57
59
|
|
|
58
60
|
const REQUIRED_KEYS: ReadonlySet<string> = new Set([
|
|
@@ -722,11 +724,34 @@ function main(): number {
|
|
|
722
724
|
}
|
|
723
725
|
return 0;
|
|
724
726
|
}
|
|
727
|
+
// An ABSENT root stays a pinned exit-0 no-op (the branch above; the memory
|
|
728
|
+
// tree is an optional surface and `task check-memory` short-circuits it).
|
|
729
|
+
// A root that EXISTS but walks to zero `*.yml` is the case nothing covered:
|
|
730
|
+
// entries moved to a subtree this walk does not reach, and the validator
|
|
731
|
+
// reported "0 error(s)" over an empty read. No `allowEmpty` for that half —
|
|
732
|
+
// a present-but-unreadable corpus is blindness, not a clean bill of health.
|
|
733
|
+
// Exit 1 is this gate's only failure code (see `_emit`).
|
|
734
|
+
const ymls = _rglobYmlSorted(root);
|
|
735
|
+
try {
|
|
736
|
+
assertScanned({
|
|
737
|
+
gate: 'check_memory',
|
|
738
|
+
scanned: ymls.length,
|
|
739
|
+
units: 'memory entry file(s)',
|
|
740
|
+
roots: [`${root}/**/*.yml`],
|
|
741
|
+
});
|
|
742
|
+
} catch (exc) {
|
|
743
|
+
if (exc instanceof DeadScopeError) {
|
|
744
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
745
|
+
return 1;
|
|
746
|
+
}
|
|
747
|
+
throw exc;
|
|
748
|
+
}
|
|
749
|
+
|
|
725
750
|
const criticalCounts: Record<string, number> = {};
|
|
726
751
|
for (const key of Object.keys(_TYPE_COUNTS)) {
|
|
727
752
|
delete _TYPE_COUNTS[key];
|
|
728
753
|
}
|
|
729
|
-
for (const yml of
|
|
754
|
+
for (const yml of ymls) {
|
|
730
755
|
_validateFile(yml, findings, criticalCounts);
|
|
731
756
|
}
|
|
732
757
|
// Tier-0 inflation warning — soft cap on `priority: critical` per type.
|
|
@@ -34,6 +34,7 @@ import process from 'node:process';
|
|
|
34
34
|
import { fileURLToPath } from 'node:url';
|
|
35
35
|
import YAML from 'yaml';
|
|
36
36
|
|
|
37
|
+
import { assertScanned } from './_lib/scan_scope.js';
|
|
37
38
|
import { jaccardSimilarity } from './_lib/text_similarity.js';
|
|
38
39
|
|
|
39
40
|
const PROG = 'check_memory_contradiction.ts';
|
|
@@ -234,7 +235,21 @@ function main(argv: string[]): number {
|
|
|
234
235
|
|
|
235
236
|
let hit: ContradictionHit | null;
|
|
236
237
|
try {
|
|
237
|
-
|
|
238
|
+
const existing = loadCuratedEntries(path.resolve(memoryRoot), type);
|
|
239
|
+
// Curated entries READ — the corpus every verdict below is decided
|
|
240
|
+
// against. Zero of them makes "no contradiction" unfalsifiable.
|
|
241
|
+
assertScanned({
|
|
242
|
+
gate: 'check_memory_contradiction',
|
|
243
|
+
scanned: existing.length,
|
|
244
|
+
units: 'curated entry(ies)',
|
|
245
|
+
roots: [path.join(memoryRoot, type), path.join(memoryRoot, `${type}.yml`)],
|
|
246
|
+
allowEmpty:
|
|
247
|
+
'OPTIONAL_INPUT: the per-type curated store is created by the first '
|
|
248
|
+
+ '/memory promote of that type. Delete it and zero still reads as "nothing '
|
|
249
|
+
+ 'of this type has been curated yet" — a state every consumer starts in, and '
|
|
250
|
+
+ 'one in which no contradiction can exist by definition.',
|
|
251
|
+
});
|
|
252
|
+
hit = findContradiction(key, body, existing);
|
|
238
253
|
} catch (exc) {
|
|
239
254
|
process.stderr.write(`${PROG}: internal error: ${String(exc)}\n`);
|
|
240
255
|
return 3;
|
|
@@ -16,6 +16,8 @@ import * as path from 'node:path';
|
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
17
|
import YAML from 'yaml';
|
|
18
18
|
|
|
19
|
+
import { assertScanned } from './_lib/scan_scope.js';
|
|
20
|
+
|
|
19
21
|
const _HERE = fileURLToPath(import.meta.url);
|
|
20
22
|
|
|
21
23
|
// Relative to cwd, mirroring Python `Path("agents/memory/intake")`.
|
|
@@ -296,6 +298,19 @@ function _json_dumps_ascii(obj: unknown): string {
|
|
|
296
298
|
|
|
297
299
|
function main(argv?: readonly string[]): number {
|
|
298
300
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
301
|
+
// Intake FILES walked — the scope both `_find_intake` (record lookup) and
|
|
302
|
+
// `_count_sibling_paths` (pattern evidence) share.
|
|
303
|
+
assertScanned({
|
|
304
|
+
gate: 'check_memory_proposal',
|
|
305
|
+
scanned: _jsonlFilesSorted().length,
|
|
306
|
+
units: 'intake signal file(s)',
|
|
307
|
+
roots: [INTAKE_ROOT],
|
|
308
|
+
allowEmpty:
|
|
309
|
+
'OPTIONAL_INPUT: the intake dir is local, gitignored capture written by '
|
|
310
|
+
+ '/memory propose. Delete it and zero still reads as "nothing captured yet". '
|
|
311
|
+
+ 'An empty scope also tightens rather than relaxes this gate: zero sibling '
|
|
312
|
+
+ 'paths forces the future_decisions branch, and --intake-id already fails 1.',
|
|
313
|
+
});
|
|
299
314
|
let record: Record_;
|
|
300
315
|
let source: string;
|
|
301
316
|
if (args.intake_id !== null) {
|
|
@@ -22,6 +22,7 @@ import * as path from 'node:path';
|
|
|
22
22
|
import process from 'node:process';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
24
|
|
|
25
|
+
import { assertScanned } from './_lib/scan_scope.js';
|
|
25
26
|
import { findMostSimilar, type Candidate } from './_lib/text_similarity.js';
|
|
26
27
|
|
|
27
28
|
const PROG = 'check_memory_similarity.ts';
|
|
@@ -106,6 +107,21 @@ export function main(argv: string[]): number {
|
|
|
106
107
|
return 2;
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
// The unit is the signal FILES the glob walks, not the type-filtered
|
|
111
|
+
// candidates: the first entry of a given type legitimately has zero
|
|
112
|
+
// candidates, so only the unfiltered walk distinguishes that from a root
|
|
113
|
+
// that moved and turned every proposal into a silent "create".
|
|
114
|
+
assertScanned({
|
|
115
|
+
gate: 'check_memory_similarity',
|
|
116
|
+
scanned: globSignals(intakeRoot).length,
|
|
117
|
+
units: 'intake signal file(s)',
|
|
118
|
+
roots: [intakeRoot],
|
|
119
|
+
allowEmpty:
|
|
120
|
+
'OPTIONAL_INPUT: intake signals are local, gitignored capture written by '
|
|
121
|
+
+ '/memory propose — never shipped source. Delete the root and zero still reads '
|
|
122
|
+
+ 'as "nothing has been captured in this project yet", which is exactly the '
|
|
123
|
+
+ 'state of the very first proposal. Advisory check: it never blocks a create.',
|
|
124
|
+
});
|
|
109
125
|
const candidates = loadCandidates(intakeRoot, type);
|
|
110
126
|
const match = findMostSimilar(body, candidates);
|
|
111
127
|
|
|
@@ -27,6 +27,8 @@ import * as fs from 'node:fs';
|
|
|
27
27
|
import * as path from 'node:path';
|
|
28
28
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
29
29
|
|
|
30
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
31
|
+
|
|
30
32
|
const _HERE = fileURLToPath(import.meta.url);
|
|
31
33
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
32
34
|
const SKILL_PATH = path.join(
|
|
@@ -55,14 +57,6 @@ const BODY_BANNED_PATTERNS: readonly BannedPattern[] = [
|
|
|
55
57
|
{ re: /App\\\\Modules\\\\/, source: 'App\\\\\\\\Modules\\\\\\\\' },
|
|
56
58
|
];
|
|
57
59
|
|
|
58
|
-
function _isFile(p: string): boolean {
|
|
59
|
-
try {
|
|
60
|
-
return fs.statSync(p).isFile();
|
|
61
|
-
} catch {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
60
|
/** POSIX relative path of `child` under `root` (mirrors relative_to().as_posix-ish for display). */
|
|
67
61
|
function _relToPosix(child: string, root: string): string {
|
|
68
62
|
return path.relative(root, child).split(path.sep).join('/');
|
|
@@ -193,9 +187,23 @@ function _scan_frontmatter(fm: string): string[] {
|
|
|
193
187
|
}
|
|
194
188
|
|
|
195
189
|
function main(): number {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
190
|
+
// One named file is the entire scan scope, so `_isFile` was this gate's
|
|
191
|
+
// zero-check; the shared assertion replaces it and names the scope. The
|
|
192
|
+
// ported message is kept verbatim ahead of it (the py2ts CLI contract pins
|
|
193
|
+
// it) and exit 2 is unchanged.
|
|
194
|
+
try {
|
|
195
|
+
assertWatchlistResolves({
|
|
196
|
+
gate: 'check_module_management_neutral',
|
|
197
|
+
candidates: [_relToPosix(SKILL_PATH, REPO_ROOT)],
|
|
198
|
+
repoRoot: REPO_ROOT,
|
|
199
|
+
});
|
|
200
|
+
} catch (exc) {
|
|
201
|
+
if (exc instanceof DeadScopeError) {
|
|
202
|
+
process.stderr.write(`error: SKILL.md not found at ${SKILL_PATH}\n`);
|
|
203
|
+
process.stderr.write(` ${exc.message}\n`);
|
|
204
|
+
return 2;
|
|
205
|
+
}
|
|
206
|
+
throw exc;
|
|
199
207
|
}
|
|
200
208
|
const text = fs.readFileSync(SKILL_PATH, 'utf-8');
|
|
201
209
|
const [fm, body] = _split_frontmatter(text);
|
|
@@ -44,6 +44,8 @@ import * as path from 'node:path';
|
|
|
44
44
|
import process from 'node:process';
|
|
45
45
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
46
46
|
|
|
47
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
48
|
+
|
|
47
49
|
const _HERE = fileURLToPath(import.meta.url);
|
|
48
50
|
// Path(__file__).resolve().parents[2] — repo root, two dirs up from src/scripts.
|
|
49
51
|
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -174,6 +176,25 @@ function main(argv?: readonly string[]): number {
|
|
|
174
176
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
175
177
|
|
|
176
178
|
const allow = load_allowlist();
|
|
179
|
+
const tracked = tracked_text_files();
|
|
180
|
+
// `_git` swallows every failure and returns "", so a run outside a work
|
|
181
|
+
// tree — or against a repo whose index this process cannot read — walks an
|
|
182
|
+
// empty file list, finds no markers, and reports the tree clean. The count
|
|
183
|
+
// the success line already prints was the evidence; nothing asserted on it.
|
|
184
|
+
try {
|
|
185
|
+
assertScanned({
|
|
186
|
+
gate: 'check_no_conflict_markers',
|
|
187
|
+
scanned: tracked.length,
|
|
188
|
+
units: 'tracked file(s)',
|
|
189
|
+
roots: [`${REPO} (git ls-files)`],
|
|
190
|
+
});
|
|
191
|
+
} catch (exc) {
|
|
192
|
+
if (exc instanceof DeadScopeError) {
|
|
193
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
throw exc;
|
|
197
|
+
}
|
|
177
198
|
const unmerged = unmerged_paths();
|
|
178
199
|
const markerHits = scan_markers(allow);
|
|
179
200
|
|
|
@@ -208,7 +229,7 @@ function main(argv?: readonly string[]): number {
|
|
|
208
229
|
if (!args.quiet) {
|
|
209
230
|
process.stdout.write(
|
|
210
231
|
`✅ check_no_conflict_markers: no conflicted index entries, no markers ` +
|
|
211
|
-
`(${
|
|
232
|
+
`(${tracked.length} tracked files scanned).\n`,
|
|
212
233
|
);
|
|
213
234
|
}
|
|
214
235
|
return 0;
|
|
@@ -36,6 +36,8 @@ import * as path from 'node:path';
|
|
|
36
36
|
import process from 'node:process';
|
|
37
37
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
38
|
|
|
39
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
40
|
+
|
|
39
41
|
const _HERE = fileURLToPath(import.meta.url);
|
|
40
42
|
// parents[2] of src/scripts/<file> is the repo root.
|
|
41
43
|
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -272,8 +274,31 @@ function main(argv: readonly string[]): number {
|
|
|
272
274
|
const patterns: Array<[string, RegExp]> = cfg.deny.map((p) => [p, new RegExp(p, 'i')]);
|
|
273
275
|
const skipGlobs = cfg.skip_paths ?? [];
|
|
274
276
|
|
|
277
|
+
// `_tracked_files` raises when git itself fails, but a git that succeeds
|
|
278
|
+
// with empty output (not a repo, nothing tracked) yields the same empty
|
|
279
|
+
// loop and the ✅ "no external-source references" line. Not a diff — this
|
|
280
|
+
// is the whole tracked tree, so zero is never a legitimate state; no
|
|
281
|
+
// `allowEmpty`. Assert the unfiltered enumeration, before the extension /
|
|
282
|
+
// skip_paths filters, so a filter change can never mask a dead scope.
|
|
283
|
+
// Exit 1 is the only failure code this pinned CLI reaches.
|
|
284
|
+
const tracked = _tracked_files();
|
|
285
|
+
try {
|
|
286
|
+
assertScanned({
|
|
287
|
+
gate: 'check_no_external_sources',
|
|
288
|
+
scanned: tracked.length,
|
|
289
|
+
units: 'tracked file(s)',
|
|
290
|
+
roots: ['git ls-files'],
|
|
291
|
+
});
|
|
292
|
+
} catch (exc) {
|
|
293
|
+
if (exc instanceof DeadScopeError) {
|
|
294
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
throw exc;
|
|
298
|
+
}
|
|
299
|
+
|
|
275
300
|
const hits: Hit[] = [];
|
|
276
|
-
for (const rel of
|
|
301
|
+
for (const rel of tracked) {
|
|
277
302
|
if (_SKIP_EXT.has(_suffixLower(rel))) {
|
|
278
303
|
continue;
|
|
279
304
|
}
|
|
@@ -21,29 +21,56 @@ import * as path from 'node:path';
|
|
|
21
21
|
import * as fs from 'node:fs';
|
|
22
22
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
23
|
|
|
24
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
25
|
+
|
|
24
26
|
const _HERE = fileURLToPath(import.meta.url);
|
|
25
27
|
|
|
26
28
|
const LOCAL_FILE = '.agent-settings.local.yml';
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
/** Every tracked path — the unfiltered corpus the basename match runs over. */
|
|
31
|
+
function _tracked_files(): string[] {
|
|
29
32
|
const res = spawnSync('git', ['ls-files'], { encoding: 'utf-8' });
|
|
30
33
|
// Mirror Python: CalledProcessError (non-zero) or FileNotFoundError
|
|
31
34
|
// (git missing) → return [] (nothing to enforce).
|
|
32
35
|
if (res.error !== undefined || res.status !== 0 || typeof res.stdout !== 'string') {
|
|
33
36
|
return [];
|
|
34
37
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
return res.stdout.split('\n').filter((line) => line !== '');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _isLocalSettings(line: string): boolean {
|
|
42
|
+
const parts = line.split('/');
|
|
43
|
+
return parts[parts.length - 1] === LOCAL_FILE;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function tracked_local_settings(): string[] {
|
|
47
|
+
return _tracked_files().filter(_isLocalSettings);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
function main(): number {
|
|
46
|
-
|
|
51
|
+
// Not a diff — `git ls-files` enumerates the WHOLE tracked tree, so zero
|
|
52
|
+
// lines means git failed or this is not a repo, not "nothing committed".
|
|
53
|
+
// The swallow above turns both into an empty list, and the gate then prints
|
|
54
|
+
// the ✅ line. Deletion test: a repo with zero tracked files is never a
|
|
55
|
+
// legitimate state here, so no `allowEmpty`. Exit 1 is the only failure
|
|
56
|
+
// code this pinned CLI has.
|
|
57
|
+
const tracked = _tracked_files();
|
|
58
|
+
try {
|
|
59
|
+
assertScanned({
|
|
60
|
+
gate: 'check_no_local_settings_committed',
|
|
61
|
+
scanned: tracked.length,
|
|
62
|
+
units: 'tracked file(s)',
|
|
63
|
+
roots: ['git ls-files'],
|
|
64
|
+
});
|
|
65
|
+
} catch (exc) {
|
|
66
|
+
if (exc instanceof DeadScopeError) {
|
|
67
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
throw exc;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const offenders = tracked.filter(_isLocalSettings);
|
|
47
74
|
if (offenders.length === 0) {
|
|
48
75
|
process.stdout.write(`✅ No tracked ${LOCAL_FILE} files.\n`);
|
|
49
76
|
return 0;
|
|
@@ -46,6 +46,8 @@ import * as path from 'node:path';
|
|
|
46
46
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
47
|
|
|
48
48
|
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
49
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
50
|
+
import { readStdinText } from './_lib/stdin.js';
|
|
49
51
|
|
|
50
52
|
const LEGACY = '.agent-src.uncondensed/';
|
|
51
53
|
const EXEMPT: ReadonlySet<string> = new Set([
|
|
@@ -263,6 +265,31 @@ export function find_hardcoded_scan_roots(
|
|
|
263
265
|
* live number, which is the whole point of committing it.
|
|
264
266
|
*/
|
|
265
267
|
export function checkHardcodedScanRoots(repoRoot: string = process.cwd()): number {
|
|
268
|
+
// The scanned unit is script FILES walked, never `findings.length` — this
|
|
269
|
+
// pass is a downward ratchet, so zero findings is the goal state and can
|
|
270
|
+
// never distinguish "clean" from "the walk root moved". `_walkScripts`
|
|
271
|
+
// swallows an unreadable directory, so a relocated `src/scripts/` yields an
|
|
272
|
+
// empty walk, an actual of 0, a verdict under baseline, and a ✅ — this gate
|
|
273
|
+
// failing the same way it exists to detect. No `allowEmpty`: a repo with no
|
|
274
|
+
// scripts under the scan dir is blindness, not cleanliness.
|
|
275
|
+
// Exit 2 (git/environment failure) over 1 (above baseline) — a dead scope
|
|
276
|
+
// means the pass could not run; `main` propagates it via Math.max.
|
|
277
|
+
const walked = [..._walkScripts(path.join(repoRoot, HARDCODED_ROOT_SCAN_DIR))];
|
|
278
|
+
try {
|
|
279
|
+
assertScanned({
|
|
280
|
+
gate: HARDCODED_ROOT_GATE,
|
|
281
|
+
scanned: walked.length,
|
|
282
|
+
units: 'script file(s)',
|
|
283
|
+
roots: [`${HARDCODED_ROOT_SCAN_DIR}/**/*.{ts,mts,mjs}`],
|
|
284
|
+
});
|
|
285
|
+
} catch (exc) {
|
|
286
|
+
if (exc instanceof DeadScopeError) {
|
|
287
|
+
process.stdout.write(`❌ ${exc.message}\n`);
|
|
288
|
+
return 2;
|
|
289
|
+
}
|
|
290
|
+
throw exc;
|
|
291
|
+
}
|
|
292
|
+
|
|
266
293
|
const findings = find_hardcoded_scan_roots(repoRoot);
|
|
267
294
|
const verdict = checkRatchet({
|
|
268
295
|
gate: HARDCODED_ROOT_GATE,
|
|
@@ -293,7 +320,9 @@ function main(): number {
|
|
|
293
320
|
// documented failure the kernel-bundle step sidesteps the same way).
|
|
294
321
|
let offenders: string[];
|
|
295
322
|
if (process.argv.includes('--stdin')) {
|
|
296
|
-
|
|
323
|
+
// `readFileSync(0)` crashes with EAGAIN once the diff outgrows the pipe
|
|
324
|
+
// buffer — this gate's own PR was the first big enough to prove it.
|
|
325
|
+
const stdin = readStdinText();
|
|
297
326
|
offenders = find_offenders(stdin);
|
|
298
327
|
} else {
|
|
299
328
|
const base = _base();
|
|
@@ -29,6 +29,8 @@ import * as fs from 'node:fs';
|
|
|
29
29
|
import * as path from 'node:path';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
31
|
|
|
32
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
33
|
+
|
|
32
34
|
// src/scripts/check_one_off_location.ts → two dirs up is the repo root.
|
|
33
35
|
// Mirrors the Python `Path(__file__).resolve().parent.parent.parent`.
|
|
34
36
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -73,6 +75,28 @@ function _rglobOneOff(root: string): string[] {
|
|
|
73
75
|
return out;
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
/** Files the guard's walk visits under `root` — the scope, not the matches. */
|
|
79
|
+
function _countFilesUnder(root: string): number {
|
|
80
|
+
let n = 0;
|
|
81
|
+
const walk = (dir: string): void => {
|
|
82
|
+
let entries: fs.Dirent[];
|
|
83
|
+
try {
|
|
84
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
85
|
+
} catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const ent of entries) {
|
|
89
|
+
if (ent.isDirectory()) {
|
|
90
|
+
walk(path.join(dir, ent.name));
|
|
91
|
+
} else {
|
|
92
|
+
n += 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
walk(root);
|
|
97
|
+
return n;
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
function _isFile(p: string): boolean {
|
|
77
101
|
try {
|
|
78
102
|
return fs.statSync(p).isFile();
|
|
@@ -126,6 +150,25 @@ function parse_args(argv: readonly string[]): Args {
|
|
|
126
150
|
export function main(argv?: readonly string[]): number {
|
|
127
151
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
128
152
|
|
|
153
|
+
// The unit is every file the walk visits, NOT the `_one_off_*.py` matches:
|
|
154
|
+
// zero matches is the normal clean state (the archive is empty today), so
|
|
155
|
+
// only the unfiltered walk can tell "nothing to move" from "nothing read".
|
|
156
|
+
try {
|
|
157
|
+
assertScanned({
|
|
158
|
+
gate: 'check_one_off_location',
|
|
159
|
+
scanned: _countFilesUnder(SCRIPTS),
|
|
160
|
+
units: 'file(s)',
|
|
161
|
+
roots: ['src/scripts'],
|
|
162
|
+
});
|
|
163
|
+
} catch (exc) {
|
|
164
|
+
if (exc instanceof DeadScopeError) {
|
|
165
|
+
// 1 = violation (3 is reserved for an internal error below).
|
|
166
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
throw exc;
|
|
170
|
+
}
|
|
171
|
+
|
|
129
172
|
let violations: string[];
|
|
130
173
|
try {
|
|
131
174
|
violations = find_violations();
|