@planu/cli 5.4.0 → 5.4.1

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,10 @@
1
+ ## [5.4.1] - 2026-08-29
2
+
3
+ ### Bug Fixes
4
+ - fix(SPEC-1675): strip FILES/FUNCTIONS/TEST metadata markers from contradiction analysis
5
+ - fix(SPEC-1676): rebuild expired project knowledge graph and re-stamp cached reuse
6
+
7
+
1
8
  ## [5.4.0] - 2026-08-29
2
9
 
3
10
  ### Features
@@ -1 +1 @@
1
- {"schemaVersion":1,"commit":"1224d53c6195e75033a881982ac489bc4f2619f3"}
1
+ {"schemaVersion":1,"commit":"c920f236390f64d91a02ca3e0566e59210f00a49"}
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { collectProjectGraphSources, diffSourceHashes, hashText, hydrateProjectGraphSource, loadProjectGraphPolicy, projectGraphCachePath, projectGraphExtractorVersion, projectGraphPath, readProjectGraphArtifact, readProjectGraphCache, withProjectGraphBuildLock, writeProjectGraphArtifacts, } from './cache.js';
2
+ import { collectProjectGraphSources, diffSourceHashes, hashText, hydrateProjectGraphSource, loadProjectGraphPolicy, projectGraphCachePath, projectGraphExtractorVersion, projectGraphPath, readProjectGraphArtifact, readProjectGraphCache, withProjectGraphBuildLock, writeProjectGraph, writeProjectGraphArtifacts, } from './cache.js';
3
3
  import { extractGitGraph, extractReleaseGraph } from './extractors/git-extractor.js';
4
4
  import { extractHandoffGraph } from './extractors/handoff-extractor.js';
5
5
  import { extractDecisionStoreGraph } from './extractors/decision-store-extractor.js';
@@ -210,13 +210,16 @@ async function buildProjectKnowledgeGraphUnlocked(args) {
210
210
  Array.isArray(existingGraph.nodes) &&
211
211
  Array.isArray(existingGraph.edges);
212
212
  if (hasConsistentExistingGraph && diff.changed.length === 0 && diff.removed.length === 0) {
213
+ const refreshedGeneratedAt = new Date().toISOString();
213
214
  const graphWithCurrentOversizedSources = {
214
215
  ...existingGraph,
216
+ generatedAt: refreshedGeneratedAt,
215
217
  oversizedSources,
216
218
  };
219
+ await writeProjectGraph(args.projectId, args.policy, graphWithCurrentOversizedSources);
217
220
  publishFreshProjectGraphProjection({
218
221
  projectId: args.projectId,
219
- generatedAt: existingGraph.generatedAt,
222
+ generatedAt: refreshedGeneratedAt,
220
223
  generation: existingGraph.generation,
221
224
  });
222
225
  return {
@@ -55,7 +55,8 @@ async function ensureFreshProjectGraph(args) {
55
55
  const freshness = await getProjectGraphFreshness(args);
56
56
  if (!freshness.exists ||
57
57
  freshness.reason === 'source_changed' ||
58
- freshness.reason === 'corrupt') {
58
+ freshness.reason === 'corrupt' ||
59
+ freshness.reason === 'expired') {
59
60
  await buildProjectKnowledgeGraph(args);
60
61
  return getProjectGraphFreshness(args);
61
62
  }
@@ -203,14 +203,35 @@ function assertsActionNearScope(clause, outOfScopeItem) {
203
203
  }
204
204
  return false;
205
205
  }
206
+ const METADATA_MARKER_KEYWORD = /\b(?:FILES?|FUNCTIONS?|TESTS?)\s*:/gi;
207
+ const SENTENCE_BOUNDARY_AFTER_MARKER = /\.\s+(?=[A-Z])/;
208
+ function stripMetadataMarkers(text) {
209
+ const markers = [...text.matchAll(METADATA_MARKER_KEYWORD)];
210
+ if (markers.length === 0) {
211
+ return text;
212
+ }
213
+ let result = '';
214
+ let cursor = 0;
215
+ for (let i = 0; i < markers.length; i++) {
216
+ const start = markers[i]?.index ?? 0;
217
+ result += text.slice(cursor, start);
218
+ const nextMarkerStart = markers[i + 1]?.index ?? text.length;
219
+ const searchRegion = text.slice(start, nextMarkerStart);
220
+ const boundaryMatch = SENTENCE_BOUNDARY_AFTER_MARKER.exec(searchRegion);
221
+ cursor = boundaryMatch ? start + boundaryMatch.index + 1 : nextMarkerStart;
222
+ }
223
+ result += text.slice(cursor);
224
+ return result.replace(/\s+/g, ' ').trim();
225
+ }
206
226
  /**
207
227
  * Determine whether a criterion text contradicts an out-of-scope item.
208
228
  * Uses substring match first, then keyword overlap as fuzzy fallback.
209
229
  */
210
230
  function contradicts(criterionText, outOfScopeItem) {
211
- const normCriterion = normalize(criterionText);
231
+ const strippedCriterionText = stripMetadataMarkers(criterionText);
232
+ const normCriterion = normalize(strippedCriterionText);
212
233
  const normScope = normalize(outOfScopeItem);
213
- const clauses = splitClauses(criterionText);
234
+ const clauses = splitClauses(strippedCriterionText);
214
235
  const relevantClauses = clauses.filter((clause) => clauseMentionsScope(clause, outOfScopeItem));
215
236
  const hasConditionalException = /\b(?:unless|except)\b/i.test(criterionText);
216
237
  if (!hasConditionalException &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "5.4.0",
3
+ "version": "5.4.1",
4
4
  "description": "Planu — MCP Server for Spec Driven Development. Cross-platform (Linux/macOS/Windows, x64/arm64).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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": "5.4.0",
5
+ "version": "5.4.1",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": ["npx", "@planu/cli@latest"],
8
8
  "packageName": "@planu/cli",