@planu/cli 5.3.4 → 5.3.6
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 +47 -0
- package/dist/engine/execution/validate-job-executor.js +25 -10
- package/dist/engine/hooks/handlers/on-impl-change.d.ts +17 -4
- package/dist/engine/hooks/handlers/on-impl-change.js +43 -44
- package/dist/engine/living-spec-analyzer.d.ts +5 -2
- package/dist/engine/living-spec-analyzer.js +5 -2
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +4 -4
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +4 -4
- package/dist/engine/reconcile/apply-changes.d.ts +0 -12
- package/dist/engine/reconcile/apply-changes.js +11 -33
- package/dist/engine/reconcile/verify-write.js +2 -3
- package/dist/engine/rules-reconciler.js +21 -2
- package/dist/engine/scope-boundaries/contradiction-checker.js +20 -13
- package/dist/engine/spec-format/acceptance-criteria.js +11 -2
- package/dist/engine/spec-migrator/filesystem-import.js +113 -73
- package/dist/engine/spec-migrator/frontmatter-parser.js +15 -13
- package/dist/engine/universal-rules/catalog.js +6 -0
- package/dist/engine/universal-rules/rules/agent-teams.js +0 -2
- package/dist/engine/universal-rules/rules/planu-approval-gates.js +0 -1
- package/dist/engine/universal-rules/rules/planu-bdd-criteria.js +0 -1
- package/dist/engine/universal-rules/rules/planu-clean-code-no-comments.d.ts +3 -0
- package/dist/engine/universal-rules/rules/planu-clean-code-no-comments.js +30 -0
- package/dist/engine/universal-rules/rules/planu-debate-review.d.ts +3 -0
- package/dist/engine/universal-rules/rules/planu-debate-review.js +38 -0
- package/dist/engine/universal-rules/rules/planu-dogfood-bugs.js +0 -2
- package/dist/engine/universal-rules/rules/planu-english-specs.js +0 -1
- package/dist/engine/universal-rules/rules/planu-minimal-change.js +0 -2
- package/dist/engine/universal-rules/rules/planu-modes.js +0 -2
- package/dist/engine/universal-rules/rules/planu-release-policy.js +0 -1
- package/dist/engine/universal-rules/rules/planu-revert-proof-tests.d.ts +3 -0
- package/dist/engine/universal-rules/rules/planu-revert-proof-tests.js +34 -0
- package/dist/engine/universal-rules/rules/planu-sdd-model-routing.js +0 -1
- package/dist/engine/universal-rules/rules/planu-workflow.js +0 -2
- package/dist/engine/validator/validation-report-writer.js +1 -1
- package/dist/tools/auto-reconcile.js +1 -1
- package/dist/tools/challenge-spec/challenge-report.js +1 -1
- package/dist/tools/init-project/portable-index-reconciler.js +2 -1
- package/dist/tools/reconcile-spec.d.ts +1 -1
- package/dist/tools/reconcile-spec.js +306 -179
- package/dist/tools/safe-handler.js +0 -2
- package/dist/tools/start-hooks/engine.d.ts +2 -3
- package/dist/tools/start-hooks/engine.js +50 -33
- package/dist/tools/start-hooks.d.ts +1 -1
- package/dist/tools/start-hooks.js +1 -2
- package/dist/tools/tool-registry/group-infra.js +36 -0
- package/dist/tools/update-status/dod-gates.js +3 -3
- package/dist/tools/update-status/transition-guard.js +17 -4
- package/dist/types/file-hooks.d.ts +4 -6
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/mcp.d.ts +0 -19
- package/dist/types/reconcile.d.ts +5 -17
- package/dist/types/reconcile.js +1 -7
- package/dist/types/spec/core.d.ts +5 -0
- package/dist/types/spec/inputs.d.ts +16 -0
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
- package/dist/engine/hooks/agent-hook-migrator.d.ts +0 -9
- package/dist/engine/hooks/agent-hook-migrator.js +0 -140
- package/dist/types/hook-status-update.d.ts +0 -10
- package/dist/types/hook-status-update.js +0 -3
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
|
-
"cliVersion": "5.3.
|
|
4
|
-
"engineVersion": "5.3.
|
|
5
|
-
"buildId": "1-5.3.
|
|
3
|
+
"cliVersion": "5.3.6",
|
|
4
|
+
"engineVersion": "5.3.6",
|
|
5
|
+
"buildId": "1-5.3.6-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": "
|
|
19
|
+
"artifactSha256": "fc1ffffc7aa8178117d75efb21a21cbf47789de550aaa09010c6acbd22a3fa3f",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.linux-x64-musl.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "016bcd4176cb0c11f7fa2890b13bd882afcaea12cb92a3316586f421a379ab9b"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "2dae5f5028632004802bad4c143eb86d8abba774",
|
|
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": "
|
|
36
|
+
"value": "evwVuvJPRtdHXxc0wulqvQSSTm+IoeWgcuZuZjXnI2OZXQ+M9P4dAS/7ChFxoIul52xBCEB1MIZObdMynhWvCA=="
|
|
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.
|
|
7
|
+
"bom-ref": "pkg:cargo/planu-core@5.3.6",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.6",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "fc1ffffc7aa8178117d75efb21a21cbf47789de550aaa09010c6acbd22a3fa3f"
|
|
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.
|
|
1109
|
+
"ref": "pkg:cargo/planu-core@5.3.6",
|
|
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.
|
|
4
|
-
"engineVersion": "5.3.
|
|
5
|
-
"buildId": "1-5.3.
|
|
3
|
+
"cliVersion": "5.3.6",
|
|
4
|
+
"engineVersion": "5.3.6",
|
|
5
|
+
"buildId": "1-5.3.6-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": "
|
|
19
|
+
"artifactSha256": "16659d578e84674eeab01f7f53943e46ae8b2c1836fef583eb126a4eee15c2ff",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.win32-arm64-msvc.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "411f35f08ebc1f8dc65a665bc355e2b10dc0b8db2bb279e33bb1a4d49f1bad34"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "2dae5f5028632004802bad4c143eb86d8abba774",
|
|
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": "
|
|
36
|
+
"value": "om/6aNSNCXZ8rbF+dw+8mWFhr1Zj0+z06pFccowCC3dcXtzDkLwDuquUlcHq8OSmFJNyjXFJ5i+OopjRPXKqCg=="
|
|
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.
|
|
7
|
+
"bom-ref": "pkg:cargo/planu-core@5.3.6",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.6",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "16659d578e84674eeab01f7f53943e46ae8b2c1836fef583eb126a4eee15c2ff"
|
|
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.
|
|
1109
|
+
"ref": "pkg:cargo/planu-core@5.3.6",
|
|
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.
|
|
4
|
-
"engineVersion": "5.3.
|
|
5
|
-
"buildId": "1-5.3.
|
|
3
|
+
"cliVersion": "5.3.6",
|
|
4
|
+
"engineVersion": "5.3.6",
|
|
5
|
+
"buildId": "1-5.3.6-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": "
|
|
19
|
+
"artifactSha256": "e6750baae63b969de18daae8150796a785638847bb7b3645b214b252f0bf32d3",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.win32-x64-msvc.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "323e01b4f75869ec688a26524c5e4aa756a6ad5c0e7fd968a9090ad6e58a75bf"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "2dae5f5028632004802bad4c143eb86d8abba774",
|
|
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": "
|
|
36
|
+
"value": "YmufISTxo4Tx7ceRSzMxv6fXdoi19oHTokfKkevZzBkaCi3msjDRWJqerPQjjDr5Vkip8LB+2oQUS2DC3Ea+AQ=="
|
|
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.
|
|
7
|
+
"bom-ref": "pkg:cargo/planu-core@5.3.6",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.6",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "e6750baae63b969de18daae8150796a785638847bb7b3645b214b252f0bf32d3"
|
|
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.
|
|
1109
|
+
"ref": "pkg:cargo/planu-core@5.3.6",
|
|
1110
1110
|
"dependsOn": [
|
|
1111
1111
|
"pkg:cargo/core-foundation@0.10.1",
|
|
1112
1112
|
"pkg:cargo/hmac@0.12.1",
|
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
import { type ReconcileChange, type ApplyChangesResult } from '../../types/index.js';
|
|
2
2
|
export type { ApplyChangesResult, SkippedChange } from '../../types/index.js';
|
|
3
|
-
/**
|
|
4
|
-
* Apply approved text changes to the spec.md body.
|
|
5
|
-
*
|
|
6
|
-
* For each non-metadata approved change:
|
|
7
|
-
* 1. Locate the named section's body via heading anchor.
|
|
8
|
-
* 2. Require exactly one occurrence of `originalValue` inside that section.
|
|
9
|
-
* 3. Replace it in-place at the resolved absolute offset.
|
|
10
|
-
*
|
|
11
|
-
* Skips (with reason) when the section heading is absent, the value is missing
|
|
12
|
-
* inside the section, or the value appears multiple times (ambiguous match).
|
|
13
|
-
* Writes only when at least one substitution succeeded.
|
|
14
|
-
*/
|
|
15
3
|
export declare function applyChangesToSpec(specPath: string, changes: ReconcileChange[]): Promise<ApplyChangesResult>;
|
|
16
4
|
//# sourceMappingURL=apply-changes.d.ts.map
|
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
// engine/reconcile/apply-changes.ts — SPEC-1011 Bug D: Apply approved text changes to spec.md
|
|
2
|
-
// Replacement is section-anchored to prevent silent cross-section mutations:
|
|
3
|
-
// the originalValue is searched only inside the named section's body (between its
|
|
4
|
-
// heading and the next heading at the same or shallower level), and the change is
|
|
5
|
-
// applied only when the match is unique within those bounds.
|
|
6
1
|
import { readFile } from 'node:fs/promises';
|
|
7
2
|
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
8
|
-
import {
|
|
3
|
+
import { SECTIONS_WITHOUT_LITERAL_BODY_TEXT, } from '../../types/index.js';
|
|
9
4
|
function escapeRegex(str) {
|
|
10
5
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
6
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Find the body bounds of a markdown section by heading name.
|
|
14
|
-
* Returns the [start, end) offsets of the section body (after the heading line,
|
|
15
|
-
* up to the next heading at the same or shallower level — or EOF).
|
|
16
|
-
*/
|
|
17
7
|
function findSectionBounds(content, section) {
|
|
18
8
|
const headingRe = new RegExp(`^(#{1,6})\\s+${escapeRegex(section)}\\s*$`, 'mi');
|
|
19
9
|
const match = headingRe.exec(content);
|
|
@@ -22,16 +12,12 @@ function findSectionBounds(content, section) {
|
|
|
22
12
|
}
|
|
23
13
|
const headingLevel = match[1]?.length ?? 0;
|
|
24
14
|
const start = match.index + match[0].length;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const next = nextRe.exec(content);
|
|
15
|
+
const nextHeadingAtSameOrShallowerLevelRe = new RegExp(`^#{1,${String(headingLevel)}}\\s+\\S`, 'gm');
|
|
16
|
+
nextHeadingAtSameOrShallowerLevelRe.lastIndex = start;
|
|
17
|
+
const next = nextHeadingAtSameOrShallowerLevelRe.exec(content);
|
|
29
18
|
return { start, end: next ? next.index : content.length };
|
|
30
19
|
}
|
|
31
|
-
|
|
32
|
-
* Count non-overlapping occurrences of `needle` in `haystack`.
|
|
33
|
-
*/
|
|
34
|
-
function countOccurrences(haystack, needle) {
|
|
20
|
+
function countNonOverlappingOccurrences(haystack, needle) {
|
|
35
21
|
if (needle.length === 0) {
|
|
36
22
|
return 0;
|
|
37
23
|
}
|
|
@@ -45,20 +31,8 @@ function countOccurrences(haystack, needle) {
|
|
|
45
31
|
}
|
|
46
32
|
return count;
|
|
47
33
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Apply approved text changes to the spec.md body.
|
|
50
|
-
*
|
|
51
|
-
* For each non-metadata approved change:
|
|
52
|
-
* 1. Locate the named section's body via heading anchor.
|
|
53
|
-
* 2. Require exactly one occurrence of `originalValue` inside that section.
|
|
54
|
-
* 3. Replace it in-place at the resolved absolute offset.
|
|
55
|
-
*
|
|
56
|
-
* Skips (with reason) when the section heading is absent, the value is missing
|
|
57
|
-
* inside the section, or the value appears multiple times (ambiguous match).
|
|
58
|
-
* Writes only when at least one substitution succeeded.
|
|
59
|
-
*/
|
|
60
34
|
export async function applyChangesToSpec(specPath, changes) {
|
|
61
|
-
const approvedTextChanges = changes.filter((c) => c.approved && !
|
|
35
|
+
const approvedTextChanges = changes.filter((c) => c.approved && !SECTIONS_WITHOUT_LITERAL_BODY_TEXT.has(c.section));
|
|
62
36
|
if (approvedTextChanges.length === 0) {
|
|
63
37
|
return { applied: 0, skipped: [], fileWritten: false };
|
|
64
38
|
}
|
|
@@ -74,6 +48,7 @@ export async function applyChangesToSpec(specPath, changes) {
|
|
|
74
48
|
section: c.section,
|
|
75
49
|
originalValue: c.originalValue,
|
|
76
50
|
reason: `Cannot read spec.md: ${reason}`,
|
|
51
|
+
reasonCategory: 'skipped',
|
|
77
52
|
})),
|
|
78
53
|
fileWritten: false,
|
|
79
54
|
};
|
|
@@ -88,16 +63,18 @@ export async function applyChangesToSpec(specPath, changes) {
|
|
|
88
63
|
section: change.section,
|
|
89
64
|
originalValue: change.originalValue,
|
|
90
65
|
reason: `Section heading "${change.section}" not found in spec.md`,
|
|
66
|
+
reasonCategory: 'skipped',
|
|
91
67
|
});
|
|
92
68
|
continue;
|
|
93
69
|
}
|
|
94
70
|
const sectionBody = modified.slice(bounds.start, bounds.end);
|
|
95
|
-
const occurrences =
|
|
71
|
+
const occurrences = countNonOverlappingOccurrences(sectionBody, change.originalValue);
|
|
96
72
|
if (occurrences === 0) {
|
|
97
73
|
skipped.push({
|
|
98
74
|
section: change.section,
|
|
99
75
|
originalValue: change.originalValue,
|
|
100
76
|
reason: `originalValue not found in section "${change.section}"`,
|
|
77
|
+
reasonCategory: 'skipped',
|
|
101
78
|
});
|
|
102
79
|
continue;
|
|
103
80
|
}
|
|
@@ -106,6 +83,7 @@ export async function applyChangesToSpec(specPath, changes) {
|
|
|
106
83
|
section: change.section,
|
|
107
84
|
originalValue: change.originalValue,
|
|
108
85
|
reason: `originalValue is ambiguous (${String(occurrences)} matches in section "${change.section}")`,
|
|
86
|
+
reasonCategory: 'ambiguous',
|
|
109
87
|
});
|
|
110
88
|
continue;
|
|
111
89
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// engine/reconcile/verify-write.ts — SPEC-1011 Bug D: Post-write integrity verification
|
|
2
2
|
// After reconcile_spec writes changes, re-reads the file to assert each newValue is present.
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import {
|
|
4
|
+
import { SECTIONS_WITHOUT_LITERAL_BODY_TEXT, } from '../../types/index.js';
|
|
5
5
|
/**
|
|
6
6
|
* Re-reads the file at `filePath` and checks that each approved change's `newValue`
|
|
7
7
|
* is present somewhere in the file content.
|
|
@@ -30,8 +30,7 @@ export async function verifyWriteSucceeded(filePath, changes) {
|
|
|
30
30
|
}
|
|
31
31
|
const missingChanges = [];
|
|
32
32
|
for (const change of approvedChanges) {
|
|
33
|
-
|
|
34
|
-
if (RECONCILE_METADATA_SECTIONS.has(change.section)) {
|
|
33
|
+
if (SECTIONS_WITHOUT_LITERAL_BODY_TEXT.has(change.section)) {
|
|
35
34
|
continue;
|
|
36
35
|
}
|
|
37
36
|
if (!content.includes(change.newValue)) {
|
|
@@ -67,7 +67,23 @@ function buildCoverageMap(rulesFiles, allRules) {
|
|
|
67
67
|
*
|
|
68
68
|
* A file is "stale" when it has NO categories in common with CLAUDE.md categories.
|
|
69
69
|
*/
|
|
70
|
-
function
|
|
70
|
+
function manifestOwnedRuleFiles(projectPath) {
|
|
71
|
+
const raw = readFileSafe(join(projectPath, '.claude', 'rules', '.planu-rules-manifest.json'));
|
|
72
|
+
if (!raw) {
|
|
73
|
+
return new Set();
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const manifest = JSON.parse(raw);
|
|
77
|
+
return new Set((manifest.rules ?? [])
|
|
78
|
+
.map((rule) => rule.path)
|
|
79
|
+
.filter((path) => typeof path === 'string')
|
|
80
|
+
.map((path) => basename(path)));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return new Set();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function detectStaleRules(rulesFiles, allRules, claudeCategories, manifestOwned) {
|
|
71
87
|
// Build per-file category sets
|
|
72
88
|
const fileCategories = new Map();
|
|
73
89
|
for (const r of allRules) {
|
|
@@ -80,6 +96,9 @@ function detectStaleRules(rulesFiles, allRules, claudeCategories) {
|
|
|
80
96
|
}
|
|
81
97
|
const stale = [];
|
|
82
98
|
for (const filePath of rulesFiles) {
|
|
99
|
+
if (manifestOwned.has(basename(filePath))) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
83
102
|
const cats = fileCategories.get(filePath);
|
|
84
103
|
if (!cats || cats.size === 0) {
|
|
85
104
|
// No parseable conventions found — candidate for stale
|
|
@@ -162,7 +181,7 @@ export function reconcileRules(projectPath) {
|
|
|
162
181
|
}
|
|
163
182
|
}
|
|
164
183
|
// 5. Detect stale rules files
|
|
165
|
-
const staleRules = detectStaleRules(rulesFiles, allRules, claudeCategories);
|
|
184
|
+
const staleRules = detectStaleRules(rulesFiles, allRules, claudeCategories, manifestOwnedRuleFiles(projectPath));
|
|
166
185
|
// 6. Auto-patch: generate new rules files for uncovered categories
|
|
167
186
|
const updatedRules = [];
|
|
168
187
|
for (const category of newRulesNeeded) {
|
|
@@ -47,6 +47,8 @@ function keywords(phrase) {
|
|
|
47
47
|
function preservesBoundary(text) {
|
|
48
48
|
const normalized = normalize(text);
|
|
49
49
|
return [
|
|
50
|
+
/\balongside the (?:existing|current)\b/,
|
|
51
|
+
/\balready (?:targets?|does|do|supports?|covers?|handles?)\b/,
|
|
50
52
|
/\bremains? unchanged\b/,
|
|
51
53
|
/\bkeeps?\b.{0,80}\bunchanged\b/,
|
|
52
54
|
/\bpreserves?\b.{0,80}\b(?:behavior|contract|boundary|semantics|state)\b/,
|
|
@@ -62,11 +64,11 @@ function splitClauses(text) {
|
|
|
62
64
|
.filter(Boolean);
|
|
63
65
|
}
|
|
64
66
|
function hasAffirmativeDrift(text, outOfScopeItem) {
|
|
65
|
-
const action = /\b(?:changes?|modif(?:y|ies)|alters?|adds?|introduces?|implements?|enables
|
|
67
|
+
const action = /\b(?:changes?|modif(?:y|ies)|alters?|adds?|introduces?|implements?|enables?|exposes?|exposed)\b/g;
|
|
66
68
|
const actionSegments = text.split(/\band\b/i).map(normalize);
|
|
67
69
|
for (const [index, segment] of actionSegments.entries()) {
|
|
68
70
|
const inheritsRelevantSubject = index > 0 &&
|
|
69
|
-
/^(?:changes?|modif(?:y|ies)|alters?|adds?|introduces?|implements?|enables
|
|
71
|
+
/^(?:changes?|modif(?:y|ies)|alters?|adds?|introduces?|implements?|enables?|exposes?|exposed)\b/.test(segment) &&
|
|
70
72
|
clauseMatchesScope(actionSegments[index - 1] ?? '', outOfScopeItem);
|
|
71
73
|
for (const match of segment.matchAll(action)) {
|
|
72
74
|
const before = segment.slice(Math.max(0, match.index - 24), match.index);
|
|
@@ -84,6 +86,9 @@ function hasAffirmativeDrift(text, outOfScopeItem) {
|
|
|
84
86
|
}
|
|
85
87
|
return false;
|
|
86
88
|
}
|
|
89
|
+
function containsKeyword(normalizedHaystack, keyword) {
|
|
90
|
+
return new RegExp(`\\b${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(normalizedHaystack);
|
|
91
|
+
}
|
|
87
92
|
function clauseMatchesScope(clause, outOfScopeItem) {
|
|
88
93
|
const normalizedClause = normalize(clause);
|
|
89
94
|
const normalizedScope = normalize(outOfScopeItem);
|
|
@@ -91,10 +96,14 @@ function clauseMatchesScope(clause, outOfScopeItem) {
|
|
|
91
96
|
return true;
|
|
92
97
|
}
|
|
93
98
|
const scopeKeywords = keywords(outOfScopeItem);
|
|
94
|
-
const matchCount = scopeKeywords.filter((keyword) => normalizedClause
|
|
99
|
+
const matchCount = scopeKeywords.filter((keyword) => containsKeyword(normalizedClause, keyword)).length;
|
|
95
100
|
const threshold = scopeKeywords.length <= 2 ? scopeKeywords.length : 2;
|
|
96
101
|
return scopeKeywords.length > 0 && matchCount >= threshold;
|
|
97
102
|
}
|
|
103
|
+
function clauseMentionsScope(clause, outOfScopeItem) {
|
|
104
|
+
const normalizedClause = normalize(clause);
|
|
105
|
+
return keywords(outOfScopeItem).some((keyword) => containsKeyword(normalizedClause, keyword));
|
|
106
|
+
}
|
|
98
107
|
/**
|
|
99
108
|
* Determine whether a criterion text contradicts an out-of-scope item.
|
|
100
109
|
* Uses substring match first, then keyword overlap as fuzzy fallback.
|
|
@@ -102,11 +111,12 @@ function clauseMatchesScope(clause, outOfScopeItem) {
|
|
|
102
111
|
function contradicts(criterionText, outOfScopeItem) {
|
|
103
112
|
const normCriterion = normalize(criterionText);
|
|
104
113
|
const normScope = normalize(outOfScopeItem);
|
|
105
|
-
const
|
|
114
|
+
const clauses = splitClauses(criterionText);
|
|
115
|
+
const relevantClauses = clauses.filter((clause) => clauseMentionsScope(clause, outOfScopeItem));
|
|
106
116
|
const hasConditionalException = /\b(?:unless|except)\b/i.test(criterionText);
|
|
107
117
|
if (!hasConditionalException &&
|
|
108
|
-
relevantClauses.
|
|
109
|
-
relevantClauses.
|
|
118
|
+
relevantClauses.some(preservesBoundary) &&
|
|
119
|
+
!relevantClauses.some((clause) => hasAffirmativeDrift(clause, outOfScopeItem))) {
|
|
110
120
|
return false;
|
|
111
121
|
}
|
|
112
122
|
// 1. Direct substring match
|
|
@@ -117,15 +127,12 @@ function contradicts(criterionText, outOfScopeItem) {
|
|
|
117
127
|
if (normScope.includes(normCriterion)) {
|
|
118
128
|
return true;
|
|
119
129
|
}
|
|
120
|
-
// 3. Keyword overlap: if 2+ significant keywords from
|
|
121
|
-
|
|
122
|
-
if (
|
|
130
|
+
// 3. Keyword overlap, scoped to a single clause: if 2+ significant keywords from
|
|
131
|
+
// outOfScope appear within the same clause of the criterion
|
|
132
|
+
if (keywords(outOfScopeItem).length === 0) {
|
|
123
133
|
return false;
|
|
124
134
|
}
|
|
125
|
-
|
|
126
|
-
// For 1 keyword: require it to match; for 2 keywords: require both; for 3+: require 2
|
|
127
|
-
const threshold = scopeKws.length === 1 ? 1 : scopeKws.length === 2 ? 2 : 2;
|
|
128
|
-
return matchCount >= threshold;
|
|
135
|
+
return clauses.some((clause) => clauseMatchesScope(clause, outOfScopeItem));
|
|
129
136
|
}
|
|
130
137
|
/**
|
|
131
138
|
* Check acceptance criteria against the spec's outOfScope declarations.
|
|
@@ -5,7 +5,7 @@ import { stripFencedBlocks } from './text-fences.js';
|
|
|
5
5
|
import { createCriterionIdentity, normalizeCriterionText } from '../criterion-identity.js';
|
|
6
6
|
const BDD_STEP = /^(GIVEN|WHEN|THEN|AND|DADO|CUANDO|ENTONCES|Y)\b/i;
|
|
7
7
|
const BDD_START = /^(GIVEN|DADO)\b/i;
|
|
8
|
-
const TECHNICAL_MARKER = /^(FILES|FUNCTIONS|TEST|OWNER):/i;
|
|
8
|
+
const TECHNICAL_MARKER = /^(FILES|FUNCTIONS|TEST|OWNER|NOTE|AB\s*-?\s*\d+|ANTICIPATED TEST BREAK|TEST BREAK):/i;
|
|
9
9
|
const EXPLICIT_CRITERION_HEADING = /^#{3,6}\s+((?:AC|AB)\s*-?\s*\d+)\b/i;
|
|
10
10
|
function extractBddBlocks(section) {
|
|
11
11
|
const lines = section.split('\n').map((line) => line.trim());
|
|
@@ -38,6 +38,12 @@ function extractBddBlocks(section) {
|
|
|
38
38
|
current.push(withoutListMarker);
|
|
39
39
|
continue;
|
|
40
40
|
}
|
|
41
|
+
// Wrapped continuation of the step above: keep it, or the criterion identity
|
|
42
|
+
// truncates at the physical Markdown wrap boundary (SPEC-1253). A line that
|
|
43
|
+
// carried its own list marker is a sibling item, never a wrap.
|
|
44
|
+
if (current.length > 0 && withoutListMarker === line) {
|
|
45
|
+
current.push(withoutListMarker);
|
|
46
|
+
}
|
|
41
47
|
}
|
|
42
48
|
if (current.length > 0) {
|
|
43
49
|
bddBlocks.push(current.join(' '));
|
|
@@ -87,7 +93,10 @@ function extractSectionCriteria(section) {
|
|
|
87
93
|
const listItems = extractListItems(section).filter((item) => !TECHNICAL_MARKER.test(item));
|
|
88
94
|
if (listItems.length > 0) {
|
|
89
95
|
const nonStepItems = listItems.filter((item) => !BDD_STEP.test(item));
|
|
90
|
-
|
|
96
|
+
if (nonStepItems.length === listItems.length) {
|
|
97
|
+
return nonStepItems.length > 0 ? nonStepItems : extractBddBlocks(section);
|
|
98
|
+
}
|
|
99
|
+
return [...extractBddBlocks(section), ...nonStepItems];
|
|
91
100
|
}
|
|
92
101
|
return extractBddBlocks(section);
|
|
93
102
|
}
|