@planu/cli 5.3.8 → 5.3.10

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/engine/code-scanner/index.d.ts +3 -2
  3. package/dist/engine/code-scanner/index.js +4 -3
  4. package/dist/engine/code-scanner/layer-scanner.d.ts +3 -2
  5. package/dist/engine/code-scanner/layer-scanner.js +38 -45
  6. package/dist/engine/next-spec-resolver/conflict-filter.d.ts +3 -21
  7. package/dist/engine/next-spec-resolver/conflict-filter.js +38 -56
  8. package/dist/engine/next-spec-resolver/index.d.ts +2 -2
  9. package/dist/engine/next-spec-resolver/index.js +5 -5
  10. package/dist/engine/next-spec-resolver/orchestration-planner.js +11 -3
  11. package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
  12. package/dist/engine/planu-core.darwin-arm64.node.sbom.json +4 -4
  13. package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
  14. package/dist/engine/planu-core.darwin-x64.node.sbom.json +4 -4
  15. package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
  16. package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +4 -4
  17. package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
  18. package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +4 -4
  19. package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
  20. package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +4 -4
  21. package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
  22. package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +4 -4
  23. package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
  24. package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +4 -4
  25. package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
  26. package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +4 -4
  27. package/dist/engine/spec-format/technical-md-populator.js +34 -12
  28. package/dist/tools/update-status/index.d.ts +3 -1
  29. package/dist/tools/update-status/index.js +16 -1
  30. package/package.json +9 -9
  31. package/planu-native.json +1 -1
  32. package/planu-plugin.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## [5.3.10] - 2026-08-09
2
+
3
+ ### Bug Fixes
4
+ - fix(orchestration): derive file-conflict ownership from persisted spec bodies (SPEC-1461)
5
+
6
+ ### Chores
7
+ - chore(spec-1461): mark done after validate 100/100 and dual review
8
+ - chore(spec-1461): sync spec v1.3.1 into branch and strip narrative comments
9
+ - chore(planu): SPEC-1461 lifecycle after dual review; file SPEC-1463/SPEC-1464
10
+ - chore(planu): file SPEC-1462 (transient smoke CLI failure) and record v5.3.9 release state
11
+
12
+
13
+ ## [5.3.9] - 2026-08-09
14
+
15
+ ### Bug Fixes
16
+ - fix(spec-format): reject prose bullets in Files-section ownership parsing
17
+ - fix(code-scanner): bound implementation-detection evidence to spec-owned paths
18
+
19
+ ### Chores
20
+ - chore(planu): record SPEC-1219 done transition and duplicate-cluster discards
21
+
22
+
1
23
  ## [5.3.8] - 2026-08-09
2
24
 
3
25
  ### Bug Fixes
@@ -6,7 +6,8 @@ export type { GapScore, ImplementationStatus } from './gap-scorer.js';
6
6
  import { type GapScore } from './gap-scorer.js';
7
7
  /**
8
8
  * Run the full code reality check for a spec in a project.
9
- * Returns a GapScore describing how much of the spec is already in code.
9
+ * Evidence is bounded to the spec's canonical owned paths (SPEC-1219);
10
+ * empty ownership yields the zero-score GapScore instead of a repo-wide scan.
10
11
  */
11
- export declare function checkCodeReality(projectPath: string, specTitle: string, specDescription?: string): Promise<GapScore>;
12
+ export declare function checkCodeReality(projectPath: string, specTitle: string, ownedPaths: string[], specDescription?: string): Promise<GapScore>;
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -7,12 +7,13 @@ import { scanLayers } from './layer-scanner.js';
7
7
  import { scoreGap } from './gap-scorer.js';
8
8
  /**
9
9
  * Run the full code reality check for a spec in a project.
10
- * Returns a GapScore describing how much of the spec is already in code.
10
+ * Evidence is bounded to the spec's canonical owned paths (SPEC-1219);
11
+ * empty ownership yields the zero-score GapScore instead of a repo-wide scan.
11
12
  */
