cap-pro 1.0.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 (275) hide show
  1. package/.claude-plugin/README.md +26 -0
  2. package/.claude-plugin/marketplace.json +24 -0
  3. package/.claude-plugin/plugin.json +24 -0
  4. package/LICENSE +21 -0
  5. package/README.ja-JP.md +834 -0
  6. package/README.ko-KR.md +823 -0
  7. package/README.md +806 -0
  8. package/README.pt-BR.md +452 -0
  9. package/README.zh-CN.md +800 -0
  10. package/agents/cap-architect.md +269 -0
  11. package/agents/cap-brainstormer.md +207 -0
  12. package/agents/cap-curator.md +276 -0
  13. package/agents/cap-debugger.md +365 -0
  14. package/agents/cap-designer.md +246 -0
  15. package/agents/cap-historian.md +464 -0
  16. package/agents/cap-migrator.md +291 -0
  17. package/agents/cap-prototyper.md +197 -0
  18. package/agents/cap-validator.md +308 -0
  19. package/bin/install.js +5433 -0
  20. package/cap/bin/cap-tools.cjs +853 -0
  21. package/cap/bin/lib/arc-scanner.cjs +344 -0
  22. package/cap/bin/lib/cap-affinity-engine.cjs +862 -0
  23. package/cap/bin/lib/cap-anchor.cjs +228 -0
  24. package/cap/bin/lib/cap-annotation-writer.cjs +340 -0
  25. package/cap/bin/lib/cap-checkpoint.cjs +434 -0
  26. package/cap/bin/lib/cap-cluster-detect.cjs +945 -0
  27. package/cap/bin/lib/cap-cluster-display.cjs +52 -0
  28. package/cap/bin/lib/cap-cluster-format.cjs +245 -0
  29. package/cap/bin/lib/cap-cluster-helpers.cjs +295 -0
  30. package/cap/bin/lib/cap-cluster-io.cjs +212 -0
  31. package/cap/bin/lib/cap-completeness.cjs +540 -0
  32. package/cap/bin/lib/cap-deps.cjs +583 -0
  33. package/cap/bin/lib/cap-design-families.cjs +332 -0
  34. package/cap/bin/lib/cap-design.cjs +966 -0
  35. package/cap/bin/lib/cap-divergence-detector.cjs +400 -0
  36. package/cap/bin/lib/cap-doctor.cjs +752 -0
  37. package/cap/bin/lib/cap-feature-map-internals.cjs +19 -0
  38. package/cap/bin/lib/cap-feature-map-migrate.cjs +335 -0
  39. package/cap/bin/lib/cap-feature-map-monorepo.cjs +885 -0
  40. package/cap/bin/lib/cap-feature-map-shard.cjs +315 -0
  41. package/cap/bin/lib/cap-feature-map.cjs +1943 -0
  42. package/cap/bin/lib/cap-fitness-score.cjs +1075 -0
  43. package/cap/bin/lib/cap-impact-analysis.cjs +652 -0
  44. package/cap/bin/lib/cap-learn-review.cjs +1072 -0
  45. package/cap/bin/lib/cap-learning-signals.cjs +627 -0
  46. package/cap/bin/lib/cap-loader.cjs +227 -0
  47. package/cap/bin/lib/cap-logger.cjs +57 -0
  48. package/cap/bin/lib/cap-memory-bridge.cjs +764 -0
  49. package/cap/bin/lib/cap-memory-confidence.cjs +452 -0
  50. package/cap/bin/lib/cap-memory-dir.cjs +987 -0
  51. package/cap/bin/lib/cap-memory-engine.cjs +698 -0
  52. package/cap/bin/lib/cap-memory-extends.cjs +398 -0
  53. package/cap/bin/lib/cap-memory-graph.cjs +790 -0
  54. package/cap/bin/lib/cap-memory-migrate.cjs +2015 -0
  55. package/cap/bin/lib/cap-memory-pin.cjs +183 -0
  56. package/cap/bin/lib/cap-memory-platform.cjs +490 -0
  57. package/cap/bin/lib/cap-memory-prune.cjs +707 -0
  58. package/cap/bin/lib/cap-memory-schema.cjs +812 -0
  59. package/cap/bin/lib/cap-migrate-tags.cjs +309 -0
  60. package/cap/bin/lib/cap-migrate.cjs +540 -0
  61. package/cap/bin/lib/cap-pattern-apply.cjs +1203 -0
  62. package/cap/bin/lib/cap-pattern-pipeline.cjs +1034 -0
  63. package/cap/bin/lib/cap-plugin-manifest.cjs +80 -0
  64. package/cap/bin/lib/cap-realtime-affinity.cjs +399 -0
  65. package/cap/bin/lib/cap-reconcile.cjs +570 -0
  66. package/cap/bin/lib/cap-research-gate.cjs +218 -0
  67. package/cap/bin/lib/cap-scope-filter.cjs +402 -0
  68. package/cap/bin/lib/cap-semantic-pipeline.cjs +1038 -0
  69. package/cap/bin/lib/cap-session-extract.cjs +987 -0
  70. package/cap/bin/lib/cap-session.cjs +445 -0
  71. package/cap/bin/lib/cap-snapshot-linkage.cjs +963 -0
  72. package/cap/bin/lib/cap-stack-docs.cjs +646 -0
  73. package/cap/bin/lib/cap-tag-observer.cjs +371 -0
  74. package/cap/bin/lib/cap-tag-scanner.cjs +1766 -0
  75. package/cap/bin/lib/cap-telemetry.cjs +466 -0
  76. package/cap/bin/lib/cap-test-audit.cjs +1438 -0
  77. package/cap/bin/lib/cap-thread-migrator.cjs +307 -0
  78. package/cap/bin/lib/cap-thread-synthesis.cjs +545 -0
  79. package/cap/bin/lib/cap-thread-tracker.cjs +519 -0
  80. package/cap/bin/lib/cap-trace.cjs +399 -0
  81. package/cap/bin/lib/cap-trust-mode.cjs +336 -0
  82. package/cap/bin/lib/cap-ui-design-editor.cjs +642 -0
  83. package/cap/bin/lib/cap-ui-mind-map.cjs +712 -0
  84. package/cap/bin/lib/cap-ui-thread-nav.cjs +693 -0
  85. package/cap/bin/lib/cap-ui.cjs +1245 -0
  86. package/cap/bin/lib/cap-upgrade.cjs +1028 -0
  87. package/cap/bin/lib/cli/arg-helpers.cjs +49 -0
  88. package/cap/bin/lib/cli/frontmatter-router.cjs +31 -0
  89. package/cap/bin/lib/cli/init-router.cjs +68 -0
  90. package/cap/bin/lib/cli/phase-router.cjs +102 -0
  91. package/cap/bin/lib/cli/state-router.cjs +61 -0
  92. package/cap/bin/lib/cli/template-router.cjs +37 -0
  93. package/cap/bin/lib/cli/uat-router.cjs +29 -0
  94. package/cap/bin/lib/cli/validation-router.cjs +26 -0
  95. package/cap/bin/lib/cli/verification-router.cjs +31 -0
  96. package/cap/bin/lib/cli/workstream-router.cjs +39 -0
  97. package/cap/bin/lib/commands.cjs +961 -0
  98. package/cap/bin/lib/config.cjs +467 -0
  99. package/cap/bin/lib/convention-reader.cjs +258 -0
  100. package/cap/bin/lib/core.cjs +1241 -0
  101. package/cap/bin/lib/feature-aggregator.cjs +423 -0
  102. package/cap/bin/lib/frontmatter.cjs +337 -0
  103. package/cap/bin/lib/init.cjs +1443 -0
  104. package/cap/bin/lib/manifest-generator.cjs +383 -0
  105. package/cap/bin/lib/milestone.cjs +253 -0
  106. package/cap/bin/lib/model-profiles.cjs +69 -0
  107. package/cap/bin/lib/monorepo-context.cjs +226 -0
  108. package/cap/bin/lib/monorepo-migrator.cjs +509 -0
  109. package/cap/bin/lib/phase.cjs +889 -0
  110. package/cap/bin/lib/profile-output.cjs +989 -0
  111. package/cap/bin/lib/profile-pipeline.cjs +540 -0
  112. package/cap/bin/lib/roadmap.cjs +330 -0
  113. package/cap/bin/lib/security.cjs +394 -0
  114. package/cap/bin/lib/session-manager.cjs +292 -0
  115. package/cap/bin/lib/skeleton-generator.cjs +179 -0
  116. package/cap/bin/lib/state.cjs +1032 -0
  117. package/cap/bin/lib/template.cjs +231 -0
  118. package/cap/bin/lib/test-detector.cjs +62 -0
  119. package/cap/bin/lib/uat.cjs +283 -0
  120. package/cap/bin/lib/verify.cjs +889 -0
  121. package/cap/bin/lib/workspace-detector.cjs +371 -0
  122. package/cap/bin/lib/workstream.cjs +492 -0
  123. package/cap/commands/gsd/workstreams.md +63 -0
  124. package/cap/references/arc-standard.md +315 -0
  125. package/cap/references/cap-agent-architecture.md +101 -0
  126. package/cap/references/cap-gitignore-template +9 -0
  127. package/cap/references/cap-zero-deps.md +158 -0
  128. package/cap/references/checkpoints.md +778 -0
  129. package/cap/references/continuation-format.md +249 -0
  130. package/cap/references/contract-test-templates.md +312 -0
  131. package/cap/references/feature-map-template.md +25 -0
  132. package/cap/references/git-integration.md +295 -0
  133. package/cap/references/git-planning-commit.md +38 -0
  134. package/cap/references/model-profiles.md +174 -0
  135. package/cap/references/phase-numbering.md +126 -0
  136. package/cap/references/planning-config.md +202 -0
  137. package/cap/references/property-test-templates.md +316 -0
  138. package/cap/references/security-test-templates.md +347 -0
  139. package/cap/references/session-template.json +8 -0
  140. package/cap/references/tdd.md +263 -0
  141. package/cap/references/user-profiling.md +681 -0
  142. package/cap/references/verification-patterns.md +612 -0
  143. package/cap/templates/UAT.md +265 -0
  144. package/cap/templates/claude-md.md +175 -0
  145. package/cap/templates/codebase/architecture.md +255 -0
  146. package/cap/templates/codebase/concerns.md +310 -0
  147. package/cap/templates/codebase/conventions.md +307 -0
  148. package/cap/templates/codebase/integrations.md +280 -0
  149. package/cap/templates/codebase/stack.md +186 -0
  150. package/cap/templates/codebase/structure.md +285 -0
  151. package/cap/templates/codebase/testing.md +480 -0
  152. package/cap/templates/config.json +44 -0
  153. package/cap/templates/context.md +352 -0
  154. package/cap/templates/continue-here.md +78 -0
  155. package/cap/templates/copilot-instructions.md +7 -0
  156. package/cap/templates/debug-subagent-prompt.md +91 -0
  157. package/cap/templates/discussion-log.md +63 -0
  158. package/cap/templates/milestone-archive.md +123 -0
  159. package/cap/templates/milestone.md +115 -0
  160. package/cap/templates/phase-prompt.md +610 -0
  161. package/cap/templates/planner-subagent-prompt.md +117 -0
  162. package/cap/templates/project.md +186 -0
  163. package/cap/templates/requirements.md +231 -0
  164. package/cap/templates/research-project/ARCHITECTURE.md +204 -0
  165. package/cap/templates/research-project/FEATURES.md +147 -0
  166. package/cap/templates/research-project/PITFALLS.md +200 -0
  167. package/cap/templates/research-project/STACK.md +120 -0
  168. package/cap/templates/research-project/SUMMARY.md +170 -0
  169. package/cap/templates/research.md +552 -0
  170. package/cap/templates/roadmap.md +202 -0
  171. package/cap/templates/state.md +176 -0
  172. package/cap/templates/summary.md +364 -0
  173. package/cap/templates/user-preferences.md +498 -0
  174. package/cap/templates/verification-report.md +322 -0
  175. package/cap/workflows/add-phase.md +112 -0
  176. package/cap/workflows/add-tests.md +351 -0
  177. package/cap/workflows/add-todo.md +158 -0
  178. package/cap/workflows/audit-milestone.md +340 -0
  179. package/cap/workflows/audit-uat.md +109 -0
  180. package/cap/workflows/autonomous.md +891 -0
  181. package/cap/workflows/check-todos.md +177 -0
  182. package/cap/workflows/cleanup.md +152 -0
  183. package/cap/workflows/complete-milestone.md +767 -0
  184. package/cap/workflows/diagnose-issues.md +231 -0
  185. package/cap/workflows/discovery-phase.md +289 -0
  186. package/cap/workflows/discuss-phase-assumptions.md +653 -0
  187. package/cap/workflows/discuss-phase.md +1049 -0
  188. package/cap/workflows/do.md +104 -0
  189. package/cap/workflows/execute-phase.md +846 -0
  190. package/cap/workflows/execute-plan.md +514 -0
  191. package/cap/workflows/fast.md +105 -0
  192. package/cap/workflows/forensics.md +265 -0
  193. package/cap/workflows/health.md +181 -0
  194. package/cap/workflows/help.md +660 -0
  195. package/cap/workflows/insert-phase.md +130 -0
  196. package/cap/workflows/list-phase-assumptions.md +178 -0
  197. package/cap/workflows/list-workspaces.md +56 -0
  198. package/cap/workflows/manager.md +362 -0
  199. package/cap/workflows/map-codebase.md +377 -0
  200. package/cap/workflows/milestone-summary.md +223 -0
  201. package/cap/workflows/new-milestone.md +486 -0
  202. package/cap/workflows/new-project.md +1250 -0
  203. package/cap/workflows/new-workspace.md +237 -0
  204. package/cap/workflows/next.md +97 -0
  205. package/cap/workflows/node-repair.md +92 -0
  206. package/cap/workflows/note.md +156 -0
  207. package/cap/workflows/pause-work.md +176 -0
  208. package/cap/workflows/plan-milestone-gaps.md +273 -0
  209. package/cap/workflows/plan-phase.md +857 -0
  210. package/cap/workflows/plant-seed.md +169 -0
  211. package/cap/workflows/pr-branch.md +129 -0
  212. package/cap/workflows/profile-user.md +449 -0
  213. package/cap/workflows/progress.md +507 -0
  214. package/cap/workflows/quick.md +757 -0
  215. package/cap/workflows/remove-phase.md +155 -0
  216. package/cap/workflows/remove-workspace.md +90 -0
  217. package/cap/workflows/research-phase.md +82 -0
  218. package/cap/workflows/resume-project.md +326 -0
  219. package/cap/workflows/review.md +228 -0
  220. package/cap/workflows/session-report.md +146 -0
  221. package/cap/workflows/settings.md +283 -0
  222. package/cap/workflows/ship.md +228 -0
  223. package/cap/workflows/stats.md +60 -0
  224. package/cap/workflows/transition.md +671 -0
  225. package/cap/workflows/ui-phase.md +298 -0
  226. package/cap/workflows/ui-review.md +161 -0
  227. package/cap/workflows/update.md +323 -0
  228. package/cap/workflows/validate-phase.md +170 -0
  229. package/cap/workflows/verify-phase.md +254 -0
  230. package/cap/workflows/verify-work.md +637 -0
  231. package/commands/cap/annotate.md +165 -0
  232. package/commands/cap/brainstorm.md +393 -0
  233. package/commands/cap/checkpoint.md +106 -0
  234. package/commands/cap/completeness.md +94 -0
  235. package/commands/cap/continue.md +72 -0
  236. package/commands/cap/debug.md +588 -0
  237. package/commands/cap/deps.md +169 -0
  238. package/commands/cap/design.md +479 -0
  239. package/commands/cap/init.md +354 -0
  240. package/commands/cap/iterate.md +249 -0
  241. package/commands/cap/learn.md +459 -0
  242. package/commands/cap/memory.md +275 -0
  243. package/commands/cap/migrate-feature-map.md +91 -0
  244. package/commands/cap/migrate-memory.md +108 -0
  245. package/commands/cap/migrate-tags.md +91 -0
  246. package/commands/cap/migrate.md +131 -0
  247. package/commands/cap/prototype.md +510 -0
  248. package/commands/cap/reconcile.md +121 -0
  249. package/commands/cap/review.md +360 -0
  250. package/commands/cap/save.md +72 -0
  251. package/commands/cap/scan.md +404 -0
  252. package/commands/cap/start.md +356 -0
  253. package/commands/cap/status.md +118 -0
  254. package/commands/cap/test-audit.md +262 -0
  255. package/commands/cap/test.md +394 -0
  256. package/commands/cap/trace.md +133 -0
  257. package/commands/cap/ui.md +167 -0
  258. package/hooks/dist/cap-check-update.js +115 -0
  259. package/hooks/dist/cap-context-monitor.js +185 -0
  260. package/hooks/dist/cap-learn-review-hook.js +114 -0
  261. package/hooks/dist/cap-learning-hook.js +192 -0
  262. package/hooks/dist/cap-memory.js +299 -0
  263. package/hooks/dist/cap-prompt-guard.js +97 -0
  264. package/hooks/dist/cap-statusline.js +157 -0
  265. package/hooks/dist/cap-tag-observer.js +115 -0
  266. package/hooks/dist/cap-version-check.js +112 -0
  267. package/hooks/dist/cap-workflow-guard.js +175 -0
  268. package/hooks/hooks.json +55 -0
  269. package/package.json +58 -0
  270. package/scripts/base64-scan.sh +262 -0
  271. package/scripts/build-hooks.js +93 -0
  272. package/scripts/cap-removal-checklist.md +202 -0
  273. package/scripts/prompt-injection-scan.sh +199 -0
  274. package/scripts/run-tests.cjs +181 -0
  275. package/scripts/secret-scan.sh +227 -0
