sequant 2.2.0 → 2.3.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 (137) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +73 -0
  4. package/dist/bin/cli.js +94 -9
  5. package/dist/src/commands/doctor.d.ts +25 -0
  6. package/dist/src/commands/doctor.js +36 -1
  7. package/dist/src/commands/locks.d.ts +67 -0
  8. package/dist/src/commands/locks.js +290 -0
  9. package/dist/src/commands/merge.js +11 -0
  10. package/dist/src/commands/prompt.d.ts +39 -0
  11. package/dist/src/commands/prompt.js +179 -0
  12. package/dist/src/commands/run-display.d.ts +11 -2
  13. package/dist/src/commands/run-display.js +62 -28
  14. package/dist/src/commands/run-progress.d.ts +32 -0
  15. package/dist/src/commands/run-progress.js +76 -0
  16. package/dist/src/commands/run.js +80 -18
  17. package/dist/src/commands/stats.d.ts +2 -0
  18. package/dist/src/commands/stats.js +94 -8
  19. package/dist/src/commands/status.js +12 -0
  20. package/dist/src/commands/watch.d.ts +16 -0
  21. package/dist/src/commands/watch.js +147 -0
  22. package/dist/src/lib/ac-linter.d.ts +1 -1
  23. package/dist/src/lib/ac-linter.js +81 -0
  24. package/dist/src/lib/assess-collision-detect.d.ts +91 -0
  25. package/dist/src/lib/assess-collision-detect.js +217 -0
  26. package/dist/src/lib/assess-comment-parser.d.ts +59 -1
  27. package/dist/src/lib/assess-comment-parser.js +124 -2
  28. package/dist/src/lib/cli-ui/format.d.ts +19 -0
  29. package/dist/src/lib/cli-ui/format.js +34 -0
  30. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +181 -0
  31. package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
  32. package/dist/src/lib/cli-ui/run-renderer.d.ts +239 -0
  33. package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
  34. package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
  35. package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
  36. package/dist/src/lib/locks/index.d.ts +7 -0
  37. package/dist/src/lib/locks/index.js +5 -0
  38. package/dist/src/lib/locks/lock-manager.d.ts +168 -0
  39. package/dist/src/lib/locks/lock-manager.js +433 -0
  40. package/dist/src/lib/locks/types.d.ts +59 -0
  41. package/dist/src/lib/locks/types.js +31 -0
  42. package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
  43. package/dist/src/lib/qa/markdown-only-ci.js +74 -0
  44. package/dist/src/lib/relay/activation.d.ts +60 -0
  45. package/dist/src/lib/relay/activation.js +122 -0
  46. package/dist/src/lib/relay/archive.d.ts +34 -0
  47. package/dist/src/lib/relay/archive.js +106 -0
  48. package/dist/src/lib/relay/frame.d.ts +20 -0
  49. package/dist/src/lib/relay/frame.js +76 -0
  50. package/dist/src/lib/relay/index.d.ts +13 -0
  51. package/dist/src/lib/relay/index.js +13 -0
  52. package/dist/src/lib/relay/paths.d.ts +43 -0
  53. package/dist/src/lib/relay/paths.js +59 -0
  54. package/dist/src/lib/relay/pid.d.ts +34 -0
  55. package/dist/src/lib/relay/pid.js +72 -0
  56. package/dist/src/lib/relay/reader.d.ts +35 -0
  57. package/dist/src/lib/relay/reader.js +115 -0
  58. package/dist/src/lib/relay/types.d.ts +68 -0
  59. package/dist/src/lib/relay/types.js +76 -0
  60. package/dist/src/lib/relay/writer.d.ts +48 -0
  61. package/dist/src/lib/relay/writer.js +113 -0
  62. package/dist/src/lib/settings.d.ts +31 -1
  63. package/dist/src/lib/settings.js +18 -3
  64. package/dist/src/lib/version-check.d.ts +60 -5
  65. package/dist/src/lib/version-check.js +97 -9
  66. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  67. package/dist/src/lib/workflow/batch-executor.js +248 -175
  68. package/dist/src/lib/workflow/config-resolver.js +4 -0
  69. package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
  70. package/dist/src/lib/workflow/heartbeat.js +194 -0
  71. package/dist/src/lib/workflow/phase-executor.d.ts +62 -8
  72. package/dist/src/lib/workflow/phase-executor.js +157 -16
  73. package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
  74. package/dist/src/lib/workflow/phase-mapper.js +17 -20
  75. package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
  76. package/dist/src/lib/workflow/platforms/github.js +20 -3
  77. package/dist/src/lib/workflow/pr-status.d.ts +18 -2
  78. package/dist/src/lib/workflow/pr-status.js +41 -9
  79. package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
  80. package/dist/src/lib/workflow/qa-stagnation.js +179 -0
  81. package/dist/src/lib/workflow/run-orchestrator.d.ts +39 -0
  82. package/dist/src/lib/workflow/run-orchestrator.js +340 -15
  83. package/dist/src/lib/workflow/run-reflect.js +1 -1
  84. package/dist/src/lib/workflow/run-state.d.ts +71 -0
  85. package/dist/src/lib/workflow/run-state.js +14 -0
  86. package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
  87. package/dist/src/lib/workflow/state-cleanup.js +17 -5
  88. package/dist/src/lib/workflow/state-manager.d.ts +12 -1
  89. package/dist/src/lib/workflow/state-manager.js +37 -0
  90. package/dist/src/lib/workflow/state-schema.d.ts +62 -0
  91. package/dist/src/lib/workflow/state-schema.js +35 -1
  92. package/dist/src/lib/workflow/types.d.ts +74 -1
  93. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  94. package/dist/src/lib/workflow/worktree-manager.js +15 -6
  95. package/dist/src/mcp/tools/run.d.ts +44 -0
  96. package/dist/src/mcp/tools/run.js +104 -13
  97. package/dist/src/ui/tui/App.d.ts +14 -0
  98. package/dist/src/ui/tui/App.js +41 -0
  99. package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
  100. package/dist/src/ui/tui/ElapsedTimer.js +31 -0
  101. package/dist/src/ui/tui/Header.d.ts +6 -0
  102. package/dist/src/ui/tui/Header.js +15 -0
  103. package/dist/src/ui/tui/IssueBox.d.ts +16 -0
  104. package/dist/src/ui/tui/IssueBox.js +68 -0
  105. package/dist/src/ui/tui/Spinner.d.ts +9 -0
  106. package/dist/src/ui/tui/Spinner.js +18 -0
  107. package/dist/src/ui/tui/index.d.ts +15 -0
  108. package/dist/src/ui/tui/index.js +29 -0
  109. package/dist/src/ui/tui/theme.d.ts +29 -0
  110. package/dist/src/ui/tui/theme.js +52 -0
  111. package/dist/src/ui/tui/truncate.d.ts +11 -0
  112. package/dist/src/ui/tui/truncate.js +31 -0
  113. package/package.json +10 -3
  114. package/templates/agents/sequant-explorer.md +1 -0
  115. package/templates/agents/sequant-qa-checker.md +2 -1
  116. package/templates/agents/sequant-testgen.md +1 -0
  117. package/templates/hooks/post-tool.sh +11 -0
  118. package/templates/hooks/pre-tool.sh +18 -9
  119. package/templates/hooks/relay-check.sh +107 -0
  120. package/templates/relay/frame.txt +11 -0
  121. package/templates/scripts/cleanup-worktree.sh +25 -3
  122. package/templates/scripts/new-feature.sh +6 -0
  123. package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
  124. package/templates/skills/_shared/references/subagent-types.md +21 -8
  125. package/templates/skills/assess/SKILL.md +103 -49
  126. package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
  127. package/templates/skills/docs/SKILL.md +141 -22
  128. package/templates/skills/exec/SKILL.md +10 -8
  129. package/templates/skills/fullsolve/SKILL.md +79 -5
  130. package/templates/skills/loop/SKILL.md +28 -0
  131. package/templates/skills/merger/SKILL.md +621 -0
  132. package/templates/skills/qa/SKILL.md +727 -8
  133. package/templates/skills/setup/SKILL.md +6 -0
  134. package/templates/skills/spec/SKILL.md +52 -0
  135. package/templates/skills/spec/references/parallel-groups.md +7 -0
  136. package/templates/skills/spec/references/recommended-workflow.md +4 -2
  137. package/templates/skills/testgen/SKILL.md +24 -17
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Behavior-Rule Detector (issue #552)
3
+ *
4
+ * Shared heuristic for `/spec` (proactive) and `/qa` (reactive) phases that
5
+ * detects when an AC describes a *behavior rule* (e.g. "default becomes X",
6
+ * "always include Y", "never skip Z") and, when triggered, surfaces all
7
+ * touchpoints in the codebase that likely implement the rule.
8
+ *
9
+ * Behavior rules are routinely duplicated across a skill prompt
10
+ * (LLM-interpreted) AND the runtime TypeScript that backs it. Without this
11
+ * detector, edits land at one site and the other goes stale — see issue #533
12
+ * (motivating miss; documented in `references/behavior-rule-detection.md`).
13
+ *
14
+ * Three exported functions:
15
+ * - `detectBehaviorRule` — cheap keyword check; gates the more expensive greps
16
+ * - `findTouchpoints` — used by `/spec` to enumerate likely implementations
17
+ * - `findSurvivingInverseSymbols` — used by `/qa` to flag OLD-rule survivors
18
+ * inside the diff blast radius
19
+ *
20
+ * The keyword set is the source of truth in this file (per the /spec Open
21
+ * Question on keyword location). The reference doc cites it.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * import { detectBehaviorRule, findTouchpoints } from "./behavior-rule-detector.ts";
26
+ *
27
+ * const detection = detectBehaviorRule(ac);
28
+ * if (detection.triggered) {
29
+ * const hits = findTouchpoints(ac, process.cwd());
30
+ * for (const h of hits) console.log(`${h.path}:${h.line} ${h.snippet}`);
31
+ * }
32
+ * ```
33
+ */
34
+ import type { AcceptanceCriterion } from "../workflow/state-schema.js";
35
+ /**
36
+ * Behavior keywords whose presence (≥2 distinct, OR matching the explicit
37
+ * pattern below) signals an AC describes a rule rather than a localized fix.
38
+ * Tunable here; cited from `references/behavior-rule-detection.md`.
39
+ */
40
+ export declare const BEHAVIOR_KEYWORDS: readonly ["default", "always", "never", "rule", "behavior", "skip"];
41
+ export type BehaviorKeyword = (typeof BEHAVIOR_KEYWORDS)[number];
42
+ /** A single touchpoint hit (file location matching a behavior-rule symbol). */
43
+ export interface TouchpointHit {
44
+ path: string;
45
+ line: number;
46
+ snippet: string;
47
+ }
48
+ export interface BehaviorRuleDetection {
49
+ triggered: boolean;
50
+ keywords: BehaviorKeyword[];
51
+ matchedPattern?: string;
52
+ }
53
+ /**
54
+ * Detect whether an AC describes a behavior rule.
55
+ *
56
+ * Trigger conditions:
57
+ * 1. ≥2 distinct {@link BEHAVIOR_KEYWORDS} present in the AC description
58
+ * (case-insensitive, word-boundary match), OR
59
+ * 2. Description matches one of the {@link EXPLICIT_PATTERNS}
60
+ * (e.g. "always X unless Y").
61
+ *
62
+ * Returns `triggered: false` for empty or undefined descriptions, single
63
+ * keyword matches without an explicit pattern, and file-specific ACs
64
+ * ("Update line 42 of foo.ts").
65
+ */
66
+ export declare function detectBehaviorRule(ac: AcceptanceCriterion): BehaviorRuleDetection;
67
+ /**
68
+ * Find touchpoints in the codebase that likely implement the behavior rule
69
+ * described by `ac`. Returns `[]` when {@link detectBehaviorRule} does not
70
+ * trigger (cheap short-circuit per the /spec performance budget).
71
+ *
72
+ * Heuristic:
73
+ * - Extract identifier-like symbols from the AC (backticked strings, file
74
+ * paths with extensions, ALL_CAPS / camelCase / kebab-case identifiers).
75
+ * - Walk {@link TOUCHPOINT_ROOTS}; for each line in matching files, mark a
76
+ * hit if the line contains any extracted symbol OR ≥2 distinct AC
77
+ * behavior keywords.
78
+ * - Hits are deduplicated by `path:line` and capped (per-file: 3, total: 200)
79
+ * to keep `/spec` output readable (callers can re-run with a tighter scope
80
+ * if needed).
81
+ */
82
+ export declare function findTouchpoints(ac: AcceptanceCriterion, repoRoot: string): TouchpointHit[];
83
+ /**
84
+ * Find OLD-rule survivors inside the diff blast radius. Used by `/qa` to flag
85
+ * an AC `NOT_MET` when the inverse of the asserted rule still has live code.
86
+ *
87
+ * Differs from {@link findTouchpoints}:
88
+ * - Scope is `diffPaths` (caller is responsible for pre-expanding to 1-hop
89
+ * importers when desired — this avoids embedding a TS-only importer scanner
90
+ * here and keeps the function language-agnostic).
91
+ * - Search terms are *inverse* keywords derived from the AC's keywords (and
92
+ * inverse English phrasing as a fallback when no symbols match).
93
+ */
94
+ export declare function findSurvivingInverseSymbols(ac: AcceptanceCriterion, repoRoot: string, diffPaths: string[]): TouchpointHit[];
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Behavior-Rule Detector (issue #552)
3
+ *
4
+ * Shared heuristic for `/spec` (proactive) and `/qa` (reactive) phases that
5
+ * detects when an AC describes a *behavior rule* (e.g. "default becomes X",
6
+ * "always include Y", "never skip Z") and, when triggered, surfaces all
7
+ * touchpoints in the codebase that likely implement the rule.
8
+ *
9
+ * Behavior rules are routinely duplicated across a skill prompt
10
+ * (LLM-interpreted) AND the runtime TypeScript that backs it. Without this
11
+ * detector, edits land at one site and the other goes stale — see issue #533
12
+ * (motivating miss; documented in `references/behavior-rule-detection.md`).
13
+ *
14
+ * Three exported functions:
15
+ * - `detectBehaviorRule` — cheap keyword check; gates the more expensive greps
16
+ * - `findTouchpoints` — used by `/spec` to enumerate likely implementations
17
+ * - `findSurvivingInverseSymbols` — used by `/qa` to flag OLD-rule survivors
18
+ * inside the diff blast radius
19
+ *
20
+ * The keyword set is the source of truth in this file (per the /spec Open
21
+ * Question on keyword location). The reference doc cites it.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * import { detectBehaviorRule, findTouchpoints } from "./behavior-rule-detector.ts";
26
+ *
27
+ * const detection = detectBehaviorRule(ac);
28
+ * if (detection.triggered) {
29
+ * const hits = findTouchpoints(ac, process.cwd());
30
+ * for (const h of hits) console.log(`${h.path}:${h.line} ${h.snippet}`);
31
+ * }
32
+ * ```
33
+ */
34
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
35
+ import { join, relative } from "node:path";
36
+ /**
37
+ * Behavior keywords whose presence (≥2 distinct, OR matching the explicit
38
+ * pattern below) signals an AC describes a rule rather than a localized fix.
39
+ * Tunable here; cited from `references/behavior-rule-detection.md`.
40
+ */
41
+ export const BEHAVIOR_KEYWORDS = [
42
+ "default",
43
+ "always",
44
+ "never",
45
+ "rule",
46
+ "behavior",
47
+ "skip",
48
+ ];
49
+ /**
50
+ * Per-stem inflection regex map. Mirrors the stem-aware pattern landed by
51
+ * #597 in `src/lib/ac-linter.ts`: build `\b<stem>(<suffixes>)?\b` so the
52
+ * keyword check matches common inflections (`defaults`, `skipping`,
53
+ * `behaviors`) without over-matching identifier-shaped tokens. Word
54
+ * boundaries are preserved so e.g. `defaultValue` does NOT match.
55
+ *
56
+ * - `default` → matches `default`/`defaults`/`defaulted`/`defaulting`
57
+ * - `always` / `never` — adverbs, no inflections
58
+ * - `rule` — e-ending: `rule`/`rules`/`ruled`/`ruling`
59
+ * - `behavior` — `behavior`/`behaviors`/`behavioral`/`behaviorally`
60
+ * - `skip` — double-`p` per English: `skip`/`skips`/`skipped`/`skipping`
61
+ *
62
+ * Source-of-truth lives in this map so the test suite can read it (rather
63
+ * than each call site rebuilding the regex inline).
64
+ */
65
+ const KEYWORD_REGEXES = {
66
+ default: /\bdefault(?:s|ed|ing)?\b/i,
67
+ always: /\balways\b/i,
68
+ never: /\bnever\b/i,
69
+ rule: /\brul(?:e|es|ed|ing)\b/i,
70
+ behavior: /\bbehavior(?:s|al|ally)?\b/i,
71
+ skip: /\bskip(?:s|ped|ping)?\b/i,
72
+ };
73
+ /**
74
+ * Explicit behavior-rule patterns that trigger detection even with a single
75
+ * keyword (false-positive guard exception). Two families:
76
+ *
77
+ * 1. Mid-sentence rule constructs:
78
+ * - "always X unless Y", "never X unless Y", "default X when Y"
79
+ * 2. Imperative AC openers — when an AC description begins with a capitalized
80
+ * `Always` / `Never` / `Default` followed by a verb, it's almost certainly
81
+ * a behavior rule even with a single keyword. Covers the AC-5 literals
82
+ * "Always include Y" and "Never skip Z" which #552's prior threshold missed.
83
+ * Case-sensitive on purpose: matches the imperative-rule register, not
84
+ * "the system always defaults to..." prose mid-paragraph.
85
+ */
86
+ const EXPLICIT_PATTERNS = [
87
+ /\balways\b[^.]*?\bunless\b/i,
88
+ /\bnever\b[^.]*?\bunless\b/i,
89
+ /\bdefault\b[^.]*?\bwhen\b/i,
90
+ /^\s*Always\s+\w+/,
91
+ /^\s*Never\s+\w+/,
92
+ /^\s*Default\s+\w+/,
93
+ ];
94
+ /**
95
+ * Inverse-keyword map — used by `findSurvivingInverseSymbols` to derive search
96
+ * terms for OLD-rule survivors inside the diff blast radius. Asymmetric on
97
+ * purpose: e.g. an AC asserting the NEW rule "always include spec" should
98
+ * search for "skip" / "exclude" / "bypass" survivors, not "always" itself.
99
+ *
100
+ * High-noise common English words (`include`, `run`, `auto`, `old`,
101
+ * `previous`) were pruned from this map after QA's self-dogfood pass on
102
+ * #552 returned 50 survivors entirely from definitional documentation —
103
+ * those words appear in any English prose. {@link deriveInverseTerms} also
104
+ * filters terms in {@link COMMON_WORDS} as defense-in-depth for future
105
+ * tuners.
106
+ */
107
+ const INVERSE_KEYWORDS = {
108
+ default: ["skip", "exclude", "bypass", "override"],
109
+ always: ["skip", "never", "exclude", "conditional", "shortcut"],
110
+ never: ["always", "default", "exclude"],
111
+ rule: ["exception", "override", "shortcut", "bypass"],
112
+ behavior: ["legacy", "deprecated"],
113
+ skip: ["always", "default", "exclude"],
114
+ };
115
+ /**
116
+ * Roots scanned by `findTouchpoints`. Order matters when `TOTAL_CAP` is hit:
117
+ * earlier roots are always represented in the results. `bin/` and
118
+ * `src/commands/` are scanned because CLI option registration (Commander.js
119
+ * `.option()` chains in `bin/cli.ts`, `RunOptions` interface in
120
+ * `src/commands/run.ts`) is a recurring rule-drift site — see the "CLI wiring
121
+ * gap" pitfall called out in this project's CLAUDE.md memory. `templates/skills/`
122
+ * and `skills/` are intentionally omitted — they mirror `.claude/skills/` 1:1
123
+ * and including them would triple-count every hit.
124
+ */
125
+ const TOUCHPOINT_ROOTS = ["src/lib", "src/commands", "bin", ".claude/skills"];
126
+ const TOUCHPOINT_EXTENSIONS = [".md", ".ts", ".tsx"];
127
+ /**
128
+ * Total survivor cap on `findSurvivingInverseSymbols` results. Lower than
129
+ * `findTouchpoints`'s total cap (200) because survivors are surfaced inside
130
+ * the QA verdict and a long list drowns out the rule-relevant hits — a
131
+ * tighter cap forces operators to triage the highest-signal blast-radius
132
+ * lines first.
133
+ */
134
+ const SURVIVOR_TOTAL_CAP = 50;
135
+ /** Skip these directories when walking — they explode the corpus without value. */
136
+ const WALK_SKIP_DIRS = new Set([
137
+ "node_modules",
138
+ ".git",
139
+ "dist",
140
+ "build",
141
+ ".next",
142
+ "coverage",
143
+ "__tests__",
144
+ "__snapshots__",
145
+ ]);
146
+ /**
147
+ * Detect whether an AC describes a behavior rule.
148
+ *
149
+ * Trigger conditions:
150
+ * 1. ≥2 distinct {@link BEHAVIOR_KEYWORDS} present in the AC description
151
+ * (case-insensitive, word-boundary match), OR
152
+ * 2. Description matches one of the {@link EXPLICIT_PATTERNS}
153
+ * (e.g. "always X unless Y").
154
+ *
155
+ * Returns `triggered: false` for empty or undefined descriptions, single
156
+ * keyword matches without an explicit pattern, and file-specific ACs
157
+ * ("Update line 42 of foo.ts").
158
+ */
159
+ export function detectBehaviorRule(ac) {
160
+ const description = ac?.description ?? "";
161
+ if (!description) {
162
+ return { triggered: false, keywords: [] };
163
+ }
164
+ const matched = new Set();
165
+ for (const kw of BEHAVIOR_KEYWORDS) {
166
+ if (KEYWORD_REGEXES[kw].test(description))
167
+ matched.add(kw);
168
+ }
169
+ // Explicit pattern overrides the ≥2-keyword threshold (e.g. "always X
170
+ // unless Y" only contains one keyword but is unambiguously a behavior rule).
171
+ for (const re of EXPLICIT_PATTERNS) {
172
+ if (re.test(description)) {
173
+ return {
174
+ triggered: true,
175
+ keywords: [...matched],
176
+ matchedPattern: re.source,
177
+ };
178
+ }
179
+ }
180
+ return {
181
+ triggered: matched.size >= 2,
182
+ keywords: [...matched],
183
+ };
184
+ }
185
+ /**
186
+ * Find touchpoints in the codebase that likely implement the behavior rule
187
+ * described by `ac`. Returns `[]` when {@link detectBehaviorRule} does not
188
+ * trigger (cheap short-circuit per the /spec performance budget).
189
+ *
190
+ * Heuristic:
191
+ * - Extract identifier-like symbols from the AC (backticked strings, file
192
+ * paths with extensions, ALL_CAPS / camelCase / kebab-case identifiers).
193
+ * - Walk {@link TOUCHPOINT_ROOTS}; for each line in matching files, mark a
194
+ * hit if the line contains any extracted symbol OR ≥2 distinct AC
195
+ * behavior keywords.
196
+ * - Hits are deduplicated by `path:line` and capped (per-file: 3, total: 200)
197
+ * to keep `/spec` output readable (callers can re-run with a tighter scope
198
+ * if needed).
199
+ */
200
+ export function findTouchpoints(ac, repoRoot) {
201
+ const detection = detectBehaviorRule(ac);
202
+ if (!detection.triggered)
203
+ return [];
204
+ if (!repoRoot || !existsSync(repoRoot))
205
+ return [];
206
+ const symbols = extractSymbols(ac.description);
207
+ const keywords = detection.keywords;
208
+ const hits = [];
209
+ const seen = new Set();
210
+ const perFileCount = new Map();
211
+ // Caps tuned to keep /spec output readable while ensuring breadth of
212
+ // coverage — a per-file ceiling prevents one chatty file from drowning out
213
+ // the rest of the corpus, and a total ceiling caps the worst case.
214
+ const PER_FILE_CAP = 3;
215
+ const TOTAL_CAP = 200;
216
+ for (const root of TOUCHPOINT_ROOTS) {
217
+ const fullRoot = join(repoRoot, root);
218
+ if (!existsSync(fullRoot))
219
+ continue;
220
+ for (const file of walkFiles(fullRoot)) {
221
+ if (!TOUCHPOINT_EXTENSIONS.some((ext) => file.endsWith(ext)))
222
+ continue;
223
+ // Test files implement *checks* of behavior rules, not the rules
224
+ // themselves. Excluding them keeps `findTouchpoints` focused on the
225
+ // implementation sites a /spec planner needs to enumerate.
226
+ if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(file))
227
+ continue;
228
+ const content = safeReadFile(file);
229
+ if (!content)
230
+ continue;
231
+ const relPath = relative(repoRoot, file);
232
+ const lines = content.split("\n");
233
+ for (let i = 0; i < lines.length; i++) {
234
+ const line = lines[i];
235
+ if (!matchesBehaviorSite(line, symbols, keywords))
236
+ continue;
237
+ const key = `${relPath}:${i + 1}`;
238
+ if (seen.has(key))
239
+ continue;
240
+ const count = perFileCount.get(relPath) ?? 0;
241
+ if (count >= PER_FILE_CAP)
242
+ break;
243
+ seen.add(key);
244
+ perFileCount.set(relPath, count + 1);
245
+ hits.push({
246
+ path: relPath,
247
+ line: i + 1,
248
+ snippet: line.trim().slice(0, 200),
249
+ });
250
+ if (hits.length >= TOTAL_CAP)
251
+ return hits;
252
+ }
253
+ }
254
+ }
255
+ return hits;
256
+ }
257
+ /**
258
+ * Find OLD-rule survivors inside the diff blast radius. Used by `/qa` to flag
259
+ * an AC `NOT_MET` when the inverse of the asserted rule still has live code.
260
+ *
261
+ * Differs from {@link findTouchpoints}:
262
+ * - Scope is `diffPaths` (caller is responsible for pre-expanding to 1-hop
263
+ * importers when desired — this avoids embedding a TS-only importer scanner
264
+ * here and keeps the function language-agnostic).
265
+ * - Search terms are *inverse* keywords derived from the AC's keywords (and
266
+ * inverse English phrasing as a fallback when no symbols match).
267
+ */
268
+ export function findSurvivingInverseSymbols(ac, repoRoot, diffPaths) {
269
+ const detection = detectBehaviorRule(ac);
270
+ if (!detection.triggered)
271
+ return [];
272
+ if (!repoRoot || !existsSync(repoRoot))
273
+ return [];
274
+ if (!Array.isArray(diffPaths) || diffPaths.length === 0)
275
+ return [];
276
+ const inverseTerms = deriveInverseTerms(detection.keywords);
277
+ if (inverseTerms.length === 0)
278
+ return [];
279
+ const hits = [];
280
+ const seen = new Set();
281
+ for (const relPath of diffPaths) {
282
+ if (!relPath)
283
+ continue;
284
+ const fullPath = join(repoRoot, relPath);
285
+ if (!existsSync(fullPath))
286
+ continue;
287
+ let stat;
288
+ try {
289
+ stat = statSync(fullPath);
290
+ }
291
+ catch {
292
+ continue;
293
+ }
294
+ if (!stat.isFile())
295
+ continue;
296
+ if (!TOUCHPOINT_EXTENSIONS.some((ext) => fullPath.endsWith(ext)))
297
+ continue;
298
+ const content = safeReadFile(fullPath);
299
+ if (!content)
300
+ continue;
301
+ const lines = content.split("\n");
302
+ for (let i = 0; i < lines.length; i++) {
303
+ const line = lines[i];
304
+ if (!matchesAnyTerm(line, inverseTerms))
305
+ continue;
306
+ const key = `${relPath}:${i + 1}`;
307
+ if (seen.has(key))
308
+ continue;
309
+ seen.add(key);
310
+ hits.push({
311
+ path: relPath,
312
+ line: i + 1,
313
+ snippet: line.trim().slice(0, 200),
314
+ });
315
+ if (hits.length >= SURVIVOR_TOTAL_CAP)
316
+ return hits;
317
+ }
318
+ }
319
+ return hits;
320
+ }
321
+ // ---------- helpers ----------
322
+ const SYMBOL_REGEXES = [
323
+ /`([^`\n]+)`/g, // backtick-quoted: `spec`, `BUG_LABELS`
324
+ /"([^"\n]{3,})"/g, // double-quoted phrase
325
+ /\*\*([^*\n]+)\*\*/g, // bold: **always X unless Y**
326
+ /\b([A-Z][A-Z0-9_]{2,})\b/g, // SCREAMING_SNAKE: BUG_LABELS, DOCS_LABELS
327
+ /\b([a-z][A-Za-z0-9]+(?:[A-Z][A-Za-z0-9]+){1,})\b/g, // camelCase
328
+ /([a-zA-Z][\w-]*\.(?:ts|tsx|js|jsx|md|json))/g, // file paths
329
+ /([a-z][\w-]+\/[\w./-]+)/g, // directory paths: src/lib/...
330
+ ];
331
+ function extractSymbols(text) {
332
+ if (!text)
333
+ return [];
334
+ const out = new Set();
335
+ for (const re of SYMBOL_REGEXES) {
336
+ const r = new RegExp(re.source, re.flags);
337
+ let m;
338
+ while ((m = r.exec(text)) !== null) {
339
+ const sym = (m[1] || "").trim();
340
+ // Reject common English words and very short tokens.
341
+ if (sym.length < 3)
342
+ continue;
343
+ if (BEHAVIOR_KEYWORDS.includes(sym.toLowerCase()))
344
+ continue;
345
+ if (COMMON_WORDS.has(sym.toLowerCase()))
346
+ continue;
347
+ out.add(sym);
348
+ }
349
+ }
350
+ return [...out];
351
+ }
352
+ const COMMON_WORDS = new Set([
353
+ "the",
354
+ "and",
355
+ "for",
356
+ "with",
357
+ "from",
358
+ "this",
359
+ "that",
360
+ "into",
361
+ "when",
362
+ "then",
363
+ "than",
364
+ "given",
365
+ "should",
366
+ "becomes",
367
+ "include",
368
+ "exists",
369
+ "true",
370
+ "false",
371
+ "not",
372
+ "yes",
373
+ "no",
374
+ "are",
375
+ "was",
376
+ "but",
377
+ "out",
378
+ "all",
379
+ "any",
380
+ "use",
381
+ "via",
382
+ "etc",
383
+ "issue",
384
+ "code",
385
+ "line",
386
+ "ok",
387
+ ]);
388
+ function matchesBehaviorSite(line, symbols, keywords) {
389
+ if (!line.trim())
390
+ return false;
391
+ for (const sym of symbols) {
392
+ if (line.includes(sym))
393
+ return true;
394
+ }
395
+ let count = 0;
396
+ for (const kw of keywords) {
397
+ if (KEYWORD_REGEXES[kw].test(line)) {
398
+ count++;
399
+ if (count >= 2)
400
+ return true;
401
+ }
402
+ }
403
+ return false;
404
+ }
405
+ function matchesAnyTerm(line, terms) {
406
+ if (!line.trim())
407
+ return false;
408
+ for (const t of terms) {
409
+ if (!t)
410
+ continue;
411
+ if (new RegExp(`\\b${escapeRegex(t)}\\b`, "i").test(line))
412
+ return true;
413
+ }
414
+ return false;
415
+ }
416
+ function escapeRegex(s) {
417
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
418
+ }
419
+ function deriveInverseTerms(keywords) {
420
+ const out = new Set();
421
+ for (const k of keywords) {
422
+ for (const inv of INVERSE_KEYWORDS[k] ?? []) {
423
+ // Defense-in-depth: drop any inverse term that's a common English
424
+ // word, even if a future tuner adds it to INVERSE_KEYWORDS. Common
425
+ // words produce false-positive survivors on every prose line.
426
+ if (COMMON_WORDS.has(inv.toLowerCase()))
427
+ continue;
428
+ out.add(inv);
429
+ }
430
+ }
431
+ return [...out];
432
+ }
433
+ function safeReadFile(path) {
434
+ try {
435
+ return readFileSync(path, "utf8");
436
+ }
437
+ catch {
438
+ return null;
439
+ }
440
+ }
441
+ function* walkFiles(dir) {
442
+ let entries;
443
+ try {
444
+ entries = readdirSync(dir);
445
+ }
446
+ catch {
447
+ return;
448
+ }
449
+ for (const entry of entries) {
450
+ if (WALK_SKIP_DIRS.has(entry))
451
+ continue;
452
+ const full = join(dir, entry);
453
+ let stat;
454
+ try {
455
+ stat = statSync(full);
456
+ }
457
+ catch {
458
+ continue;
459
+ }
460
+ if (stat.isDirectory()) {
461
+ yield* walkFiles(full);
462
+ }
463
+ else if (stat.isFile()) {
464
+ yield full;
465
+ }
466
+ }
467
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Public surface for the issue-level concurrency lock (#625).
3
+ */
4
+ export { LockManager, classifyStaleness, defaultIsPidAlive, formatLockedMessage, isOrchestratorMode, resolveLocksDir, } from "./lock-manager.js";
5
+ export type { LockManagerOptions } from "./lock-manager.js";
6
+ export { DEFAULT_LOCKS_DIR, DEFAULT_STALE_AGE_MS, LockFileSchema, } from "./types.js";
7
+ export type { AcquireResult, LockFile, LockListing, SignalOtherResult, SignalReason, } from "./types.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Public surface for the issue-level concurrency lock (#625).
3
+ */
4
+ export { LockManager, classifyStaleness, defaultIsPidAlive, formatLockedMessage, isOrchestratorMode, resolveLocksDir, } from "./lock-manager.js";
5
+ export { DEFAULT_LOCKS_DIR, DEFAULT_STALE_AGE_MS, LockFileSchema, } from "./types.js";