@planu/cli 4.10.3 → 4.10.4

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 CHANGED
@@ -1,3 +1,14 @@
1
+ ## [4.10.4] - 2026-07-06
2
+
3
+ ### Bug Fixes
4
+ - fix: resolve Supabase testimonials and Planu bug backlog
5
+
6
+ ### Chores
7
+ - chore(deps): update tsc-alias
8
+ - chore(planu): clear released pending specs
9
+ - chore(format): apply prettier baseline
10
+
11
+
1
12
  ## [4.10.3] - 2026-07-02
2
13
 
3
14
  ### Bug Fixes
@@ -2,7 +2,6 @@
2
2
  // Output: ~30-50 lines. No generic sections, no OWASP, no STRIDE, no resilience criteria.
3
3
  import { parseBddScenarios, renderBddScenariosYaml, convertCheckboxToBdd } from './bdd-parser.js';
4
4
  import { deriveModelBudget } from './model-budget-deriver.js';
5
- import { renderHistoryYaml } from '../spec-versioning/render-history.js';
6
5
  import { renderGroundingFrontmatter, renderTechnicalReferenceGroundingFrontmatter, } from '../spec-grounding/contract.js';
7
6
  /** Strip redundant "SPEC-XXX — " or "SPEC-XXX: " prefix from spec titles.
8
7
  * Titles must not repeat the ID since it is already stored in the `id` field. */
@@ -60,15 +59,11 @@ export function generateLeanSpecContent(input) {
60
59
  else {
61
60
  acLines = buildCheckboxLines(description, extraCriteria, input.criteriaOverride);
62
61
  }
63
- // SPEC-717: render empty version history for new specs
64
- const historyYaml = renderHistoryYaml([]);
65
62
  const lines = [
66
63
  ...frontmatterBase,
67
64
  ...acLines,
68
65
  'history:',
69
66
  ...history.map((h) => ` - date: ${h.date}\n event: ${h.event}`),
70
- // SPEC-717: version history (empty for new specs)
71
- historyYaml,
72
67
  '---',
73
68
  '',
74
69
  description.trim(),
@@ -62,11 +62,12 @@ export function generateMigrations(tables, indexes, rlsPolicies, engine) {
62
62
  });
63
63
  }