12
- export async function checkCodeReality(projectPath, specTitle, specDescription) {
13
+ export async function checkCodeReality(projectPath, specTitle, ownedPaths, specDescription) {
13
14
  const keywords = extractKeywords(specTitle, specDescription);
14
15
  const variants = toSymbolVariants(keywords);
15
- const layers = await scanLayers(projectPath, keywords, variants);
16
+ const layers = await scanLayers(projectPath, keywords, variants, ownedPaths);
16
17
  return scoreGap(layers);
17
18
  }
18
19
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,8 @@
1
1
  import type { LayerScanResult } from '../../types/index.js';
2
2
  export type { LayerScanResult };
3
3
  /**
4
- * Scan a project directory for evidence of spec implementation across 4 layers.
4
+ * Scan the spec's owned paths for evidence of implementation across 4 layers.
5
+ * Empty ownership yields an empty result — never a repository-wide scan.
5
6
  */
6
- export declare function scanLayers(projectPath: string, keywords: string[], symbolVariants: string[]): Promise<LayerScanResult>;
7
+ export declare function scanLayers(projectPath: string, keywords: string[], symbolVariants: string[], ownedPaths: string[]): Promise<LayerScanResult>;
7
8
  //# sourceMappingURL=layer-scanner.d.ts.map
@@ -1,42 +1,47 @@
1
1
  // src/engine/code-scanner/layer-scanner.ts — Scans the 4 implementation layers (SPEC-441)
2
+ // SPEC-1219: evidence is bounded to the spec's canonical owned paths.
2
3
  import { stat } from 'node:fs/promises';
3
- import { join, relative, basename } from 'node:path';
4
- import { fastFindFilesByExt, fastReadFiles } from '../core-bridge.js';
4
+ import { join, relative, basename, isAbsolute, normalize } from 'node:path';
5
+ import { fastReadFiles } from '../core-bridge.js';
6
+ const CODE_FILE_RE = /\.(ts|js|tsx|jsx)$/;
5
7
  /**
6
- * Scan a project directory for evidence of spec implementation across 4 layers.
8
+ * Scan the spec's owned paths for evidence of implementation across 4 layers.
9
+ * Empty ownership yields an empty result — never a repository-wide scan.
7
10
  */
8
- export async function scanLayers(projectPath, keywords, symbolVariants) {
9
- const srcDir = join(projectPath, 'src');
10
- const testsDir = join(projectPath, 'tests');
11
- const [srcExists, testsExists] = await Promise.all([dirExists(srcDir), dirExists(testsDir)]);
12
- let srcFiles = [];
13
- let testFiles = [];
14
- if (srcExists) {
15
- srcFiles = fastFindFilesByExt(srcDir, ['ts', 'js', 'tsx', 'jsx']).filter((f) => !f.endsWith('.d.ts'));
16
- }
17
- if (testsExists) {
18
- testFiles = fastFindFilesByExt(testsDir, ['ts', 'js', 'tsx', 'jsx']).filter((f) => !f.endsWith('.d.ts'));
19
- }
20
- // Layer 1: files whose names match keywords
21
- const matchedFiles = srcFiles.filter((f) => keywords.some((kw) => basename(f).toLowerCase().includes(kw)));
22
- // Layer 2: exported symbols in matched files (and register/index files)
23
- const registerFiles = srcFiles.filter((f) => /register.*\.ts$|index\.ts$/.test(f));
24
- const filesToScanForSymbols = [...new Set([...matchedFiles, ...registerFiles])];
25
- const symbols = findSymbols(filesToScanForSymbols, symbolVariants);
26
- // Layer 3: test files matching keywords
11
+ export async function scanLayers(projectPath, keywords, symbolVariants, ownedPaths) {
12
+ const ownedFiles = await resolveExistingOwnedFiles(projectPath, ownedPaths);
13
+ const testFiles = ownedFiles.filter(isTestPath);
14
+ const sourceFiles = ownedFiles.filter((f) => !isTestPath(f));
15
+ const sourceCodeFiles = sourceFiles.filter((f) => CODE_FILE_RE.test(f) && !f.endsWith('.d.ts'));
16
+ const matchedFiles = sourceFiles.filter((f) => keywords.some((kw) => basename(f).toLowerCase().includes(kw)));
17
+ const symbols = findSymbols(sourceCodeFiles.map((f) => join(projectPath, f)), symbolVariants);
27
18
  const matchedTests = testFiles.filter((f) => keywords.some((kw) => basename(f).toLowerCase().includes(kw)));
28
- // Layer 4: registrations in canonical registry source files.
29
- const registrations = findRegistrations(projectPath, srcFiles, keywords);
30
- return {
31
- files: matchedFiles.map((f) => relative(projectPath, f)),
32
- symbols,
33
- tests: matchedTests.map((f) => relative(projectPath, f)),
34
- registrations,
35
- };
19
+ const registrations = findRegistrations(sourceCodeFiles.map((f) => join(projectPath, f)), keywords);
20
+ return { files: matchedFiles, symbols, tests: matchedTests, registrations };
36
21
  }
37
22
  // ---------------------------------------------------------------------------
38
23
  // Internal helpers
39
24
  // ---------------------------------------------------------------------------
25
+ async function resolveExistingOwnedFiles(projectPath, ownedPaths) {
26
+ const inRoot = [
27
+ ...new Set(ownedPaths
28
+ .map((p) => normalize(p))
29
+ .filter((p) => !isAbsolute(p) && !p.split(/[\\/]/).includes('..'))),
30
+ ];
31
+ const existing = await Promise.all(inRoot.map(async (p) => {
32
+ try {
33
+ const s = await stat(join(projectPath, p));
34
+ return s.isFile() ? relative(projectPath, join(projectPath, p)) : null;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }));
40
+ return existing.filter((p) => p !== null);
41
+ }
42
+ function isTestPath(relativePath) {
43
+ return relativePath.startsWith('tests/') || /\.(test|spec)\.[a-z]+$/.test(relativePath);
44
+ }
40
45
  function findSymbols(files, variants) {
41
46
  const found = new Set();
42
47
  if (files.length > 0) {
@@ -51,16 +56,14 @@ function findSymbols(files, variants) {
51
56
  }
52
57
  EXPORT_RE.lastIndex = 0;
53
58
  }
54
- return [...found];
55
59
  }
56
60
  return [...found];
57
61
  }
58
- function findRegistrations(_projectPath, srcFiles, keywords) {
62
+ function findRegistrations(files, keywords) {
59
63
  const found = new Set();
60
- const registerFiles = srcFiles.filter((f) => /register.*\.ts$|tool-registry\/.*\.ts$/.test(f));
61
- if (registerFiles.length > 0) {
64
+ if (files.length > 0) {
62
65
  const REGISTER_RE = /(?:registerTool\s*\(\s*|name:\s*)['"]([^'"]+)['"]/g;
63
- for (const file of fastReadFiles(registerFiles)) {
66
+ for (const file of fastReadFiles(files)) {
64
67
  let match;
65
68
  while ((match = REGISTER_RE.exec(file.content)) !== null) {
66
69
  const toolName = match[1] ?? '';
@@ -70,17 +73,7 @@ function findRegistrations(_projectPath, srcFiles, keywords) {
70
73
  }
71
74
  REGISTER_RE.lastIndex = 0;
72
75
  }
73
- return [...found];
74
76
  }
75
77
  return [...found];
76
78
  }
77
- async function dirExists(p) {
78
- try {
79
- const s = await stat(p);
80
- return s.isDirectory();
81
- }
82
- catch {
83
- return false;
84
- }
85
- }
86
79
  //# sourceMappingURL=layer-scanner.js.map
@@ -1,23 +1,5 @@
1
1
  import type { Spec } from '../../types/spec/core.js';
2
- /**
3
- * SPEC-686 Scenario 4: Filter out candidates that share files with implementing specs.
4
- *
5
- * A conflict exists when candidate.technicalFiles ∩ activeSpec.technicalFiles is non-empty.
6
- * technicalFiles extracted from spec.technical?.filesToModify + spec.technical?.filesToCreate.
7
- *
8
- * Specs with no file info (draft/rough specs) are treated as conflict-free and included.
9
- *
10
- * @param candidates - specs to evaluate (approved, not blocked by deps)
11
- * @param activeSpecs - all specs currently with status === 'implementing'
12
- */
13
- export declare function filterConflicting(candidates: Spec[], activeSpecs: Spec[]): Spec[];
14
- /**
15
- * Extract all file paths from a spec's technical section.
16
- * Reads filesToModify and filesToCreate from spec.technical.
17
- */
18
- export declare function extractFiles(spec: Spec): string[];
19
- /**
20
- * Check if two specs share any files (used by orchestration planner).
21
- */
22
- export declare function hasFileConflict(specA: Spec, specB: Spec): boolean;
2
+ export declare function filterConflicting(candidates: Spec[], activeSpecs: Spec[], projectPath: string, cache?: Map<string, string[]>): Promise<Spec[]>;
3
+ export declare function extractFiles(spec: Spec, projectPath: string): Promise<string[]>;
4
+ export declare function hasFileConflict(specA: Spec, specB: Spec, projectPath: string, cache?: Map<string, string[]>): Promise<boolean>;
23
5
  //# sourceMappingURL=conflict-filter.d.ts.map
@@ -1,80 +1,62 @@
1
- // ---------------------------------------------------------------------------
2
- // Public API
3
- // ---------------------------------------------------------------------------
4
- /**
5
- * SPEC-686 Scenario 4: Filter out candidates that share files with implementing specs.
6
- *
7
- * A conflict exists when candidate.technicalFiles ∩ activeSpec.technicalFiles is non-empty.
8
- * technicalFiles extracted from spec.technical?.filesToModify + spec.technical?.filesToCreate.
9
- *
10
- * Specs with no file info (draft/rough specs) are treated as conflict-free and included.
11
- *
12
- * @param candidates - specs to evaluate (approved, not blocked by deps)
13
- * @param activeSpecs - all specs currently with status === 'implementing'
14
- */
15
- export function filterConflicting(candidates, activeSpecs) {
1
+ // engine/next-spec-resolver/conflict-filter.ts — SPEC-1461: derive file ownership from persisted spec bodies
2
+ import { isAbsolute, join } from 'node:path';
3
+ import { readSpecContent } from '../spec-reader.js';
4
+ import { extractFilesFromSpecBody } from '../spec-format/technical-md-populator.js';
5
+ export async function filterConflicting(candidates, activeSpecs, projectPath, cache = new Map()) {
16
6
  if (activeSpecs.length === 0) {
17
7
  return candidates;
18
8
  }
19
- const activeFiles = buildFileSet(activeSpecs);
9
+ const activeFiles = await buildFileSet(activeSpecs, projectPath, cache);
20
10
  if (activeFiles.size === 0) {
21
11
  return candidates;
22
12
  }
23
- return candidates.filter((spec) => {
24
- const specFiles = extractFiles(spec);
25
- // No file info → no conflict (treat as safe)
26
- if (specFiles.length === 0) {
27
- return true;
13
+ const result = [];
14
+ for (const spec of candidates) {
15
+ const specFiles = await extractFilesMemoized(spec, projectPath, cache);
16
+ if (specFiles.length === 0 || !specFiles.some((f) => activeFiles.has(f))) {
17
+ result.push(spec);
28
18
  }
29
- return !specFiles.some((f) => activeFiles.has(f));
30
- });
19
+ }
20
+ return result;
31
21
  }
32
- /**
33
- * Build a set of all files owned by the active (implementing) specs.
34
- */
35
- function buildFileSet(specs) {
22
+ async function buildFileSet(specs, projectPath, cache) {
36
23
  const files = new Set();
37
24
  for (const spec of specs) {
38
- for (const f of extractFiles(spec)) {
25
+ for (const f of await extractFilesMemoized(spec, projectPath, cache)) {
39
26
  files.add(f);
40
27
  }
41
28
  }
42
29
  return files;
43
30
  }
44
- /**
45
- * Extract all file paths from a spec's technical section.
46
- * Reads filesToModify and filesToCreate from spec.technical.
47
- */
48
- export function extractFiles(spec) {
49
- const technical = spec.technical;
50
- if (!technical) {
31
+ export async function extractFiles(spec, projectPath) {
32
+ if (!spec.specPath) {
51
33
  return [];
52
34
  }
53
- const files = [];
54
- const filesToModify = technical.filesToModify;
55
- if (Array.isArray(filesToModify)) {
56
- for (const f of filesToModify) {
57
- if (typeof f === 'string' && f.trim()) {
58
- files.push(f.trim());
59
- }
60
- }
35
+ const specPath = isAbsolute(spec.specPath) ? spec.specPath : join(projectPath, spec.specPath);
36
+ const body = await readSpecContent(specPath);
37
+ if (body === null) {
38
+ return [];
61
39
  }
62
- const filesToCreate = technical.filesToCreate;
63
- if (Array.isArray(filesToCreate)) {
64
- for (const f of filesToCreate) {
65
- if (typeof f === 'string' && f.trim()) {
66
- files.push(f.trim());
67
- }
68
- }
40
+ const ownership = await extractFilesFromSpecBody(body, projectPath);
41
+ if (ownership === null) {
42
+ return [];
43
+ }
44
+ return [
45
+ ...new Set([...ownership.create, ...ownership.modify, ...ownership.test].map((e) => e.path)),
46
+ ];
47
+ }
48
+ async function extractFilesMemoized(spec, projectPath, cache) {
49
+ const cached = cache.get(spec.id);
50
+ if (cached !== undefined) {
51
+ return cached;
69
52
  }
53
+ const files = await extractFiles(spec, projectPath);
54
+ cache.set(spec.id, files);
70
55
  return files;
71
56
  }
72
- /**
73
- * Check if two specs share any files (used by orchestration planner).
74
- */
75
- export function hasFileConflict(specA, specB) {
76
- const filesA = extractFiles(specA);
77
- const filesB = new Set(extractFiles(specB));
57
+ export async function hasFileConflict(specA, specB, projectPath, cache = new Map()) {
58
+ const filesA = await extractFilesMemoized(specA, projectPath, cache);
59
+ const filesB = new Set(await extractFilesMemoized(specB, projectPath, cache));
78
60
  return filesA.some((f) => filesB.has(f));
79
61
  }
80
62
  //# sourceMappingURL=conflict-filter.js.map
@@ -11,8 +11,8 @@ export type { NextSpecResult } from '../../types/next-spec.js';
11
11
  * 4. Sort by scoreSpec() DESC, then by id ASC (tie-break)
12
12
  * 5. Return top-1 as NextSpecResult
13
13
  *
14
- * @param _projectPath - absolute path to project root (reserved for future use)
14
+ * @param projectPath - absolute path to project root, used to resolve spec.md paths for conflict detection
15
15
  * @param allSpecs - full list of all specs in the project
16
16
  */
17
- export declare function resolveNextSpec(_projectPath: string, allSpecs: Spec[]): Promise<NextSpecResult>;
17
+ export declare function resolveNextSpec(projectPath: string, allSpecs: Spec[]): Promise<NextSpecResult>;
18
18
  //# sourceMappingURL=index.d.ts.map
@@ -14,13 +14,13 @@ import { scoreSpec } from './scorer.js';
14
14
  * 4. Sort by scoreSpec() DESC, then by id ASC (tie-break)
15
15
  * 5. Return top-1 as NextSpecResult
16
16
  *
17
- * @param _projectPath - absolute path to project root (reserved for future use)
17
+ * @param projectPath - absolute path to project root, used to resolve spec.md paths for conflict detection
18
18
  * @param allSpecs - full list of all specs in the project
19
19
  */
20
- export function resolveNextSpec(_projectPath, allSpecs) {
21
- return Promise.resolve(computeNextSpec(allSpecs));
20
+ export function resolveNextSpec(projectPath, allSpecs) {
21
+ return computeNextSpec(projectPath, allSpecs);
22
22
  }
23
- function computeNextSpec(allSpecs) {
23
+ async function computeNextSpec(projectPath, allSpecs) {
24
24
  // Scenario 8: empty spec list
25
25
  if (allSpecs.length === 0) {
26
26
  return { spec: null, reason: 'No specs in project yet' };
@@ -55,7 +55,7 @@ function computeNextSpec(allSpecs) {
55
55
  }
56
56
  // Step 3: Filter conflicting with implementing specs
57
57
  const activeSpecs = allSpecs.filter((s) => s.status === 'implementing');
58
- const candidates = filterConflicting(unblocked, activeSpecs);
58
+ const candidates = await filterConflicting(unblocked, activeSpecs, projectPath);
59
59
  if (candidates.length === 0) {
60
60
  return {
61
61
  spec: null,
@@ -33,11 +33,12 @@ export async function resolveOrchestrationPlan(projectPath, allSpecs) {
33
33
  const levels = buildTopologicalLevels(unblocked, inDegree, dependents);
34
34
  const waves = [];
35
35
  const resolvedIds = new Set();
36
+ const conflictCache = new Map();
36
37
  for (const level of levels) {
37
38
  if (waves.length >= MAX_WAVES) {
38
39
  break;
39
40
  }
40
- const waveGroups = groupByConflict(level);
41
+ const waveGroups = await groupByConflict(level, projectPath, conflictCache);
41
42
  for (const group of waveGroups) {
42
43
  if (waves.length >= MAX_WAVES) {
43
44
  break;
@@ -121,7 +122,7 @@ function buildWaveEntry(spec, group, resolvedIds) {
121
122
  dependsOn: (Array.isArray(spec.dependencies) ? spec.dependencies : []).filter((d) => resolvedIds.has(d)),
122
123
  };
123
124
  }
124
- function groupByConflict(specs) {
125
+ async function groupByConflict(specs, projectPath, cache) {
125
126
  const groups = [];
126
127
  const assigned = new Set();
127
128
  for (const spec of specs) {
@@ -133,7 +134,14 @@ function groupByConflict(specs) {
133
134
  if (group.length >= MAX_PARALLEL_PER_WAVE) {
134
135
  continue;
135
136
  }
136
- if (!group.some((other) => hasFileConflict(spec, other))) {
137
+ let conflicts = false;
138
+ for (const other of group) {
139
+ if (await hasFileConflict(spec, other, projectPath, cache)) {
140
+ conflicts = true;
141
+ break;
142
+ }
143
+ }
144
+ if (!conflicts) {
137
145
  group.push(spec);
138
146
  assigned.add(spec.id);
139
147
  placed = true;
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.8",
4
- "engineVersion": "5.3.8",
5
- "buildId": "1-5.3.8-aarch64-apple-darwin",
3
+ "cliVersion": "5.3.10",
4
+ "engineVersion": "5.3.10",
5
+ "buildId": "1-5.3.10-aarch64-apple-darwin",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -17,15 +17,15 @@
17
17
  "platform": "darwin",
18
18
  "arch": "arm64",
19
19
  "libc": null,
20
- "artifactSha256": "8dcbd7a6a1225d68658cb50ce32b174e52e820a3ebdc3f125cdec568966a7759",
20
+ "artifactSha256": "d8beac69c20433cc67304bfe9e6975447c6d18aa744887ebe8982858a5c9b50e",
21
21
  "sbom": {
22
22
  "format": "CycloneDX-1.5",
23
23
  "path": "planu-core.darwin-arm64.node.sbom.json",
24
- "sha256": "34ae39426563dbd6b2d4f61119ac53e362ddb2fb78a27b074518b477cb808d37"
24
+ "sha256": "d14e32fc056c4d533366178a95f503ea18623c9c7502a78dede88bdfe080d149"
25
25
  },
26
26
  "provenance": {
27
27
  "builder": "scripts/build-rust-local.sh",
28
- "sourceCommit": "c0d406e9b49a5ed393b2dcade39eb951223974c6",
28
+ "sourceCommit": "fb419d210888ffbb972e48b7382a7d01b50a9625",
29
29
  "sourceTreeDirty": false,
30
30
  "sourceDateEpoch": 1,
31
31
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -34,6 +34,6 @@
34
34
  "signature": {
35
35
  "algorithm": "Ed25519",
36
36
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
37
- "value": "zulz2seIniwXGYJINphaouLeL8/CHmjKezWwOUf/8l1tBlpk8z/7jl/SL3SspVFih3PWPM9+0kJAo7aHixOUDA=="
37
+ "value": "rUhhOkekI/nd5O6msXScY3LSPnVEsXjt6NeTF94SFzkwkh8kMGthFycMcVWx35HfeWbWf7gs1AUCtliXbv8DDA=="
38
38
  }
39
39
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.8",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.10",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.8",
10
+ "version": "5.3.10",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "8dcbd7a6a1225d68658cb50ce32b174e52e820a3ebdc3f125cdec568966a7759"
14
+ "content": "d8beac69c20433cc67304bfe9e6975447c6d18aa744887ebe8982858a5c9b50e"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.8",
1109
+ "ref": "pkg:cargo/planu-core@5.3.10",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.8",
4
- "engineVersion": "5.3.8",
5
- "buildId": "1-5.3.8-x86_64-apple-darwin",
3
+ "cliVersion": "5.3.10",
4
+ "engineVersion": "5.3.10",
5
+ "buildId": "1-5.3.10-x86_64-apple-darwin",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -17,15 +17,15 @@
17
17
  "platform": "darwin",
18
18
  "arch": "x64",
19
19
  "libc": null,
20
- "artifactSha256": "f3affc187e9c20766667848f9742e59cbba41b4791238e5aeaffaeccaaaa5dc2",
20
+ "artifactSha256": "0ff4b1ebedbbae23b9e16e336f17e4eba1be291e1ef264ef39fc63dd954f4aac",
21
21
  "sbom": {
22
22
  "format": "CycloneDX-1.5",
23
23
  "path": "planu-core.darwin-x64.node.sbom.json",
24
- "sha256": "417f4b08fcf6c6668ad620c9019fad7bbe79a09f613c253ee59863d6f44b8039"
24
+ "sha256": "dc1aca4f7f611ade3456586ac75b3a61850fc2d23bde6ec57abf2545f128eb17"
25
25
  },
26
26
  "provenance": {
27
27
  "builder": "scripts/build-rust-local.sh",
28
- "sourceCommit": "c0d406e9b49a5ed393b2dcade39eb951223974c6",
28
+ "sourceCommit": "fb419d210888ffbb972e48b7382a7d01b50a9625",
29
29
  "sourceTreeDirty": false,
30
30
  "sourceDateEpoch": 1,
31
31
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -34,6 +34,6 @@
34
34
  "signature": {
35
35
  "algorithm": "Ed25519",
36
36
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
37
- "value": "AepYMvl7NAYszjYlB691/Cp+P7MEM3/9rd1CcbHbYaz8G+T0HTNQ8U80LddrdKnPAcje/nrwolUapbRScWs+Bg=="
37
+ "value": "R9zcCaXmBVoWPdrBiF6/GqvfVK41W3NWgJ5JgSymiSmiwx45BjOZF7HIrvGq1tr/zruCeBBzGYl05FR/JA8ADQ=="
38
38
  }
39
39
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.8",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.10",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.8",
10
+ "version": "5.3.10",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "f3affc187e9c20766667848f9742e59cbba41b4791238e5aeaffaeccaaaa5dc2"
14
+ "content": "0ff4b1ebedbbae23b9e16e336f17e4eba1be291e1ef264ef39fc63dd954f4aac"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.8",
1109
+ "ref": "pkg:cargo/planu-core@5.3.10",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.8",
4
- "engineVersion": "5.3.8",
5
- "buildId": "1-5.3.8-aarch64-unknown-linux-gnu",
3
+ "cliVersion": "5.3.10",
4
+ "engineVersion": "5.3.10",
5
+ "buildId": "1-5.3.10-aarch64-unknown-linux-gnu",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -16,15 +16,15 @@
16
16
  "platform": "linux",
17
17
  "arch": "arm64",
18
18
  "libc": "glibc",
19
- "artifactSha256": "c8f8315bb60f966e6250dffc7c094adcb1516499056b6c88a4d714e1b85ae52e",
19
+ "artifactSha256": "28e9486017819da5031ba39be725f749b91e575df2f2fcb90b1529d673a82749",
20
20
  "sbom": {
21
21
  "format": "CycloneDX-1.5",
22
22
  "path": "planu-core.linux-arm64-gnu.node.sbom.json",
23
- "sha256": "14a46a7d84851076b7549b8ff8d412a31d845c7ff291c90fff08bfb39e4b2ea0"
23
+ "sha256": "845dd005c78338dd6597aa8583919e4d1d545263cc6792e7929fe1dc41458377"
24
24
  },
25
25
  "provenance": {
26
26
  "builder": "scripts/build-rust-local.sh",
27
- "sourceCommit": "c0d406e9b49a5ed393b2dcade39eb951223974c6",
27
+ "sourceCommit": "fb419d210888ffbb972e48b7382a7d01b50a9625",
28
28
  "sourceTreeDirty": false,
29
29
  "sourceDateEpoch": 1,
30
30
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -33,6 +33,6 @@
33
33
  "signature": {
34
34
  "algorithm": "Ed25519",
35
35
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
36
- "value": "3002w64/CbUXeA8G0etnA8f8k6EnI8HpTXL0D97EghZLaU4lmqJ4KTgnjaVV7XtemRz4QkGXrlfQ8MW6nJTnAg=="
36
+ "value": "UTQoqkDsSuju0czEEjP2TYgDSUoWkiC31y5cUchXvSf9LmvHNiJEG+Dq6dE66NUF2vRuTb0mxqbxPq0PwlW3Dg=="
37
37
  }
38
38
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.8",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.10",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.8",
10
+ "version": "5.3.10",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "c8f8315bb60f966e6250dffc7c094adcb1516499056b6c88a4d714e1b85ae52e"
14
+ "content": "28e9486017819da5031ba39be725f749b91e575df2f2fcb90b1529d673a82749"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.8",
1109
+ "ref": "pkg:cargo/planu-core@5.3.10",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.8",
4
- "engineVersion": "5.3.8",
5
- "buildId": "1-5.3.8-aarch64-unknown-linux-musl",
3
+ "cliVersion": "5.3.10",
4
+ "engineVersion": "5.3.10",
5
+ "buildId": "1-5.3.10-aarch64-unknown-linux-musl",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -16,15 +16,15 @@
16
16
  "platform": "linux",
17
17
  "arch": "arm64",
18
18
  "libc": "musl",
19
- "artifactSha256": "ffc21930a3ffa37a8ffb963d1c3bd88194261c5f47d6645df01024d9e2b60d5a",
19
+ "artifactSha256": "cb3e41dbb3826bbe7d7df0a24c133d2492c31ffe856ad71a4c5d8bd08560b06f",
20
20
  "sbom": {
21
21
  "format": "CycloneDX-1.5",
22
22
  "path": "planu-core.linux-arm64-musl.node.sbom.json",
23
- "sha256": "421f01e0c237604db423ba74d1a1e91849d4bbf6223f2fa5d2c6c110888b08d1"
23
+ "sha256": "e9d225953b5b2d459d11d671cb3023a3523f98ab0a8f9576bceccff155e52877"
24
24
  },
25
25
  "provenance": {
26
26
  "builder": "scripts/build-rust-local.sh",
27
- "sourceCommit": "c0d406e9b49a5ed393b2dcade39eb951223974c6",
27
+ "sourceCommit": "fb419d210888ffbb972e48b7382a7d01b50a9625",
28
28
  "sourceTreeDirty": false,
29
29
  "sourceDateEpoch": 1,
30
30
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -33,6 +33,6 @@
33
33
  "signature": {
34
34
  "algorithm": "Ed25519",
35
35
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
36
- "value": "Euk8g0nxITjsBrUfmZltIfPHxy6x8s6iySzbxjxYSuuLlgHPYgCQwR8blrV+CNnzXbLhCf7bDseprKw9ZxhcDg=="
36
+ "value": "WJljyakoVmJfKTow05wBS/zH8bDYZG7KKqbWi6BwJORPZx1Ds3Sb9Y5F8WKv9RA+5BuFSFIJp4zHyJt9Tw6GDQ=="
37
37
  }
38
38
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.8",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.10",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.8",
10
+ "version": "5.3.10",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "ffc21930a3ffa37a8ffb963d1c3bd88194261c5f47d6645df01024d9e2b60d5a"
14
+ "content": "cb3e41dbb3826bbe7d7df0a24c133d2492c31ffe856ad71a4c5d8bd08560b06f"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.8",
1109
+ "ref": "pkg:cargo/planu-core@5.3.10",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.8",
4
- "engineVersion": "5.3.8",
5
- "buildId": "1-5.3.8-x86_64-unknown-linux-gnu",
3
+ "cliVersion": "5.3.10",
4
+ "engineVersion": "5.3.10",
5
+ "buildId": "1-5.3.10-x86_64-unknown-linux-gnu",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -16,15 +16,15 @@
16
16
  "platform": "linux",
17
17
  "arch": "x64",
18
18
  "libc": "glibc",
19
- "artifactSha256": "2fd2f2b88fba7345f0d0e19ea16b9600230ef6fae6b1f1a044ee02764214c5e3",
19
+ "artifactSha256": "7787ae4a0980dbbb2e63a771d7570b7006c950b1ed1acf48513aef47ea74738f",
20
20
  "sbom": {
21
21
  "format": "CycloneDX-1.5",
22
22
  "path": "planu-core.linux-x64-gnu.node.sbom.json",
23
- "sha256": "51e56ab54ee969b571b7009169db4edbc533c5a92476395c20764cd2ab6654bf"
23
+ "sha256": "983b6e5dd328ba08e035996568d2dcda48419876e22ff2c54ab09479cfdb4fd0"
24
24
  },
25
25
  "provenance": {
26
26
  "builder": "scripts/build-rust-local.sh",
27
- "sourceCommit": "c0d406e9b49a5ed393b2dcade39eb951223974c6",
27
+ "sourceCommit": "fb419d210888ffbb972e48b7382a7d01b50a9625",
28
28
  "sourceTreeDirty": false,
29
29
  "sourceDateEpoch": 1,
30
30
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -33,6 +33,6 @@
33
33
  "signature": {
34
34
  "algorithm": "Ed25519",
35
35
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
36
- "value": "3dDrNnVtW/juPnD67kL7ZA3tvrDFvyfbIBX+eVVi26Ly9tCDUM9abpvX28TPazuGUJ+Paipj+W1/cOEyGsKhDA=="
36
+ "value": "QQVd67qTw4BrhqCyOmd8SBKa8IeiTwzo6zV5UKv8206KvXxT+bqcv6ZcN1EsLYkjw9/g8DzcQSbgbpbS+oo/Ag=="
37
37
  }
38
38
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.8",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.10",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.8",
10
+ "version": "5.3.10",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "2fd2f2b88fba7345f0d0e19ea16b9600230ef6fae6b1f1a044ee02764214c5e3"
14
+ "content": "7787ae4a0980dbbb2e63a771d7570b7006c950b1ed1acf48513aef47ea74738f"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.8",
1109
+ "ref": "pkg:cargo/planu-core@5.3.10",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.8",
4
- "engineVersion": "5.3.8",
5
- "buildId": "1-5.3.8-x86_64-unknown-linux-musl",
3
+ "cliVersion": "5.3.10",
4
+ "engineVersion": "5.3.10",
5
+ "buildId": "1-5.3.10-x86_64-unknown-linux-musl",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -16,15 +16,15 @@
16
16
  "platform": "linux",
17
17
  "arch": "x64",
18
18
  "libc": "musl",
19
- "artifactSha256": "d93c3691b0f835b4056bf2d8b17d0448566616222d5221fc292fc87019120318",
19
+ "artifactSha256": "5c73eb5c0aacfbd3bd3a658b7037962ead65cbd568b6d503e3a94a0c676f2542",
20
20
  "sbom": {
21
21
  "format": "CycloneDX-1.5",
22
22
  "path": "planu-core.linux-x64-musl.node.sbom.json",
23
- "sha256": "02609054c32df79175cfa245c503e1df1ca4b32b15ad34ec0eda5b34a28fda27"
23
+ "sha256": "c754c7acf3f4bbd331670f4bbbdacfaaa634198449e960faaa9b39c922f2a249"
24
24
  },
25
25
  "provenance": {
26
26
  "builder": "scripts/build-rust-local.sh",
27
- "sourceCommit": "c0d406e9b49a5ed393b2dcade39eb951223974c6",
27
+ "sourceCommit": "fb419d210888ffbb972e48b7382a7d01b50a9625",
28
28
  "sourceTreeDirty": false,
29
29
  "sourceDateEpoch": 1,
30
30
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -33,6 +33,6 @@
33
33
  "signature": {
34
34
  "algorithm": "Ed25519",
35
35
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
36
- "value": "/echWzVigxiZs4/TF6wUHv1MEHBi7ulWLA9+JpDnd+otBDX8HIM3O6oOhDc4Lu1wmfQJMPyPCvutWkZxcXj6Dg=="
36
+ "value": "7p545clM056yw/1PTMLxeR6GrjnqnleW5oUlhLtuI6oWTWSOooTm68CuTQnvmyFFSQmTCU37zwHZIQvolmQmBw=="
37
37
  }
38
38
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.8",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.10",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.8",
10
+ "version": "5.3.10",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "d93c3691b0f835b4056bf2d8b17d0448566616222d5221fc292fc87019120318"
14
+ "content": "5c73eb5c0aacfbd3bd3a658b7037962ead65cbd568b6d503e3a94a0c676f2542"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.8",
1109
+ "ref": "pkg:cargo/planu-core@5.3.10",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.8",
4
- "engineVersion": "5.3.8",
5
- "buildId": "1-5.3.8-aarch64-pc-windows-msvc",
3
+ "cliVersion": "5.3.10",
4
+ "engineVersion": "5.3.10",
5
+ "buildId": "1-5.3.10-aarch64-pc-windows-msvc",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -16,15 +16,15 @@
16
16
  "platform": "win32",
17
17
  "arch": "arm64",
18
18
  "libc": null,
19
- "artifactSha256": "1e7250f120e58d9ebbf26b22ae9b0fd3d963ad80479b61e3f4be59feb0bcc97e",
19
+ "artifactSha256": "5cf4072e6f2f4623ebde20134cb670a50fefcf79f849cde428a3e68aeef4dbcf",
20
20
  "sbom": {
21
21
  "format": "CycloneDX-1.5",
22
22
  "path": "planu-core.win32-arm64-msvc.node.sbom.json",
23
- "sha256": "9a10362845e48f91b5604552de6509fcf1125c0161b43f2b6b0b3abb0fbc4295"
23
+ "sha256": "42a88feefc24921eda52cdd7ec4208a996136bb38871891386dadd2efbff08a6"
24
24
  },
25
25
  "provenance": {
26
26
  "builder": "scripts/build-rust-local.sh",
27
- "sourceCommit": "c0d406e9b49a5ed393b2dcade39eb951223974c6",
27
+ "sourceCommit": "fb419d210888ffbb972e48b7382a7d01b50a9625",
28
28
  "sourceTreeDirty": false,
29
29
  "sourceDateEpoch": 1,
30
30
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -33,6 +33,6 @@
33
33
  "signature": {
34
34
  "algorithm": "Ed25519",
35
35
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
36
- "value": "xvRJaOZmJ82Ru9UzRQENpyiU64mTOOcdVfh8gdS6jK0T5QUTJ9wUtz28xhnh24uyZLjyO3niKQky3Sq2GpEhCw=="
36
+ "value": "efNuvSLjD0P2taPCPrLqhjh28lFR4xQFX2+cV85YTuMP6Rodzy+Y3w+YqkUyfWLGjTTDnji6QJrXtIYYNK8vAg=="
37
37
  }
38
38
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.8",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.10",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.8",
10
+ "version": "5.3.10",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "1e7250f120e58d9ebbf26b22ae9b0fd3d963ad80479b61e3f4be59feb0bcc97e"
14
+ "content": "5cf4072e6f2f4623ebde20134cb670a50fefcf79f849cde428a3e68aeef4dbcf"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.8",
1109
+ "ref": "pkg:cargo/planu-core@5.3.10",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.8",
4
- "engineVersion": "5.3.8",
5
- "buildId": "1-5.3.8-x86_64-pc-windows-msvc",
3
+ "cliVersion": "5.3.10",
4
+ "engineVersion": "5.3.10",
5
+ "buildId": "1-5.3.10-x86_64-pc-windows-msvc",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -16,15 +16,15 @@
16
16
  "platform": "win32",
17
17
  "arch": "x64",
18
18
  "libc": null,
19
- "artifactSha256": "00609e4d5c5cb1f41a9b3824bbe4775bdab365a7bc95f9936a947b5ce2103e69",
19
+ "artifactSha256": "0f393c02b1cfe7145bfbc4485de5ad127a07f8e8955e61d5a3bce69135f717f7",
20
20
  "sbom": {
21
21
  "format": "CycloneDX-1.5",
22
22
  "path": "planu-core.win32-x64-msvc.node.sbom.json",
23
- "sha256": "805d4becff3818b502a44b83b3152b74d2064a74b0808f5e0587e6d9b8eec274"
23
+ "sha256": "ecc00d784269220e168e90db418228c6e92f7be9fb01409b9e9abeabe8265c1f"
24
24
  },
25
25
  "provenance": {
26
26
  "builder": "scripts/build-rust-local.sh",
27
- "sourceCommit": "c0d406e9b49a5ed393b2dcade39eb951223974c6",
27
+ "sourceCommit": "fb419d210888ffbb972e48b7382a7d01b50a9625",
28
28
  "sourceTreeDirty": false,
29
29
  "sourceDateEpoch": 1,
30
30
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -33,6 +33,6 @@
33
33
  "signature": {
34
34
  "algorithm": "Ed25519",
35
35
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
36
- "value": "fZQLZB7+7E55Gy/9luaF7isRmDht9nrNnvy8a922yCp4AnYXqRfhztkVPQPkMgoN5+wv793XOfY/6iS4rvmhBw=="
36
+ "value": "9Q/RF7gB8t++GQdo4RaoBNjeCdWxXKNm3NT8sXfxDmiEPgqaAR60USiR21v3D00wStMSPoZK2UcGHKkpvqmdBg=="
37
37
  }
38
38
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.8",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.10",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.8",
10
+ "version": "5.3.10",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "00609e4d5c5cb1f41a9b3824bbe4775bdab365a7bc95f9936a947b5ce2103e69"
14
+ "content": "0f393c02b1cfe7145bfbc4485de5ad127a07f8e8955e61d5a3bce69135f717f7"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.8",
1109
+ "ref": "pkg:cargo/planu-core@5.3.10",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,25 +1,45 @@
1
1
  // engine/spec-format/technical-md-populator.ts — SPEC-586: Parse spec body to populate ## Technical
2
2
  import { stat } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
+ import { findMarkdownSectionRange } from './markdown-sections.js';
4
5
  const FILE_PATH_RE = /\b(?:src|tests)\/[a-zA-Z0-9/_.-]+\.[a-z]+\b/g;
6
+ function stripFencedBlocks(content) {
7
+ const kept = [];
8
+ let fence = null;
9
+ for (const line of content.split('\n')) {
10
+ const fenceMatch = /^[ \t]{0,3}(`{3,}|~{3,})/.exec(line);
11
+ if (fenceMatch?.[1]) {
12
+ const marker = fenceMatch[1][0] ?? '`';
13
+ if (fence === null) {
14
+ fence = { marker, length: fenceMatch[1].length };
15
+ }
16
+ else if (fence.marker === marker && fenceMatch[1].length >= fence.length) {
17
+ fence = null;
18
+ }
19
+ continue;
20
+ }
21
+ if (fence === null) {
22
+ kept.push(line);
23
+ }
24
+ }
25
+ return kept.join('\n');
26
+ }
27
+ const SLASH_PATH_BULLET_RE = /^-\s+`?((?:[\w.-]+\/)+[\w-][\w.-]*\.[a-zA-Z][a-zA-Z0-9]*)`?(?=[\s`]|$)/;
28
+ const BARE_FILE_BULLET_RE = /^-\s+`?([\w-][\w.-]*\.[a-zA-Z][a-zA-Z0-9]*)`?\s*$/;
5
29
  function extractPathsFromSection(sectionText) {
6
30
  const paths = [];
7
- const listRe = /^-\s+`?(src\/[\w./-]+|tests\/[\w./-]+)`?/gm;
8
- let m;
9
- while ((m = listRe.exec(sectionText)) !== null) {
10
- const match = m[0]
11
- .replace(/^-\s+`?/, '')
12
- .replace(/`.*$/, '')
13
- .trim();
14
- if (match) {
15
- paths.push(match);
31
+ for (const line of sectionText.split('\n')) {
32
+ const candidate = SLASH_PATH_BULLET_RE.exec(line)?.[1] ?? BARE_FILE_BULLET_RE.exec(line)?.[1];
33
+ if (candidate !== undefined && !candidate.split('/').includes('..')) {
34
+ paths.push(candidate);
16
35
  }
17
36
  }
18
37
  return paths;
19
38
  }
20
39
  function parseSectionedFileLists(body) {
21
- const filesSection = /##\s+Files(?:\s+likely\s+to\s+change)?\s*[\s\S]*?(?=\n##\s|$)/.exec(body);
22
- const scopedBody = filesSection?.[0] ?? body;
40
+ const filesRange = findMarkdownSectionRange(body, 'Files') ??
41
+ findMarkdownSectionRange(body, 'Files likely to change');
42
+ const scopedBody = stripFencedBlocks(filesRange ? body.slice(filesRange.contentStart, filesRange.end) : body);
23
43
  const createSection = /###\s+Create[\s\S]*?(?=\n###|\n##\s|$)/.exec(scopedBody);
24
44
  const modifySection = /###\s+Modify[\s\S]*?(?=\n###|\n##\s|$)/.exec(scopedBody);
25
45
  const testSection = /###\s+Test[\s\S]*?(?=\n###|\n##\s|$)/.exec(scopedBody);
@@ -71,7 +91,9 @@ export async function extractFilesFromSpecBody(specBody, projectPath) {
71
91
  (structured.create.length > 0 || structured.modify.length > 0 || structured.test.length > 0)) {
72
92
  return structured;
73
93
  }
74
- const allPaths = [...new Set(Array.from(specBody.matchAll(FILE_PATH_RE), (m) => m[0]))];
94
+ const allPaths = [
95
+ ...new Set(Array.from(stripFencedBlocks(specBody).matchAll(FILE_PATH_RE), (m) => m[0])),
96
+ ];
75
97
  if (allPaths.length === 0) {
76
98
  return null;
77
99
  }
@@ -1,5 +1,6 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import type { UpdateStatusInput, ToolResult, Actuals, ReconciliationInvocationContext } from '../../types/index.js';
2
+ import type { UpdateStatusInput, ToolResult, Actuals, Spec, ReconciliationInvocationContext } from '../../types/index.js';
3
+ import { type GapScore } from '../../engine/code-scanner/index.js';
3
4
  type LifecycleAuthorityRoute = {
4
5
  route: 'ordinary';
5
6
  } | {
@@ -9,6 +10,7 @@ type LifecycleAuthorityRoute = {
9
10
  result: ToolResult;
10
11
  };
11
12
  /** Select the lifecycle path before any transition, artifact, receipt, or log mutation. */
13
+ export declare function runOwnershipBoundedCodeReality(spec: Spec, projectPath: string): Promise<GapScore>;
12
14
  export declare function classifyLifecycleAuthority(input: UpdateStatusInput, context: ReconciliationInvocationContext, options?: {
13
15
  requireDoneDigest?: boolean;
14
16
  }): LifecycleAuthorityRoute;
@@ -28,6 +28,7 @@ import { recordForceUsage } from '../../storage/force-analytics-store.js';
28
28
  import { maybeSafePushOnDone, queuePostCommitTasks, runCascadeForResponse, } from './side-effects.js';
29
29
  import { formatKeyValue } from '../output-formatter.js';
30
30
  import { checkCodeReality } from '../../engine/code-scanner/index.js';
31
+ import { extractFilesFromSpecBody } from '../../engine/spec-format/technical-md-populator.js';
31
32
  import { isAbsolute, join, resolve } from 'node:path';
32
33
  import { readSpecContent } from '../../engine/spec-reader.js';
33
34
  import { trackCost } from '../../engine/cost-tracking/operation-tracker.js';
@@ -51,6 +52,20 @@ function lifecycleAuthorityError(code, message, fixHint) {
51
52
  };
52
53
  }
53
54
  /** Select the lifecycle path before any transition, artifact, receipt, or log mutation. */
55
+ export async function runOwnershipBoundedCodeReality(spec, projectPath) {
56
+ const specPath = isAbsolute(spec.specPath) ? spec.specPath : join(projectPath, spec.specPath);
57
+ const specBody = await readSpecContent(specPath);
58
+ const ownership = specBody === null ? null : await extractFilesFromSpecBody(specBody, projectPath);
59
+ const ownedPaths = ownership === null
60
+ ? []
61
+ : [
62
+ ...new Set([...ownership.create, ...ownership.modify, ...ownership.test].map((e) => e.path)),
63
+ ];
64
+ if (ownedPaths.length === 0) {
65
+ reportClassifiedDegradation('CODE_REALITY_OWNERSHIP_MISSING', new Error(`spec ${spec.id} declares no canonical owned paths`));
66
+ }
67
+ return checkCodeReality(projectPath, spec.title, ownedPaths);
68
+ }
54
69
  export function classifyLifecycleAuthority(input, context, options = {}) {
55
70
  const hasRequestId = input.reconciliationRequestId !== undefined;
56
71
  const hasTransitionId = input.expectedImplementingTransitionId !== undefined;
@@ -1052,7 +1067,7 @@ export async function handleUpdateStatus(params, server) {
1052
1067
  const [codeRealityResult, doneGatesResult] = await Promise.all([
1053
1068
  // Code reality: only relevant for 'implementing'
1054
1069
  newStatus === 'implementing' && !reverseTransition && effectiveGatePath
1055
- ? checkCodeReality(effectiveGatePath, spec.title).catch((error) => {
1070
+ ? runOwnershipBoundedCodeReality(spec, effectiveGatePath).catch((error) => {
1056
1071
  // reliability-optional: CODE_REALITY_ENRICHMENT — downstream evidence gates remain binding
1057
1072
  reportClassifiedDegradation('CODE_REALITY_ENRICHMENT', error);
1058
1073
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "5.3.8",
3
+ "version": "5.3.10",
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",
@@ -35,14 +35,14 @@
35
35
  "packageName": "@planu/core"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@planu/core-darwin-arm64": "5.3.8",
39
- "@planu/core-darwin-x64": "5.3.8",
40
- "@planu/core-linux-arm64-gnu": "5.3.8",
41
- "@planu/core-linux-arm64-musl": "5.3.8",
42
- "@planu/core-linux-x64-gnu": "5.3.8",
43
- "@planu/core-linux-x64-musl": "5.3.8",
44
- "@planu/core-win32-arm64-msvc": "5.3.8",
45
- "@planu/core-win32-x64-msvc": "5.3.8"
38
+ "@planu/core-darwin-arm64": "5.3.10",
39
+ "@planu/core-darwin-x64": "5.3.10",
40
+ "@planu/core-linux-arm64-gnu": "5.3.10",
41
+ "@planu/core-linux-arm64-musl": "5.3.10",
42
+ "@planu/core-linux-x64-gnu": "5.3.10",
43
+ "@planu/core-linux-x64-musl": "5.3.10",
44
+ "@planu/core-win32-arm64-msvc": "5.3.10",
45
+ "@planu/core-win32-x64-msvc": "5.3.10"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=24.0.0"
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": "5.3.8",
4
+ "version": "5.3.10",
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": "5.3.8",
5
+ "version": "5.3.10",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": ["npx", "@planu/cli@latest"],
8
8
  "packageName": "@planu/cli",