@event4u/agent-config 9.20.0 → 9.21.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 +51 -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 +1 -1
- 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 +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- 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/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/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/check_bite_sized_granularity.ts +0 -116
- package/src/scripts/lint_workspace_boundary.ts +0 -267
|
@@ -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();
|
|
@@ -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) {
|