@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.
Files changed (231) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +51 -0
  3. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  4. package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
  5. package/dist/cli-delegate/cmd_handoff.js +1 -1
  6. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  7. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  8. package/dist/discovery/deprecation-report.md +1 -1
  9. package/dist/discovery/discovery-manifest.json +1 -1
  10. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  11. package/dist/discovery/discovery-manifest.summary.md +1 -1
  12. package/dist/discovery/orphan-report.md +1 -1
  13. package/dist/discovery/packs.json +1 -1
  14. package/dist/discovery/trust-report.md +1 -1
  15. package/dist/discovery/workspaces.json +1 -1
  16. package/dist/hooks/dispatch.js +58 -2
  17. package/dist/mcp/registry-manifest.json +2 -2
  18. package/dist/mcp/server.json +2 -2
  19. package/dist/mcp/server.mjs +47 -29
  20. package/package.json +1 -1
  21. package/src/config/gate-coverage.yml +22 -4
  22. package/src/config/gate-violation-baselines.json +3 -8
  23. package/src/scripts/_lib/gate_population.ts +70 -0
  24. package/src/scripts/_lib/roadmap_granularity.ts +157 -0
  25. package/src/scripts/_lib/scan_scope.ts +44 -0
  26. package/src/scripts/_lib/stdin.ts +82 -0
  27. package/src/scripts/audit_adr_coverage.ts +38 -0
  28. package/src/scripts/audit_auto_rules.ts +17 -3
  29. package/src/scripts/audit_command_surface.ts +51 -0
  30. package/src/scripts/audit_initial_context.ts +39 -3
  31. package/src/scripts/audit_mcp_tools.ts +25 -0
  32. package/src/scripts/audit_overlap.ts +24 -1
  33. package/src/scripts/audit_skill_descriptions.ts +19 -0
  34. package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
  35. package/src/scripts/check_adr_frontmatter.ts +23 -3
  36. package/src/scripts/check_always_budget.ts +24 -0
  37. package/src/scripts/check_artefact_checksums.ts +24 -1
  38. package/src/scripts/check_artefact_count_messaging.ts +20 -0
  39. package/src/scripts/check_augmentignore.ts +22 -15
  40. package/src/scripts/check_backstop_debt.ts +22 -0
  41. package/src/scripts/check_beta_review_markers.ts +22 -1
  42. package/src/scripts/check_bridge_derivation.ts +22 -1
  43. package/src/scripts/check_bundle_path_leakage.ts +26 -0
  44. package/src/scripts/check_ci_strict_superset.ts +21 -3
  45. package/src/scripts/check_claims.ts +22 -0
  46. package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
  47. package/src/scripts/check_cluster_patterns.ts +26 -4
  48. package/src/scripts/check_command_count_messaging.ts +20 -4
  49. package/src/scripts/check_comparison.ts +18 -0
  50. package/src/scripts/check_condensed_paths.ts +23 -2
  51. package/src/scripts/check_consumer_scope_flip.ts +21 -2
  52. package/src/scripts/check_council_config_location.ts +20 -0
  53. package/src/scripts/check_council_layout.ts +22 -2
  54. package/src/scripts/check_council_references.ts +23 -1
  55. package/src/scripts/check_dependency_floors.ts +19 -3
  56. package/src/scripts/check_discovery_determinism.ts +26 -0
  57. package/src/scripts/check_enforcement_coverage.ts +20 -0
  58. package/src/scripts/check_evaluator_budgets.ts +23 -0
  59. package/src/scripts/check_finding_dispositions.ts +29 -0
  60. package/src/scripts/check_gate_coverage.ts +63 -22
  61. package/src/scripts/check_gate_paths.ts +30 -5
  62. package/src/scripts/check_generated_artefact_headers.ts +21 -0
  63. package/src/scripts/check_generator_output_coverage.ts +23 -0
  64. package/src/scripts/check_gitignore_freshness.ts +23 -0
  65. package/src/scripts/check_host_loadability.ts +34 -8
  66. package/src/scripts/check_installer_import_purity.ts +22 -8
  67. package/src/scripts/check_kernel_prefix_stability.ts +19 -1
  68. package/src/scripts/check_kernel_rule_bundle.ts +23 -0
  69. package/src/scripts/check_knowledge_cards.ts +13 -0
  70. package/src/scripts/check_knowledge_pages.ts +23 -0
  71. package/src/scripts/check_knowledge_sharing.ts +27 -0
  72. package/src/scripts/check_md_language.ts +24 -0
  73. package/src/scripts/check_media_deps.ts +23 -0
  74. package/src/scripts/check_memory.ts +26 -1
  75. package/src/scripts/check_memory_contradiction.ts +16 -1
  76. package/src/scripts/check_memory_proposal.ts +15 -0
  77. package/src/scripts/check_memory_similarity.ts +16 -0
  78. package/src/scripts/check_module_management_neutral.ts +19 -11
  79. package/src/scripts/check_no_conflict_markers.ts +22 -1
  80. package/src/scripts/check_no_external_sources.ts +26 -1
  81. package/src/scripts/check_no_local_settings_committed.ts +37 -10
  82. package/src/scripts/check_no_new_legacy_path.ts +30 -1
  83. package/src/scripts/check_one_off_location.ts +43 -0
  84. package/src/scripts/check_originality_freshness.ts +25 -1
  85. package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
  86. package/src/scripts/check_pack_size.ts +22 -0
  87. package/src/scripts/check_preamble_payload_budget.ts +35 -5
  88. package/src/scripts/check_proposal.ts +24 -1
  89. package/src/scripts/check_public_catalog_links.ts +27 -0
  90. package/src/scripts/check_public_links.ts +19 -0
  91. package/src/scripts/check_quality_regression.ts +19 -0
  92. package/src/scripts/check_reach_channels.ts +44 -0
  93. package/src/scripts/check_reach_staleness.ts +27 -0
  94. package/src/scripts/check_release_adjacent_health.ts +28 -0
  95. package/src/scripts/check_release_highlights.ts +34 -0
  96. package/src/scripts/check_release_includes_discovery.ts +25 -6
  97. package/src/scripts/check_release_pr_shape.ts +18 -1
  98. package/src/scripts/check_release_published.ts +21 -0
  99. package/src/scripts/check_release_surface_equality.ts +22 -0
  100. package/src/scripts/check_release_trunk_sync.ts +19 -0
  101. package/src/scripts/check_reply_consistency.ts +22 -0
  102. package/src/scripts/check_roadmap_trackable.ts +20 -0
  103. package/src/scripts/check_role_doc_links.ts +23 -0
  104. package/src/scripts/check_rule_invariants.ts +14 -0
  105. package/src/scripts/check_secret_leak.ts +47 -0
  106. package/src/scripts/check_skill_gaps.ts +32 -0
  107. package/src/scripts/check_skill_requires.ts +19 -0
  108. package/src/scripts/check_source_pointer_freshness.ts +20 -0
  109. package/src/scripts/check_structural_breaking.ts +29 -2
  110. package/src/scripts/check_surface_tiers.ts +34 -0
  111. package/src/scripts/check_template_pin_drift.ts +23 -1
  112. package/src/scripts/check_test_coverage_diff.ts +17 -1
  113. package/src/scripts/check_token_optimizer_freshness.ts +18 -3
  114. package/src/scripts/check_token_quality_golden.ts +21 -0
  115. package/src/scripts/check_token_regression.ts +25 -0
  116. package/src/scripts/check_tracked_but_ignored.ts +26 -0
  117. package/src/scripts/check_trigger_eval_presence.ts +23 -1
  118. package/src/scripts/check_trigger_evals.ts +36 -0
  119. package/src/scripts/check_trunk_drift.ts +23 -0
  120. package/src/scripts/check_update_banner.ts +24 -0
  121. package/src/scripts/gate_scope_classify.ts +513 -0
  122. package/src/scripts/hook_manifest.yaml +1 -1
  123. package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
  124. package/src/scripts/hooks/concern_registry.ts +2 -2
  125. package/src/scripts/lint_agent_security.ts +19 -0
  126. package/src/scripts/lint_agent_skill_names.ts +30 -0
  127. package/src/scripts/lint_agents_layout.ts +20 -0
  128. package/src/scripts/lint_agents_md.ts +20 -0
  129. package/src/scripts/lint_archived_skills.ts +24 -3
  130. package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
  131. package/src/scripts/lint_bench_ab.ts +25 -0
  132. package/src/scripts/lint_bench_corpus.ts +20 -4
  133. package/src/scripts/lint_breaking_changes_index.ts +24 -0
  134. package/src/scripts/lint_budget_ownership.ts +20 -3
  135. package/src/scripts/lint_changelog_rollback.ts +24 -0
  136. package/src/scripts/lint_command_flow_coverage.ts +22 -0
  137. package/src/scripts/lint_command_tiers.ts +29 -5
  138. package/src/scripts/lint_commit_subjects.ts +15 -0
  139. package/src/scripts/lint_confusables.ts +24 -0
  140. package/src/scripts/lint_consumer_internal_refs.ts +24 -0
  141. package/src/scripts/lint_design_quality.ts +22 -0
  142. package/src/scripts/lint_design_slop.ts +23 -0
  143. package/src/scripts/lint_discovery_manifest.ts +23 -0
  144. package/src/scripts/lint_discovery_vocabulary.ts +19 -0
  145. package/src/scripts/lint_documented_commands.ts +23 -0
  146. package/src/scripts/lint_empty_roadmaps.ts +23 -0
  147. package/src/scripts/lint_eval_fixture_citations.ts +19 -0
  148. package/src/scripts/lint_eval_freshness.ts +35 -0
  149. package/src/scripts/lint_examples.ts +18 -3
  150. package/src/scripts/lint_explain_trace.ts +23 -0
  151. package/src/scripts/lint_featured_skills.ts +26 -1
  152. package/src/scripts/lint_flows.ts +19 -0
  153. package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
  154. package/src/scripts/lint_ghostwriter_source.ts +26 -6
  155. package/src/scripts/lint_glama_drift.ts +24 -0
  156. package/src/scripts/lint_global_paths.ts +24 -0
  157. package/src/scripts/lint_governed_writes.ts +20 -0
  158. package/src/scripts/lint_hidden_unicode.ts +25 -0
  159. package/src/scripts/lint_hook_concern_budget.ts +39 -6
  160. package/src/scripts/lint_hook_manifest.ts +22 -0
  161. package/src/scripts/lint_instruction_smuggling.ts +23 -0
  162. package/src/scripts/lint_knowledge_scale.ts +32 -2
  163. package/src/scripts/lint_legal_pack.ts +31 -0
  164. package/src/scripts/lint_marketplace.ts +26 -0
  165. package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
  166. package/src/scripts/lint_mcp_config_security.ts +24 -0
  167. package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
  168. package/src/scripts/lint_memory_tripwire.ts +35 -0
  169. package/src/scripts/lint_missions.ts +23 -7
  170. package/src/scripts/lint_model_tier_coverage.ts +18 -0
  171. package/src/scripts/lint_namespace_collisions.ts +19 -0
  172. package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
  173. package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
  174. package/src/scripts/lint_one_off_age.ts +38 -2
  175. package/src/scripts/lint_orchestration_dsl.ts +23 -3
  176. package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
  177. package/src/scripts/lint_originality.ts +22 -4
  178. package/src/scripts/lint_originality_shingles.ts +28 -3
  179. package/src/scripts/lint_output_slop.ts +19 -0
  180. package/src/scripts/lint_override_kernel_guard.ts +30 -0
  181. package/src/scripts/lint_pack_first_win.ts +23 -0
  182. package/src/scripts/lint_pack_risk_class.ts +19 -3
  183. package/src/scripts/lint_persistence.ts +19 -0
  184. package/src/scripts/lint_persona_governance.ts +18 -3
  185. package/src/scripts/lint_positioning.ts +23 -0
  186. package/src/scripts/lint_pre_migration_refs.ts +24 -0
  187. package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
  188. package/src/scripts/lint_provenance.ts +21 -0
  189. package/src/scripts/lint_provenance_vocabulary.ts +24 -1
  190. package/src/scripts/lint_readme_jargon.ts +21 -3
  191. package/src/scripts/lint_readme_serial_comma.ts +21 -3
  192. package/src/scripts/lint_readme_size.ts +21 -3
  193. package/src/scripts/lint_regression.ts +28 -0
  194. package/src/scripts/lint_roadmap_blockers.ts +43 -0
  195. package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
  196. package/src/scripts/lint_roadmap_complexity.ts +47 -0
  197. package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
  198. package/src/scripts/lint_rule_interactions.ts +16 -2
  199. package/src/scripts/lint_rule_tiers.ts +19 -6
  200. package/src/scripts/lint_showcase_sessions.ts +19 -3
  201. package/src/scripts/lint_skill_descriptions.ts +19 -0
  202. package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
  203. package/src/scripts/lint_skill_originality.ts +18 -3
  204. package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
  205. package/src/scripts/lint_skill_tools.ts +84 -145
  206. package/src/scripts/lint_spawn_payload.ts +27 -1
  207. package/src/scripts/lint_store_boundary.ts +19 -0
  208. package/src/scripts/lint_subagent_determinism.ts +23 -1
  209. package/src/scripts/lint_supported_tools_matrix.ts +18 -0
  210. package/src/scripts/lint_surface_matrix.ts +19 -3
  211. package/src/scripts/lint_ticket_buildable.ts +23 -0
  212. package/src/scripts/lint_topics_yaml.ts +19 -12
  213. package/src/scripts/lint_trigger_precision.ts +27 -0
  214. package/src/scripts/lint_trust_coherence.ts +21 -0
  215. package/src/scripts/lint_ui_stack_bundles.ts +17 -3
  216. package/src/scripts/lint_value_dashboard.ts +19 -3
  217. package/src/scripts/lint_versioned_cache.ts +19 -1
  218. package/src/scripts/lint_workflow_security.ts +22 -14
  219. package/src/scripts/pr_url_reminder_hook.ts +1 -1
  220. package/src/scripts/skill_collision_clusters.ts +37 -3
  221. package/src/scripts/skill_discovery.ts +20 -0
  222. package/src/scripts/skill_eval_coverage.ts +22 -0
  223. package/src/scripts/skill_overlap.ts +32 -6
  224. package/src/scripts/skill_preview.ts +31 -0
  225. package/src/scripts/skill_trigger_eval.ts +18 -0
  226. package/src/scripts/skill_usage_collect.ts +21 -0
  227. package/src/scripts/skill_usage_report.ts +21 -0
  228. package/src/scripts/sweep_dead_scan_roots.ts +11 -3
  229. package/src/scripts/verify_physical_move.ts +24 -0
  230. package/src/scripts/check_bite_sized_granularity.ts +0 -116
  231. 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
- /** Population: the gate-name prefix convention the census uses. */
114
- const GATE_RE = /^(lint|audit|check|verify)_.*\.ts$/;
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((f) => GATE_RE.test(f)).sort()
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
- };