peaks-loop 4.0.21 → 4.0.24
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/CHANGELOG.md +109 -0
- package/dist/cli/commands/_register.js +2 -1
- package/dist/cli/commands/best-practice-scan-command.d.ts +36 -0
- package/dist/cli/commands/best-practice-scan-command.js +118 -0
- package/dist/cli/commands/request-commands.js +21 -9
- package/dist/services/best-practice/language-detector.d.ts +7 -0
- package/dist/services/best-practice/language-detector.js +96 -0
- package/dist/services/best-practice/output-formatter.d.ts +41 -0
- package/dist/services/best-practice/output-formatter.js +116 -0
- package/dist/services/best-practice/scan-orchestrator.d.ts +39 -0
- package/dist/services/best-practice/scan-orchestrator.js +85 -0
- package/dist/services/context/collector.d.ts +5 -12
- package/dist/services/context/context-builder.d.ts +12 -20
- package/dist/services/context/context-builder.js +1 -1
- package/dist/services/context/context-schema.d.ts +51 -364
- package/dist/services/context/context-schema.js +3 -3
- package/dist/services/crystallization/crystallization-service.d.ts +2 -12
- package/dist/services/crystallization/crystallization-service.js +3 -9
- package/dist/services/crystallization/crystallization-types.d.ts +45 -153
- package/dist/services/crystallization/crystallization-types.js +17 -11
- package/dist/services/crystallization/evidence-brief-builder.d.ts +12 -112
- package/dist/services/evolution/evolution-types.d.ts +83 -497
- package/dist/services/evolution/evolution-types.js +6 -10
- package/dist/services/fixture/fixture-sanitize-service.d.ts +10 -36
- package/dist/services/fixture/fixture-sanitize-service.js +1 -1
- package/dist/services/job/job-progress-store.d.ts +1 -17
- package/dist/services/job/job-progress-store.js +2 -2
- package/dist/services/job/job-types.d.ts +63 -370
- package/dist/services/job/job-types.js +46 -11
- package/dist/services/loop/loop-bee-relation-types.d.ts +20 -32
- package/dist/services/loop/loop-bee-relation-types.js +2 -2
- package/dist/services/loop/loop-release-types.d.ts +31 -110
- package/dist/services/migrate-skill-name/schema.d.ts +3 -19
- package/dist/services/migrate-skill-name/schema.js +4 -4
- package/dist/services/observability/observability-service.d.ts +21 -19
- package/dist/services/observability/observability-service.js +1 -1
- package/dist/services/prd/best-practice-auto-trigger.d.ts +61 -0
- package/dist/services/prd/best-practice-auto-trigger.js +126 -0
- package/dist/services/rd/types.d.ts +25 -194
- package/dist/services/sediment/json-schema.d.ts +39 -224
- package/dist/services/sediment/json-schema.js +2 -2
- package/dist/services/session/binding-store.d.ts +5 -47
- package/dist/services/session/binding-store.js +1 -1
- package/dist/services/share/bundle-types.d.ts +12 -180
- package/dist/services/share/bundle-types.js +15 -9
- package/dist/services/share/run-state-contract.d.ts +8 -25
- package/dist/services/skill/skill-search-service.d.ts +26 -46
- package/dist/services/skill/skill-search-service.js +2 -2
- package/dist/services/skills/hooks-settings-service.d.ts +9 -0
- package/dist/services/skills/hooks-settings-service.js +31 -1
- package/dist/services/skills/skill-presence-service.d.ts +24 -0
- package/dist/services/skills/skill-presence-service.js +33 -0
- package/package.json +8 -8
- package/schemas/context.schema.json +17 -8
- package/skills/bee/peaks-prd/SKILL.md +17 -0
- package/skills/peaks-code/SKILL.md +28 -1
- package/skills/peaks-code/references/skill-presence-and-title.md +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,114 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.24 — 2026-08-12 (zod v4 + Context7 + Step 2.5 best-practice scan)
|
|
4
|
+
|
|
5
|
+
**6 atomic commits from session 2026-08-12-session-4aaf2b** (5 functional + 1 sediment):
|
|
6
|
+
|
|
7
|
+
- `545a7d15` feat(peaks-prd): auto-trigger best-practice-scan on prd → handed-off transition (Slice F, 3 files, +446/-13)
|
|
8
|
+
- `3d866138` docs(peaks-code): wire Step 2.5 best-practice scan sub-step into workflow narrative (Slice D, 3 files, +48/-4)
|
|
9
|
+
- `a0f3efc0` feat(peaks-code): add Step 2.5 best-practice scan core (Slice C, 8 files, +947/-1)
|
|
10
|
+
- `a8f31b1b` chore(deps): add @upstash/context7-mcp@^4.0.2 for downstream best-practice docs (Slice B, 2 files, +490/-2)
|
|
11
|
+
- `24dfd8da` refactor(zod): bump zod 3.25.76 → 4.4.3 across monorepo (Slice A, 21 files, +148/-132)
|
|
12
|
+
- `f2fc76de` chore(memory): sediment pre-existing dirty — .peaks/PROJECT.md + .peaks/memory/index.json
|
|
13
|
+
|
|
14
|
+
**Highlights**:
|
|
15
|
+
|
|
16
|
+
1. **zod v3 → v4 upgrade** — 21 src files migrated, 4 v3 API families fixed (`.nonnegative/.positive`, `z.record` 1-arg, `errorMap`, `.refine()` path). Lockstep peaks-loop-shared + peaks-loop-internal-runtime bump. D-2 deviation (RD fabrication) caught by QA and fixed in same session — `zod-to-json-schema` replaced by zod v4 native `z.toJSONSchema()`.
|
|
17
|
+
|
|
18
|
+
2. **`@upstash/context7-mcp@^4.0.2` added** as production dep — Context7's zod v4 peer dep is now satisfied. 8 transitive deps resolved (express/jose/undici/MCP packages). Downstream projects consuming peaks-loop now have Context7 transitively available.
|
|
19
|
+
|
|
20
|
+
3. **peak-code Step 2.5 best-practice scan (BPS)** shipped — new sub-step between Step 2 (PRD) and Step 3 (RD). Auto-triggered on prd → handed-off transition. 8-row markdown table output with ★-marked LLM recommendation + mandatory ⚠️ catch-gate. 5-lang detector (TS/JS/Python/Go/Java). 53 vitest cases (44 core + 9 auto-trigger). 0 forbidden-token regressions. All 8 ACs PASS.
|
|
21
|
+
|
|
22
|
+
**Lockstep contract** (per peaks-cli-version-shared-chicken-egg):
|
|
23
|
+
- peaks-loop root `package.json#version`: 4.0.23 → **4.0.24**
|
|
24
|
+
- peaks-loop-shared `src/version.ts` `CLI_VERSION`: 4.0.23 → **4.0.24**
|
|
25
|
+
- peaks-loop-shared `package.json#version`: 0.0.57 → **0.0.58**
|
|
26
|
+
- peaks-loop-internal-runtime `src/index.ts` `RUNTIME_VERSION`: 4.0.23 → **4.0.24**
|
|
27
|
+
- peaks-loop-internal-runtime `src/index.ts` `RUNTIME_NPM_VERSION`: 0.0.8 → **0.0.9**
|
|
28
|
+
- peaks-loop-internal-runtime `package.json#version`: 0.0.8 → **0.0.9**
|
|
29
|
+
|
|
30
|
+
**Pre-publish registry state**:
|
|
31
|
+
- `peaks-loop@4.0.23` ✓ on registry (current latest)
|
|
32
|
+
- `peaks-loop@4.0.22` ✗ unpublished + in 24-72h grace (out of scope)
|
|
33
|
+
- `peaks-loop-shared@0.0.57` ✓ on registry
|
|
34
|
+
- `peaks-loop-internal-runtime@0.0.8` ✓ on registry
|
|
35
|
+
|
|
36
|
+
**Sister subpackages NOT bumped** (out of Step 2.5 scope):
|
|
37
|
+
- peaks-loop-mut: 0.1.22 (unchanged; only its zod dep was bumped in Slice A)
|
|
38
|
+
- peaks-loop-shared-channel: 0.0.26 (unchanged)
|
|
39
|
+
|
|
40
|
+
**Pending follow-ups** (out of 4.0.24 scope):
|
|
41
|
+
- real MCP wiring for Context7 (replace v1 stubs in `src/services/best-practice/scan-orchestrator.ts`)
|
|
42
|
+
- per-language libraries lookup (currently `...` in non-parallel rows)
|
|
43
|
+
- `startup-sequence.md:100` legacy "Step 2.5: Set session title" → 2.5a rename (Slice D deviation #1)
|
|
44
|
+
- 4 unit pre-existing failures unrelated to this release (verified against pre-migration main)
|
|
45
|
+
|
|
46
|
+
## 4.0.23 — 2026-08-11 (Bump to NEW version — per Rule 3 unpublish 24-72h grace)
|
|
47
|
+
|
|
48
|
+
**1 atomic commits from session 2026-08-11** + Layer 3 fix verify (publish.yml no auto-bump):
|
|
49
|
+
|
|
50
|
+
**Why 4.0.23 not 4.0.22**: per peaks-loop-publishing-critical-hard-rules rule 3:
|
|
51
|
+
> Cannot unpublish on npm (OIDC Trusted Publishing fails `npm unpublish` / `npm deprecate`). For repeated retries, prefer bumping to a NEW version.
|
|
52
|
+
|
|
53
|
+
Per npm unpublish policy: versions can only be re-published within 24-72 hours of unpublish. Operator manually unpublished `peaks-loop@4.0.22` earlier this session (~10:20Z); 4.0.22 npm slot is now in the 24-72h grace window where republish is blocked. 5 force-push attempts of `v4.0.22` all failed at the final `Publish to npm` step with the same symptom (14/15 gates pass; final OIDC publish step fails for unpublish-related reason invisible to non-admin logs).
|
|
54
|
+
|
|
55
|
+
**Solution per rule 3**: bump to a NEW version (`4.0.23`); 4.0.22 npm slot permanently retired (or wait 72h then retry — out of session scope).
|
|
56
|
+
|
|
57
|
+
**Lockstep contract** (per Lesson 2):
|
|
58
|
+
- peaks-loop root `package.json#version`: 4.0.22 → **4.0.23**
|
|
59
|
+
- peaks-loop-shared `src/version.ts` `CLI_VERSION`: 4.0.22 → **4.0.23**
|
|
60
|
+
- peaks-loop-shared `package.json#version`: 0.0.56 → **0.0.57**
|
|
61
|
+
- peaks-loop-internal-runtime `src/index.ts` `RUNTIME_VERSION`: 4.0.22 → **4.0.23**
|
|
62
|
+
- peaks-loop-internal-runtime `src/index.ts` `RUNTIME_NPM_VERSION`: 0.0.7 → **0.0.8**
|
|
63
|
+
- peaks-loop-internal-runtime `package.json#version`: 0.0.7 → **0.0.8**
|
|
64
|
+
|
|
65
|
+
**Code unchanged from 4.0.21 / intended-4.0.22** — this is purely a version bump to bypass the npm unpublish grace window.
|
|
66
|
+
|
|
67
|
+
**Pre-publish registry state** (this version is bumping over):
|
|
68
|
+
- `peaks-loop@4.0.21` ✓ on registry (current latest)
|
|
69
|
+
- `peaks-loop@4.0.22` ✗ unpublished + in 24-72h grace
|
|
70
|
+
- `peaks-loop-shared@0.0.56` ✓ auto-shipped in earlier partial-publish
|
|
71
|
+
- `peaks-loop-internal-runtime@0.0.7` ✓ auto-shipped in earlier partial-publish
|
|
72
|
+
|
|
73
|
+
**Pending follow-up (out of 4.0.23 scope)**:
|
|
74
|
+
- F2 detached architecture heavy refactor (in-shell background subprocess) — already shipped as `0622933d`
|
|
75
|
+
- F8 skill-resolution slice (session 7f7f78 original intent; rid-002/003/004 scope unknown — needs user scoping first)
|
|
76
|
+
- Layer 4 dual-side lockstep guard test (per Lesson 2 — peaks-loop-shared CLI_VERSION + peaks-loop-internal-runtime RUNTIME_VERSION)
|
|
77
|
+
- F3 vendor-detect Windows ENOENT true fix (PATHEXT silent catch)
|
|
78
|
+
- 4.0.22 npm slot retry after 72h grace window expires (~13:00Z + 72h = ~Sat 13:00Z)
|
|
79
|
+
|
|
80
|
+
## 4.0.22 — 2026-08-11 (Layer 3 publish.yml fix verification — same code as 4.0.21)
|
|
81
|
+
|
|
82
|
+
**1 atomic commits from session 2026-08-11 continuation + Layer 3 fix verify**:
|
|
83
|
+
|
|
84
|
+
- `fix(runtime): detached sub-agent now in-shell background subprocess` (`0622933d`, 4 files / +164/-11)
|
|
85
|
+
- `fix(publish.yml): DELETE Auto-bump version step at line 189 (Layer 3 follow-up to peaks-loop-publishing-critical-hard-rules rule 1)` (`9aff3545`, 1 file / +20/-48)
|
|
86
|
+
- `fix(publish.yml): convert DELETED Auto-bump step to YAML comment (no run: clause = workflow parse error)` (`7e3cec22`, 1 file / +7/-22)
|
|
87
|
+
- `chore(release): bump to 4.0.22 — Layer 3 fix verification` (`8be4d694`, 5 files / +6/-6)
|
|
88
|
+
|
|
89
|
+
**Layer 3 fix verification**: this 4.0.22 publish is the FIRST publish after DELETEing publish.yml:189 (`Auto-bump version per smallest-semver policy` step). Previous 4.0.21 + 4.0.22 retry attempts both got auto-bumped past operator intent by line 189; the auto-bump step was the root cause of the 4.0.21/4.0.22 redirect saga. This 4.0.22 publish uses `bump-version.mjs` idempotently (--to 4.0.22 no-op since local is already 4.0.22) + exact-tag-as-authoritative contract enforced by `Verify exact tag matches bumped root version` gate (rid-017 D3).
|
|
90
|
+
|
|
91
|
+
**publish.yml run progression** (#31489608863):
|
|
92
|
+
1. ✓ Checkout / setup pnpm / setup Node.js / npm 11+ upgrade
|
|
93
|
+
2. ✓ Strict vX.Y.Z tag format gate
|
|
94
|
+
3. ✓ Idempotency guard
|
|
95
|
+
4. ✓ **(no Auto-bump step)** — Layer 3 fix verified effective
|
|
96
|
+
5. ✓ Sync README badges
|
|
97
|
+
6. ✓ Build + Vitest
|
|
98
|
+
7. ✓ Verify peaks-loop-shared tarball CLI_VERSION parity (4.0.22 == 4.0.22)
|
|
99
|
+
8. ✓ gate-capability-baseline
|
|
100
|
+
9. ✓ Verify peaks-loop-internal-runtime RUNTIME_VERSION parity (4.0.22 == 4.0.22)
|
|
101
|
+
|
|
102
|
+
**Code unchanged from 4.0.21 ship** — this is a re-version to verify the publish.yml fix, not a feature release.
|
|
103
|
+
|
|
104
|
+
**Lockstep contract** (per Lesson 2):
|
|
105
|
+
- peaks-loop root `package.json#version`: 4.0.22
|
|
106
|
+
- peaks-loop-shared `src/version.ts` `CLI_VERSION`: 4.0.22
|
|
107
|
+
- peaks-loop-shared `package.json#version`: 0.0.56 (next available after 0.0.55 — skipped 0.0.55 because auto-bump during 4.0.21 publish already shipped 0.0.55)
|
|
108
|
+
- peaks-loop-internal-runtime `src/index.ts` `RUNTIME_VERSION`: 4.0.22
|
|
109
|
+
- peaks-loop-internal-runtime `src/index.ts` `RUNTIME_NPM_VERSION`: 0.0.7 (next available after 0.0.6)
|
|
110
|
+
- peaks-loop-internal-runtime `package.json#version`: 0.0.7
|
|
111
|
+
|
|
3
112
|
## 4.0.21 — 2026-08-11 (Batch A: vendor-detect recovery + codegraph integration + anti-fake-green gate)
|
|
4
113
|
|
|
5
114
|
**7 atomic commits from session 2026-08-11 (rid-001 redo → 8-子任务 codegraph → Batch A F1-F7)** + 4.0.21 lockstep bump + changelog entry:
|
|
@@ -4,6 +4,7 @@ import { registerAssetCommands } from './asset-commands.js';
|
|
|
4
4
|
import { registerAuditCommands } from './audit-commands.js';
|
|
5
5
|
import { registerBaselineCommands } from './baseline-commands.js';
|
|
6
6
|
import { registerBeeCommands } from './bee-commands.js';
|
|
7
|
+
import { registerBestPracticeScanCommand } from './best-practice-scan-command.js';
|
|
7
8
|
import { registerCapabilityWorkerConfigAndSCCommands } from './capability-worker-config-sc-commands.js';
|
|
8
9
|
import { registerCodeCommands } from './code-commands.js';
|
|
9
10
|
import { registerCodeGateCommand } from './code-gate-command.js';
|
|
@@ -122,7 +123,7 @@ const REGISTRATIONS = [
|
|
|
122
123
|
['security-audit-commands', registerSecurityAuditCommands], ['perf-audit-commands', registerPerfAuditCommands],
|
|
123
124
|
['verdict-aggregate-command', registerVerdictAggregateCommands], ['log-commands', registerLogCommands],
|
|
124
125
|
['qa-commands', registerQaCommands], ['test-commands', registerTestCommands],
|
|
125
|
-
['playwright-commands', registerPlaywrightCommands], ['code-commands', registerCodeCommands], ['code-gate-command', registerCodeGateCommand], ['dashboard-commands', registerDashboardCommands],
|
|
126
|
+
['playwright-commands', registerPlaywrightCommands], ['code-commands', registerCodeCommands], ['code-gate-command', registerCodeGateCommand], ['dashboard-commands', registerDashboardCommands], ['best-practice-scan-command', registerBestPracticeScanCommand],
|
|
126
127
|
['mut-commands', registerMutCommands], ['fixture-commands', registerFixtureCommands],
|
|
127
128
|
['reviewer-commands', registerReviewerCommands], ['ide-commands', registerIdeCommands],
|
|
128
129
|
['observability-commands', registerObservabilityCommands], ['compact-command', registerCompactCommands],
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-08-12 best-practice-scan — CLI subcommand.
|
|
3
|
+
*
|
|
4
|
+
* `peaks best-practice-scan --project <path> [--lang <lang>] [--commit]`
|
|
5
|
+
*
|
|
6
|
+
* Pipeline:
|
|
7
|
+
* 1. detect language via language-detector (or use --lang override)
|
|
8
|
+
* 2. scan via scan-orchestrator (Context7 → WebSearch → empty)
|
|
9
|
+
* 3. render 8-row table via output-formatter
|
|
10
|
+
* 4. write artifact into <projectRoot>/best-practice/<date>-<intent>.md
|
|
11
|
+
* (gitignored directory by convention; --commit flag reserved for
|
|
12
|
+
* a future slice that opts the artifact into git tracking)
|
|
13
|
+
* 5. print artifact path + the 8-row table to stdout
|
|
14
|
+
*
|
|
15
|
+
* Catch-gate (spec §7): the command emits a 3-line prompt asking the
|
|
16
|
+
* user to ack / pick alt / reject + reason. The gate is read from
|
|
17
|
+
* stdin via `PEAKS_BEST_PRACTICE_STDIN` (test seam) or the real stdin
|
|
18
|
+
* (production). The result is written to stdout as a JSON envelope
|
|
19
|
+
* suffix so the orchestrator can react programmatically.
|
|
20
|
+
*/
|
|
21
|
+
import type { Command } from 'commander';
|
|
22
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
23
|
+
import { type RecommendationChoice } from '../../services/best-practice/output-formatter.js';
|
|
24
|
+
export type CatchGateOutcome = {
|
|
25
|
+
kind: 'accept';
|
|
26
|
+
choice: RecommendationChoice;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'alternative';
|
|
29
|
+
choice: RecommendationChoice;
|
|
30
|
+
reason?: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'reject';
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function parseCatchGateReply(raw: string, recommended: RecommendationChoice): CatchGateOutcome;
|
|
36
|
+
export declare function registerBestPracticeScanCommand(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
4
|
+
import { fail, ok } from 'peaks-loop-shared/result';
|
|
5
|
+
import { detectLanguage } from '../../services/best-practice/language-detector.js';
|
|
6
|
+
import { scanBestPractice } from '../../services/best-practice/scan-orchestrator.js';
|
|
7
|
+
import { findForbiddenTokens, formatOutputTable } from '../../services/best-practice/output-formatter.js';
|
|
8
|
+
const CATCH_GATE_PROMPT = [
|
|
9
|
+
'⚠️ 任何跟你真实业务不一样,改 — LLM 推荐可能错。',
|
|
10
|
+
'回应 (默认 = 接受): 接受 / 接受方案 A|接受方案 B|接受方案 C / 拒绝 + 原因'
|
|
11
|
+
].join('\n');
|
|
12
|
+
async function readUserInput() {
|
|
13
|
+
const override = process.env.PEAKS_BEST_PRACTICE_STDIN;
|
|
14
|
+
if (override !== undefined)
|
|
15
|
+
return override;
|
|
16
|
+
if (process.stdin.isTTY)
|
|
17
|
+
return '';
|
|
18
|
+
return new Promise((resolveStdin) => {
|
|
19
|
+
let data = '';
|
|
20
|
+
process.stdin.setEncoding('utf8');
|
|
21
|
+
process.stdin.on('data', (chunk) => {
|
|
22
|
+
data += chunk;
|
|
23
|
+
});
|
|
24
|
+
process.stdin.on('end', () => resolveStdin(data.trim()));
|
|
25
|
+
process.stdin.on('error', () => resolveStdin(data.trim()));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function parseCatchGateReply(raw, recommended) {
|
|
29
|
+
const trimmed = raw.trim();
|
|
30
|
+
if (trimmed.length === 0) {
|
|
31
|
+
return { kind: 'accept', choice: recommended };
|
|
32
|
+
}
|
|
33
|
+
const altMatch = /^接受方案\s*([ABC])(?:\s+(.+))?$/u.exec(trimmed);
|
|
34
|
+
if (altMatch !== null) {
|
|
35
|
+
const letter = altMatch[1];
|
|
36
|
+
const reason = altMatch[2];
|
|
37
|
+
if (letter === 'A' || letter === 'B' || letter === 'C') {
|
|
38
|
+
return reason === undefined
|
|
39
|
+
? { kind: 'alternative', choice: letter }
|
|
40
|
+
: { kind: 'alternative', choice: letter, reason };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const rejectMatch = /^拒绝(?:\s+(.+))?$/u.exec(trimmed);
|
|
44
|
+
if (rejectMatch !== null) {
|
|
45
|
+
const reason = rejectMatch[1];
|
|
46
|
+
return reason === undefined
|
|
47
|
+
? { kind: 'reject', reason: 'unspecified' }
|
|
48
|
+
: { kind: 'reject', reason };
|
|
49
|
+
}
|
|
50
|
+
if (trimmed === '接受' || trimmed === 'accept' || trimmed === 'y' || trimmed === 'Y') {
|
|
51
|
+
return { kind: 'accept', choice: recommended };
|
|
52
|
+
}
|
|
53
|
+
// Anything else is treated as a rejection-with-reason.
|
|
54
|
+
return { kind: 'reject', reason: trimmed };
|
|
55
|
+
}
|
|
56
|
+
export function registerBestPracticeScanCommand(program, io) {
|
|
57
|
+
addJsonOption(program
|
|
58
|
+
.command('best-practice-scan')
|
|
59
|
+
.description('2026-08-12 best-practice-scan: language-aware + business-aware doc-fragment lookup ' +
|
|
60
|
+
'via Context7 (priority 1) → WebSearch (priority 2) → empty fallback. ' +
|
|
61
|
+
'Renders the 8-row comparison table from spec §5 + §6 + §7. ' +
|
|
62
|
+
'Writes the artifact to <project>/best-practice/<date>-<intent>.md (gitignored).')
|
|
63
|
+
.option('--project <path>', 'project root (default cwd)', process.cwd())
|
|
64
|
+
.option('--lang <lang>', 'language override (skip auto-detect)')
|
|
65
|
+
.option('--commit', 'reserved flag — opts the artifact into git tracking (future slice)')).action(async (opts) => {
|
|
66
|
+
try {
|
|
67
|
+
const detection = opts.lang === undefined ? detectLanguage(opts.project) : null;
|
|
68
|
+
const language = opts.lang ?? detection?.language ?? 'unknown';
|
|
69
|
+
io.stdout(`[best-practice-scan] project=${opts.project} language=${language}`);
|
|
70
|
+
const scan = await scanBestPractice({
|
|
71
|
+
intent: opts.project,
|
|
72
|
+
language,
|
|
73
|
+
projectRoot: opts.project,
|
|
74
|
+
io
|
|
75
|
+
});
|
|
76
|
+
const recommendation = scan.results.length > 0 ? 'A' : 'B';
|
|
77
|
+
const reasoning = `基于 ${scan.fragments.length} 个 ${scan.source} 文档片段;` +
|
|
78
|
+
`语言=${language};LLM 推断方案 ${recommendation} 与项目阶段最匹配。`;
|
|
79
|
+
const table = formatOutputTable({
|
|
80
|
+
intent: opts.project,
|
|
81
|
+
language,
|
|
82
|
+
fragments: scan.results,
|
|
83
|
+
recommendation,
|
|
84
|
+
reasoning
|
|
85
|
+
});
|
|
86
|
+
io.stdout(table);
|
|
87
|
+
const forbiddenHits = findForbiddenTokens(table);
|
|
88
|
+
if (forbiddenHits.length > 0) {
|
|
89
|
+
io.stderr(`[best-practice-scan] WARNING: forbidden tokens detected: ${forbiddenHits.join(', ')}`);
|
|
90
|
+
}
|
|
91
|
+
io.stdout('');
|
|
92
|
+
io.stdout(CATCH_GATE_PROMPT);
|
|
93
|
+
const userInput = await readUserInput();
|
|
94
|
+
const outcome = parseCatchGateReply(userInput, recommendation);
|
|
95
|
+
const outDir = join(opts.project, 'best-practice');
|
|
96
|
+
mkdirSync(outDir, { recursive: true });
|
|
97
|
+
const dateStr = new Date().toISOString().slice(0, 10);
|
|
98
|
+
const slug = opts.project.replace(/[^a-zA-Z0-9_-]+/g, '-').slice(0, 32) || 'intent';
|
|
99
|
+
const artifactPath = join(outDir, `${dateStr}-${slug}.md`);
|
|
100
|
+
writeFileSync(artifactPath, table + '\n', 'utf8');
|
|
101
|
+
printResult(io, ok('best-practice.scan', {
|
|
102
|
+
project: opts.project,
|
|
103
|
+
language,
|
|
104
|
+
scanSource: scan.source,
|
|
105
|
+
fragments: scan.results,
|
|
106
|
+
recommendation,
|
|
107
|
+
catchGate: outcome,
|
|
108
|
+
artifactPath,
|
|
109
|
+
commitFlag: opts.commit === true,
|
|
110
|
+
forbiddenHits
|
|
111
|
+
}, forbiddenHits.length > 0 ? [`forbidden tokens present: ${forbiddenHits.join(', ')}`] : [], [`Artifact written to ${artifactPath}`, `catch-gate outcome: ${outcome.kind}`]), opts.json === true);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
printResult(io, fail('best-practice.scan', 'BEST_PRACTICE_SCAN_FAILED', getErrorMessage(err), { stack: err instanceof Error ? err.stack : undefined }, ['Rerun with --json for machine-readable envelope']), opts.json === true);
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -5,6 +5,7 @@ import { recordBypass, isBypassLimitReached, MAX_BYPASSES_PER_SESSION } from '..
|
|
|
5
5
|
import { lintRequestArtifact } from '../../services/artifacts/artifact-lint-service.js';
|
|
6
6
|
import { getRepairCycleStatus } from '../../services/artifacts/repair-cycle-service.js';
|
|
7
7
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
8
|
+
import { triggerBestPracticeScan } from '../../services/prd/best-practice-auto-trigger.js';
|
|
8
9
|
import { formatMdCompact } from '../../shared/format-md-compact.js';
|
|
9
10
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
10
11
|
/**
|
|
@@ -371,6 +372,10 @@ export function registerRequestCommands(program, io) {
|
|
|
371
372
|
}
|
|
372
373
|
// v2.13.2 AC-4 — auto-regen prd/handoff.md on prd:handed-off success.
|
|
373
374
|
// Only fires when the handoff is missing; existing handoffs are not overwritten.
|
|
375
|
+
// Slice 2026-08-12 best-practice-scan (Slice F) — auto-trigger BPS as a
|
|
376
|
+
// post-step after peaks-prd's businessGoal artifact transitions to
|
|
377
|
+
// `handed-off` (the canonical "complete" state in the prd state machine).
|
|
378
|
+
// The trigger is fire-and-forget: failure NEVER blocks the transition.
|
|
374
379
|
if (role === 'prd' && newState === 'handed-off' && options.sessionId !== undefined) {
|
|
375
380
|
const { autoRegenPrdHandoff } = await import('../../services/prd/handoff-auto-regen.js');
|
|
376
381
|
const regen = await autoRegenPrdHandoff({
|
|
@@ -379,17 +384,24 @@ export function registerRequestCommands(program, io) {
|
|
|
379
384
|
requestId,
|
|
380
385
|
role: 'prd'
|
|
381
386
|
});
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
387
|
+
const bpsTrigger = await triggerBestPracticeScan({
|
|
388
|
+
projectRoot: options.project,
|
|
389
|
+
sessionId: result.sessionId,
|
|
390
|
+
requestId
|
|
391
|
+
});
|
|
392
|
+
const handoffAutoRegen = regen.status === 'created'
|
|
393
|
+
? { status: 'created', path: regen.path, sha256: regen.sha256 }
|
|
394
|
+
: regen.status === 'skipped-exists'
|
|
395
|
+
? { status: 'skipped-exists', path: regen.path }
|
|
396
|
+
: { status: 'failed', reason: regen.reason };
|
|
397
|
+
const notes = [];
|
|
398
|
+
if (regen.status === 'failed') {
|
|
399
|
+
notes.push(`prd handoff auto-regen failed: ${regen.reason}`);
|
|
386
400
|
}
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
return;
|
|
401
|
+
if (bpsTrigger.status === 'failed') {
|
|
402
|
+
notes.push(`best-practice-scan auto-trigger failed: ${bpsTrigger.reason ?? 'unknown'}`);
|
|
390
403
|
}
|
|
391
|
-
|
|
392
|
-
printResult(io, ok('request.transition', { ...result, handoffAutoRegen: { status: 'failed', reason: regen.reason } }, [`prd handoff auto-regen failed: ${regen.reason}`]), options.json);
|
|
404
|
+
printResult(io, ok('request.transition', { ...result, handoffAutoRegen, bestPracticeScanTrigger: bpsTrigger }, notes), options.json);
|
|
393
405
|
return;
|
|
394
406
|
}
|
|
395
407
|
// Slice 2026-07-01-strategic-compact-cli: stitch the pre-compact
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type DetectedLanguage = 'typescript' | 'javascript' | 'python' | 'go' | 'java' | 'unknown';
|
|
2
|
+
export type LanguageDetection = {
|
|
3
|
+
readonly language: DetectedLanguage;
|
|
4
|
+
readonly confidence: number;
|
|
5
|
+
readonly signals: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
export declare function detectLanguage(projectRoot: string): LanguageDetection;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-08-12 best-practice-scan — language detector.
|
|
3
|
+
*
|
|
4
|
+
* Detects the project's primary language from well-known marker files.
|
|
5
|
+
* Returns one of: typescript | javascript | python | go | java | unknown.
|
|
6
|
+
*
|
|
7
|
+
* Detection priority (first hit wins):
|
|
8
|
+
* 1. Java — `pom.xml` or `build.gradle` present
|
|
9
|
+
* 2. Go — `go.mod` present
|
|
10
|
+
* 3. Python — `requirements.txt` / `pyproject.toml` / `setup.py` present
|
|
11
|
+
* 4. Node — `package.json` present:
|
|
12
|
+
* a. `tsconfig.json` also present → typescript (high confidence)
|
|
13
|
+
* b. `package.json` `"type": "module"` + no tsconfig → javascript (medium confidence)
|
|
14
|
+
* c. default `package.json` (no tsconfig, no `"type"`) → javascript (lower confidence)
|
|
15
|
+
* 5. none of the above → `unknown`
|
|
16
|
+
*
|
|
17
|
+
* Signals are exposed in the result so the caller (orchestrator) can
|
|
18
|
+
* optionally surface the detected marker list to the user.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
const HIGH = 0.95;
|
|
23
|
+
const MEDIUM = 0.8;
|
|
24
|
+
const LOW = 0.6;
|
|
25
|
+
function hasFile(root, name) {
|
|
26
|
+
return existsSync(join(root, name));
|
|
27
|
+
}
|
|
28
|
+
function readPackageType(root) {
|
|
29
|
+
const pkgPath = join(root, 'package.json');
|
|
30
|
+
if (!existsSync(pkgPath))
|
|
31
|
+
return undefined;
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(pkgPath, 'utf8');
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
if (parsed !== null && typeof parsed === 'object' && 'type' in parsed) {
|
|
36
|
+
const typeVal = parsed.type;
|
|
37
|
+
if (typeof typeVal === 'string')
|
|
38
|
+
return typeVal;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* malformed package.json → treat as absent */
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
export function detectLanguage(projectRoot) {
|
|
47
|
+
if (!statSync(projectRoot, { throwIfNoEntry: false })) {
|
|
48
|
+
throw new Error(`detectLanguage: projectRoot does not exist: ${projectRoot}`);
|
|
49
|
+
}
|
|
50
|
+
if (hasFile(projectRoot, 'pom.xml') || hasFile(projectRoot, 'build.gradle')) {
|
|
51
|
+
const signals = [];
|
|
52
|
+
if (hasFile(projectRoot, 'pom.xml'))
|
|
53
|
+
signals.push('pom.xml');
|
|
54
|
+
if (hasFile(projectRoot, 'build.gradle'))
|
|
55
|
+
signals.push('build.gradle');
|
|
56
|
+
return { language: 'java', confidence: HIGH, signals };
|
|
57
|
+
}
|
|
58
|
+
if (hasFile(projectRoot, 'go.mod')) {
|
|
59
|
+
return { language: 'go', confidence: HIGH, signals: ['go.mod'] };
|
|
60
|
+
}
|
|
61
|
+
if (hasFile(projectRoot, 'requirements.txt') ||
|
|
62
|
+
hasFile(projectRoot, 'pyproject.toml') ||
|
|
63
|
+
hasFile(projectRoot, 'setup.py')) {
|
|
64
|
+
const signals = [];
|
|
65
|
+
if (hasFile(projectRoot, 'requirements.txt'))
|
|
66
|
+
signals.push('requirements.txt');
|
|
67
|
+
if (hasFile(projectRoot, 'pyproject.toml'))
|
|
68
|
+
signals.push('pyproject.toml');
|
|
69
|
+
if (hasFile(projectRoot, 'setup.py'))
|
|
70
|
+
signals.push('setup.py');
|
|
71
|
+
return { language: 'python', confidence: HIGH, signals };
|
|
72
|
+
}
|
|
73
|
+
if (hasFile(projectRoot, 'package.json')) {
|
|
74
|
+
if (hasFile(projectRoot, 'tsconfig.json')) {
|
|
75
|
+
return {
|
|
76
|
+
language: 'typescript',
|
|
77
|
+
confidence: HIGH,
|
|
78
|
+
signals: ['package.json', 'tsconfig.json']
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const pkgType = readPackageType(projectRoot);
|
|
82
|
+
if (pkgType === 'module') {
|
|
83
|
+
return {
|
|
84
|
+
language: 'javascript',
|
|
85
|
+
confidence: MEDIUM,
|
|
86
|
+
signals: ['package.json', '"type": "module"']
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
language: 'javascript',
|
|
91
|
+
confidence: LOW,
|
|
92
|
+
signals: ['package.json']
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { language: 'unknown', confidence: 0, signals: [] };
|
|
96
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-08-12 best-practice-scan — output formatter.
|
|
3
|
+
*
|
|
4
|
+
* Renders the 8-row business-decision comparison table (per spec §5) plus
|
|
5
|
+
* the 3-line footer (spec §5 footer + §7 ⚠️ gate). The formatter is the
|
|
6
|
+
* single source of truth for the artifact's text shape; the standalone
|
|
7
|
+
* template file at `src/templates/best-practice-scan.md` mirrors this
|
|
8
|
+
* shape for human reference.
|
|
9
|
+
*
|
|
10
|
+
* Hard rules enforced here (defensive scans):
|
|
11
|
+
* - Forbidden words: 会爆 / 头疼 / 黑暗 / 崩
|
|
12
|
+
* - Forbidden developer-perspective phrases: 代码 50 行 / 学习曲线 / 你的难
|
|
13
|
+
* - Forbidden binary framing: MVP vs 长期
|
|
14
|
+
* - Forbidden project-stage judgment: MVP 项目
|
|
15
|
+
*
|
|
16
|
+
* The `★` recommendation marker is placed on the column header whose
|
|
17
|
+
* `recommendation` value (A / B / C) matches `opts.recommendation`.
|
|
18
|
+
*/
|
|
19
|
+
import type { DocFragment } from './scan-orchestrator.js';
|
|
20
|
+
export type RecommendationChoice = 'A' | 'B' | 'C';
|
|
21
|
+
export type FormatOptions = {
|
|
22
|
+
readonly intent: string;
|
|
23
|
+
readonly language: string;
|
|
24
|
+
readonly fragments: readonly DocFragment[];
|
|
25
|
+
readonly recommendation: RecommendationChoice;
|
|
26
|
+
readonly reasoning: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function formatOutputTable(opts: FormatOptions): string;
|
|
29
|
+
/**
|
|
30
|
+
* Defensive scan: returns the list of forbidden-word / phrase hits
|
|
31
|
+
* present in the rendered output. The catch-gate test calls this to
|
|
32
|
+
* fail-loud if a future code change reintroduces a banned token.
|
|
33
|
+
*/
|
|
34
|
+
export declare function findForbiddenTokens(rendered: string): readonly string[];
|
|
35
|
+
export declare const __TEST__: {
|
|
36
|
+
ROW_LABELS: readonly ["技术组合(通俗)", "peaks-code 估算", "user 看到", "user 操作", "业务影响(6 个月后)", "适用场景", "适用场景 (parallel)", "LLM 推荐"];
|
|
37
|
+
FORBIDDEN_WORDS: readonly ["会爆", "头疼", "黑暗", "崩"];
|
|
38
|
+
FORBIDDEN_PHRASES: readonly ["代码 50 行", "学习曲线", "你的难"];
|
|
39
|
+
FORBIDDEN_BINARY: readonly ["MVP vs 长期"];
|
|
40
|
+
FORBIDDEN_STAGE: readonly ["MVP 项目"];
|
|
41
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const ROW_LABELS = [
|
|
2
|
+
'技术组合(通俗)',
|
|
3
|
+
'peaks-code 估算',
|
|
4
|
+
'user 看到',
|
|
5
|
+
'user 操作',
|
|
6
|
+
'业务影响(6 个月后)',
|
|
7
|
+
'适用场景',
|
|
8
|
+
'适用场景 (parallel)',
|
|
9
|
+
'LLM 推荐'
|
|
10
|
+
];
|
|
11
|
+
const FORBIDDEN_WORDS = ['会爆', '头疼', '黑暗', '崩'];
|
|
12
|
+
const FORBIDDEN_PHRASES = ['代码 50 行', '学习曲线', '你的难'];
|
|
13
|
+
const FORBIDDEN_BINARY = ['MVP vs 长期'];
|
|
14
|
+
const FORBIDDEN_STAGE = ['MVP 项目'];
|
|
15
|
+
function rowLabel(row) {
|
|
16
|
+
return `| **${row}** | ... | ... | ... |`;
|
|
17
|
+
}
|
|
18
|
+
function recommendedColumnHeader(label, isRecommended) {
|
|
19
|
+
return isRecommended ? `**${label} ★**` : label;
|
|
20
|
+
}
|
|
21
|
+
export function formatOutputTable(opts) {
|
|
22
|
+
const cols = [
|
|
23
|
+
{
|
|
24
|
+
choice: 'A',
|
|
25
|
+
label: recommendedColumnHeader('方案 A', opts.recommendation === 'A'),
|
|
26
|
+
body: '面向长期维护,组件复用度高,适合多人协作。'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
choice: 'B',
|
|
30
|
+
label: recommendedColumnHeader('方案 B', opts.recommendation === 'B'),
|
|
31
|
+
body: '中等复杂度,上手快,适合迭代中的小团队。'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
choice: 'C',
|
|
35
|
+
label: recommendedColumnHeader('方案 C', opts.recommendation === 'C'),
|
|
36
|
+
body: '极简实现,适合一次性脚本或概念验证。'
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
const colA = cols[0];
|
|
40
|
+
const colB = cols[1];
|
|
41
|
+
const colC = cols[2];
|
|
42
|
+
if (colA === undefined || colB === undefined || colC === undefined) {
|
|
43
|
+
throw new Error('formatOutputTable: column triple must be present');
|
|
44
|
+
}
|
|
45
|
+
const head = `| ${ROW_LABELS[0]} | ${colA.label} | ${colB.label} | ${colC.label} |`;
|
|
46
|
+
const sep = '| --- | --- | --- | --- |';
|
|
47
|
+
const bodyRows = [];
|
|
48
|
+
for (let i = 0; i < ROW_LABELS.length; i += 1) {
|
|
49
|
+
const label = ROW_LABELS[i];
|
|
50
|
+
if (label === undefined)
|
|
51
|
+
continue;
|
|
52
|
+
if (label === 'LLM 推荐') {
|
|
53
|
+
const recCell = cols
|
|
54
|
+
.map((c) => (c.choice === opts.recommendation ? `**${c.choice}**` : c.choice))
|
|
55
|
+
.join(' / ');
|
|
56
|
+
bodyRows.push(`| **${label}** | ${recCell} | ${recCell} | ${recCell} |`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (label === '适用场景 (parallel)') {
|
|
60
|
+
bodyRows.push(`| **${label}** | < 5 字段 + < 2 月就用掉 | 字段会增长 + 1 年+ 持续维护 | 灵活配置 + 长期演进 |`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
bodyRows.push(rowLabel(label));
|
|
64
|
+
}
|
|
65
|
+
const reasoningBlock = `> **LLM 推理:** ${opts.reasoning}`;
|
|
66
|
+
const footer = [
|
|
67
|
+
'↩ 默认走方案 ' + opts.recommendation + ';觉得估错 / 推荐不对就告诉我。',
|
|
68
|
+
'⚠️ 任何跟你真实业务不一样,改 — LLM 推荐可能错。',
|
|
69
|
+
'📝 备注:user 体验在 2/3 个方案里几乎一致,差别在 6 个月后的维护成本和你的项目长期节奏。'
|
|
70
|
+
].join('\n');
|
|
71
|
+
const meta = [
|
|
72
|
+
`language: ${opts.language}`,
|
|
73
|
+
`intent: ${opts.intent}`,
|
|
74
|
+
`fragments: ${opts.fragments.length}`,
|
|
75
|
+
`recommendation: ${opts.recommendation}`
|
|
76
|
+
].join(' | ');
|
|
77
|
+
return [
|
|
78
|
+
`# Best-Practice Scan — ${opts.intent}`,
|
|
79
|
+
'',
|
|
80
|
+
`> ${meta}`,
|
|
81
|
+
'',
|
|
82
|
+
reasoningBlock,
|
|
83
|
+
'',
|
|
84
|
+
head,
|
|
85
|
+
sep,
|
|
86
|
+
...bodyRows,
|
|
87
|
+
'',
|
|
88
|
+
footer
|
|
89
|
+
].join('\n');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Defensive scan: returns the list of forbidden-word / phrase hits
|
|
93
|
+
* present in the rendered output. The catch-gate test calls this to
|
|
94
|
+
* fail-loud if a future code change reintroduces a banned token.
|
|
95
|
+
*/
|
|
96
|
+
export function findForbiddenTokens(rendered) {
|
|
97
|
+
const hits = [];
|
|
98
|
+
for (const word of FORBIDDEN_WORDS) {
|
|
99
|
+
if (rendered.includes(word))
|
|
100
|
+
hits.push(word);
|
|
101
|
+
}
|
|
102
|
+
for (const phrase of FORBIDDEN_PHRASES) {
|
|
103
|
+
if (rendered.includes(phrase))
|
|
104
|
+
hits.push(phrase);
|
|
105
|
+
}
|
|
106
|
+
for (const binary of FORBIDDEN_BINARY) {
|
|
107
|
+
if (rendered.includes(binary))
|
|
108
|
+
hits.push(binary);
|
|
109
|
+
}
|
|
110
|
+
for (const stage of FORBIDDEN_STAGE) {
|
|
111
|
+
if (rendered.includes(stage))
|
|
112
|
+
hits.push(stage);
|
|
113
|
+
}
|
|
114
|
+
return hits;
|
|
115
|
+
}
|
|
116
|
+
export const __TEST__ = { ROW_LABELS, FORBIDDEN_WORDS, FORBIDDEN_PHRASES, FORBIDDEN_BINARY, FORBIDDEN_STAGE };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-08-12 best-practice-scan — scan orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Resolves a doc-fragment set for a (intent, language) query via the
|
|
5
|
+
* following priority chain:
|
|
6
|
+
* 1. Context7 MCP (priority 1; 30 s timeout)
|
|
7
|
+
* 2. WebSearch (priority 2 fallback; 200 ms simulated delay)
|
|
8
|
+
* 3. Empty (both sources exhausted)
|
|
9
|
+
*
|
|
10
|
+
* The Context7 + WebSearch calls are STUB implementations for v1:
|
|
11
|
+
* they sleep for a fixed delay and return synthetic doc fragments so
|
|
12
|
+
* the orchestrator's source-priority logic is testable end-to-end.
|
|
13
|
+
* Real MCP wiring is a future slice — the stubs are clearly marked.
|
|
14
|
+
*
|
|
15
|
+
* The orchestrator emits structured log lines via the injected `io`
|
|
16
|
+
* (stdout for progress, stderr for warnings) so a CLI caller can see
|
|
17
|
+
* which fallback path was taken.
|
|
18
|
+
*/
|
|
19
|
+
import type { ProgramIO } from '../../cli/cli-helpers.js';
|
|
20
|
+
export type DocFragment = {
|
|
21
|
+
readonly title: string;
|
|
22
|
+
readonly url: string;
|
|
23
|
+
readonly snippet: string;
|
|
24
|
+
};
|
|
25
|
+
export type ScanSource = 'context7' | 'websearch' | 'fallback';
|
|
26
|
+
export type ScanResult = {
|
|
27
|
+
readonly results: readonly DocFragment[];
|
|
28
|
+
readonly fragments: readonly DocFragment[];
|
|
29
|
+
readonly source: ScanSource;
|
|
30
|
+
readonly elapsedMs: number;
|
|
31
|
+
};
|
|
32
|
+
export type ScanOptions = {
|
|
33
|
+
readonly intent: string;
|
|
34
|
+
readonly language: string;
|
|
35
|
+
readonly projectRoot: string;
|
|
36
|
+
readonly io: ProgramIO;
|
|
37
|
+
readonly context7TimeoutMs?: number;
|
|
38
|
+
};
|
|
39
|
+
export declare function scanBestPractice(opts: ScanOptions): Promise<ScanResult>;
|