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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +73 -0
- package/dist/bin/cli.js +94 -9
- package/dist/src/commands/doctor.d.ts +25 -0
- package/dist/src/commands/doctor.js +36 -1
- package/dist/src/commands/locks.d.ts +67 -0
- package/dist/src/commands/locks.js +290 -0
- package/dist/src/commands/merge.js +11 -0
- package/dist/src/commands/prompt.d.ts +39 -0
- package/dist/src/commands/prompt.js +179 -0
- package/dist/src/commands/run-display.d.ts +11 -2
- package/dist/src/commands/run-display.js +62 -28
- package/dist/src/commands/run-progress.d.ts +32 -0
- package/dist/src/commands/run-progress.js +76 -0
- package/dist/src/commands/run.js +80 -18
- package/dist/src/commands/stats.d.ts +2 -0
- package/dist/src/commands/stats.js +94 -8
- package/dist/src/commands/status.js +12 -0
- package/dist/src/commands/watch.d.ts +16 -0
- package/dist/src/commands/watch.js +147 -0
- package/dist/src/lib/ac-linter.d.ts +1 -1
- package/dist/src/lib/ac-linter.js +81 -0
- package/dist/src/lib/assess-collision-detect.d.ts +91 -0
- package/dist/src/lib/assess-collision-detect.js +217 -0
- package/dist/src/lib/assess-comment-parser.d.ts +59 -1
- package/dist/src/lib/assess-comment-parser.js +124 -2
- package/dist/src/lib/cli-ui/format.d.ts +19 -0
- package/dist/src/lib/cli-ui/format.js +34 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +181 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +239 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
- package/dist/src/lib/locks/index.d.ts +7 -0
- package/dist/src/lib/locks/index.js +5 -0
- package/dist/src/lib/locks/lock-manager.d.ts +168 -0
- package/dist/src/lib/locks/lock-manager.js +433 -0
- package/dist/src/lib/locks/types.d.ts +59 -0
- package/dist/src/lib/locks/types.js +31 -0
- package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
- package/dist/src/lib/qa/markdown-only-ci.js +74 -0
- package/dist/src/lib/relay/activation.d.ts +60 -0
- package/dist/src/lib/relay/activation.js +122 -0
- package/dist/src/lib/relay/archive.d.ts +34 -0
- package/dist/src/lib/relay/archive.js +106 -0
- package/dist/src/lib/relay/frame.d.ts +20 -0
- package/dist/src/lib/relay/frame.js +76 -0
- package/dist/src/lib/relay/index.d.ts +13 -0
- package/dist/src/lib/relay/index.js +13 -0
- package/dist/src/lib/relay/paths.d.ts +43 -0
- package/dist/src/lib/relay/paths.js +59 -0
- package/dist/src/lib/relay/pid.d.ts +34 -0
- package/dist/src/lib/relay/pid.js +72 -0
- package/dist/src/lib/relay/reader.d.ts +35 -0
- package/dist/src/lib/relay/reader.js +115 -0
- package/dist/src/lib/relay/types.d.ts +68 -0
- package/dist/src/lib/relay/types.js +76 -0
- package/dist/src/lib/relay/writer.d.ts +48 -0
- package/dist/src/lib/relay/writer.js +113 -0
- package/dist/src/lib/settings.d.ts +31 -1
- package/dist/src/lib/settings.js +18 -3
- package/dist/src/lib/version-check.d.ts +60 -5
- package/dist/src/lib/version-check.js +97 -9
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +248 -175
- package/dist/src/lib/workflow/config-resolver.js +4 -0
- package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
- package/dist/src/lib/workflow/heartbeat.js +194 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +62 -8
- package/dist/src/lib/workflow/phase-executor.js +157 -16
- package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
- package/dist/src/lib/workflow/phase-mapper.js +17 -20
- package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
- package/dist/src/lib/workflow/platforms/github.js +20 -3
- package/dist/src/lib/workflow/pr-status.d.ts +18 -2
- package/dist/src/lib/workflow/pr-status.js +41 -9
- package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
- package/dist/src/lib/workflow/qa-stagnation.js +179 -0
- package/dist/src/lib/workflow/run-orchestrator.d.ts +39 -0
- package/dist/src/lib/workflow/run-orchestrator.js +340 -15
- package/dist/src/lib/workflow/run-reflect.js +1 -1
- package/dist/src/lib/workflow/run-state.d.ts +71 -0
- package/dist/src/lib/workflow/run-state.js +14 -0
- package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
- package/dist/src/lib/workflow/state-cleanup.js +17 -5
- package/dist/src/lib/workflow/state-manager.d.ts +12 -1
- package/dist/src/lib/workflow/state-manager.js +37 -0
- package/dist/src/lib/workflow/state-schema.d.ts +62 -0
- package/dist/src/lib/workflow/state-schema.js +35 -1
- package/dist/src/lib/workflow/types.d.ts +74 -1
- package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
- package/dist/src/lib/workflow/worktree-manager.js +15 -6
- package/dist/src/mcp/tools/run.d.ts +44 -0
- package/dist/src/mcp/tools/run.js +104 -13
- package/dist/src/ui/tui/App.d.ts +14 -0
- package/dist/src/ui/tui/App.js +41 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
- package/dist/src/ui/tui/ElapsedTimer.js +31 -0
- package/dist/src/ui/tui/Header.d.ts +6 -0
- package/dist/src/ui/tui/Header.js +15 -0
- package/dist/src/ui/tui/IssueBox.d.ts +16 -0
- package/dist/src/ui/tui/IssueBox.js +68 -0
- package/dist/src/ui/tui/Spinner.d.ts +9 -0
- package/dist/src/ui/tui/Spinner.js +18 -0
- package/dist/src/ui/tui/index.d.ts +15 -0
- package/dist/src/ui/tui/index.js +29 -0
- package/dist/src/ui/tui/theme.d.ts +29 -0
- package/dist/src/ui/tui/theme.js +52 -0
- package/dist/src/ui/tui/truncate.d.ts +11 -0
- package/dist/src/ui/tui/truncate.js +31 -0
- package/package.json +10 -3
- package/templates/agents/sequant-explorer.md +1 -0
- package/templates/agents/sequant-qa-checker.md +2 -1
- package/templates/agents/sequant-testgen.md +1 -0
- package/templates/hooks/post-tool.sh +11 -0
- package/templates/hooks/pre-tool.sh +18 -9
- package/templates/hooks/relay-check.sh +107 -0
- package/templates/relay/frame.txt +11 -0
- package/templates/scripts/cleanup-worktree.sh +25 -3
- package/templates/scripts/new-feature.sh +6 -0
- package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
- package/templates/skills/_shared/references/subagent-types.md +21 -8
- package/templates/skills/assess/SKILL.md +103 -49
- package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
- package/templates/skills/docs/SKILL.md +141 -22
- package/templates/skills/exec/SKILL.md +10 -8
- package/templates/skills/fullsolve/SKILL.md +79 -5
- package/templates/skills/loop/SKILL.md +28 -0
- package/templates/skills/merger/SKILL.md +621 -0
- package/templates/skills/qa/SKILL.md +727 -8
- package/templates/skills/setup/SKILL.md +6 -0
- package/templates/skills/spec/SKILL.md +52 -0
- package/templates/skills/spec/references/parallel-groups.md +7 -0
- package/templates/skills/spec/references/recommended-workflow.md +4 -2
- 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";
|