64
64
  for (const policy of rlsPolicies) {
65
+ const rolesClause = policy.roles?.length ? ` TO ${policy.roles.join(', ')}` : '';
65
66
  const usingClause = `USING (${policy.using})`;
66
67
  const withCheckClause = policy.withCheck ? ` WITH CHECK (${policy.withCheck})` : '';
67
68
  migrations.push({
68
69
  order: order++,
69
- sql: `CREATE POLICY ${policy.name} ON ${policy.table} FOR ${policy.operation} ${usingClause}${withCheckClause};`,
70
+ sql: `CREATE POLICY ${policy.name} ON ${policy.table} FOR ${policy.operation}${rolesClause} ${usingClause}${withCheckClause};`,
70
71
  reversible: true,
71
72
  reverseSql: `DROP POLICY IF EXISTS ${policy.name} ON ${policy.table};`,
72
73
  dataLoss: false,
@@ -170,33 +170,41 @@ export function generateIndexes(tables, _engine) {
170
170
  export function generateRLSPolicies(tables) {
171
171
  const policies = [];
172
172
  for (const table of tables) {
173
+ const ownerCol = table.columns.find((c) => c.name === 'user_id' || c.name === 'owner_id' || c.name === 'created_by');
174
+ const ownerPredicate = ownerCol ? `(select auth.uid()) = ${ownerCol.name}` : 'true';
173
175
  policies.push({
174
176
  name: `${table.name}_select_authenticated`,
175
177
  table: table.name,
176
178
  operation: 'SELECT',
177
- using: "auth.role() = 'authenticated'",
178
- justification: 'Only authenticated users can read records',
179
+ roles: ['authenticated'],
180
+ using: ownerPredicate,
181
+ justification: ownerCol
182
+ ? 'Authenticated users can only read records they own'
183
+ : 'Only authenticated users can read records',
179
184
  });
180
- const ownerCol = table.columns.find((c) => c.name === 'user_id' || c.name === 'owner_id' || c.name === 'created_by');
181
185
  if (ownerCol) {
182
186
  policies.push({
183
187
  name: `${table.name}_insert_owner`,
184
188
  table: table.name,
185
189
  operation: 'INSERT',
190
+ roles: ['authenticated'],
186
191
  using: 'true',
187
- withCheck: `auth.uid() = ${ownerCol.name}`,
192
+ withCheck: ownerPredicate,
188
193
  justification: 'Users can only insert records they own',
189
194
  }, {
190
195
  name: `${table.name}_update_owner`,
191
196
  table: table.name,
192
197
  operation: 'UPDATE',
193
- using: `auth.uid() = ${ownerCol.name}`,
198
+ roles: ['authenticated'],
199
+ using: ownerPredicate,
200
+ withCheck: ownerPredicate,
194
201
  justification: 'Users can only update their own records',
195
202
  }, {
196
203
  name: `${table.name}_delete_owner`,
197
204
  table: table.name,
198
205
  operation: 'DELETE',
199
- using: `auth.uid() = ${ownerCol.name}`,
206
+ roles: ['authenticated'],
207
+ using: ownerPredicate,
200
208
  justification: 'Users can only delete their own records',
201
209
  });
202
210
  }
@@ -205,7 +213,8 @@ export function generateRLSPolicies(tables) {
205
213
  name: `${table.name}_all_authenticated`,
206
214
  table: table.name,
207
215
  operation: 'ALL',
208
- using: "auth.role() = 'authenticated'",
216
+ roles: ['authenticated'],
217
+ using: 'true',
209
218
  justification: 'All CRUD operations require authentication',
210
219
  });
211
220
  }
@@ -181,6 +181,8 @@ export function buildInitProjectResult(input) {
181
181
  healthScore: input.healthReport.score,
182
182
  healthSummary: input.healthReport.summary,
183
183
  items: input.healthReport.items,
184
+ conventionScanStatus: input.conventionScanStatus ?? 'skipped',
185
+ conventionWarning: buildConventionHealthWarning(input),
184
186
  }
185
187
  : null,
186
188
  feedbackHint: 'Found a bug or have a suggestion? Just say "I want to report a bug" or "I have a suggestion" and Planu will track it for you.',
@@ -256,4 +258,15 @@ export function buildInitProjectResult(input) {
256
258
  },
257
259
  };
258
260
  }
261
+ function buildConventionHealthWarning(input) {
262
+ const status = input.conventionScanStatus ?? 'skipped';
263
+ if (status === 'failed') {
264
+ return 'Convention scan failed; project health may not include all convention violations.';
265
+ }
266
+ const conventionItems = input.healthReport?.items.filter((item) => /convention|rule/i.test(item.category) || /convention|rule/i.test(item.message)) ?? [];
267
+ if (conventionItems.length === 0) {
268
+ return null;
269
+ }
270
+ return `${String(conventionItems.length)} convention-related health warning(s) detected.`;
271
+ }
259
272
  //# sourceMappingURL=result-builder.js.map
@@ -13,6 +13,9 @@ function formatTestimonial(t) {
13
13
  const github = t.authorGithub ? ` (@${t.authorGithub})` : '';
14
14
  return `- "${t.quote}" — ${t.authorName}${github}, ${t.authorRole}${featured} (${t.rating}/5, ${t.locale})`;
15
15
  }
16
+ function nonBlank(value) {
17
+ return typeof value === 'string' && value.trim().length > 0;
18
+ }
16
19
  export async function handleApproveTestimonial(input) {
17
20
  const config = await getSupabaseConfig(input.projectPath);
18
21
  if (!config) {
@@ -26,23 +29,21 @@ export async function handleApproveTestimonial(input) {
26
29
  isError: true,
27
30
  };
28
31
  }
29
- const hasFeedbackId = Boolean(input.feedbackId);
30
- const hasManualData = Boolean(input.authorName) && Boolean(input.quote);
31
- if (!hasFeedbackId && !hasManualData) {
32
+ if (!nonBlank(input.authorName) || !nonBlank(input.quote)) {
32
33
  return {
33
34
  content: [
34
35
  {
35
36
  type: 'text',
36
- text: 'Validation failed: either feedbackId or both authorName and quote are required.',
37
+ text: 'Validation failed: authorName and quote are required before approving a testimonial. feedbackId-only approval is not supported yet.',
37
38
  },
38
39
  ],
39
40
  isError: true,
40
41
  };
41
42
  }
42
43
  const payload = {
43
- author_name: input.authorName ?? '',
44
- author_role: input.authorRole ?? '',
45
- quote: input.quote ?? '',
44
+ author_name: input.authorName.trim(),
45
+ author_role: input.authorRole?.trim() ?? '',
46
+ quote: input.quote.trim(),
46
47
  rating: input.rating ?? 5,
47
48
  source: input.source ?? 'manual',
48
49
  approved: true,
@@ -50,19 +51,19 @@ export async function handleApproveTestimonial(input) {
50
51
  locale: input.locale ?? 'en',
51
52
  };
52
53
  if (input.authorAvatarUrl !== undefined) {
53
- payload.author_avatar_url = input.authorAvatarUrl;
54
+ payload.author_avatar_url = input.authorAvatarUrl.trim();
54
55
  }
55
56
  if (input.authorGithub !== undefined) {
56
- payload.author_github = input.authorGithub;
57
+ payload.author_github = input.authorGithub.trim();
57
58
  }
58
59
  if (input.feedbackId !== undefined) {
59
- payload.feedback_id = input.feedbackId;
60
+ payload.feedback_id = input.feedbackId.trim();
60
61
  }
61
62
  if (input.projectType !== undefined) {
62
- payload.project_type = input.projectType;
63
+ payload.project_type = input.projectType.trim();
63
64
  }
64
65
  if (input.companySize !== undefined) {
65
- payload.company_size = input.companySize;
66
+ payload.company_size = input.companySize.trim();
66
67
  }
67
68
  const response = await fetch(TESTIMONIALS_URL(config.projectUrl), {
68
69
  method: 'POST',
@@ -87,7 +87,7 @@ export declare function checkSpecReviewGate(specId: string, projectId: string, _
87
87
  * When force=true, gates are not blocking but failing items are recorded in
88
88
  * the project's force-bypass-log.json for traceability (guard: done ≠ implemented).
89
89
  */
90
- export declare function checkDoneGates(spec: Spec, specId: string, projectId: string, projectPath: string | undefined, force: boolean | undefined): Promise<DoneGateResult>;
90
+ export declare function checkDoneGates(spec: Spec, specId: string, projectId: string, projectPath: string | undefined, force: boolean | undefined, forceReason?: string): Promise<DoneGateResult>;
91
91
  export interface ComplianceGateResult {
92
92
  skipped: boolean;
93
93
  score: number | null;
@@ -240,7 +240,7 @@ function forceDoneBypassLogPath(projectId) {
240
240
  * Writes an audit record when force=true bypasses DoD gates on 'done' transition.
241
241
  * Never throws — audit failures must not block transitions.
242
242
  */
243
- async function recordForceDoneBypass(specId, projectId, failingItems, warningItems) {
243
+ async function recordForceDoneBypass(specId, projectId, reason, failingItems, warningItems) {
244
244
  try {
245
245
  const logPath = forceDoneBypassLogPath(projectId);
246
246
  const entries = await readJson(logPath, []);
@@ -250,6 +250,7 @@ async function recordForceDoneBypass(specId, projectId, failingItems, warningIte
250
250
  action: 'force_done_bypass',
251
251
  specId,
252
252
  projectId,
253
+ reason,
253
254
  failingItems,
254
255
  warningItems,
255
256
  });
@@ -448,7 +449,7 @@ function specReviewGateError(args) {
448
449
  * When force=true, gates are not blocking but failing items are recorded in
449
450
  * the project's force-bypass-log.json for traceability (guard: done ≠ implemented).
450
451
  */
451
- export async function checkDoneGates(spec, specId, projectId, projectPath, force) {
452
+ export async function checkDoneGates(spec, specId, projectId, projectPath, force, forceReason = 'No force reason provided') {
452
453
  if (!force) {
453
454
  const dodError = await checkDodGate(spec, specId, projectId, projectPath, false);
454
455
  if (dodError) {
@@ -472,7 +473,7 @@ export async function checkDoneGates(spec, specId, projectId, projectPath, force
472
473
  .map((i) => i.description);
473
474
  if (failingItems.length > 0) {
474
475
  // Record bypass in audit log (fire-and-forget)
475
- void recordForceDoneBypass(specId, projectId, failingItems, warningItems);
476
+ void recordForceDoneBypass(specId, projectId, forceReason, failingItems, warningItems);
476
477
  forcedBypassWarning =
477
478
  `⚠️ force bypass: ${String(failingItems.length)} DoD item(s) were failing ` +
478
479
  `(${failingItems.slice(0, 2).join('; ')}${failingItems.length > 2 ? '…' : ''}). ` +
@@ -481,7 +481,7 @@ export async function handleUpdateStatus(params, server) {
481
481
  : Promise.resolve(null),
482
482
  // Done gates: only relevant for 'done'
483
483
  newStatus === 'done'
484
- ? checkDoneGates(spec, specId, projectId, effectiveGatePath, params.force)
484
+ ? checkDoneGates(spec, specId, projectId, effectiveGatePath, params.force, params.forceStatusReason ?? params.reason ?? 'No force reason provided')
485
485
  : Promise.resolve(null),
486
486
  ]);
487
487
  // Process code reality result
@@ -76,30 +76,7 @@ export async function handleValidate(args, server) {
76
76
  const implementationQualityScore = calcQualityScore(result.qualityIssues);
77
77
  const auditedFiles = [...new Set(result.qualityIssues.map((i) => i.file))];
78
78
  // 6. SPEC-190: Convention compliance check
79
- let conventionViolations = [];
80
- let regressionDetected = false;
81
- try {
82
- const rules = parseConventions(projectPath);
83
- if (rules.length > 0) {
84
- conventionViolations = scanConventions(projectPath, rules);
85
- // Check regression against baseline
86
- const comparison = await compareWithBaseline(projectId, {
87
- rules,
88
- violations: conventionViolations,
89
- summary: {
90
- totalRules: rules.length,
91
- totalViolations: conventionViolations.length,
92
- bySeverity: {},
93
- byCategory: {},
94
- },
95
- scannedAt: new Date().toISOString(),
96
- });
97
- regressionDetected = comparison.regressionDetected;
98
- }
99
- }
100
- catch {
101
- /* best-effort — don't fail validation */
102
- }
79
+ const { conventionViolations, regressionDetected } = await scanProjectConventions(projectId, projectPath);
103
80
  // 7. Lint check (best-effort — non-blocking)
104
81
  const lintCheck = runLintCheck(projectPath, knowledge.lintCommand ?? null);
105
82
  const assuranceGates = runAssuranceGates(projectPath);
@@ -123,6 +100,14 @@ export async function handleValidate(args, server) {
123
100
  conventionRegression: regressionDetected,
124
101
  minimalityReport: minimalityReport ?? undefined,
125
102
  });
103
+ const qualityGateSummary = buildQualityGateSummary({
104
+ conventionViolations,
105
+ regressionDetected,
106
+ lintPassed: lintCheck.passed,
107
+ assurancePassed: assuranceGates.newCode.passed,
108
+ minimalityBlocked: minimalityReport?.blocked === true,
109
+ score: result.score,
110
+ });
126
111
  // 8. Build output
127
112
  const output = {
128
113
  specId,
@@ -198,6 +183,7 @@ export async function handleValidate(args, server) {
198
183
  },
199
184
  lintCheck,
200
185
  assuranceGates,
186
+ qualityGateSummary,
201
187
  validationReport,
202
188
  minimalityReport,
203
189
  graphCoverage,
@@ -304,8 +290,9 @@ export async function handleValidate(args, server) {
304
290
  const passedCount = result.fieldsImplemented;
305
291
  const failedCount = totalCriteria - passedCount;
306
292
  const indeterminate = result.indeterminate ?? [];
293
+ const effectiveScore = qualityGateSummary.effectiveScore ?? scoreValue;
307
294
  const compactText = buildCompactValidateText({
308
- score: scoreValue,
295
+ score: effectiveScore,
309
296
  passedCount,
310
297
  failedCount,
311
298
  totalCriteria,
@@ -316,7 +303,7 @@ export async function handleValidate(args, server) {
316
303
  });
317
304
  const graphText = formatGraphCoverageText(graphCoverage);
318
305
  // SPEC-512: Compact structuredContent — essential fields only at top level
319
- const compactSummary = buildCompactSummary(specId, result.score, passedCount, failedCount, totalCriteria);
306
+ const compactSummary = buildCompactSummary(specId, result.score === null ? null : effectiveScore, passedCount, failedCount + (qualityGateSummary.passed ? 0 : 1), totalCriteria);
320
307
  const structuredBase = { ...outputWithSuggestions, summary: compactSummary, indeterminate };
321
308
  // SPEC-595: Elicit next action when validation fails and a server is available
322
309
  if (server !== undefined && failedCount > 0) {
@@ -358,6 +345,50 @@ export async function handleValidate(args, server) {
358
345
  structuredContent: structuredBase,
359
346
  };
360
347
  }
348
+ async function scanProjectConventions(projectId, projectPath) {
349
+ try {
350
+ const rules = parseConventions(projectPath);
351
+ if (rules.length === 0) {
352
+ return { conventionViolations: [], regressionDetected: false };
353
+ }
354
+ const conventionViolations = scanConventions(projectPath, rules);
355
+ const comparison = await compareWithBaseline(projectId, {
356
+ rules,
357
+ violations: conventionViolations,
358
+ summary: {
359
+ totalRules: rules.length,
360
+ totalViolations: conventionViolations.length,
361
+ bySeverity: {},
362
+ byCategory: {},
363
+ },
364
+ scannedAt: new Date().toISOString(),
365
+ });
366
+ return { conventionViolations, regressionDetected: comparison.regressionDetected };
367
+ }
368
+ catch {
369
+ return { conventionViolations: [], regressionDetected: false };
370
+ }
371
+ }
372
+ function buildQualityGateSummary(input) {
373
+ const blockingConventionViolations = input.conventionViolations.filter((violation) => violation.severity === 'critical');
374
+ const failures = [
375
+ ...(input.regressionDetected ? ['convention-regression'] : []),
376
+ ...(blockingConventionViolations.length > 0 ? ['blocking-convention-violations'] : []),
377
+ ...(!input.lintPassed ? ['lint'] : []),
378
+ ...(!input.assurancePassed ? ['assurance'] : []),
379
+ ...(input.minimalityBlocked ? ['minimality'] : []),
380
+ ];
381
+ return {
382
+ passed: failures.length === 0,
383
+ effectiveScore: failures.length === 0 ? input.score : Math.min(input.score ?? 0, 99),
384
+ failures,
385
+ conventionRegression: input.regressionDetected,
386
+ blockingConventionViolations: blockingConventionViolations.length,
387
+ lintPassed: input.lintPassed,
388
+ assurancePassed: input.assurancePassed,
389
+ minimalityPassed: !input.minimalityBlocked,
390
+ };
391
+ }
361
392
  function buildCompactSummary(specId, score, passedCount, failedCount, totalCriteria) {
362
393
  return {
363
394
  specId,
@@ -43,6 +43,7 @@ export interface RLSPolicy {
43
43
  name: string;
44
44
  table: string;
45
45
  operation: 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'ALL';
46
+ roles?: string[];
46
47
  using: string;
47
48
  withCheck?: string;
48
49
  justification: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.10.3",
3
+ "version": "4.10.4",
4
4
  "description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,19 +34,19 @@
34
34
  "packageName": "@planu/core"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@planu/core-darwin-arm64": "4.10.3",
38
- "@planu/core-darwin-x64": "4.10.3",
39
- "@planu/core-linux-arm64-gnu": "4.10.3",
40
- "@planu/core-linux-arm64-musl": "4.10.3",
41
- "@planu/core-linux-x64-gnu": "4.10.3",
42
- "@planu/core-linux-x64-musl": "4.10.3",
43
- "@planu/core-win32-arm64-msvc": "4.10.3",
44
- "@planu/core-win32-x64-msvc": "4.10.3"
37
+ "@planu/core-darwin-arm64": "4.10.4",
38
+ "@planu/core-darwin-x64": "4.10.4",
39
+ "@planu/core-linux-arm64-gnu": "4.10.4",
40
+ "@planu/core-linux-arm64-musl": "4.10.4",
41
+ "@planu/core-linux-x64-gnu": "4.10.4",
42
+ "@planu/core-linux-x64-musl": "4.10.4",
43
+ "@planu/core-win32-arm64-msvc": "4.10.4",
44
+ "@planu/core-win32-x64-msvc": "4.10.4"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24.0.0"
48
48
  },
49
- "packageManager": "pnpm@10.28.2",
49
+ "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
50
50
  "scripts": {
51
51
  "build:rust": "bash scripts/build-rust-local.sh --host",
52
52
  "build:rust:all": "bash scripts/build-rust-local.sh --all",
@@ -135,38 +135,6 @@
135
135
  "yaml": "^2.9.0",
136
136
  "zod": "^4.4.3"
137
137
  },
138
- "pnpm": {
139
- "overrides": {
140
- "flatted": ">=3.4.2",
141
- "path-to-regexp": ">=8.4.0",
142
- "brace-expansion": ">=5.0.5",
143
- "handlebars": ">=4.7.9",
144
- "picomatch": ">=4.0.4",
145
- "yaml": ">=2.8.3",
146
- "lodash": ">=4.18.0",
147
- "lodash-es": ">=4.18.0",
148
- "hono": ">=4.12.14",
149
- "postcss": ">=8.5.10",
150
- "esbuild": "0.28.1",
151
- "fast-uri": ">=3.1.2",
152
- "qs": ">=6.15.2"
153
- },
154
- "supportedArchitectures": {
155
- "os": [
156
- "darwin",
157
- "linux",
158
- "win32"
159
- ],
160
- "cpu": [
161
- "arm64",
162
- "x64"
163
- ],
164
- "libc": [
165
- "glibc",
166
- "musl"
167
- ]
168
- }
169
- },
170
138
  "devDependencies": {
171
139
  "@commitlint/cli": "^21.2.0",
172
140
  "@commitlint/config-conventional": "^21.2.0",
@@ -200,7 +168,7 @@
200
168
  "prettier": "^3.9.4",
201
169
  "secretlint": "^13.0.2",
202
170
  "semantic-release": "^25.0.5",
203
- "tsc-alias": "^1.8.17",
171
+ "tsc-alias": "^1.9.0",
204
172
  "type-coverage": "^2.29.7",
205
173
  "typescript": "^6.0.3",
206
174
  "typescript-eslint": "^8.62.1",
package/planu-native.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "4.10.3",
4
+ "version": "4.10.4",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
package/planu-plugin.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dev.planu.cli",
3
3
  "displayName": "Planu — Spec Driven Development",
4
4
  "description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
5
- "version": "4.10.3",
5
+ "version": "4.10.4",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": [
8
8
  "npx",