@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
|
@@ -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']) ?? {};
|
|
@@ -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
|
-
};
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
/**
|
|
3
|
-
* Workspace-boundary drift check — import-edge enforcement.
|
|
4
|
-
*
|
|
5
|
-
* Ported from the retired Python `src/scripts/lint_workspace_boundary.py` (ADR-200,
|
|
6
|
-
* Python→TypeScript migration). The CLI contract is pinned — the
|
|
7
|
-
* `--quiet` flag, the `sorted(repo.glob(WORKSPACE_GLOB))` scan order (pathlib
|
|
8
|
-
* component-wise), the FORBIDDEN-pattern set (Python `re` → JS `RegExp`, all
|
|
9
|
-
* ASCII so a 1:1 translation), the `# boundary-exception:` pragma skip, the
|
|
10
|
-
* per-violation message shape, and the exit codes (0 holds · 1 forbidden
|
|
11
|
-
* import · 2 internal). snake_case is kept on the public surface.
|
|
12
|
-
*
|
|
13
|
-
* Governed by `docs/contracts/workspace-boundary.md` + ADR-095. Fails when a
|
|
14
|
-
* workspace module (`src/cli/python/workspace_*.py`) imports an owner-module of
|
|
15
|
-
* a domain the workspace does NOT own: skill design, profile/pack semantics,
|
|
16
|
-
* video-provider logic, MCP-registry policy, analytics product strategy.
|
|
17
|
-
*
|
|
18
|
-
* Scope (read this before trusting a green run): this enforces **import edges
|
|
19
|
-
* only**. Semantic drift — a workspace module that encodes profile-semantics or
|
|
20
|
-
* analytics-product-strategy judgement without importing anything forbidden — is
|
|
21
|
-
* NOT catchable here and stays doc-governance, enforced in review against the
|
|
22
|
-
* contract. A green run is a supplement to boundary thinking, not a substitute.
|
|
23
|
-
*
|
|
24
|
-
* Allowed: stdlib, third-party deps, intra-workspace imports (`workspace_*`),
|
|
25
|
-
* and any import line carrying a `# boundary-exception: <reason>` pragma.
|
|
26
|
-
*
|
|
27
|
-
* Usage: python3 src/scripts/lint_workspace_boundary.py [--quiet]
|
|
28
|
-
* Exit: 0 = boundary holds · 1 = a forbidden import was found · 2 = internal.
|
|
29
|
-
*
|
|
30
|
-
* --- Parity notes (ADR-200) ---
|
|
31
|
-
*
|
|
32
|
-
* - The Python source uses `ast.walk` to find every `ast.Import` /
|
|
33
|
-
* `ast.ImportFrom` at any nesting level and records `node.lineno`. The twin
|
|
34
|
-
* reproduces that with a small line scanner that recognises top-level AND
|
|
35
|
-
* indented `import …` / `from … import …` statements (matching the only
|
|
36
|
-
* shapes Python's grammar admits for these node types), yielding the same
|
|
37
|
-
* `(module, lineno)` pairs — `lineno` is the line of the `import` / `from`
|
|
38
|
-
* keyword, exactly as `ast` reports it. A parenthesized multi-line
|
|
39
|
-
* `from x import (\n…\n)` records the `from` line, like `ast`.
|
|
40
|
-
* - `Path.read_text(encoding="utf-8")` → `fs.readFileSync(p, "utf-8")`; a
|
|
41
|
-
* decode/read error surfaces as the `unparseable` violation line (the Python
|
|
42
|
-
* path only hits this on `SyntaxError`, marked `pragma: no cover`).
|
|
43
|
-
* - `process.exitCode` is set; `process.exit()` is never called.
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
import * as fs from 'node:fs';
|
|
47
|
-
import * as path from 'node:path';
|
|
48
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
49
|
-
|
|
50
|
-
const _HERE = fileURLToPath(import.meta.url);
|
|
51
|
-
|
|
52
|
-
const WORKSPACE_GLOB = 'src/cli/python/workspace_*.py';
|
|
53
|
-
const PRAGMA = 'boundary-exception:';
|
|
54
|
-
|
|
55
|
-
// Owner-modules of the NOT-owned domains. Matched against each dotted segment
|
|
56
|
-
// of an imported module name with segment boundaries, so `packaging` does not
|
|
57
|
-
// trip `pack` and `workspace_skills` is handled by the intra-workspace allow.
|
|
58
|
-
const FORBIDDEN: ReadonlyArray<readonly [RegExp, string]> = [
|
|
59
|
-
[/(?:^|[._-])condense(?:$|[._-])/, 'skill design / condensation'],
|
|
60
|
-
[/(?:^|[._-])skill_linter(?:$|[._-])/, 'skill design'],
|
|
61
|
-
[/(?:^|[._-])skill_management(?:$|[._-])/, 'skill design'],
|
|
62
|
-
[/(?:^|[._-])skill_writing(?:$|[._-])/, 'skill design'],
|
|
63
|
-
[/(?:^|[._-])discovery_manifest(?:$|[._-])/, 'profile/pack semantics'],
|
|
64
|
-
[/(?:^|[._-])(?:profiles?|packs?)(?:$|[._-])/, 'profile/pack semantics'],
|
|
65
|
-
[/ai[_-]?video/, 'video-provider logic'],
|
|
66
|
-
[/(?:^|[._-])mcp(?:$|[._-])/, 'MCP-registry policy'],
|
|
67
|
-
[/(?:^|[._-])router(?:$|[._-])/, 'router / projection policy'],
|
|
68
|
-
[/(?:^|[._-])persona/, 'persona / skill design'],
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
function _is_intra_workspace(module: string): boolean {
|
|
72
|
-
const head = module.split('.', 1)[0] as string;
|
|
73
|
-
return head === 'workspace' || head.startsWith('workspace_');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function _forbidden_reason(module: string): string | null {
|
|
77
|
-
if (_is_intra_workspace(module)) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
for (const [pat, reason] of FORBIDDEN) {
|
|
81
|
-
if (pat.test(module)) {
|
|
82
|
-
return reason;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Yield `[module_name, lineno]` for every import in `src`, mirroring
|
|
90
|
-
* `ast.walk` over `ast.Import` / `ast.ImportFrom`.
|
|
91
|
-
*
|
|
92
|
-
* `ast` reports the keyword line as `lineno` (1-based). `ast.Import` for
|
|
93
|
-
* `import a.b, c` yields one entry per alias, all on the same line; the module
|
|
94
|
-
* name is the dotted path. `ast.ImportFrom` yields one entry: `node.module`
|
|
95
|
-
* (the dotted module after `from`, `None` for `from . import x`).
|
|
96
|
-
*/
|
|
97
|
-
function* _imported_modules(src: string): Generator<[string, number]> {
|
|
98
|
-
const lines = src.split('\n');
|
|
99
|
-
for (let i = 0; i < lines.length; i++) {
|
|
100
|
-
const raw = lines[i] as string;
|
|
101
|
-
const stripped = raw.replace(/^[ \t\f\v]+/, '');
|
|
102
|
-
const lineno = i + 1;
|
|
103
|
-
|
|
104
|
-
// from <module> import … — module is the dotted path after `from`.
|
|
105
|
-
// `from . import x` / `from .pkg import y` → ast.module is the part
|
|
106
|
-
// after leading dots (or None). The leading-dot (relative) form has
|
|
107
|
-
// module = the text after the dots; a bare `from . import` has
|
|
108
|
-
// module=None and is skipped (no name to test).
|
|
109
|
-
const fromMatch = /^from[ \t]+(\.*)([A-Za-z_][\w.]*)?[ \t]+import\b/.exec(stripped);
|
|
110
|
-
if (fromMatch) {
|
|
111
|
-
const mod = fromMatch[2];
|
|
112
|
-
if (mod !== undefined) {
|
|
113
|
-
yield [mod, lineno];
|
|
114
|
-
}
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// import a, b.c, d as e — one alias per dotted name; ast records the
|
|
119
|
-
// dotted name (`alias.name`), all on this single keyword line.
|
|
120
|
-
const importMatch = /^import[ \t]+(.+)$/.exec(stripped);
|
|
121
|
-
if (importMatch) {
|
|
122
|
-
// Strip a trailing comment, then split on commas; for each clause
|
|
123
|
-
// take the dotted module name before any `as` alias.
|
|
124
|
-
let body = importMatch[1] as string;
|
|
125
|
-
const hash = body.indexOf('#');
|
|
126
|
-
if (hash >= 0) {
|
|
127
|
-
body = body.slice(0, hash);
|
|
128
|
-
}
|
|
129
|
-
for (const clause of body.split(',')) {
|
|
130
|
-
const name = clause.trim().split(/[ \t]+as[ \t]+/)[0]?.trim();
|
|
131
|
-
if (name) {
|
|
132
|
-
yield [name, lineno];
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function check_file(p: string): string[] {
|
|
140
|
-
let src: string;
|
|
141
|
-
try {
|
|
142
|
-
src = fs.readFileSync(p, { encoding: 'utf-8' });
|
|
143
|
-
} catch (exc) {
|
|
144
|
-
// pragma: no cover — mirrors the Python SyntaxError path.
|
|
145
|
-
return [`${p}: unparseable (${(exc as Error).message})`];
|
|
146
|
-
}
|
|
147
|
-
const lines = src.split('\n');
|
|
148
|
-
const out: string[] = [];
|
|
149
|
-
for (const [module, lineno] of _imported_modules(src)) {
|
|
150
|
-
const reason = _forbidden_reason(module);
|
|
151
|
-
if (reason === null) {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
const line = lineno > 0 && lineno <= lines.length ? (lines[lineno - 1] as string) : '';
|
|
155
|
-
if (line.includes(PRAGMA)) {
|
|
156
|
-
continue; // reviewed, deliberate exception
|
|
157
|
-
}
|
|
158
|
-
out.push(
|
|
159
|
-
`${path.basename(p)}:${lineno}: imports \`${module}\` ` +
|
|
160
|
-
`(not-owned domain: ${reason})`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
return out;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** `sorted(repo.glob("src/cli/python/workspace_*.py"))` — pathlib order. */
|
|
167
|
-
function _globWorkspaceSorted(repo: string): string[] {
|
|
168
|
-
const dir = path.join(repo, 'src', 'cli', 'python');
|
|
169
|
-
let entries: string[];
|
|
170
|
-
try {
|
|
171
|
-
entries = fs.readdirSync(dir);
|
|
172
|
-
} catch {
|
|
173
|
-
return [];
|
|
174
|
-
}
|
|
175
|
-
const out: string[] = [];
|
|
176
|
-
for (const name of entries) {
|
|
177
|
-
if (name.startsWith('workspace_') && name.endsWith('.py')) {
|
|
178
|
-
const full = path.join(dir, name);
|
|
179
|
-
try {
|
|
180
|
-
if (fs.statSync(full).isFile()) {
|
|
181
|
-
out.push(full);
|
|
182
|
-
}
|
|
183
|
-
} catch {
|
|
184
|
-
// ignore
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
out.sort();
|
|
189
|
-
return out;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function main(argv: readonly string[]): number {
|
|
193
|
-
const quiet = argv.includes('--quiet');
|
|
194
|
-
// Path(__file__).resolve().parent.parent.parent — src/scripts → repo root.
|
|
195
|
-
const repo = path.resolve(path.dirname(_HERE), '..', '..');
|
|
196
|
-
const files = _globWorkspaceSorted(repo);
|
|
197
|
-
if (files.length === 0) {
|
|
198
|
-
process.stdout.write(
|
|
199
|
-
`⚠️ lint-workspace-boundary: no files match ${WORKSPACE_GLOB}\n`,
|
|
200
|
-
);
|
|
201
|
-
return 0;
|
|
202
|
-
}
|
|
203
|
-
const violations: string[] = [];
|
|
204
|
-
for (const f of files) {
|
|
205
|
-
violations.push(...check_file(f));
|
|
206
|
-
}
|
|
207
|
-
if (violations.length) {
|
|
208
|
-
process.stdout.write(
|
|
209
|
-
'❌ Workspace-boundary violation(s) — a workspace module imports ' +
|
|
210
|
-
'an owner-module of a domain the workspace does NOT own ' +
|
|
211
|
-
'(docs/contracts/workspace-boundary.md):\n',
|
|
212
|
-
);
|
|
213
|
-
for (const v of violations) {
|
|
214
|
-
process.stdout.write(` 🔴 ${v}\n`);
|
|
215
|
-
}
|
|
216
|
-
process.stdout.write(
|
|
217
|
-
'\nFix: move the logic to the owning surface and consume its ' +
|
|
218
|
-
'output, or add `# boundary-exception: <reason>` if the import is ' +
|
|
219
|
-
'genuinely justified (reviewed like any boundary change).\n',
|
|
220
|
-
);
|
|
221
|
-
return 1;
|
|
222
|
-
}
|
|
223
|
-
if (!quiet) {
|
|
224
|
-
process.stdout.write(
|
|
225
|
-
`✅ Workspace boundary holds — ${files.length} module(s), ` +
|
|
226
|
-
'no forbidden imports.\n',
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
return 0;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function _isCliEntry(): boolean {
|
|
233
|
-
if (process.argv[1] === undefined) {
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
237
|
-
if (import.meta.url === argvUrl) {
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
// A symlinked invocation (e.g. via an installed `.augment/` projection,
|
|
241
|
-
// or macOS /var → /private/var temp dirs) makes the raw URLs differ:
|
|
242
|
-
// import.meta.url is the resolved real path while argv[1] keeps the
|
|
243
|
-
// symlink path. Compare realpaths so the entry guard still fires
|
|
244
|
-
// (without this the CLI silently no-ops when run through a symlink).
|
|
245
|
-
try {
|
|
246
|
-
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
247
|
-
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
248
|
-
return here === argv;
|
|
249
|
-
} catch {
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
255
|
-
process.exitCode = main(process.argv.slice(2));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export {
|
|
259
|
-
WORKSPACE_GLOB,
|
|
260
|
-
PRAGMA,
|
|
261
|
-
FORBIDDEN,
|
|
262
|
-
_is_intra_workspace,
|
|
263
|
-
_forbidden_reason,
|
|
264
|
-
_imported_modules,
|
|
265
|
-
check_file,
|
|
266
|
-
main,
|
|
267
|
-
};
|