@@ -0,0 +1,218 @@
1
+ // @cap-feature(feature:F-059) Research-First Gate Before Prototype — pure-logic module.
2
+ // @cap-decision Pure logic + reporting only. The prototype command is responsible for prompting the user and invoking refresh-docs; this module never blocks and never reads stdin.
3
+ // @cap-decision Library extraction is a two-pass match: (1) exact token match against package.json dependency names, (2) substring match inside AC descriptions. A library must appear in package.json to be considered (zero false-positives from prose mentions like "proven pattern").
4
+ // @cap-constraint Zero external dependencies — node: built-ins only.
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+
11
+ const stackDocs = require('./cap-stack-docs.cjs');
12
+
13
+ /** Default staleness threshold (days). ACs spec 30 days. */
14
+ const DEFAULT_MAX_AGE_DAYS = 30;
15
+
16
+ /**
17
+ * @typedef {Object} GateResult
18
+ * @property {string[]} libraries - Libraries referenced by the scoped ACs + present in package.json
19
+ * @property {string[]} missing - Libraries with no cached docs at all
20
+ * @property {string[]} stale - Libraries with docs older than maxAgeDays
21
+ * @property {string[]} fresh - Libraries with docs within the freshness window
22
+ * @property {number} maxAgeDays - Staleness threshold applied
23
+ */
24
+
25
+ /**
26
+ * Load the direct (dependencies + devDependencies) name list from a package.json path.
27
+ * Tolerant of missing / malformed input — returns [] on any failure.
28
+ * @param {string} projectRoot
29
+ * @returns {string[]}
30
+ */
31
+ function readPackageDependencies(projectRoot) {
32
+ const pkgPath = path.join(projectRoot, 'package.json');
33
+ if (!fs.existsSync(pkgPath)) return [];
34
+ try {
35
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
36
+ const deps = pkg.dependencies && typeof pkg.dependencies === 'object' ? Object.keys(pkg.dependencies) : [];
37
+ const devDeps = pkg.devDependencies && typeof pkg.devDependencies === 'object' ? Object.keys(pkg.devDependencies) : [];
38
+ const seen = new Set();
39
+ const out = [];
40
+ for (const n of [...deps, ...devDeps]) {
41
+ if (typeof n === 'string' && n.length > 0 && !seen.has(n)) {
42
+ seen.add(n);
43
+ out.push(n);
44
+ }
45
+ }
46
+ return out.sort();
47
+ } catch {
48
+ return [];
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Escape a dependency name for safe embedding in a RegExp. npm scoped names
54
+ * contain '/', '@', '.', '-' which are regex metacharacters in some positions.
55
+ * @param {string} name
56
+ * @returns {string}
57
+ */
58
+ function escapeRegex(name) {
59
+ return name.replace(/[\\^$.*+?()[\]{}|/]/g, '\\$&');
60
+ }
61
+
62
+ // @cap-todo(ac:F-059/AC-1) parseLibraryMentions scans AC descriptions for any dependency listed in package.json. We deliberately use package.json as the whitelist — a mention like "proven pattern" or "stripe webhook" in prose will not count unless the dep is installed. This keeps the gate specific and actionable.
63
+ /**
64
+ * Scan AC description strings for references to any library listed in `dependencies`.
65
+ * Uses whole-token boundaries so `react` doesn't match `overreacted` or `reactivity`.
66
+ *
67
+ * @param {string[]} acDescriptions - One description per AC
68
+ * @param {string[]} dependencies - Library names from package.json
69
+ * @returns {string[]} Sorted unique dependency names that appeared in any description
70
+ */
71
+ function parseLibraryMentions(acDescriptions, dependencies) {
72
+ if (!Array.isArray(acDescriptions) || acDescriptions.length === 0) return [];
73
+ if (!Array.isArray(dependencies) || dependencies.length === 0) return [];
74
+ const haystack = acDescriptions.filter((s) => typeof s === 'string').join('\n').toLowerCase();
75
+ if (haystack.length === 0) return [];
76
+
77
+ const hits = new Set();
78
+ for (const dep of dependencies) {
79
+ if (typeof dep !== 'string' || dep.length === 0) continue;
80
+ const lower = dep.toLowerCase();
81
+ // Word-boundary matching: the name must not be embedded inside a longer identifier.
82
+ // Scoped packages ("@org/pkg") already contain non-word characters so plain \b works.
83
+ const re = new RegExp(`(?:^|[^a-z0-9@/_-])${escapeRegex(lower)}(?:$|[^a-z0-9@/_-])`, 'i');
84
+ if (re.test(haystack)) hits.add(dep);
85
+ }
86
+ return Array.from(hits).sort();
87
+ }
88
+
89
+ // @cap-todo(ac:F-059/AC-2) checkStackDocs buckets each library into missing / stale / fresh using cap-stack-docs.checkFreshness. The spec named `.cap/stack-docs/{library}/` as a directory; F-004 stores docs as `{library}.md` files — we honour the existing on-disk convention and treat the AC wording as the *intent* (a per-library artefact, not its exact layout).
90
+ /**
91
+ * For each library, check whether its stack doc is missing, stale, or fresh.
92
+ * @param {string} projectRoot
93
+ * @param {string[]} libraries
94
+ * @param {number} [maxAgeDays=DEFAULT_MAX_AGE_DAYS]
95
+ * @returns {{missing:string[], stale:string[], fresh:string[]}}
96
+ */
97
+ function checkStackDocs(projectRoot, libraries, maxAgeDays = DEFAULT_MAX_AGE_DAYS) {
98
+ const missing = [];
99
+ const stale = [];
100
+ const fresh = [];
101
+ const maxAgeHours = maxAgeDays * 24;
102
+
103
+ for (const lib of libraries || []) {
104
+ if (typeof lib !== 'string' || lib.length === 0) continue;
105
+ const freshness = stackDocs.checkFreshness(projectRoot, lib, maxAgeHours);
106
+ if (!freshness.filePath) {
107
+ missing.push(lib);
108
+ } else if (!freshness.fresh) {
109
+ stale.push(lib);
110
+ } else {
111
+ fresh.push(lib);
112
+ }
113
+ }
114
+
115
+ return {
116
+ missing: missing.sort(),
117
+ stale: stale.sort(),
118
+ fresh: fresh.sort(),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Run the full research-first gate against a scoped set of AC descriptions.
124
+ * Pure function — reads filesystem (package.json, stack-docs mtimes) but never prompts or logs.
125
+ *
126
+ * @param {Object} opts
127
+ * @param {string} opts.projectRoot - Absolute project root
128
+ * @param {string[]} opts.acDescriptions - AC description strings (already scoped to the features being prototyped)
129
+ * @param {number} [opts.maxAgeDays=DEFAULT_MAX_AGE_DAYS]
130
+ * @param {string[]} [opts.dependencies] - Override package-json detection (for testing)
131
+ * @returns {GateResult}
132
+ */
133
+ function runGate(opts) {
134
+ const projectRoot = opts && opts.projectRoot;
135
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
136
+ throw new TypeError('runGate: projectRoot must be a non-empty string');
137
+ }
138
+ const acDescriptions = Array.isArray(opts.acDescriptions) ? opts.acDescriptions : [];
139
+ const maxAgeDays = typeof opts.maxAgeDays === 'number' && opts.maxAgeDays > 0
140
+ ? opts.maxAgeDays
141
+ : DEFAULT_MAX_AGE_DAYS;
142
+ const dependencies = Array.isArray(opts.dependencies) ? opts.dependencies : readPackageDependencies(projectRoot);
143
+
144
+ const libraries = parseLibraryMentions(acDescriptions, dependencies);
145
+ const buckets = checkStackDocs(projectRoot, libraries, maxAgeDays);
146
+
147
+ return {
148
+ libraries,
149
+ missing: buckets.missing,
150
+ stale: buckets.stale,
151
+ fresh: buckets.fresh,
152
+ maxAgeDays,
153
+ };
154
+ }
155
+
156
+ // @cap-todo(ac:F-059/AC-3) formatWarning renders the user-facing block including the /cap:refresh-docs hint. Empty when nothing is missing/stale (caller can skip printing).
157
+ /**
158
+ * Render a human-readable warning block for the prototype orchestrator to print.
159
+ * Returns an empty string when there is nothing to warn about.
160
+ * @param {GateResult} result
161
+ * @returns {string}
162
+ */
163
+ function formatWarning(result) {
164
+ const missing = result && Array.isArray(result.missing) ? result.missing : [];
165
+ const stale = result && Array.isArray(result.stale) ? result.stale : [];
166
+ if (missing.length === 0 && stale.length === 0) return '';
167
+
168
+ const lines = ['Research-First Gate — missing or stale stack docs detected:'];
169
+ if (missing.length > 0) {
170
+ lines.push(` Missing: ${missing.join(', ')}`);
171
+ }
172
+ if (stale.length > 0) {
173
+ lines.push(` Stale (> ${result.maxAgeDays} days): ${stale.join(', ')}`);
174
+ }
175
+ const refreshTargets = [...missing, ...stale];
176
+ lines.push('');
177
+ lines.push(` Recommendation: /cap:refresh-docs ${refreshTargets.join(' ')}`);
178
+ lines.push(' Proceed anyway? [y/N]');
179
+ return lines.join('\n');
180
+ }
181
+
182
+ // @cap-todo(ac:F-059/AC-6) logGateCheck appends a compact session-log record so post-run diagnostics can correlate low-quality prototypes with skipped research.
183
+ /**
184
+ * Append a JSONL record describing the gate outcome to the session log.
185
+ * Best-effort — I/O failures are swallowed so the gate never blocks the prototype flow.
186
+ *
187
+ * @param {string} projectRoot
188
+ * @param {{skipped?:boolean, libsChecked:number, missing:number, stale:number}} record
189
+ * @param {Date} [now]
190
+ */
191
+ function logGateCheck(projectRoot, record, now) {
192
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) return;
193
+ const logPath = path.join(projectRoot, '.cap', 'session-log.jsonl');
194
+ const entry = {
195
+ timestamp: (now instanceof Date ? now : new Date()).toISOString(),
196
+ event: 'research-gate',
197
+ skipped: !!(record && record.skipped),
198
+ libsChecked: record && Number.isFinite(record.libsChecked) ? record.libsChecked : 0,
199
+ missing: record && Number.isFinite(record.missing) ? record.missing : 0,
200
+ stale: record && Number.isFinite(record.stale) ? record.stale : 0,
201
+ };
202
+ try {
203
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
204
+ fs.appendFileSync(logPath, JSON.stringify(entry) + '\n', 'utf8');
205
+ } catch {
206
+ // Best-effort — never propagate logging failure to the caller.
207
+ }
208
+ }
209
+
210
+ module.exports = {
211
+ DEFAULT_MAX_AGE_DAYS,
212
+ readPackageDependencies,
213
+ parseLibraryMentions,
214
+ checkStackDocs,
215
+ runGate,
216
+ formatWarning,
217
+ logGateCheck,
218
+ };
@@ -0,0 +1,402 @@
1
+ 'use strict';
2
+
3
+ // @cap-feature(feature:F-085, primary:true) Scope filter shared by cap-tag-scanner and cap-migrate-tags.
4
+ //
5
+ // @cap-decision(F-085/AC-1) One module, two consumers. Scanner and migrator both walk the same
6
+ // tree with the same exclusion semantics. Duplicating the rules in both modules drifts —
7
+ // centralising them here keeps DEFAULT_DIR_EXCLUDES, DEFAULT_PATH_EXCLUDES and gitignore
8
+ // handling consistent.
9
+ //
10
+ // @cap-decision(F-085/AC-2) Gitignore is honoured at scan-projectRoot only (not nested .gitignore
11
+ // files). 99% of the noise on real repos is in top-level ignored dirs (.claude, node_modules,
12
+ // coverage). Recursive .gitignore parsing would multiply complexity for marginal coverage.
13
+ //
14
+ // @cap-decision(F-085/AC-3) Path-pattern excludes are PREFIX-matched on relative paths (not full
15
+ // glob). Patterns starting with `**/` are treated as suffix-anywhere. Real-world need is
16
+ // covered by these two shapes; full glob would require a glob compiler we don't have.
17
+
18
+ const fs = require('node:fs');
19
+ const path = require('node:path');
20
+
21
+ // @cap-decision(F-085/AC-3) DEFAULT_DIR_EXCLUDES preserves the legacy basename-matched list from
22
+ // cap-tag-scanner.cjs so the scanner's behaviour is byte-identical when no extra config is set.
23
+ const DEFAULT_DIR_EXCLUDES = Object.freeze([
24
+ '.git', '.cap', '.planning',
25
+ 'node_modules', 'dist', 'build', 'coverage', 'out',
26
+ '.next', '.turbo', '.nx', '.cache', '.parcel-cache', '.vercel', '.svelte-kit',
27
+ '__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache', '.tox', 'venv', '.venv',
28
+ 'target', '.gradle', 'Pods', '.expo',
29
+ ]);
30
+
31
+ // @cap-decision(F-085/AC-3, F-085/AC-4) DEFAULT_PATH_EXCLUDES catches three classes that
32
+ // basename-matching alone misses:
33
+ // - .claude/worktrees: agent worktrees, gitignored on most projects but defensive here too
34
+ // - .claude/cap: plugin-self-mirror, would let migrate-tags rewrite the user-global install
35
+ // - tests/fixtures (and **/fixtures/polyglot): scanner test inputs are intentionally raw-tagged
36
+ const DEFAULT_PATH_EXCLUDES = Object.freeze([
37
+ '.claude/worktrees',
38
+ '.claude/cap',
39
+ 'tests/fixtures',
40
+ '**/fixtures/polyglot',
41
+ '.cap/snapshots',
42
+ ]);
43
+
44
+ // @cap-decision(F-085/AC-7) LARGE_DIFF_THRESHOLD is the count above which a destructive batch
45
+ // operation (cap:migrate-tags --apply) requires an extra confirm gate. 500 was chosen by
46
+ // inspecting the realistic worst-case in this repo (~89 legitimate files) and adding a 5x
47
+ // margin. Apply against >500 files is almost always a scope-filter bug, never an intent.
48
+ const LARGE_DIFF_THRESHOLD = 500;
49
+
50
+ // @cap-decision(F-086/AC-2) Bundle-detection thresholds. The line-count budget catches
51
+ // concatenated outputs (Next.js dev bundles routinely hit 5–50k lines, Webpack chunks 10k+);
52
+ // honest source files in this codebase peak around 1600 (cap-tag-scanner.cjs). 5000 is a 3x
53
+ // margin against the largest legitimate file, well below the smallest typical bundle.
54
+ const BUNDLE_LINE_THRESHOLD = 5000;
55
+
56
+ // @cap-decision(F-086/AC-2) Bundle-typical path patterns. RegExps catching shapes that recur
57
+ // across bundlers (Next.js, Webpack, esbuild, Turbopack). Matched against the project-relative
58
+ // POSIX path. Path-pattern check is the cheap pre-filter; the line-count probe is the
59
+ // expensive last resort and only fires when callers explicitly opt in via isBundle().
60
+ const BUNDLE_PATH_PATTERNS = Object.freeze([
61
+ /\/chunks\//, // Next.js / Webpack chunk dir
62
+ /\[root-of-/, // Next.js dev-server bundle naming: [root-of-the-server]
63
+ /__[a-z0-9_]+\._\.js$/i, // Webpack-style hashed bundle: __0p_l47z._.js
64
+ /\.bundle\.[mc]?js$/, // Generic .bundle.js
65
+ /\.min\.[mc]?js$/, // Minified outputs
66
+ /\.chunk\.[mc]?js$/, // .chunk.js
67
+ ]);
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Gitignore handling
71
+
72
+ // @cap-todo(ac:F-085/AC-2) parseGitignore returns an array of compiled matchers from the
73
+ // project's top-level .gitignore. Negations (`!pattern`) are dropped with a quiet ignore;
74
+ // gitignore precedence rules across multiple files are out of scope for the MVP.
75
+ /**
76
+ * Parse a top-level `.gitignore` file into a list of matcher functions.
77
+ *
78
+ * Each matcher takes (relativePath, isDir) and returns true when the path is ignored.
79
+ *
80
+ * @param {string} projectRoot
81
+ * @returns {Array<(relPath: string, isDir: boolean) => boolean>}
82
+ */
83
+ function parseGitignore(projectRoot) {
84
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) return [];
85
+ const giPath = path.join(projectRoot, '.gitignore');
86
+ let raw;
87
+ try {
88
+ raw = fs.readFileSync(giPath, 'utf8');
89
+ } catch (_e) {
90
+ return [];
91
+ }
92
+ const matchers = [];
93
+ for (const lineRaw of raw.split(/\r?\n/)) {
94
+ const line = lineRaw.trim();
95
+ if (line === '') continue;
96
+ if (line.startsWith('#')) continue;
97
+ if (line.startsWith('!')) continue; // negations not supported in MVP
98
+ matchers.push(_compileGitignorePattern(line));
99
+ }
100
+ return matchers.filter((m) => m !== null);
101
+ }
102
+
103
+ // @cap-todo(ac:F-085/AC-2) _compileGitignorePattern handles the four common shapes seen in real
104
+ // .gitignore files: `dir/`, `/anchored`, `*.ext`, `path/segment`. Anything more exotic falls
105
+ // back to the literal-substring matcher rather than failing closed.
106
+ function _compileGitignorePattern(pattern) {
107
+ let p = pattern;
108
+ // Trailing slash → "directory" pattern. Per gitignore semantics this matches the directory
109
+ // ITSELF only when isDir, BUT files inside that directory still belong to "ignored content"
110
+ // because their parent dir is ignored. Since our matcher gets called per-path (not as a
111
+ // tree-walk), we accept files when their path starts with `pattern/` — that's the only way
112
+ // a file can be "inside an ignored directory" given a single-path API.
113
+ let dirOnly = false;
114
+ if (p.endsWith('/')) {
115
+ dirOnly = true;
116
+ p = p.slice(0, -1);
117
+ }
118
+ // Leading slash → anchored at repo root
119
+ let anchored = false;
120
+ if (p.startsWith('/')) {
121
+ anchored = true;
122
+ p = p.slice(1);
123
+ }
124
+ // No-glob fast path: literal segment (the dominant case: `node_modules`, `.claude`, `dist`)
125
+ if (!p.includes('*') && !p.includes('?')) {
126
+ return (relPath, isDir) => {
127
+ if (anchored) {
128
+ // Anchored: exact match (must be dir if dirOnly) OR path-prefix (any descendant)
129
+ if (dirOnly) {
130
+ if (relPath === p) return !!isDir;
131
+ return relPath.startsWith(p + '/');
132
+ }
133
+ return relPath === p || relPath.startsWith(p + '/');
134
+ }
135
+ // Unanchored: match anywhere in the tree
136
+ const segments = relPath.split('/');
137
+ if (dirOnly) {
138
+ // For dir-only patterns, accept if any non-leaf segment equals p (descendant case)
139
+ // OR if relPath is exactly p AND it's a directory.
140
+ for (let i = 0; i < segments.length - 1; i++) {
141
+ if (segments[i] === p) return true;
142
+ }
143
+ return relPath === p && !!isDir;
144
+ }
145
+ // Non-dir-only: any segment match OR exact path-prefix
146
+ if (segments.includes(p)) return true;
147
+ return relPath === p || relPath.startsWith(p + '/');
148
+ };
149
+ }
150
+ // Glob path: compile to regex. ** = any segments, * = within segment, ? = single char.
151
+ const re = _globToRegex(p, { anchored });
152
+ return (relPath, isDir) => {
153
+ if (dirOnly && !isDir) return false;
154
+ if (anchored) return re.test(relPath);
155
+ // Unanchored glob: try against the full path AND any suffix that starts at a segment boundary.
156
+ if (re.test(relPath)) return true;
157
+ const segments = relPath.split('/');
158
+ for (let i = 1; i < segments.length; i++) {
159
+ if (re.test(segments.slice(i).join('/'))) return true;
160
+ }
161
+ return false;
162
+ };
163
+ }
164
+
165
+ function _globToRegex(glob, opts) {
166
+ let out = opts && opts.anchored ? '^' : '^';
167
+ let i = 0;
168
+ while (i < glob.length) {
169
+ const c = glob[i];
170
+ if (c === '*' && glob[i + 1] === '*') {
171
+ out += '.*';
172
+ i += 2;
173
+ if (glob[i] === '/') i += 1;
174
+ } else if (c === '*') {
175
+ out += '[^/]*';
176
+ i += 1;
177
+ } else if (c === '?') {
178
+ out += '[^/]';
179
+ i += 1;
180
+ } else if ('.+()[]{}^$|\\'.includes(c)) {
181
+ out += '\\' + c;
182
+ i += 1;
183
+ } else {
184
+ out += c;
185
+ i += 1;
186
+ }
187
+ }
188
+ out += '(?:/.*)?$';
189
+ return new RegExp(out);
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Path-pattern matching for DEFAULT_PATH_EXCLUDES + user includes/excludes
194
+
195
+ function _matchPathPattern(relPath, pattern) {
196
+ if (typeof pattern !== 'string' || pattern.length === 0) return false;
197
+ // **/foo → match suffix anywhere in the tree
198
+ if (pattern.startsWith('**/')) {
199
+ const tail = pattern.slice(3);
200
+ return relPath === tail || relPath.endsWith('/' + tail) || relPath.startsWith(tail + '/') || relPath.includes('/' + tail + '/');
201
+ }
202
+ // Plain prefix match against project-relative path
203
+ return relPath === pattern || relPath.startsWith(pattern + '/');
204
+ }
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Bundle detection (F-086/AC-2)
208
+
209
+ // @cap-todo(ac:F-086/AC-2) isBundle decides whether a file is a generated artefact (Webpack
210
+ // chunk, Next.js dev-bundle, minified output, …). Two probes:
211
+ // - PATH probe (cheap, default): regex match on project-relative path against
212
+ // BUNDLE_PATH_PATTERNS. Catches the typical bundler output naming.
213
+ // - LINE-COUNT probe (expensive, opt-in via deep:true): reads the file and counts lines.
214
+ // Files with > BUNDLE_LINE_THRESHOLD lines are flagged — concatenated bundles routinely
215
+ // exceed this while honest source code in this codebase peaks at ~1600.
216
+ /**
217
+ * @param {string} absPath - absolute or relative path; only the basename + dir-segments matter
218
+ * @param {{ deep?: boolean, lineThreshold?: number }} [opts]
219
+ * deep — if true, also runs the line-count probe (file I/O). Default false.
220
+ * lineThreshold — override BUNDLE_LINE_THRESHOLD.
221
+ * @returns {boolean}
222
+ */
223
+ function isBundle(absPath, opts) {
224
+ if (typeof absPath !== 'string' || absPath.length === 0) return false;
225
+ const posixPath = absPath.split(path.sep).join('/');
226
+ // Path probe — cheap
227
+ for (const re of BUNDLE_PATH_PATTERNS) {
228
+ if (re.test(posixPath)) return true;
229
+ }
230
+ // Line-count probe — opt-in, performs file I/O
231
+ if (opts && opts.deep) {
232
+ const limit = (opts && typeof opts.lineThreshold === 'number') ? opts.lineThreshold : BUNDLE_LINE_THRESHOLD;
233
+ let raw;
234
+ try {
235
+ raw = fs.readFileSync(absPath, 'utf8');
236
+ } catch (_e) {
237
+ return false; // unreadable → can't decide; default to "not bundle"
238
+ }
239
+ // Quick line count via splitting; for very large files the cost is dominated by readFileSync
240
+ // anyway, so a strchr-style loop wouldn't help meaningfully.
241
+ let lineCount = 1;
242
+ for (let i = 0; i < raw.length; i++) {
243
+ if (raw.charCodeAt(i) === 10 /* \n */) lineCount++;
244
+ // Early exit once we've crossed the threshold — no need to count the rest of a 50k-line bundle.
245
+ if (lineCount > limit) return true;
246
+ }
247
+ }
248
+ return false;
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Plugin-self-mirror detection (F-085/AC-4)
253
+
254
+ // @cap-todo(ac:F-085/AC-4) Plugin-self-mirror = a directory under cwd that exact-mirrors
255
+ // $HOME/.claude/cap/. Detected by walking up from the scanner's installed location and
256
+ // checking whether projectRoot has the same nested layout. This protects users running CAP
257
+ // from inside a clone of the CAP repo itself, where the mirror is real and writeable.
258
+ function detectPluginMirror(projectRoot) {
259
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) return null;
260
+ const candidate = path.join(projectRoot, '.claude', 'cap');
261
+ try {
262
+ const st = fs.statSync(candidate);
263
+ if (!st.isDirectory()) return null;
264
+ } catch (_e) {
265
+ return null;
266
+ }
267
+ // Heuristic: if `.claude/cap/bin/` and `.claude/cap/commands/` both exist, this is the
268
+ // plugin-self-mirror layout. (One of those alone could be a coincidence.)
269
+ const binExists = fs.existsSync(path.join(candidate, 'bin'));
270
+ const cmdExists = fs.existsSync(path.join(candidate, 'commands'));
271
+ if (binExists && cmdExists) return path.relative(projectRoot, candidate).split(path.sep).join('/');
272
+ return null;
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // Public: buildScopeFilter
277
+
278
+ /**
279
+ * @typedef {Object} ScopeFilterOptions
280
+ * @property {string[]} [dirExcludes] - Directory basenames to exclude. Defaults to DEFAULT_DIR_EXCLUDES.
281
+ * @property {string[]} [pathExcludes] - Project-relative path patterns to exclude. Defaults to DEFAULT_PATH_EXCLUDES.
282
+ * @property {string[]} [includes] - When non-empty, ONLY paths matching at least one include pattern pass.
283
+ * @property {string[]} [excludes] - User-supplied additional excludes (additive on top of pathExcludes).
284
+ * @property {boolean} [respectGitignore] - Default true. Set false for tests / sandbox runs.
285
+ * @property {boolean} [bundleDetection] - Default true. Set false to skip the path-based bundle filter (F-086/AC-2).
286
+ * @property {boolean} [deepBundleCheck] - Default false. Enables the line-count probe — expensive, opt-in.
287
+ */
288
+
289
+ /**
290
+ * @typedef {Object} ScopeFilter
291
+ * @property {(absPath: string, isDir: boolean) => boolean} isExcluded
292
+ * @property {(items: Array<string|{file:string}>) => Array<[string, number]>} bucketize
293
+ * @property {string[]} pathExcludes
294
+ * @property {string[]} dirExcludes
295
+ * @property {string|null} pluginMirror
296
+ */
297
+
298
+ /**
299
+ * Build a scope filter for the given project root.
300
+ *
301
+ * The returned `isExcluded(absPath, isDir)` returns true when the path should be skipped by
302
+ * downstream walkers. It is the single decision point used by both cap-tag-scanner and
303
+ * cap-migrate-tags so their scope semantics never drift apart.
304
+ *
305
+ * @param {string} projectRoot
306
+ * @param {ScopeFilterOptions} [options]
307
+ * @returns {ScopeFilter}
308
+ */
309
+ function buildScopeFilter(projectRoot, options) {
310
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
311
+ throw new TypeError('projectRoot must be a non-empty string');
312
+ }
313
+ const opts = options || {};
314
+ const dirExcludes = new Set(opts.dirExcludes || DEFAULT_DIR_EXCLUDES);
315
+ const userExcludes = Array.isArray(opts.excludes) ? opts.excludes : [];
316
+ const pathExcludes = [...(opts.pathExcludes || DEFAULT_PATH_EXCLUDES), ...userExcludes];
317
+ const includes = Array.isArray(opts.includes) ? opts.includes : [];
318
+ const respectGitignore = opts.respectGitignore !== false;
319
+ const gitignoreMatchers = respectGitignore ? parseGitignore(projectRoot) : [];
320
+ // @cap-todo(ac:F-086/AC-2) Bundle-detection runs as part of the file-level exclude check.
321
+ // Path-pattern probe is on by default (cheap); deep line-count probe is opt-in (deepBundleCheck).
322
+ const bundleDetection = opts.bundleDetection !== false;
323
+ const deepBundleCheck = opts.deepBundleCheck === true;
324
+
325
+ const pluginMirror = detectPluginMirror(projectRoot);
326
+ // If we detected a plugin mirror, ensure it's in pathExcludes (defense in depth — the
327
+ // gitignore + DEFAULT_PATH_EXCLUDES already cover this, but a user could override both).
328
+ if (pluginMirror && !pathExcludes.includes(pluginMirror)) {
329
+ pathExcludes.push(pluginMirror);
330
+ }
331
+
332
+ function isExcluded(absPath, isDir) {
333
+ const rel = path.relative(projectRoot, absPath).split(path.sep).join('/');
334
+ // A path that's outside projectRoot is, by definition, not in scope.
335
+ if (rel === '' || rel.startsWith('..')) return false;
336
+ const baseName = path.basename(absPath);
337
+
338
+ // 1. Directory-basename fast path (preserves legacy behaviour)
339
+ if (isDir && dirExcludes.has(baseName)) return true;
340
+
341
+ // 2. Path-pattern excludes (project-relative)
342
+ for (const p of pathExcludes) {
343
+ if (_matchPathPattern(rel, p)) return true;
344
+ }
345
+
346
+ // 3. Gitignore matchers
347
+ for (const m of gitignoreMatchers) {
348
+ if (m(rel, !!isDir)) return true;
349
+ }
350
+
351
+ // 4. Bundle-detection (F-086/AC-2): only for files, not directories.
352
+ if (!isDir && bundleDetection) {
353
+ if (isBundle(absPath, { deep: deepBundleCheck })) return true;
354
+ }
355
+
356
+ // 5. Includes are a positive filter: when set, only matches pass
357
+ if (includes.length > 0) {
358
+ let matched = false;
359
+ for (const p of includes) {
360
+ if (_matchPathPattern(rel, p)) { matched = true; break; }
361
+ }
362
+ if (!matched) return true;
363
+ }
364
+
365
+ return false;
366
+ }
367
+
368
+ function bucketize(items) {
369
+ const buckets = new Map();
370
+ for (const it of items) {
371
+ const p = typeof it === 'string' ? it : (it && typeof it.file === 'string' ? it.file : '');
372
+ if (p === '') continue;
373
+ const top = p.split('/').slice(0, 2).join('/');
374
+ buckets.set(top, (buckets.get(top) || 0) + 1);
375
+ }
376
+ return [...buckets.entries()].sort((a, b) => b[1] - a[1]);
377
+ }
378
+
379
+ return {
380
+ isExcluded,
381
+ bucketize,
382
+ pathExcludes,
383
+ dirExcludes: [...dirExcludes],
384
+ pluginMirror,
385
+ };
386
+ }
387
+
388
+ module.exports = {
389
+ buildScopeFilter,
390
+ parseGitignore,
391
+ detectPluginMirror,
392
+ isBundle,
393
+ DEFAULT_DIR_EXCLUDES,
394
+ DEFAULT_PATH_EXCLUDES,
395
+ BUNDLE_LINE_THRESHOLD,
396
+ BUNDLE_PATH_PATTERNS,
397
+ LARGE_DIFF_THRESHOLD,
398
+ // Internal helpers exported for unit tests.
399
+ _matchPathPattern,
400
+ _compileGitignorePattern,
401
+ _globToRegex,
402
+ };