peaks-cli 1.1.1 → 1.1.2
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/bin/peaks.js +0 -0
- package/dist/src/cli/commands/request-commands.js +13 -1
- package/dist/src/services/artifacts/artifact-lint-service.js +20 -1
- package/dist/src/services/artifacts/artifact-prerequisites.js +34 -4
- package/dist/src/services/scan/type-sanity-service.js +11 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
package/bin/peaks.js
CHANGED
|
File without changes
|
|
@@ -126,6 +126,18 @@ export function registerRequestCommands(program, io) {
|
|
|
126
126
|
try {
|
|
127
127
|
const role = options.role;
|
|
128
128
|
const newState = parseStateForRole(role, options.state);
|
|
129
|
+
// Resolve the artifact's real session up front. Falling back to a literal
|
|
130
|
+
// 'default' (the previous behavior) points the bypass counter at a
|
|
131
|
+
// non-existent .peaks/default/ dir and crashes with ENOENT, so when
|
|
132
|
+
// --session-id is omitted we look the artifact up to find its session.
|
|
133
|
+
let resolvedSessionId = options.sessionId;
|
|
134
|
+
if (resolvedSessionId === undefined) {
|
|
135
|
+
const { showRequestArtifact: showForSession } = await import('../../services/artifacts/request-artifact-service.js');
|
|
136
|
+
const located = await showForSession({ projectRoot: options.project, role, requestId });
|
|
137
|
+
if (located !== null) {
|
|
138
|
+
resolvedSessionId = located.sessionId;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
129
141
|
if (options.allowIncomplete === true && (options.reason === undefined || options.reason.trim().length === 0)) {
|
|
130
142
|
printResult(io, fail('request.transition', 'BYPASS_REASON_REQUIRED', '--allow-incomplete requires --reason explaining why prerequisites are skipped', { role, requestId }, ['Add --reason "<short justification>" or remove --allow-incomplete and produce the missing artifacts']), options.json);
|
|
131
143
|
process.exitCode = 1;
|
|
@@ -142,7 +154,7 @@ export function registerRequestCommands(program, io) {
|
|
|
142
154
|
return;
|
|
143
155
|
}
|
|
144
156
|
// Check bypass count
|
|
145
|
-
const sessionRoot = (await import('node:path')).join(options.project, '.peaks',
|
|
157
|
+
const sessionRoot = (await import('node:path')).join(options.project, '.peaks', resolvedSessionId ?? 'default');
|
|
146
158
|
if (isBypassLimitReached(sessionRoot)) {
|
|
147
159
|
printResult(io, fail('request.transition', 'BYPASS_LIMIT_REACHED', `--allow-incomplete limit reached (${MAX_BYPASSES_PER_SESSION} per session)`, { role, requestId, limit: MAX_BYPASSES_PER_SESSION }, ['Produce the missing artifacts instead of bypassing.']), options.json);
|
|
148
160
|
process.exitCode = 1;
|
|
@@ -35,6 +35,15 @@ const ALLOWLIST_PATTERNS = [
|
|
|
35
35
|
function isAllowlisted(line) {
|
|
36
36
|
return ALLOWLIST_PATTERNS.some((pattern) => pattern.test(line));
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Remove inline code spans (`...`) before applying placeholder rules. Content
|
|
40
|
+
* inside backticks is literal example text — e.g. a documented command syntax
|
|
41
|
+
* `peaks sop init <id>` — not an unfilled prose placeholder. Lint checks prose,
|
|
42
|
+
* not code, so a `<...>` token only counts when it appears outside code spans.
|
|
43
|
+
*/
|
|
44
|
+
function stripInlineCode(line) {
|
|
45
|
+
return line.replace(/`[^`]*`/g, '');
|
|
46
|
+
}
|
|
38
47
|
export async function lintRequestArtifact(options) {
|
|
39
48
|
const showOptions = {
|
|
40
49
|
projectRoot: options.projectRoot,
|
|
@@ -50,14 +59,24 @@ export async function lintRequestArtifact(options) {
|
|
|
50
59
|
}
|
|
51
60
|
const lines = artifact.content.split(/\r?\n/);
|
|
52
61
|
const findings = [];
|
|
62
|
+
let insideFence = false;
|
|
53
63
|
for (let index = 0; index < lines.length; index += 1) {
|
|
54
64
|
const rawLine = lines[index];
|
|
55
65
|
if (rawLine === undefined)
|
|
56
66
|
continue;
|
|
67
|
+
// Fenced code blocks hold literal examples, not prose to fill; skip their
|
|
68
|
+
// contents entirely (the fence delimiters themselves toggle the state).
|
|
69
|
+
if (/^\s*```/.test(rawLine)) {
|
|
70
|
+
insideFence = !insideFence;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (insideFence)
|
|
74
|
+
continue;
|
|
57
75
|
if (isAllowlisted(rawLine))
|
|
58
76
|
continue;
|
|
77
|
+
const testLine = stripInlineCode(rawLine);
|
|
59
78
|
for (const rule of RULES) {
|
|
60
|
-
if (rule.test(
|
|
79
|
+
if (rule.test(testLine)) {
|
|
61
80
|
findings.push({
|
|
62
81
|
line: index + 1,
|
|
63
82
|
text: rawLine.trim(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
1
|
+
import { join, dirname, basename } from 'node:path';
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
3
|
import { pathExists } from '../../shared/fs.js';
|
|
4
4
|
export const VALID_REQUEST_TYPES = [
|
|
5
5
|
'feature',
|
|
@@ -111,6 +111,36 @@ export function getPrerequisitesFor(role, newState, requestType = DEFAULT_REQUES
|
|
|
111
111
|
function resolvePrerequisitePath(prerequisite, requestId) {
|
|
112
112
|
return prerequisite.relativePath.replace('<rid>', requestId);
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a prerequisite to an on-disk path, tolerating the numbered filename
|
|
116
|
+
* prefix that `request init` writes (e.g. `001-<rid>.md`). When the prerequisite
|
|
117
|
+
* path contains `<rid>`, we accept either the legacy bare `<rid>.md` form or any
|
|
118
|
+
* `NNN-<rid>.md` numbered form — mirroring the matcher in request-artifact-service.
|
|
119
|
+
* Returns the matched absolute path, or null when nothing matches.
|
|
120
|
+
*/
|
|
121
|
+
async function resolvePrerequisiteAbsolutePath(sessionRoot, prerequisite, requestId) {
|
|
122
|
+
const relative = resolvePrerequisitePath(prerequisite, requestId);
|
|
123
|
+
const exact = join(sessionRoot, relative);
|
|
124
|
+
if (await pathExists(exact)) {
|
|
125
|
+
return exact;
|
|
126
|
+
}
|
|
127
|
+
// Only `<rid>`-templated prerequisites can carry a numbered prefix; fixed paths
|
|
128
|
+
// (e.g. rd/tech-doc.md) are matched exactly above.
|
|
129
|
+
if (!prerequisite.relativePath.includes('<rid>')) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const dir = dirname(exact);
|
|
133
|
+
const targetSuffix = `-${basename(exact)}`;
|
|
134
|
+
let entries;
|
|
135
|
+
try {
|
|
136
|
+
entries = await readdir(dir);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const match = entries.find((name) => /^\d+-/.test(name) && name.endsWith(targetSuffix));
|
|
142
|
+
return match ? join(dir, match) : null;
|
|
143
|
+
}
|
|
114
144
|
export async function checkPrerequisites(options) {
|
|
115
145
|
const requirements = getPrerequisitesFor(options.role, options.newState, options.requestType);
|
|
116
146
|
if (requirements.length === 0) {
|
|
@@ -120,8 +150,8 @@ export async function checkPrerequisites(options) {
|
|
|
120
150
|
const missing = [];
|
|
121
151
|
for (const prerequisite of requirements) {
|
|
122
152
|
const relative = resolvePrerequisitePath(prerequisite, options.requestId);
|
|
123
|
-
const absolute =
|
|
124
|
-
if (
|
|
153
|
+
const absolute = await resolvePrerequisiteAbsolutePath(sessionRoot, prerequisite, options.requestId);
|
|
154
|
+
if (absolute === null) {
|
|
125
155
|
missing.push({ path: relative, description: prerequisite.description });
|
|
126
156
|
continue;
|
|
127
157
|
}
|
|
@@ -25,6 +25,16 @@ function classifyFile(filePath) {
|
|
|
25
25
|
return 'source';
|
|
26
26
|
return 'unknown';
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Peaks' own artifact workspace. Changes here (PRD/RD/QA markdown, session
|
|
30
|
+
* state) are never the "code change" a request type describes, so they must be
|
|
31
|
+
* excluded from the diff — otherwise a PRD-planning-phase handoff that only
|
|
32
|
+
* wrote `.peaks/**` markdown would be misclassified as a docs change.
|
|
33
|
+
*/
|
|
34
|
+
function isArtifactWorkspaceFile(filePath) {
|
|
35
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
36
|
+
return normalized === '.peaks' || normalized.startsWith('.peaks/');
|
|
37
|
+
}
|
|
28
38
|
function tryGitDiffFiles(projectRoot, baseRef) {
|
|
29
39
|
try {
|
|
30
40
|
// Combine: tracked changes vs baseRef + untracked files. Use porcelain status for untracked too.
|
|
@@ -32,7 +42,7 @@ function tryGitDiffFiles(projectRoot, baseRef) {
|
|
|
32
42
|
const tracked = trackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
33
43
|
const untrackedRaw = execFileSync('git', ['-C', projectRoot, 'ls-files', '--others', '--exclude-standard'], { encoding: 'utf8' });
|
|
34
44
|
const untracked = untrackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
35
|
-
const merged = Array.from(new Set([...tracked, ...untracked]));
|
|
45
|
+
const merged = Array.from(new Set([...tracked, ...untracked])).filter((file) => !isArtifactWorkspaceFile(file));
|
|
36
46
|
return { ok: true, files: merged };
|
|
37
47
|
}
|
|
38
48
|
catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.1.
|
|
1
|
+
export declare const CLI_VERSION = "1.1.2";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.1.
|
|
1
|
+
export const CLI_VERSION = "1.1.2";
|