peaks-cli 1.2.8 → 1.2.9
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/README.md +12 -0
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/project-commands.js +1 -1
- package/dist/src/cli/commands/scan-commands.js +22 -0
- package/dist/src/services/memory/project-memory-service.d.ts +1 -1
- package/dist/src/services/memory/project-memory-service.js +52 -23
- package/dist/src/services/scan/libraries-service.d.ts +24 -0
- package/dist/src/services/scan/libraries-service.js +419 -0
- package/dist/src/services/scan/libraries-types.d.ts +59 -0
- package/dist/src/services/scan/libraries-types.js +9 -0
- package/dist/src/services/skills/skill-runbook-service.js +34 -1
- package/dist/src/services/workflow/autonomous-resume-writer.js +7 -7
- package/dist/src/shared/change-id.d.ts +30 -0
- package/dist/src/shared/change-id.js +40 -6
- package/dist/src/shared/paths.d.ts +1 -1
- package/dist/src/shared/paths.js +2 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/schemas/library-breaking-changes.data.json +141 -0
- package/schemas/library-breaking-changes.meta.json +6 -0
- package/schemas/library-breaking-changes.schema.json +50 -0
- package/skills/peaks-qa/SKILL.md +12 -0
- package/skills/peaks-rd/SKILL.md +145 -2
- package/skills/peaks-solo/SKILL.md +76 -316
- package/skills/peaks-solo/references/runbook.md +166 -0
- package/skills/peaks-solo/references/workflow-gates-and-types.md +177 -0
- package/skills/peaks-solo-resume/SKILL.md +81 -0
- package/skills/peaks-solo-status/SKILL.md +120 -0
- package/skills/peaks-solo-test/SKILL.md +84 -0
- package/skills/peaks-txt/SKILL.md +8 -5
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library version scan types.
|
|
3
|
+
* Joins the existing scan family (archetype, existing-system, type-sanity,
|
|
4
|
+
* acceptance-coverage, diff-vs-scope, file-size) but is intentionally scoped
|
|
5
|
+
* to library version enumeration only. Does NOT extend scan-types.ts —
|
|
6
|
+
* each scan service ships its own types to keep the scan-types module
|
|
7
|
+
* focused on the archetype + existing-system pair (see src/services/scan/scan-types.ts).
|
|
8
|
+
*/
|
|
9
|
+
export type Ecosystem = 'npm';
|
|
10
|
+
export type DependencyScope = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies';
|
|
11
|
+
export type LibraryEntry = {
|
|
12
|
+
/** npm package name (e.g. "antd", "@mui/material", "react-router-dom"). */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Raw version spec as written in package.json (e.g. "^5.18.0", "workspace:*", "git+https://..."). */
|
|
15
|
+
version: string;
|
|
16
|
+
/**
|
|
17
|
+
* Parsed major version, or null when the spec is non-semver (e.g.
|
|
18
|
+
* "workspace:*", "file:../local", "git+https://..."). The LLM should
|
|
19
|
+
* treat null as "cannot determine major; consult breaking-changes table
|
|
20
|
+
* by other signals (e.g. lockfile or import statement shape)".
|
|
21
|
+
*/
|
|
22
|
+
major: number | null;
|
|
23
|
+
scope: DependencyScope;
|
|
24
|
+
ecosystem: Ecosystem;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Per-workspace provenance for monorepo scans.
|
|
28
|
+
*
|
|
29
|
+
* `path` is the absolute path of the `package.json` that contributed
|
|
30
|
+
* libraries. `count` is the number of `LibraryEntry` rows produced by
|
|
31
|
+
* reading that single `package.json` (i.e. NOT the aggregate across
|
|
32
|
+
* the whole monorepo — use `LibraryReport.totalCount` for the aggregate).
|
|
33
|
+
*
|
|
34
|
+
* `name` and `version` are the workspace's own `name` / `version` from
|
|
35
|
+
* its `package.json`, when present. They are optional because some
|
|
36
|
+
* workspace `package.json` files omit them.
|
|
37
|
+
*/
|
|
38
|
+
export type WorkspaceEntry = {
|
|
39
|
+
path: string;
|
|
40
|
+
count: number;
|
|
41
|
+
name?: string;
|
|
42
|
+
version?: string;
|
|
43
|
+
};
|
|
44
|
+
export type LibraryReport = {
|
|
45
|
+
projectRoot: string;
|
|
46
|
+
libraries: LibraryEntry[];
|
|
47
|
+
totalCount: number;
|
|
48
|
+
byScope: Record<DependencyScope, number>;
|
|
49
|
+
/**
|
|
50
|
+
* Per-workspace provenance for monorepo (pnpm / npm / yarn workspaces,
|
|
51
|
+
* lerna) projects. Empty for single-package projects so the field is
|
|
52
|
+
* always present (additive; consumers can rely on the shape).
|
|
53
|
+
*/
|
|
54
|
+
workspaces: WorkspaceEntry[];
|
|
55
|
+
/** ISO timestamp at scan time. */
|
|
56
|
+
scannedAt: string;
|
|
57
|
+
/** Soft signals — e.g. "package.json not found" or "package.json is not valid JSON". */
|
|
58
|
+
warnings: string[];
|
|
59
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library version scan types.
|
|
3
|
+
* Joins the existing scan family (archetype, existing-system, type-sanity,
|
|
4
|
+
* acceptance-coverage, diff-vs-scope, file-size) but is intentionally scoped
|
|
5
|
+
* to library version enumeration only. Does NOT extend scan-types.ts —
|
|
6
|
+
* each scan service ships its own types to keep the scan-types module
|
|
7
|
+
* focused on the archetype + existing-system pair (see src/services/scan/scan-types.ts).
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
1
2
|
import { readText } from '../../shared/fs.js';
|
|
2
3
|
import { loadSkillRegistry } from './skill-registry.js';
|
|
3
4
|
const DESTRUCTIVE_APPLY_PATTERNS = [
|
|
@@ -13,6 +14,38 @@ function extractRunbookSection(body) {
|
|
|
13
14
|
const match = /## Default runbook\n+([\s\S]*?)(?=\n## |$)/.exec(body);
|
|
14
15
|
return match === null ? null : match[1];
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Load the runbook section, falling back to `references/runbook.md` if the
|
|
19
|
+
* SKILL.md only has a pointer section. This supports skills (notably
|
|
20
|
+
* `peaks-solo`) that extracted their 150-line bash runbook to a sibling
|
|
21
|
+
* reference to keep SKILL.md under the 800-line cap. The CLI
|
|
22
|
+
* `peaks skill runbook` command uses the same fallback so a human
|
|
23
|
+
* reviewer sees the full runbook regardless of where it lives.
|
|
24
|
+
*
|
|
25
|
+
* Strategy: prefer the LONGER of the two sections. A short pointer section
|
|
26
|
+
* in SKILL.md (~ 1-2 lines) is treated as a "this runbook is in the
|
|
27
|
+
* reference" marker; a long inline section (>= the reference length) is
|
|
28
|
+
* treated as the canonical runbook. This avoids the false positive where
|
|
29
|
+
* the pointer section's regex match returns a non-null but content-poor
|
|
30
|
+
* string.
|
|
31
|
+
*/
|
|
32
|
+
async function loadRunbookSection(skillPath, body) {
|
|
33
|
+
const inline = extractRunbookSection(body);
|
|
34
|
+
const refPath = join(dirname(skillPath), 'references', 'runbook.md');
|
|
35
|
+
let refSection = null;
|
|
36
|
+
try {
|
|
37
|
+
const refBody = await readText(refPath);
|
|
38
|
+
refSection = extractRunbookSection(refBody);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// reference file does not exist or is not readable
|
|
42
|
+
}
|
|
43
|
+
if (inline === null)
|
|
44
|
+
return refSection;
|
|
45
|
+
if (refSection === null)
|
|
46
|
+
return inline;
|
|
47
|
+
return inline.length >= refSection.length ? inline : refSection;
|
|
48
|
+
}
|
|
16
49
|
function findDestructiveApplyLines(section) {
|
|
17
50
|
const lines = section.split(/\r?\n/);
|
|
18
51
|
return lines.filter((line) => DESTRUCTIVE_APPLY_PATTERNS.some((pattern) => pattern.test(line)));
|
|
@@ -30,7 +63,7 @@ export async function inspectSkillRunbook(name, baseDir) {
|
|
|
30
63
|
throw new Error(`Skill "${name}" not found under skills directory`);
|
|
31
64
|
}
|
|
32
65
|
const body = await readText(skill.skillPath);
|
|
33
|
-
const section =
|
|
66
|
+
const section = await loadRunbookSection(skill.skillPath, body);
|
|
34
67
|
if (section === null) {
|
|
35
68
|
return {
|
|
36
69
|
name: skill.name,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { buildArtifactRelativePathInRoot, validateChangeIdOrThrow } from '../../shared/change-id.js';
|
|
4
4
|
import { pathExists } from '../../shared/fs.js';
|
|
5
5
|
function defaultClock() {
|
|
6
6
|
return new Date().toISOString();
|
|
@@ -109,27 +109,27 @@ Next actions:
|
|
|
109
109
|
function buildFiles(changeId, goal, createdAt, artifactWorkspacePath) {
|
|
110
110
|
return [
|
|
111
111
|
{
|
|
112
|
-
path: join(artifactWorkspacePath,
|
|
112
|
+
path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'prd', 'autonomous-goal-package.json')),
|
|
113
113
|
content: renderGoalPackage(changeId, goal)
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
|
-
path: join(artifactWorkspacePath,
|
|
116
|
+
path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'autonomous-rd-plan.json')),
|
|
117
117
|
content: renderRdPlan(changeId)
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
|
-
path: join(artifactWorkspacePath,
|
|
120
|
+
path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'checkpoints', 'checkpoint-1.json')),
|
|
121
121
|
content: renderCheckpoint(changeId, createdAt)
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
|
-
path: join(artifactWorkspacePath,
|
|
124
|
+
path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'evidence', 'unit-tests.md')),
|
|
125
125
|
content: renderUnitTestsEvidence(changeId)
|
|
126
126
|
},
|
|
127
127
|
{
|
|
128
|
-
path: join(artifactWorkspacePath,
|
|
128
|
+
path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'evidence', 'validation-report.md')),
|
|
129
129
|
content: renderValidationReport(changeId)
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
|
-
path: join(artifactWorkspacePath,
|
|
132
|
+
path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'resume-instructions.md')),
|
|
133
133
|
content: renderResumeInstructions(changeId)
|
|
134
134
|
}
|
|
135
135
|
];
|
|
@@ -11,6 +11,30 @@ export declare class ChangeIdValidationError extends Error {
|
|
|
11
11
|
constructor(changeId: string);
|
|
12
12
|
}
|
|
13
13
|
export declare function isUnsafeArtifactPath(path: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Build an artifact-relative path using a caller-supplied project root, so
|
|
16
|
+
* the helper does not need to walk `process.cwd()` to find a session.
|
|
17
|
+
*
|
|
18
|
+
* If a session exists for `projectRoot`, files are stored in:
|
|
19
|
+
* .peaks/<sessionId>/<role>/<number>-<changeId>.md
|
|
20
|
+
*
|
|
21
|
+
* If no session exists, falls back to legacy behavior:
|
|
22
|
+
* .peaks/<changeId>/<segments>
|
|
23
|
+
*
|
|
24
|
+
* Use this from callers that have a workspace or `artifactWorkspacePath` in
|
|
25
|
+
* hand (e.g. CLI subcommands that received `--project`, or test fixtures
|
|
26
|
+
* that created a tmpdir workspace). Legacy callers without an explicit
|
|
27
|
+
* `projectRoot` should continue to use `buildArtifactRelativePath`.
|
|
28
|
+
*
|
|
29
|
+
* @param projectRoot - The project root to use for session lookup and dirPath
|
|
30
|
+
* computation. Must be an absolute path. Falls back to `process.cwd()` if
|
|
31
|
+
* the empty string is passed (defensive only; should not happen via the
|
|
32
|
+
* public API).
|
|
33
|
+
* @param changeId - Used as file description/slug (e.g., "auth-system", "add-user-auth")
|
|
34
|
+
* @param segments - Optional path segments (first segment is typically the role: 'prd', 'rd', 'qa', etc.)
|
|
35
|
+
* @returns Relative path to the artifact file
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildArtifactRelativePathInRoot(projectRoot: string, changeId: string, ...segments: string[]): string;
|
|
14
38
|
/**
|
|
15
39
|
* Build an artifact-relative path using session-based storage.
|
|
16
40
|
*
|
|
@@ -20,6 +44,12 @@ export declare function isUnsafeArtifactPath(path: string): boolean;
|
|
|
20
44
|
* If no session exists, falls back to legacy behavior:
|
|
21
45
|
* .peaks/<changeId>/<segments>
|
|
22
46
|
*
|
|
47
|
+
* This function walks `process.cwd()` to find the project root and reads
|
|
48
|
+
* `.peaks/.session.json` from it. Callers that already have an explicit
|
|
49
|
+
* `projectRoot` (workspace handle, test fixture, or CLI `--project` flag)
|
|
50
|
+
* should prefer `buildArtifactRelativePathInRoot(projectRoot, ...)` to
|
|
51
|
+
* avoid being polluted by the host environment's session binding.
|
|
52
|
+
*
|
|
23
53
|
* @param changeId - Used as file description/slug (e.g., "auth-system", "add-user-auth")
|
|
24
54
|
* @param segments - Optional path segments (first segment is typically the role: 'prd', 'rd', 'qa', etc.)
|
|
25
55
|
* @returns Relative path to the artifact file
|
|
@@ -62,25 +62,37 @@ export function isUnsafeArtifactPath(path) {
|
|
|
62
62
|
return isUnsafePathInput(path);
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
-
* Build an artifact-relative path using
|
|
65
|
+
* Build an artifact-relative path using a caller-supplied project root, so
|
|
66
|
+
* the helper does not need to walk `process.cwd()` to find a session.
|
|
66
67
|
*
|
|
67
|
-
* If a session exists
|
|
68
|
+
* If a session exists for `projectRoot`, files are stored in:
|
|
68
69
|
* .peaks/<sessionId>/<role>/<number>-<changeId>.md
|
|
69
70
|
*
|
|
70
71
|
* If no session exists, falls back to legacy behavior:
|
|
71
72
|
* .peaks/<changeId>/<segments>
|
|
72
73
|
*
|
|
74
|
+
* Use this from callers that have a workspace or `artifactWorkspacePath` in
|
|
75
|
+
* hand (e.g. CLI subcommands that received `--project`, or test fixtures
|
|
76
|
+
* that created a tmpdir workspace). Legacy callers without an explicit
|
|
77
|
+
* `projectRoot` should continue to use `buildArtifactRelativePath`.
|
|
78
|
+
*
|
|
79
|
+
* @param projectRoot - The project root to use for session lookup and dirPath
|
|
80
|
+
* computation. Must be an absolute path. Falls back to `process.cwd()` if
|
|
81
|
+
* the empty string is passed (defensive only; should not happen via the
|
|
82
|
+
* public API).
|
|
73
83
|
* @param changeId - Used as file description/slug (e.g., "auth-system", "add-user-auth")
|
|
74
84
|
* @param segments - Optional path segments (first segment is typically the role: 'prd', 'rd', 'qa', etc.)
|
|
75
85
|
* @returns Relative path to the artifact file
|
|
76
86
|
*/
|
|
77
|
-
export function
|
|
87
|
+
export function buildArtifactRelativePathInRoot(projectRoot, changeId, ...segments) {
|
|
78
88
|
validateChangeIdOrThrow(changeId);
|
|
79
|
-
const
|
|
80
|
-
|
|
89
|
+
const resolvedProjectRoot = projectRoot && projectRoot.length > 0
|
|
90
|
+
? projectRoot
|
|
91
|
+
: (findProjectRoot(process.cwd()) ?? process.cwd());
|
|
92
|
+
const sessionId = getSessionId(resolvedProjectRoot);
|
|
81
93
|
if (sessionId && segments.length > 0 && segments[0]) {
|
|
82
94
|
const role = normalizeForwardSlashes(segments[0]);
|
|
83
|
-
const dirPath = join(
|
|
95
|
+
const dirPath = join(resolvedProjectRoot, '.peaks', sessionId, role);
|
|
84
96
|
if (isUnsafeArtifactPath(role) || isUnsafeArtifactPath(sessionId)) {
|
|
85
97
|
throw new ChangeIdValidationError(changeId);
|
|
86
98
|
}
|
|
@@ -97,6 +109,28 @@ export function buildArtifactRelativePath(changeId, ...segments) {
|
|
|
97
109
|
}
|
|
98
110
|
return normalizeArtifactPath(candidatePath);
|
|
99
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Build an artifact-relative path using session-based storage.
|
|
114
|
+
*
|
|
115
|
+
* If a session exists, files are stored in:
|
|
116
|
+
* .peaks/<sessionId>/<role>/<number>-<changeId>.md
|
|
117
|
+
*
|
|
118
|
+
* If no session exists, falls back to legacy behavior:
|
|
119
|
+
* .peaks/<changeId>/<segments>
|
|
120
|
+
*
|
|
121
|
+
* This function walks `process.cwd()` to find the project root and reads
|
|
122
|
+
* `.peaks/.session.json` from it. Callers that already have an explicit
|
|
123
|
+
* `projectRoot` (workspace handle, test fixture, or CLI `--project` flag)
|
|
124
|
+
* should prefer `buildArtifactRelativePathInRoot(projectRoot, ...)` to
|
|
125
|
+
* avoid being polluted by the host environment's session binding.
|
|
126
|
+
*
|
|
127
|
+
* @param changeId - Used as file description/slug (e.g., "auth-system", "add-user-auth")
|
|
128
|
+
* @param segments - Optional path segments (first segment is typically the role: 'prd', 'rd', 'qa', etc.)
|
|
129
|
+
* @returns Relative path to the artifact file
|
|
130
|
+
*/
|
|
131
|
+
export function buildArtifactRelativePath(changeId, ...segments) {
|
|
132
|
+
return buildArtifactRelativePathInRoot(findProjectRoot(process.cwd()) ?? process.cwd(), changeId, ...segments);
|
|
133
|
+
}
|
|
100
134
|
export function isPathInsideArtifactRoot(path, artifactRoot) {
|
|
101
135
|
if (!path || !artifactRoot)
|
|
102
136
|
return false;
|
|
@@ -3,4 +3,4 @@ export declare const skillsDir: string;
|
|
|
3
3
|
export declare const schemasDir: string;
|
|
4
4
|
export declare const templatesDir: string;
|
|
5
5
|
export declare const requiredSkillNames: readonly ["peaks-solo", "peaks-prd", "peaks-ui", "peaks-rd", "peaks-qa", "peaks-sc", "peaks-txt", "peaks-sop"];
|
|
6
|
-
export declare const requiredSchemaFiles: readonly ["artifact-manifest.schema.json", "context-capsule.schema.json", "approval-record.schema.json", "change-impact.schema.json", "refactor-slice-spec.schema.json", "artifact-retention-report.schema.json", "capability-source.schema.json", "capability-item.schema.json", "capability-availability.schema.json", "recommendation-plan.schema.json", "artifact-workspace.schema.json", "mcp-server.schema.json", "mcp-install-spec.schema.json", "mcp-install-plan.schema.json", "mcp-apply-result.schema.json", "openspec-change-summary.schema.json", "openspec-render-request.schema.json", "openspec-validation-result.schema.json", "doctor-report.schema.json"];
|
|
6
|
+
export declare const requiredSchemaFiles: readonly ["artifact-manifest.schema.json", "context-capsule.schema.json", "approval-record.schema.json", "change-impact.schema.json", "refactor-slice-spec.schema.json", "artifact-retention-report.schema.json", "capability-source.schema.json", "capability-item.schema.json", "capability-availability.schema.json", "recommendation-plan.schema.json", "artifact-workspace.schema.json", "mcp-server.schema.json", "mcp-install-spec.schema.json", "mcp-install-plan.schema.json", "mcp-apply-result.schema.json", "openspec-change-summary.schema.json", "openspec-render-request.schema.json", "openspec-validation-result.schema.json", "doctor-report.schema.json", "library-breaking-changes.schema.json"];
|
package/dist/src/shared/paths.js
CHANGED
|
@@ -45,5 +45,6 @@ export const requiredSchemaFiles = [
|
|
|
45
45
|
'openspec-change-summary.schema.json',
|
|
46
46
|
'openspec-render-request.schema.json',
|
|
47
47
|
'openspec-validation-result.schema.json',
|
|
48
|
-
'doctor-report.schema.json'
|
|
48
|
+
'doctor-report.schema.json',
|
|
49
|
+
'library-breaking-changes.schema.json'
|
|
49
50
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.2.
|
|
1
|
+
export declare const CLI_VERSION = "1.2.9";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.2.
|
|
1
|
+
export const CLI_VERSION = "1.2.9";
|
package/package.json
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"library": "antd",
|
|
4
|
+
"fromMajor": 4,
|
|
5
|
+
"toMajor": 5,
|
|
6
|
+
"breakingChanges": [
|
|
7
|
+
{ "api": "Drawer.width", "replacement": "Drawer.size", "since": "5.0.0" },
|
|
8
|
+
{ "api": "Modal.width (string)", "replacement": "Modal.width (number) or styles.body.width (number)", "since": "5.0.0" },
|
|
9
|
+
{ "api": "Form.create()", "replacement": "App.useForm() (under <App>)", "since": "5.0.0" },
|
|
10
|
+
{ "api": "message.useMessage()", "replacement": "App.useApp().message (under <App>)", "since": "5.0.0" },
|
|
11
|
+
{ "api": "notification.useNotification()", "replacement": "App.useApp().notification (under <App>)", "since": "5.0.0" },
|
|
12
|
+
{ "api": "import 'antd/dist/antd.css'", "replacement": "import 'antd/dist/reset.css'", "since": "5.0.0" },
|
|
13
|
+
{ "api": "<Form.Item {...layout}", "replacement": "<Form layout='vertical' | 'horizontal'> (no per-item layout prop)", "since": "5.0.0" }
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"library": "@antv/g6",
|
|
18
|
+
"fromMajor": 4,
|
|
19
|
+
"toMajor": 5,
|
|
20
|
+
"breakingChanges": [
|
|
21
|
+
{ "api": "new G6.Graph({ mode: 'default' })", "replacement": "new G6.Graph({ type: 'graph' })", "since": "5.0.0" },
|
|
22
|
+
{ "api": "graph.read(data)", "replacement": "graph.setData(data); graph.render()", "since": "5.0.0" },
|
|
23
|
+
{ "api": "graph.changeData(data)", "replacement": "graph.setData(data); graph.render()", "since": "5.0.0" },
|
|
24
|
+
{ "api": "G6.registerBehavior('drag-canvas', { getEvents() { return { ... } } })", "replacement": "G6.registerBehavior({ key: 'drag-canvas', type: 'drag-canvas', getEvents() { return { ... } } })", "since": "5.0.0" }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"library": "@mui/material",
|
|
29
|
+
"fromMajor": 4,
|
|
30
|
+
"toMajor": 5,
|
|
31
|
+
"breakingChanges": [
|
|
32
|
+
{ "api": "import { Grid } from '@mui/material'", "replacement": "import Grid from '@mui/material/Grid2' (Grid renamed Grid2, separate import path)", "since": "5.0.0" },
|
|
33
|
+
{ "api": "createTheme({ unstable_createRheaTheme: true })", "replacement": "createTheme({ cssVariables: true })", "since": "5.0.0" },
|
|
34
|
+
{ "api": "<DialogTitle disableTypography>", "replacement": "<DialogTitle component='div'> (disableTypography removed; use component prop)", "since": "5.0.0" },
|
|
35
|
+
{ "api": "<TextField variant='standard' InputLabelProps={{ shrink: true }}>", "replacement": "<TextField variant='standard' slotProps={{ inputLabel: { shrink: true } }}>", "since": "5.0.0" }
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"library": "react-router-dom",
|
|
40
|
+
"fromMajor": 5,
|
|
41
|
+
"toMajor": 6,
|
|
42
|
+
"breakingChanges": [
|
|
43
|
+
{ "api": "<Switch>", "replacement": "<Routes>", "since": "6.0.0" },
|
|
44
|
+
{ "api": "<Redirect>", "replacement": "<Navigate>", "since": "6.0.0" },
|
|
45
|
+
{ "api": "useHistory()", "replacement": "useNavigate()", "since": "6.0.0" },
|
|
46
|
+
{ "api": "match.params", "replacement": "useParams()", "since": "6.0.0" },
|
|
47
|
+
{ "api": "withRouter(Component)", "replacement": "useNavigate() + useLocation() + useParams() hooks", "since": "6.0.0" },
|
|
48
|
+
{ "api": "<Route component={Component} />", "replacement": "<Route element={<Component />} /> (component prop renamed element)", "since": "6.0.0" },
|
|
49
|
+
{ "api": "<Route path='/:id' exact />", "replacement": "<Route path='/:id' /> (exact prop removed; paths match exactly by default)", "since": "6.0.0" }
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"library": "tailwindcss",
|
|
54
|
+
"fromMajor": 2,
|
|
55
|
+
"toMajor": 3,
|
|
56
|
+
"breakingChanges": [
|
|
57
|
+
{ "api": "tailwind.config.js (CommonJS, no 'type' in package.json)", "replacement": "tailwind.config.cjs (CommonJS) or tailwind.config.ts (ESM)", "since": "3.0.0" },
|
|
58
|
+
{ "api": "content: ['./src/**/*.{html,js}']", "replacement": "content: ['./src/**/*.{html,js,jsx,ts,tsx,vue,svelte}']", "since": "3.0.0" },
|
|
59
|
+
{ "api": "@tailwindcss/typography (default import)", "replacement": "import typography from '@tailwindcss/typography' (named default; config: { plugins: [typography] })", "since": "3.0.0" },
|
|
60
|
+
{ "api": "colors: { 'custom-blue': '#1da1f2' }", "replacement": "colors: { custom: { blue: '#1da1f2' } } (nested under named palette; tailwind.config reads as 'text-custom-blue' = 'text-custom-blue' still works but is a single nesting level removed)", "since": "3.0.0" }
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"library": "@reduxjs/toolkit",
|
|
65
|
+
"fromMajor": 1,
|
|
66
|
+
"toMajor": 2,
|
|
67
|
+
"breakingChanges": [
|
|
68
|
+
{ "api": "import { createAsyncThunk, createReducer } from '@reduxjs/toolkit' (with manual pending/fulfilled/rejected handlers)", "replacement": "import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; createSlice auto-generates pending/fulfilled/rejected matchers", "since": "2.0.0" },
|
|
69
|
+
{ "api": "createSlice({ reducers: { ... }, extraReducers: (builder) => builder.addCase(...) })", "replacement": "createSlice({ reducers, extraReducers: (builder) => builder.addCase(...) }) — unchanged shape; check RTK 2.0 matchers for createAsyncThunk with builder.addMatcher", "since": "2.0.0" },
|
|
70
|
+
{ "api": "configureStore({ reducer, middleware: (gDM) => gDM().concat(logger) })", "replacement": "configureStore({ reducer, middleware: (gDM) => gDM({ serializableCheck: false }).concat(logger) })", "since": "2.0.0" }
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"library": "next",
|
|
75
|
+
"fromMajor": 14,
|
|
76
|
+
"toMajor": 15,
|
|
77
|
+
"breakingChanges": [
|
|
78
|
+
{ "api": "params (in pages/)", "replacement": "await params (params is now a Promise; await it before reading properties)", "since": "15.0.0" },
|
|
79
|
+
{ "api": "searchParams (in pages/)", "replacement": "await searchParams (searchParams is now a Promise)", "since": "15.0.0" },
|
|
80
|
+
{ "api": "cookies().get('name') (synchronous)", "replacement": "await cookies() (cookies() now returns a Promise; await before calling .get)", "since": "15.0.0" },
|
|
81
|
+
{ "api": "headers().get('name') (synchronous)", "replacement": "await headers() (headers() now returns a Promise)", "since": "15.0.0" },
|
|
82
|
+
{ "api": "fetch(url, { cache: 'force-cache' })", "replacement": "fetch(url, { cache: 'force-cache' }) + export const dynamic = 'force-static' OR use a Server Action", "since": "15.0.0" }
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"library": "react",
|
|
87
|
+
"fromMajor": 17,
|
|
88
|
+
"toMajor": 18,
|
|
89
|
+
"breakingChanges": [
|
|
90
|
+
{ "api": "ReactDOM.render(<App />, container)", "replacement": "createRoot(container).render(<App />)", "since": "18.0.0" },
|
|
91
|
+
{ "api": "ReactDOM.hydrate(<App />, container)", "replacement": "hydrateRoot(container, <App />)", "since": "18.0.0" },
|
|
92
|
+
{ "api": "import { render } from 'react-dom'", "replacement": "import { createRoot } from 'react-dom/client' (separate entry point)", "since": "18.0.0" },
|
|
93
|
+
{ "api": "ReactDOM.unmountComponentAtNode(container)", "replacement": "root.unmount() (returned from createRoot)", "since": "18.0.0" },
|
|
94
|
+
{ "api": "defaultProps on function components", "replacement": "default parameters (defaultProps will be removed in a future major; use destructuring defaults: function Foo({ x = 1 } = {}) {})", "since": "18.3.0" }
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"library": "typescript",
|
|
99
|
+
"fromMajor": 4,
|
|
100
|
+
"toMajor": 5,
|
|
101
|
+
"breakingChanges": [
|
|
102
|
+
{ "api": "import type { ... } from 'mod' (mixed type + value import without explicit type modifier)", "replacement": "Add `type` modifier to type-only imports explicitly (TS 5.0 enforces with verbatimModuleSyntax)", "since": "5.0.0" },
|
|
103
|
+
{ "api": "namespace X { function f(): X.Type }", "replacement": "Use `import type` or `export type` from namespaces (TS 5.0 changes how namespaces interop with modules)", "since": "5.0.0" },
|
|
104
|
+
{ "api": "experimentalDecorators metadata emit", "replacement": "Use the new TC39 stage-3 decorators; experimentalDecorators flag will be removed", "since": "5.0.0" },
|
|
105
|
+
{ "api": "enum X { A, B = A * 2 } (numeric enum with computed member before non-computed)", "replacement": "Reorder enum members: place all non-computed members before computed ones", "since": "5.0.0" }
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"library": "prisma",
|
|
110
|
+
"fromMajor": 4,
|
|
111
|
+
"toMajor": 5,
|
|
112
|
+
"breakingChanges": [
|
|
113
|
+
{ "api": "previewFeatures = ['xxx'] (in schema.prisma)", "replacement": "Move preview features to a separate block; some promoted to stable without flag", "since": "5.0.0" },
|
|
114
|
+
{ "api": "prisma.user.findMany({ include: { posts: true } }) (implicit include via relation)", "replacement": "Use `with: { posts: true }` (include renamed to with)", "since": "5.0.0" },
|
|
115
|
+
{ "api": "prisma.user.findMany({ relationJumps: { ... } })", "replacement": "Use `include` with `_count: { select: ... }` or nested select on relation fields", "since": "5.0.0" },
|
|
116
|
+
{ "api": "@prisma/client v4 ESM/CJS interop (require vs import)", "replacement": "Pure ESM in v5; use `import` syntax only; dynamic require() of generated client throws", "since": "5.0.0" }
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"library": "eslint",
|
|
121
|
+
"fromMajor": 8,
|
|
122
|
+
"toMajor": 9,
|
|
123
|
+
"breakingChanges": [
|
|
124
|
+
{ "api": ".eslintrc.json (legacy config)", "replacement": "eslint.config.js (flat config; legacy mode requires @eslint/eslintrc)", "since": "9.0.0" },
|
|
125
|
+
{ "api": "module.exports = { rules: { ... } } (CommonJS rules)", "replacement": "export default [{ rules: { ... } }] (ESM flat config; rules must be inside an array entry)", "since": "9.0.0" },
|
|
126
|
+
{ "api": "context.getSourceCode()", "replacement": "context.sourceCode (renamed; ESLint 9 deprecates getSourceCode)", "since": "9.0.0" },
|
|
127
|
+
{ "api": "rule.create({ visitor }) without meta.schema or meta.type", "replacement": "rule.create({ meta: { schema: [...], type: 'problem'|'suggestion'|'layout' }, visitor }) (ESLint 9 requires meta)", "since": "9.0.0" }
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"library": "react-hook-form",
|
|
132
|
+
"fromMajor": 6,
|
|
133
|
+
"toMajor": 7,
|
|
134
|
+
"breakingChanges": [
|
|
135
|
+
{ "api": "useForm({ defaultValues: { ... } }) (defaultValues is now required when using field arrays)", "replacement": "Always provide defaultValues; v7 removed the implicit empty default", "since": "7.0.0" },
|
|
136
|
+
{ "api": "Controller({ name, control, render: ({ field }) => ... }) (Controller is now a generic — must pass field type)", "replacement": "Controller<FieldValues, TFieldPath>({ name: 'field' as TFieldPath, ... }) (cast path to TFieldPath)", "since": "7.0.0" },
|
|
137
|
+
{ "api": "useFormContext() (no generic)", "replacement": "useFormContext<FieldValues>() (generic required for type inference)", "since": "7.0.0" },
|
|
138
|
+
{ "api": "formState.errors.foo (loose type)", "replacement": "formState.errors.foo (typed as FieldError | undefined; use ?.message for safe access)", "since": "7.0.0" }
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Peaks Library Breaking Changes",
|
|
4
|
+
"description": "Manually-curated table of known breaking changes per (library, fromMajor, toMajor). Used by peaks-rd/qa to warn the LLM about deprecated APIs when writing code for projects on a newer major than the LLM's training data. Read by the LLM via the Read tool; the data file at library-breaking-changes.data.json holds the actual rows. Doctor validates parse-ability of this schema file.",
|
|
5
|
+
"type": "array",
|
|
6
|
+
"items": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"required": ["library", "fromMajor", "toMajor", "breakingChanges"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"library": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"description": "npm package name, e.g. 'antd', '@mui/material', 'react-router-dom'."
|
|
14
|
+
},
|
|
15
|
+
"fromMajor": {
|
|
16
|
+
"type": "integer",
|
|
17
|
+
"minimum": 0,
|
|
18
|
+
"description": "The major version the breaking change applies FROM. LLM cross-references (project major === toMajor) to fire the warning."
|
|
19
|
+
},
|
|
20
|
+
"toMajor": {
|
|
21
|
+
"type": "integer",
|
|
22
|
+
"minimum": 1,
|
|
23
|
+
"description": "The major version the breaking change applies TO (i.e. the new major where the API changed)."
|
|
24
|
+
},
|
|
25
|
+
"breakingChanges": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"items": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"required": ["api", "replacement", "since"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"api": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"minLength": 1,
|
|
34
|
+
"description": "The deprecated/removed API call, e.g. 'Drawer.width'. Should be a substring that the LLM can grep for in the diff's import + JSX blocks."
|
|
35
|
+
},
|
|
36
|
+
"replacement": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "The v2 API to use instead, e.g. 'Drawer.size'. If empty, the API is removed entirely (no v2 equivalent)."
|
|
39
|
+
},
|
|
40
|
+
"since": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"pattern": "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$",
|
|
43
|
+
"description": "The semver string where this breaking change took effect, e.g. '5.0.0'. Matches the toMajor."
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/skills/peaks-qa/SKILL.md
CHANGED
|
@@ -168,6 +168,17 @@ peaks openspec validate <change-id> --project <repo> --prefer-external --json
|
|
|
168
168
|
|
|
169
169
|
# 4. generate test cases — MANDATORY, write to .peaks/<session-id>/qa/test-cases/<request-id>.md
|
|
170
170
|
# categories: unit, integration, UI regression (frontend only)
|
|
171
|
+
#
|
|
172
|
+
# Optimization (slice 004): peaks-rd's parallel fan-out now includes a 4th
|
|
173
|
+
# sub-agent (`qa-test-cases-writer`) that pre-drafts this file at the
|
|
174
|
+
# end of RD implementation. If `.peaks/<sid>/qa/test-cases/<rid>.md`
|
|
175
|
+
# already exists when QA's main loop reaches this step, **QA does NOT
|
|
176
|
+
# re-draft it** — it just verifies the file is present and the
|
|
177
|
+
# per-criterion `ts` snippets are syntactically valid, then proceeds
|
|
178
|
+
# to step 5 (EXECUTE). The wall-clock win: QA's first action is
|
|
179
|
+
# "execute pre-drafted test plan" instead of "draft + execute".
|
|
180
|
+
# Fallback: if the file is missing (sub-agent failed / degraded to
|
|
181
|
+
# inline), QA drafts it inline as before.
|
|
171
182
|
|
|
172
183
|
# 5. EXECUTE tests against the actual implementation — Peaks-Cli Gate A2
|
|
173
184
|
# Run the project test command. Record output. Tests on paper are worthless.
|
|
@@ -458,6 +469,7 @@ QA cannot pass a change until the report contains evidence for every applicable
|
|
|
458
469
|
5. **Browser-error feedback loop** — if Playwright MCP observation surfaces a page error, console exception, broken network request, hydration/render failure, or visible regression, return the work to RD/development with the exact evidence. Do not pass QA until the fixed build is retested in the browser.
|
|
459
470
|
6. **Security check** — run security review for the changed surface and dependency/config changes. Record findings, fixes, and unresolved risks.
|
|
460
471
|
7. **Performance check** — run the project’s available performance check, build-size check, Lighthouse-equivalent check, or browser performance inspection appropriate to the change. Record baseline/after numbers when available.
|
|
472
|
+
8. **Library version regressions** — when the slice's diff contains an `import` statement that matches a `breakingChanges[].api` entry in `schemas/library-breaking-changes.data.json` for the library's installed major (read from the RD-handoff's `## Library versions` section), record a `## Library version regressions` block in `qa/test-reports/<rid>.md` listing each hit. Per row: `<api>` → `<replacement>`, source `schemas/library-breaking-changes.data.json`. Treat each unreplaced hit as a **return-to-rd** reason — the LLM should fix the diff before re-handoff. (This is the QA-side counterpart of the RD `## Library version awareness` preflight; the two together form a check-and-verify pair.)
|
|
461
473
|
8. **Validation report** — write or link a report containing scope, environment, commands, sanitized browser evidence, security/performance results, pass/fail summary, residual risks, and next action.
|
|
462
474
|
9. **Acceptance coverage** — every PRD acceptance item has at least one linked QA test case (`peaks scan acceptance-coverage --rid <rid>`). **→ verified by Peaks-Cli Gate E**. This is the deterministic check that no requirement was forgotten between PRD and verdict.
|
|
463
475
|
10. **QA artifact lint** — the QA request artifact body has no unfilled placeholders (`peaks request lint <rid> --role qa`). **→ verified by Peaks-Cli Gate F**. Catches the "wrote the template, forgot to fill it" failure mode that template-style reports invite.
|