@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
|
@@ -25,6 +25,8 @@ import * as path from 'node:path';
|
|
|
25
25
|
import process from 'node:process';
|
|
26
26
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
27
|
|
|
28
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
29
|
+
|
|
28
30
|
const _HERE = fileURLToPath(import.meta.url);
|
|
29
31
|
|
|
30
32
|
// src/scripts/skill_usage_collect.ts → parents[2] of the .py file is repo root.
|
|
@@ -405,6 +407,25 @@ export function main(argv: string[] | null = null): number {
|
|
|
405
407
|
return 0;
|
|
406
408
|
}
|
|
407
409
|
const known = load_known_slugs(REPO);
|
|
410
|
+
// The asserted unit is the in-repo slug vocabulary, not the session files
|
|
411
|
+
// above: sessions live under $HOME and are legitimately absent on a fresh
|
|
412
|
+
// clone or in CI (handled by the early return), whereas a vanished skills
|
|
413
|
+
// projection silently degrades every mention to the path-regex subset and
|
|
414
|
+
// still reports a successful collection.
|
|
415
|
+
try {
|
|
416
|
+
assertScanned({
|
|
417
|
+
gate: 'skill_usage_collect',
|
|
418
|
+
scanned: known.size,
|
|
419
|
+
units: 'known skill slug(s)',
|
|
420
|
+
roots: ['.augment/skills', '.claude/skills', 'dist/agent-src/skills'],
|
|
421
|
+
});
|
|
422
|
+
} catch (exc) {
|
|
423
|
+
if (exc instanceof DeadScopeError) {
|
|
424
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
425
|
+
return 2;
|
|
426
|
+
}
|
|
427
|
+
throw exc;
|
|
428
|
+
}
|
|
408
429
|
const seen = new Set<string>();
|
|
409
430
|
fs.mkdirSync(path.dirname(args.out), { recursive: true });
|
|
410
431
|
if (fs.existsSync(args.out)) {
|
|
@@ -18,6 +18,8 @@ import * as fs from 'node:fs';
|
|
|
18
18
|
import * as path from 'node:path';
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
20
|
|
|
21
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
22
|
+
|
|
21
23
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
22
24
|
// parent.parent.parent of the .py file (src/scripts/skill_usage_report.py)
|
|
23
25
|
// is the repo root — two dirs up from src/scripts.
|
|
@@ -332,6 +334,25 @@ export function main(argv?: string[]): number {
|
|
|
332
334
|
const now = new Date();
|
|
333
335
|
const per = aggregate(records, now, args.window);
|
|
334
336
|
const known = all_known_slugs(REPO);
|
|
337
|
+
// Same split as `skill_usage_collect`: the JSONL is legitimately empty
|
|
338
|
+
// before any turn is recorded, but a vanished skills projection makes the
|
|
339
|
+
// report list only the slugs that happen to appear in the metrics — and a
|
|
340
|
+
// report certifying over nothing is the same lie in a quieter voice.
|
|
341
|
+
try {
|
|
342
|
+
assertScanned({
|
|
343
|
+
gate: 'skill_usage_report',
|
|
344
|
+
scanned: known.size,
|
|
345
|
+
units: 'known skill slug(s)',
|
|
346
|
+
roots: ['.augment/skills', '.claude/skills', 'dist/agent-src/skills'],
|
|
347
|
+
});
|
|
348
|
+
} catch (exc) {
|
|
349
|
+
if (exc instanceof DeadScopeError) {
|
|
350
|
+
// 2 is the only non-zero code this CLI has (argparse errors).
|
|
351
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
352
|
+
return 2;
|
|
353
|
+
}
|
|
354
|
+
throw exc;
|
|
355
|
+
}
|
|
335
356
|
fs.mkdirSync(path.dirname(args.out), { recursive: true });
|
|
336
357
|
fs.writeFileSync(args.out, render(per, known), 'utf-8');
|
|
337
358
|
if (!args.quiet) {
|
|
@@ -103,6 +103,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
103
103
|
|
|
104
104
|
import * as agentSrc from './_lib/agent_src.js';
|
|
105
105
|
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
106
|
+
import { matchesGatePattern } from './_lib/gate_population.js';
|
|
106
107
|
|
|
107
108
|
const _HERE = fileURLToPath(import.meta.url);
|
|
108
109
|
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -110,8 +111,15 @@ const DEFAULT_SCRIPTS = path.join(REPO, 'src', 'scripts');
|
|
|
110
111
|
const DEFAULT_LEDGER = path.join(REPO, 'agents', 'evidence', 'sweep-dispositions.json');
|
|
111
112
|
const LEDGER_CAP = 15;
|
|
112
113
|
|
|
113
|
-
/**
|
|
114
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Population: shared with the ratchet and the registration test.
|
|
116
|
+
*
|
|
117
|
+
* This file used to carry its own `(lint|audit|check|verify)_` regex and the
|
|
118
|
+
* three sites disagreed — 223 / 225 / 232 gates. Reading one definition is what
|
|
119
|
+
* makes "the census covers every gate the ratchet counts" a fact rather than a
|
|
120
|
+
* hope; see `_lib/gate_population.ts` for what the filter deliberately includes.
|
|
121
|
+
*/
|
|
122
|
+
const isGateFile = matchesGatePattern;
|
|
115
123
|
|
|
116
124
|
/** Identifiers accepted as a repo-root base for a literal join. */
|
|
117
125
|
const BASE_IDS: ReadonlySet<string> = new Set([
|
|
@@ -806,7 +814,7 @@ export function main(argv: string[]): number {
|
|
|
806
814
|
}
|
|
807
815
|
|
|
808
816
|
const pop = fs.existsSync(args.scripts)
|
|
809
|
-
? fs.readdirSync(args.scripts).filter(
|
|
817
|
+
? fs.readdirSync(args.scripts).filter(isGateFile).sort()
|
|
810
818
|
: [];
|
|
811
819
|
// Census needs every gate's roots; the finding report needs only the ones
|
|
812
820
|
// with hits. Keep both, and filter at the point of use rather than dropping
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
_SKIP_NAMES,
|
|
26
26
|
type Snapshot,
|
|
27
27
|
} from './snapshot_agent_outputs.js';
|
|
28
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
28
29
|
|
|
29
30
|
const _HERE = fileURLToPath(import.meta.url);
|
|
30
31
|
|
|
@@ -362,6 +363,29 @@ export function main(argv: string[] | null = null): number {
|
|
|
362
363
|
const before = JSON.parse(fs.readFileSync(args.snapshot, 'utf-8')) as Obj;
|
|
363
364
|
const after: Snapshot = _build_snapshot();
|
|
364
365
|
|
|
366
|
+
// The live half of the comparison. If both projection trees are gone the
|
|
367
|
+
// diff has nothing to disagree about and the gate reports "byte-identity
|
|
368
|
+
// OK — 0 files". Summed rather than asserted per tree: a worktree that
|
|
369
|
+
// never had `.augment` generated is a normal state, an empty BOTH is not.
|
|
370
|
+
// Exit 2 is the existing "an input is not there" code (snapshot-missing
|
|
371
|
+
// above); 1 stays "a regression was found".
|
|
372
|
+
try {
|
|
373
|
+
assertScanned({
|
|
374
|
+
gate: 'verify_physical_move',
|
|
375
|
+
scanned:
|
|
376
|
+
Object.keys(after.trees['dist/agent-src'] ?? {}).length +
|
|
377
|
+
Object.keys(after.trees['.augment'] ?? {}).length,
|
|
378
|
+
units: 'projected file(s)',
|
|
379
|
+
roots: ['dist/agent-src', '.augment'],
|
|
380
|
+
});
|
|
381
|
+
} catch (e) {
|
|
382
|
+
if (e instanceof DeadScopeError) {
|
|
383
|
+
process.stderr.write(`ERROR: ${e.message}\n`);
|
|
384
|
+
return 2;
|
|
385
|
+
}
|
|
386
|
+
throw e;
|
|
387
|
+
}
|
|
388
|
+
|
|
365
389
|
_normalise_loaded_snapshot(before);
|
|
366
390
|
|
|
367
391
|
const beforeTrees = _asObj(before['trees']) ?? {};
|
|
@@ -16,11 +16,18 @@
|
|
|
16
16
|
* scope-overlap Scan every worktree's .worktree-scope.md and
|
|
17
17
|
* report pairwise `owns:` overlaps (the hazard
|
|
18
18
|
* /worktree status + verify must surface).
|
|
19
|
+
* inventory Classify EVERY worktree safe / review / live and
|
|
20
|
+
* report the counts, the review reasons, and a
|
|
21
|
+
* prepared removal plan for the safe set. Reporting
|
|
22
|
+
* only — it never removes anything, because bulk
|
|
23
|
+
* worktree + branch deletion is a Hard-Floor action
|
|
24
|
+
* (`non-destructive-by-default`) that needs the
|
|
25
|
+
* user's explicit this-turn approval.
|
|
19
26
|
*
|
|
20
27
|
* Reachability counts ALL other refs — local branches, remotes, AND tags
|
|
21
28
|
* (a branch whose only other ref is a tag is still reachable elsewhere).
|
|
22
|
-
* Exit codes: 0 = allowed / no overlap, 1 = refused /
|
|
23
|
-
* 2 = usage error, 3 = internal error.
|
|
29
|
+
* Exit codes: 0 = allowed / no overlap / inventory reported, 1 = refused /
|
|
30
|
+
* overlap found, 2 = usage error, 3 = internal error.
|
|
24
31
|
*/
|
|
25
32
|
|
|
26
33
|
import { execFileSync } from 'node:child_process';
|
|
@@ -53,6 +60,17 @@ function tryGit(cwd: string, args: string[]): string | null {
|
|
|
53
60
|
}
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
/**
|
|
64
|
+
* `git status` with the index left alone. Plain `git status` refreshes the
|
|
65
|
+
* on-disk index, which bumps its mtime — and the inventory mode reads that
|
|
66
|
+
* mtime as the "another session is working here" signal. Without
|
|
67
|
+
* `--no-optional-locks` the check corrupts the very signal it is measured
|
|
68
|
+
* against: two consecutive inventory runs moved 10 worktrees from safe to live.
|
|
69
|
+
*/
|
|
70
|
+
function statusPorcelain(cwd: string): string {
|
|
71
|
+
return git(cwd, ['--no-optional-locks', 'status', '--porcelain']);
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
/** Refs (heads + remotes + tags) EXCLUDING the given branch's own head ref. */
|
|
57
75
|
function otherRefs(cwd: string, branch: string): string[] {
|
|
58
76
|
const out = git(cwd, ['for-each-ref', '--format=%(refname)']);
|
|
@@ -77,7 +95,7 @@ export function checkWorktree(worktreePath: string): CheckResult {
|
|
|
77
95
|
}
|
|
78
96
|
const branch = headRef.trim().replace(/^refs\/heads\//, '');
|
|
79
97
|
|
|
80
|
-
const status =
|
|
98
|
+
const status = statusPorcelain(worktreePath);
|
|
81
99
|
if (status.trim().length > 0) {
|
|
82
100
|
const files = status
|
|
83
101
|
.trimEnd()
|
|
@@ -194,9 +212,260 @@ export function findScopeOverlaps(repoPath: string): ScopeOverlap[] {
|
|
|
194
212
|
return overlaps;
|
|
195
213
|
}
|
|
196
214
|
|
|
215
|
+
/**
|
|
216
|
+
* A worktree is "live" when another session may be working in it. Git touches
|
|
217
|
+
* the per-worktree index on almost every command, so its mtime is the cheapest
|
|
218
|
+
* available activity signal.
|
|
219
|
+
*/
|
|
220
|
+
export const LIVE_WINDOW_HOURS = 48;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The two conventional worktree roots, relative to the repo. Anything else is
|
|
224
|
+
* a non-standard location: it is not wrong, but it is excluded from the safe
|
|
225
|
+
* set on purpose — worktrees that sit beside the repo can be mistaken for
|
|
226
|
+
* sibling packages, so their removal is a judgement the maintainer makes.
|
|
227
|
+
*/
|
|
228
|
+
export const STANDARD_WORKTREE_DIRS = ['.claude/worktrees', '.worktrees'] as const;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Symlink-resolved absolute path, falling back to a plain resolve when the
|
|
232
|
+
* path is gone. Git reports worktree paths as realpaths, so a repo reached
|
|
233
|
+
* through a symlinked parent (`/tmp` → `/private/tmp` on macOS, or any
|
|
234
|
+
* symlinked checkout root) would otherwise fail every path comparison and
|
|
235
|
+
* mis-report conventional worktrees as non-standard.
|
|
236
|
+
*/
|
|
237
|
+
function canonical(p: string): string {
|
|
238
|
+
const abs = path.resolve(p);
|
|
239
|
+
const tail: string[] = [];
|
|
240
|
+
let head = abs;
|
|
241
|
+
for (;;) {
|
|
242
|
+
try {
|
|
243
|
+
// Resolve the longest existing ancestor, then re-append the rest —
|
|
244
|
+
// a registration pointing at an already-deleted directory still has
|
|
245
|
+
// to classify its location correctly.
|
|
246
|
+
const real = fs.realpathSync(head);
|
|
247
|
+
return tail.length === 0 ? real : path.join(real, ...tail);
|
|
248
|
+
} catch {
|
|
249
|
+
const parent = path.dirname(head);
|
|
250
|
+
if (parent === head) return abs;
|
|
251
|
+
tail.unshift(path.basename(head));
|
|
252
|
+
head = parent;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export type Classification = 'safe' | 'review' | 'live';
|
|
258
|
+
|
|
259
|
+
export interface InventoryRow {
|
|
260
|
+
path: string;
|
|
261
|
+
branch: string | null;
|
|
262
|
+
classification: Classification;
|
|
263
|
+
/** Why it is not safe. Empty exactly when `classification === 'safe'`. */
|
|
264
|
+
reasons: string[];
|
|
265
|
+
/** The branch is an ancestor of the trunk, so `git branch -d` is safe. */
|
|
266
|
+
mergedIntoTrunk: boolean;
|
|
267
|
+
standardLocation: boolean;
|
|
268
|
+
/** Main worktree — never removable, and never counted as residue. */
|
|
269
|
+
isMain: boolean;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface Inventory {
|
|
273
|
+
trunk: string;
|
|
274
|
+
rows: InventoryRow[];
|
|
275
|
+
counts: {
|
|
276
|
+
total: number;
|
|
277
|
+
safe: number;
|
|
278
|
+
review: number;
|
|
279
|
+
live: number;
|
|
280
|
+
/** Review rows grouped by reason, highest first. */
|
|
281
|
+
reviewReasons: Record<string, number>;
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** First existing trunk ref, preferring the remote (the merge authority). */
|
|
286
|
+
export function resolveTrunk(repoPath: string): string {
|
|
287
|
+
for (const ref of [
|
|
288
|
+
'refs/remotes/origin/main',
|
|
289
|
+
'refs/remotes/origin/master',
|
|
290
|
+
'refs/heads/main',
|
|
291
|
+
'refs/heads/master',
|
|
292
|
+
]) {
|
|
293
|
+
if (tryGit(repoPath, ['rev-parse', '--verify', '--quiet', ref]) !== null) return ref;
|
|
294
|
+
}
|
|
295
|
+
return 'HEAD';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function isAncestor(repoPath: string, branch: string, trunk: string): boolean {
|
|
299
|
+
try {
|
|
300
|
+
git(repoPath, ['merge-base', '--is-ancestor', branch, trunk]);
|
|
301
|
+
return true;
|
|
302
|
+
} catch {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function isStandardLocation(repoPath: string, worktreePath: string): boolean {
|
|
308
|
+
const rel = path.relative(canonical(repoPath), canonical(worktreePath));
|
|
309
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) return false;
|
|
310
|
+
return STANDARD_WORKTREE_DIRS.some(
|
|
311
|
+
(dir) => rel === dir || rel.startsWith(`${dir}${path.sep}`),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Per-worktree git-dir mtime, or null when it cannot be read. A linked
|
|
317
|
+
* worktree's `.git` is a file holding `gitdir: <path>`; the main worktree's is
|
|
318
|
+
* a directory.
|
|
319
|
+
*/
|
|
320
|
+
function lastGitActivity(worktreePath: string): Date | null {
|
|
321
|
+
const dotGit = path.join(worktreePath, '.git');
|
|
322
|
+
let gitDir: string;
|
|
323
|
+
try {
|
|
324
|
+
const st = fs.statSync(dotGit);
|
|
325
|
+
if (st.isDirectory()) {
|
|
326
|
+
gitDir = dotGit;
|
|
327
|
+
} else {
|
|
328
|
+
const m = fs.readFileSync(dotGit, 'utf-8').match(/^gitdir:\s*(.+)$/m);
|
|
329
|
+
if (!m) return null;
|
|
330
|
+
gitDir = path.resolve(worktreePath, m[1]!.trim());
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
let newest: number | null = null;
|
|
336
|
+
for (const name of ['index', 'HEAD']) {
|
|
337
|
+
try {
|
|
338
|
+
const t = fs.statSync(path.join(gitDir, name)).mtimeMs;
|
|
339
|
+
if (newest === null || t > newest) newest = t;
|
|
340
|
+
} catch {
|
|
341
|
+
/* absent is not an error — the other candidate may still exist */
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return newest === null ? null : new Date(newest);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Classify every worktree. `now` is injectable so the live-window boundary is
|
|
349
|
+
* testable rather than wall-clock dependent.
|
|
350
|
+
*/
|
|
351
|
+
export function buildInventory(repoPath: string, now: Date = new Date()): Inventory {
|
|
352
|
+
const trunk = resolveTrunk(repoPath);
|
|
353
|
+
const entries = listWorktrees(repoPath);
|
|
354
|
+
const mainPath = canonical(entries.length > 0 ? entries[0]!.path : repoPath);
|
|
355
|
+
const liveCutoff = now.getTime() - LIVE_WINDOW_HOURS * 3600 * 1000;
|
|
356
|
+
|
|
357
|
+
const rows: InventoryRow[] = entries.map((e) => {
|
|
358
|
+
const resolved = canonical(e.path);
|
|
359
|
+
const isMain = resolved === mainPath;
|
|
360
|
+
const standardLocation = isMain ? true : isStandardLocation(repoPath, resolved);
|
|
361
|
+
const reasons: string[] = [];
|
|
362
|
+
|
|
363
|
+
if (!fs.existsSync(resolved)) {
|
|
364
|
+
return {
|
|
365
|
+
path: e.path,
|
|
366
|
+
branch: e.branch,
|
|
367
|
+
classification: 'review',
|
|
368
|
+
reasons: ['registration points at a missing directory — `git worktree prune` clears it'],
|
|
369
|
+
mergedIntoTrunk: false,
|
|
370
|
+
standardLocation,
|
|
371
|
+
isMain,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const activity = lastGitActivity(resolved);
|
|
376
|
+
if (activity !== null && activity.getTime() >= liveCutoff) {
|
|
377
|
+
return {
|
|
378
|
+
path: e.path,
|
|
379
|
+
branch: e.branch,
|
|
380
|
+
classification: 'live',
|
|
381
|
+
reasons: [`git activity within the last ${LIVE_WINDOW_HOURS}h — another session may hold it`],
|
|
382
|
+
mergedIntoTrunk: e.branch !== null && isAncestor(resolved, e.branch, trunk),
|
|
383
|
+
standardLocation,
|
|
384
|
+
isMain,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (isMain) reasons.push('main worktree — cannot be removed');
|
|
389
|
+
if (e.branch === null) {
|
|
390
|
+
reasons.push('detached HEAD — no branch to judge reachability for');
|
|
391
|
+
}
|
|
392
|
+
if (!standardLocation) {
|
|
393
|
+
reasons.push('non-standard location — outside the conventional worktree roots');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const mergedIntoTrunk = e.branch !== null && isAncestor(resolved, e.branch, trunk);
|
|
397
|
+
|
|
398
|
+
// A branch that is an ancestor of the trunk has every commit reachable
|
|
399
|
+
// from the trunk ref, so the expensive rev-list gate cannot find a
|
|
400
|
+
// unique commit. Only run it for the branches that are NOT merged —
|
|
401
|
+
// that is where it can actually refuse.
|
|
402
|
+
if (e.branch !== null && !mergedIntoTrunk) {
|
|
403
|
+
reasons.push(`branch is not an ancestor of ${trunk} — unmerged work`);
|
|
404
|
+
const gate = checkWorktree(resolved);
|
|
405
|
+
if (!gate.allowed) reasons.push(...gate.reasons);
|
|
406
|
+
} else if (e.branch !== null) {
|
|
407
|
+
const status = statusPorcelain(resolved);
|
|
408
|
+
if (status.trim().length > 0) {
|
|
409
|
+
const n = status.trimEnd().split('\n').length;
|
|
410
|
+
reasons.push(`unsaved work — ${n} dirty or untracked path(s)`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
path: e.path,
|
|
416
|
+
branch: e.branch,
|
|
417
|
+
classification: reasons.length === 0 ? 'safe' : 'review',
|
|
418
|
+
reasons,
|
|
419
|
+
mergedIntoTrunk,
|
|
420
|
+
standardLocation,
|
|
421
|
+
isMain,
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const reviewReasons: Record<string, number> = {};
|
|
426
|
+
for (const r of rows) {
|
|
427
|
+
if (r.classification !== 'review') continue;
|
|
428
|
+
// Group by the first reason: it is the primary disqualifier, and the
|
|
429
|
+
// per-row reasons stay available in --json for the full picture.
|
|
430
|
+
const key = (r.reasons[0] ?? 'unknown').split('\n')[0]!;
|
|
431
|
+
reviewReasons[key] = (reviewReasons[key] ?? 0) + 1;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
trunk,
|
|
436
|
+
rows,
|
|
437
|
+
counts: {
|
|
438
|
+
total: rows.length,
|
|
439
|
+
safe: rows.filter((r) => r.classification === 'safe').length,
|
|
440
|
+
review: rows.filter((r) => r.classification === 'review').length,
|
|
441
|
+
live: rows.filter((r) => r.classification === 'live').length,
|
|
442
|
+
reviewReasons: Object.fromEntries(
|
|
443
|
+
Object.entries(reviewReasons).sort((a, b) => b[1] - a[1]),
|
|
444
|
+
),
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* The removal commands for the safe set — printed for a human to review and
|
|
451
|
+
* run, never executed here. `git branch -d` (never `-D`) so git itself
|
|
452
|
+
* re-checks the merge before the branch goes.
|
|
453
|
+
*/
|
|
454
|
+
export function removalPlan(inv: Inventory): string[] {
|
|
455
|
+
return inv.rows
|
|
456
|
+
.filter((r) => r.classification === 'safe')
|
|
457
|
+
.map((r) => {
|
|
458
|
+
const remove = `git worktree remove ${JSON.stringify(r.path)}`;
|
|
459
|
+
return r.branch !== null && r.mergedIntoTrunk
|
|
460
|
+
? `${remove} && git branch -d ${JSON.stringify(r.branch)}`
|
|
461
|
+
: remove;
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
197
465
|
function usage(): never {
|
|
198
466
|
process.stderr.write(
|
|
199
|
-
'usage: worktree_cleanup_check check <worktree-path> | scope-overlap [repo-path]
|
|
467
|
+
'usage: worktree_cleanup_check check <worktree-path> | scope-overlap [repo-path] | ' +
|
|
468
|
+
'inventory [repo-path] [--json|--plan]\n',
|
|
200
469
|
);
|
|
201
470
|
process.exit(2);
|
|
202
471
|
}
|
|
@@ -236,6 +505,52 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
236
505
|
}
|
|
237
506
|
return 1;
|
|
238
507
|
}
|
|
508
|
+
if (mode === 'inventory') {
|
|
509
|
+
const rest = argv.slice(1);
|
|
510
|
+
const flags = rest.filter((a) => a.startsWith('--'));
|
|
511
|
+
const positional = rest.filter((a) => !a.startsWith('--'));
|
|
512
|
+
if (positional.length > 1 || flags.some((f) => f !== '--json' && f !== '--plan')) usage();
|
|
513
|
+
const repo = path.resolve(positional[0] ?? '.');
|
|
514
|
+
const inv = buildInventory(repo);
|
|
515
|
+
|
|
516
|
+
if (flags.includes('--json')) {
|
|
517
|
+
process.stdout.write(`${JSON.stringify(inv, null, 2)}\n`);
|
|
518
|
+
return 0;
|
|
519
|
+
}
|
|
520
|
+
if (flags.includes('--plan')) {
|
|
521
|
+
const plan = removalPlan(inv);
|
|
522
|
+
if (plan.length === 0) {
|
|
523
|
+
process.stdout.write('# no safe worktrees — nothing to propose.\n');
|
|
524
|
+
return 0;
|
|
525
|
+
}
|
|
526
|
+
process.stdout.write(
|
|
527
|
+
`# Prepared removal plan for ${plan.length} safe worktree(s).\n` +
|
|
528
|
+
'# NOT executed: bulk worktree + branch deletion is a Hard-Floor action that\n' +
|
|
529
|
+
'# needs the maintainer\'s explicit approval. Review, then run deliberately.\n',
|
|
530
|
+
);
|
|
531
|
+
process.stdout.write(`${plan.join('\n')}\n`);
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const c = inv.counts;
|
|
536
|
+
process.stdout.write(
|
|
537
|
+
`worktree inventory · ${c.total} registered · trunk ${inv.trunk}\n` +
|
|
538
|
+
` safe ${c.safe}\n` +
|
|
539
|
+
` review ${c.review}\n` +
|
|
540
|
+
` live ${c.live}\n`,
|
|
541
|
+
);
|
|
542
|
+
if (c.review > 0) {
|
|
543
|
+
process.stdout.write('\nreview reasons (primary disqualifier):\n');
|
|
544
|
+
for (const [reason, n] of Object.entries(c.reviewReasons)) {
|
|
545
|
+
process.stdout.write(` ${String(n).padStart(4)} ${reason}\n`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
process.stdout.write(
|
|
549
|
+
'\nThis mode reports only. `--plan` prints the removal commands for the safe set;\n' +
|
|
550
|
+
'running them is a Hard-Floor action needing the maintainer\'s explicit approval.\n',
|
|
551
|
+
);
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
239
554
|
usage();
|
|
240
555
|
}
|
|
241
556
|
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
/**
|
|
3
|
-
* Bite-sized task granularity gate for structural roadmaps (P1.5).
|
|
4
|
-
*
|
|
5
|
-
* Ported from the retired Python `src/scripts/check_bite_sized_granularity.py` (ADR-200,
|
|
6
|
-
* Phase 4 / Wave 4c). This module is a pure library (no CLI / main); the
|
|
7
|
-
* Python module is imported as a test surface. The exported API mirrors the
|
|
8
|
-
* Python public API EXACTLY:
|
|
9
|
-
*
|
|
10
|
-
* read_complexity(text) -> 'structural' | 'lightweight' | null
|
|
11
|
-
* scan_placeholders(text) -> Placeholder[]
|
|
12
|
-
* check_granularity(text) -> Result(complexity, gated, violations)
|
|
13
|
-
*
|
|
14
|
-
* `gated` is true only when `complexity === 'structural'`. Violations are
|
|
15
|
-
* empty when the gate is not active, regardless of placeholder presence.
|
|
16
|
-
* No behaviour changes — placeholder patterns, frontmatter slice, and the
|
|
17
|
-
* task-bullet prefixes are replicated.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
// Placeholder patterns in (kind, regex) order. Mirrors Python re flags:
|
|
21
|
-
// IGNORECASE for the angle/tbd patterns. JS uses `i`. The angle pattern
|
|
22
|
-
// is global-less here; we use `.test()` per line (re.search semantics).
|
|
23
|
-
const PLACEHOLDER_PATTERNS: ReadonlyArray<[string, RegExp]> = [
|
|
24
|
-
['angle-placeholder', /<[a-z][a-z0-9 _\-/]*>/i],
|
|
25
|
-
['todo', /\bTODO\b/],
|
|
26
|
-
['fixme', /\bFIXME\b/],
|
|
27
|
-
['xxx', /\bXXX\b/],
|
|
28
|
-
['tbd', /\btbd\b/i],
|
|
29
|
-
['triple-question', /\?\?\?/],
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const COMPLEXITY_PAT = /^complexity:\s*(lightweight|structural)\s*$/m;
|
|
33
|
-
|
|
34
|
-
interface Placeholder {
|
|
35
|
-
kind: string;
|
|
36
|
-
line: number;
|
|
37
|
-
text: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface Result {
|
|
41
|
-
complexity: string | null;
|
|
42
|
-
gated: boolean;
|
|
43
|
-
violations: Placeholder[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function _frontmatter(text: string): string {
|
|
47
|
-
if (!text.startsWith('---\n')) {
|
|
48
|
-
return '';
|
|
49
|
-
}
|
|
50
|
-
const end = text.indexOf('\n---\n', 4);
|
|
51
|
-
return end !== -1 ? text.slice(4, end) : '';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function read_complexity(text: string): string | null {
|
|
55
|
-
const fm = _frontmatter(text);
|
|
56
|
-
if (fm === '') {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
const m = COMPLEXITY_PAT.exec(fm);
|
|
60
|
-
return m ? m[1]! : null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const _TASK_PREFIXES = ['- [ ]', '- [x]', '- [/]', '- [-]'];
|
|
64
|
-
|
|
65
|
-
/** Mirror Python `str.lstrip()` — strip leading whitespace (incl. tabs). */
|
|
66
|
-
function _lstrip(s: string): string {
|
|
67
|
-
return s.replace(/^\s+/, '');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Mirror Python `str.rstrip()` — strip trailing whitespace. */
|
|
71
|
-
function _rstrip(s: string): string {
|
|
72
|
-
return s.replace(/\s+$/, '');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function scan_placeholders(text: string): Placeholder[] {
|
|
76
|
-
const hits: Placeholder[] = [];
|
|
77
|
-
const lines = text.split('\n');
|
|
78
|
-
for (let idx = 0; idx < lines.length; idx++) {
|
|
79
|
-
const line_no = idx + 1;
|
|
80
|
-
const line = lines[idx]!;
|
|
81
|
-
const stripped = _lstrip(line);
|
|
82
|
-
if (!_TASK_PREFIXES.some((p) => stripped.startsWith(p))) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
for (const [kind, pat] of PLACEHOLDER_PATTERNS) {
|
|
86
|
-
if (pat.test(line)) {
|
|
87
|
-
hits.push({ kind, line: line_no, text: _rstrip(line) });
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return hits;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function check_granularity(text: string): Result {
|
|
96
|
-
const complexity = read_complexity(text);
|
|
97
|
-
const gated = complexity === 'structural';
|
|
98
|
-
if (!gated) {
|
|
99
|
-
return { complexity, gated: false, violations: [] };
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
complexity,
|
|
103
|
-
gated: true,
|
|
104
|
-
violations: scan_placeholders(text),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export {
|
|
109
|
-
type Placeholder,
|
|
110
|
-
type Result,
|
|
111
|
-
PLACEHOLDER_PATTERNS,
|
|
112
|
-
COMPLEXITY_PAT,
|
|
113
|
-
read_complexity,
|
|
114
|
-
scan_placeholders,
|
|
115
|
-
check_granularity,
|
|
116
|
-
};
|