@planu/cli 5.3.22 → 5.3.24
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 +24 -0
- package/dist/engine/evidence-index/done-drift.js +15 -9
- package/dist/engine/execution/validate-job-executor.js +22 -9
- 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/readiness-checker.d.ts +5 -0
- package/dist/engine/readiness-checker.js +6 -7
- package/dist/engine/spec-format/technical-md-populator.js +37 -5
- package/dist/tools/challenge-spec.js +67 -2
- package/dist/tools/create-spec.d.ts +21 -0
- package/dist/tools/create-spec.js +92 -2
- package/dist/tools/schemas/validate-output-schema.d.ts +1 -0
- package/dist/tools/schemas/validate-output-schema.js +1 -0
- package/dist/tools/validate.js +4 -3
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
## [5.3.24] - 2026-08-18
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(spec-1558): regenerate source-quality receipt bound to the amended release commit
|
|
5
|
+
- fix(spec-1553): declare attachedAt in the published validate output schema
|
|
6
|
+
- fix(spec-1553): name the receipt publication cause instead of one flat string
|
|
7
|
+
- fix(spec-1554): bound the Technical extractor to the section heading level
|
|
8
|
+
- fix(spec-1552): surface the stored rejection reason in done-drift diagnostics
|
|
9
|
+
- fix(spec-1549): resolve out-of-scope items from the spec document, not the cache
|
|
10
|
+
- fix(spec-1525): admit prose file paths outside src/ and tests/ without truncating them
|
|
11
|
+
- fix(spec-1540): scope the native lockfile mask to a version-controlled lockfile
|
|
12
|
+
- fix(spec-1524): bind the source-quality receipt to a mask-normalized tree digest
|
|
13
|
+
|
|
14
|
+
### Chores
|
|
15
|
+
- chore(spec-1553): close lifecycle with reconciled scope and done evidence
|
|
16
|
+
- chore(spec-1524): close lifecycle with reconciled scope and done evidence
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## [5.3.23] - 2026-08-13
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
- fix(spec-1520): exclude paths whose only mention negates the work from grounded modify targets
|
|
23
|
+
|
|
24
|
+
|
|
1
25
|
## [5.3.22] - 2026-08-13
|
|
2
26
|
|
|
3
27
|
### Bug Fixes
|
|
@@ -4,6 +4,20 @@ function normalizedChangedFile(value) {
|
|
|
4
4
|
const normalized = normalizeChangedFilePath(value);
|
|
5
5
|
return normalized.status === 'valid' ? normalized.path : null;
|
|
6
6
|
}
|
|
7
|
+
function withReason(status, reason) {
|
|
8
|
+
return reason ? `${status}: ${reason}` : status;
|
|
9
|
+
}
|
|
10
|
+
function formatStaleOrInvalidRecord(entry, record) {
|
|
11
|
+
if (record.kind !== 'changed-file') {
|
|
12
|
+
return `${entry.criterion}: ${record.value} (${withReason(record.status, record.reason)})`;
|
|
13
|
+
}
|
|
14
|
+
const normalized = normalizedChangedFile(record.value);
|
|
15
|
+
if (!normalized) {
|
|
16
|
+
return `${entry.criterion}: invalid changed-file (${record.status})`;
|
|
17
|
+
}
|
|
18
|
+
const normalizedReason = record.reason?.split(record.value).join(normalized);
|
|
19
|
+
return `${entry.criterion}: ${normalized} (${withReason(record.status, normalizedReason)})`;
|
|
20
|
+
}
|
|
7
21
|
function buildNeverDriftPaths(ownSpecMdPath) {
|
|
8
22
|
const neverDriftPaths = new Set(PLANU_BOOKKEEPING_FILES);
|
|
9
23
|
const normalizedOwnSpecMdPath = ownSpecMdPath ? normalizedChangedFile(ownSpecMdPath) : null;
|
|
@@ -49,15 +63,7 @@ export function checkDoneDriftContract(args) {
|
|
|
49
63
|
}
|
|
50
64
|
const staleOrInvalid = args.index.criteria.flatMap((entry) => entry.evidence
|
|
51
65
|
.filter((record) => record.status === 'stale' || record.status === 'invalid')
|
|
52
|
-
.map((record) =>
|
|
53
|
-
if (record.kind !== 'changed-file') {
|
|
54
|
-
return `${entry.criterion}: ${record.value} (${record.status})`;
|
|
55
|
-
}
|
|
56
|
-
const normalized = normalizedChangedFile(record.value);
|
|
57
|
-
return normalized
|
|
58
|
-
? `${entry.criterion}: ${normalized} (${record.status})`
|
|
59
|
-
: `${entry.criterion}: invalid changed-file (${record.status})`;
|
|
60
|
-
}));
|
|
66
|
+
.map((record) => formatStaleOrInvalidRecord(entry, record)));
|
|
61
67
|
if (staleOrInvalid.length > 0) {
|
|
62
68
|
issues.push({
|
|
63
69
|
code: 'done_drift_stale_evidence',
|
|
@@ -22,7 +22,11 @@ import { MacOSKeychainOperationError } from '../../security/adapters/macos-keych
|
|
|
22
22
|
import { createDurableJobHeartbeat } from './durable-job-heartbeat-runtime.js';
|
|
23
23
|
const WORKSPACE_ID = 'global';
|
|
24
24
|
const activeScopes = new Map();
|
|
25
|
-
const
|
|
25
|
+
const RECEIPT_PUBLICATION_FAILURE_MESSAGES = {
|
|
26
|
+
keychain_timeout: '[Planu] VALIDATION_RECEIPT_PENDING action=restart_job cause=keychain_timeout: the signing key could not be read from the keychain before the read timed out',
|
|
27
|
+
publication_deadline_elapsed: '[Planu] VALIDATION_RECEIPT_PENDING action=restart_job cause=publication_deadline_elapsed: the receipt publication deadline elapsed before the receipt was signed',
|
|
28
|
+
publication_interrupted: '[Planu] VALIDATION_RECEIPT_PENDING action=restart_job cause=publication_interrupted: receipt publication was interrupted after the receipt was signed and persisted; check the provenance files under handoffs/<spec>/*.provenance.json for an already-persisted receiptId before restarting',
|
|
29
|
+
};
|
|
26
30
|
class ReceiptPublicationTimeoutError extends Error {
|
|
27
31
|
constructor() {
|
|
28
32
|
super('[Planu] Receipt publication deadline elapsed');
|
|
@@ -43,10 +47,17 @@ class ValidationAuthorityLostError extends Error {
|
|
|
43
47
|
this.name = 'ValidationAuthorityLostError';
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
function classifyReceiptPublicationFailure(error) {
|
|
51
|
+
if (error instanceof ReceiptPublicationTimeoutError) {
|
|
52
|
+
return 'publication_deadline_elapsed';
|
|
53
|
+
}
|
|
54
|
+
if (error instanceof ReceiptPublicationInterruptedError) {
|
|
55
|
+
return 'publication_interrupted';
|
|
56
|
+
}
|
|
57
|
+
if (error instanceof MacOSKeychainOperationError && error.code === 'KEYCHAIN_TIMEOUT') {
|
|
58
|
+
return 'keychain_timeout';
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
50
61
|
}
|
|
51
62
|
function decodeUtf8Prefix(encoded, prefixLength) {
|
|
52
63
|
try {
|
|
@@ -321,13 +332,14 @@ export class ValidateJobExecutor {
|
|
|
321
332
|
if (current.state === 'cancelled') {
|
|
322
333
|
return current;
|
|
323
334
|
}
|
|
324
|
-
|
|
335
|
+
const receiptFailureCause = classifyReceiptPublicationFailure(error);
|
|
336
|
+
if (receiptFailureCause &&
|
|
325
337
|
parseValidationReceiptPendingCheckpoint(current.checkpoint, this.receiptCheckpointLimits())) {
|
|
326
338
|
return this.runtime.fail({
|
|
327
339
|
...identity,
|
|
328
340
|
workerId: this.workerId,
|
|
329
341
|
fencingToken,
|
|
330
|
-
error:
|
|
342
|
+
error: RECEIPT_PUBLICATION_FAILURE_MESSAGES[receiptFailureCause],
|
|
331
343
|
});
|
|
332
344
|
}
|
|
333
345
|
if (parseValidationReceiptPendingCheckpoint(current.checkpoint, this.receiptCheckpointLimits())) {
|
|
@@ -430,12 +442,13 @@ export class ValidateJobExecutor {
|
|
|
430
442
|
if (current.state === 'cancelled') {
|
|
431
443
|
return current;
|
|
432
444
|
}
|
|
433
|
-
|
|
445
|
+
const receiptFailureCause = classifyReceiptPublicationFailure(error);
|
|
446
|
+
if (receiptFailureCause) {
|
|
434
447
|
return this.runtime.fail({
|
|
435
448
|
...identity,
|
|
436
449
|
workerId: this.workerId,
|
|
437
450
|
fencingToken,
|
|
438
|
-
error:
|
|
451
|
+
error: RECEIPT_PUBLICATION_FAILURE_MESSAGES[receiptFailureCause],
|
|
439
452
|
});
|
|
440
453
|
}
|
|
441
454
|
this.recordFencedProgress(identity, fencingToken, {
|
|
@@ -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.24",
|
|
4
|
+
"engineVersion": "5.3.24",
|
|
5
|
+
"buildId": "1-5.3.24-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": "
|
|
20
|
+
"artifactSha256": "2df1b275f0d3987ef6fa75a7d46b274c798aece674ff001ab7fb7af169cfc1f3",
|
|
21
21
|
"sbom": {
|
|
22
22
|
"format": "CycloneDX-1.5",
|
|
23
23
|
"path": "planu-core.darwin-arm64.node.sbom.json",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "9899ebee8d32c9b58992bd9a95647e85bd86ac76ee64c34f534f5ce18ec33c2c"
|
|
25
25
|
},
|
|
26
26
|
"provenance": {
|
|
27
27
|
"builder": "scripts/build-rust-local.sh",
|
|
28
|
-
"sourceCommit": "
|
|
28
|
+
"sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
|
|
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": "
|
|
37
|
+
"value": "0/ir1zotrdZMQ3fKgbpHGoU6fXXo8qZp+Ugs18Oslbxb6vVvRBuj2UoKofDsyPGjx2mHadvTUPX42B/EZd1SAg=="
|
|
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.
|
|
7
|
+
"bom-ref": "pkg:cargo/planu-core@5.3.24",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.24",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "2df1b275f0d3987ef6fa75a7d46b274c798aece674ff001ab7fb7af169cfc1f3"
|
|
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.24",
|
|
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.24",
|
|
4
|
+
"engineVersion": "5.3.24",
|
|
5
|
+
"buildId": "1-5.3.24-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": "
|
|
20
|
+
"artifactSha256": "136a1351ca4278942b7c5b0130eb7cdfee35b85b667888d4041bffcd0b213ace",
|
|
21
21
|
"sbom": {
|
|
22
22
|
"format": "CycloneDX-1.5",
|
|
23
23
|
"path": "planu-core.darwin-x64.node.sbom.json",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "3f8f6cace3e98634bc5fb5b8debb71c45507b890b4aa918437358cf16b55d776"
|
|
25
25
|
},
|
|
26
26
|
"provenance": {
|
|
27
27
|
"builder": "scripts/build-rust-local.sh",
|
|
28
|
-
"sourceCommit": "
|
|
28
|
+
"sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
|
|
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": "
|
|
37
|
+
"value": "Aj0I1f16YmStyyCshvbh7h+leFhLBtGAOv5Jy8lCH5oXVc2RBirqfHNAU/4yQ1vFu3JTl3H5hIaa1Kc1LwQtCg=="
|
|
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.
|
|
7
|
+
"bom-ref": "pkg:cargo/planu-core@5.3.24",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.24",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "136a1351ca4278942b7c5b0130eb7cdfee35b85b667888d4041bffcd0b213ace"
|
|
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.24",
|
|
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.24",
|
|
4
|
+
"engineVersion": "5.3.24",
|
|
5
|
+
"buildId": "1-5.3.24-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": "
|
|
19
|
+
"artifactSha256": "5c16c2a04af25efc03e74eecfacaad43f5835dfb01f0a898f6d2b93efd9a3c33",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.linux-arm64-gnu.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "48a3812ed288b4801a82bf40f37d02e48e014a90f58f7bc955c2b480749cd44e"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
|
|
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": "ogNVi4bjUVYnZ3o32PbiLKjRM4FhpcRuLoWSBpHe+vMe/M+oL+aZAX3M86UdTHoxEtUm32rJJPBBXA3CpQzIBQ=="
|
|
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.24",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.24",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "5c16c2a04af25efc03e74eecfacaad43f5835dfb01f0a898f6d2b93efd9a3c33"
|
|
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.24",
|
|
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.24",
|
|
4
|
+
"engineVersion": "5.3.24",
|
|
5
|
+
"buildId": "1-5.3.24-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": "
|
|
19
|
+
"artifactSha256": "8a4cd204fed117c35b4daea2d03d7f36c5ac2309967ed1d0f48e4291c19a705f",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.linux-arm64-musl.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "812aeaa48a5c2d31b1b108a005eb47586f8a3cf48de44ee7a43b356d7626a286"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
|
|
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": "3w2XPj4GH8F/UdRBJMNWiOacR+v5h2H/aof6SqdJDADu5S2sGg0IEolD9m7sB3HE/ZgPL7iulY4KPckXGMNnAQ=="
|
|
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.24",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.24",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "8a4cd204fed117c35b4daea2d03d7f36c5ac2309967ed1d0f48e4291c19a705f"
|
|
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.24",
|
|
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.24",
|
|
4
|
+
"engineVersion": "5.3.24",
|
|
5
|
+
"buildId": "1-5.3.24-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": "
|
|
19
|
+
"artifactSha256": "a9158723fa6e3e5972ffc03b6115035c1f0dd6be8b109b4ae1f7f5dad54f9062",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.linux-x64-gnu.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "34a5c6540f4234a5ac3d4b3d1be2ff50e28b294161a4b4190996e28b3defc899"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
|
|
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": "Gz3e+x1vn4R0ppFCaBwO4vQH1RqrV3PvTp7/Zi6V0mu3u6o1GYmo7JrLvlmR4I34IWkfO+cG6Hk/Ls++j7dnBw=="
|
|
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.24",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.24",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "a9158723fa6e3e5972ffc03b6115035c1f0dd6be8b109b4ae1f7f5dad54f9062"
|
|
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.24",
|
|
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.24",
|
|
4
|
+
"engineVersion": "5.3.24",
|
|
5
|
+
"buildId": "1-5.3.24-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": "165e48d9d7bb68965782b9f690af378cee9651c9c1b9ab31e6b9fa4bedad514a",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.linux-x64-musl.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "101e2eca5854d33b36a9570a10e8f4816f43961ad899863110ba4d2596cf89dc"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
|
|
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": "bHo9hflPuXFXSTyWqcxf2rV+OlBmqGvUMOgp4WV7TfKOC1nCUV7Ay/1LJXlgqN/XykbfHC8821FpHWLXQSnJDQ=="
|
|
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.24",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.24",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "165e48d9d7bb68965782b9f690af378cee9651c9c1b9ab31e6b9fa4bedad514a"
|
|
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.24",
|
|
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.24",
|
|
4
|
+
"engineVersion": "5.3.24",
|
|
5
|
+
"buildId": "1-5.3.24-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": "363abf41c92e4a6d5e4f7cc821185cca9a84fe5fb99006b8e01d16d106bbfa52",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.win32-arm64-msvc.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "5f31abd4a8f741b8a72701f428fa777b41a504c1c095f903bdf54b5ccd93b74e"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
|
|
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": "Dj6P860+E1Yi4MGQd3lnA6OmAmf5EvL8mZqfiI81OwYH19GQwnKnBUi5vjh64I781B1lyOwUOPjU+jLpJ8lhCQ=="
|
|
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.24",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.24",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "363abf41c92e4a6d5e4f7cc821185cca9a84fe5fb99006b8e01d16d106bbfa52"
|
|
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.24",
|
|
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.24",
|
|
4
|
+
"engineVersion": "5.3.24",
|
|
5
|
+
"buildId": "1-5.3.24-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": "0b6374ac67d414e13e2e02be6be96a388ed9096f517587f812c533c5fb37992f",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.win32-x64-msvc.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "aaaafe7c267638012a520782ad059014a0641b55005e2a09a5bace53a00d3d3e"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
|
|
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": "zUMqIwI3WSWZ0QbesWdiZfnIlwAnwkhfwN2Us0lmUCtoKOtQEkqckxNZCY7tuxbzhS091T5LUp7Y+HOUeUViCg=="
|
|
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.24",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.24",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "0b6374ac67d414e13e2e02be6be96a388ed9096f517587f812c533c5fb37992f"
|
|
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.24",
|
|
1110
1110
|
"dependsOn": [
|
|
1111
1111
|
"pkg:cargo/core-foundation@0.10.1",
|
|
1112
1112
|
"pkg:cargo/hmac@0.12.1",
|
|
@@ -17,6 +17,11 @@ import type { Spec, ReadinessReport } from '../types/index.js';
|
|
|
17
17
|
* paragraphs inside the AC section are visible to checkSpecificityGate.
|
|
18
18
|
*/
|
|
19
19
|
export declare function extractCriteriaLines(huContent: string): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Extract the content of the `## Technical` section from a spec body.
|
|
22
|
+
* Returns an empty string when the section is absent.
|
|
23
|
+
*/
|
|
24
|
+
export declare function extractTechnicalSection(body: string): string;
|
|
20
25
|
/**
|
|
21
26
|
* SPEC-784: Returns true when the ## Technical body contains a pointer
|
|
22
27
|
* like "See technical.md" or "See `some-file` spec.md" — a placeholder
|
|
@@ -205,17 +205,16 @@ function buildRecommendations(score, huPoints, criteriaPoints, filesPoints, deps
|
|
|
205
205
|
* Extract the content of the `## Technical` section from a spec body.
|
|
206
206
|
* Returns an empty string when the section is absent.
|
|
207
207
|
*/
|
|
208
|
-
function extractTechnicalSection(body) {
|
|
209
|
-
|
|
210
|
-
const sectionRe = /^#{1,6}\s+technical\b[^\n]*/im;
|
|
208
|
+
export function extractTechnicalSection(body) {
|
|
209
|
+
const sectionRe = /^(#{1,6})\s+technical\b[^\n]*/im;
|
|
211
210
|
const match = sectionRe.exec(body);
|
|
212
|
-
|
|
211
|
+
const headingHashes = match?.[1];
|
|
212
|
+
if (!match || !headingHashes) {
|
|
213
213
|
return '';
|
|
214
214
|
}
|
|
215
215
|
const afterHeader = body.slice(match.index + match[0].length);
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
const nextMatch = nextHeadingRe.exec(afterHeader);
|
|
216
|
+
const sameOrHigherHeadingRe = new RegExp(`^#{1,${String(headingHashes.length)}}\\s+`, 'm');
|
|
217
|
+
const nextMatch = sameOrHigherHeadingRe.exec(afterHeader);
|
|
219
218
|
return nextMatch ? afterHeader.slice(0, nextMatch.index) : afterHeader;
|
|
220
219
|
}
|
|
221
220
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
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
|
-
import { join } from 'node:path';
|
|
3
|
+
import { dirname, extname, join } from 'node:path';
|
|
4
4
|
import { findMarkdownSectionRange } from './markdown-sections.js';
|
|
5
|
-
const FILE_PATH_RE =
|
|
5
|
+
const FILE_PATH_RE = /(?<![\w/.:-])\.?[\w-]+(?:\.[\w-]+)*(?:\/\.?[\w-]+(?:\.[\w-]+)*)+/g;
|
|
6
6
|
function stripFencedBlocks(content) {
|
|
7
7
|
const kept = [];
|
|
8
8
|
let fence = null;
|
|
@@ -24,8 +24,8 @@ function stripFencedBlocks(content) {
|
|
|
24
24
|
}
|
|
25
25
|
return kept.join('\n');
|
|
26
26
|
}
|
|
27
|
-
const SLASH_PATH_BULLET_RE = /^-\s+`?((?:[\w.-]+\/)+[\w-][\w.-]
|
|
28
|
-
const BARE_FILE_BULLET_RE = /^-\s+`?([\w-][\w.-]*\.[a-zA-Z][a-zA-Z0-9]
|
|
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]*|[\w-]+)`?\s*$/;
|
|
29
29
|
function extractPathsFromSection(sectionText) {
|
|
30
30
|
const paths = [];
|
|
31
31
|
for (const line of sectionText.split('\n')) {
|
|
@@ -81,6 +81,32 @@ async function categorizeByExistence(paths, projectPath) {
|
|
|
81
81
|
}
|
|
82
82
|
return { create, modify, test };
|
|
83
83
|
}
|
|
84
|
+
function hasLetterInitialExtension(candidate) {
|
|
85
|
+
return /^[a-zA-Z]/.test(extname(candidate).slice(1));
|
|
86
|
+
}
|
|
87
|
+
async function isExistingFile(path) {
|
|
88
|
+
try {
|
|
89
|
+
return (await stat(path)).isFile();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function isExistingDirectory(path) {
|
|
96
|
+
try {
|
|
97
|
+
return (await stat(path)).isDirectory();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function isAdmissibleProseCandidate(candidate, projectPath) {
|
|
104
|
+
const full = join(projectPath, candidate);
|
|
105
|
+
if (await isExistingFile(full)) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return hasLetterInitialExtension(candidate) && (await isExistingDirectory(dirname(full)));
|
|
109
|
+
}
|
|
84
110
|
/**
|
|
85
111
|
* Attempt to extract file paths from spec body for auto-populating ## Technical.
|
|
86
112
|
* Returns null when no structured file section is found (caller uses placeholder).
|
|
@@ -91,9 +117,15 @@ export async function extractFilesFromSpecBody(specBody, projectPath) {
|
|
|
91
117
|
(structured.create.length > 0 || structured.modify.length > 0 || structured.test.length > 0)) {
|
|
92
118
|
return structured;
|
|
93
119
|
}
|
|
94
|
-
const
|
|
120
|
+
const candidates = [
|
|
95
121
|
...new Set(Array.from(stripFencedBlocks(specBody).matchAll(FILE_PATH_RE), (m) => m[0])),
|
|
96
122
|
];
|
|
123
|
+
const allPaths = [];
|
|
124
|
+
for (const candidate of candidates) {
|
|
125
|
+
if (await isAdmissibleProseCandidate(candidate, projectPath)) {
|
|
126
|
+
allPaths.push(candidate);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
97
129
|
if (allPaths.length === 0) {
|
|
98
130
|
return null;
|
|
99
131
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// tools/challenge-spec.ts — Devil's advocate resilience testing (SPEC-595: elicitation added)
|
|
2
2
|
// Analyzes a spec from adversarial perspectives: failure scenarios,
|
|
3
3
|
// concurrency issues, scale limits, security holes, and data consistency.
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
4
5
|
import { specStore, knowledgeStore } from '../storage/index.js';
|
|
5
6
|
// SPEC-1011 Bug F: fallback resolver using disk fingerprints
|
|
6
7
|
import { resolveProjectFromPath } from '../storage/project-resolver.js';
|
|
@@ -27,6 +28,69 @@ const ALL_FOCUS_AREAS = [
|
|
|
27
28
|
'security',
|
|
28
29
|
'data-consistency',
|
|
29
30
|
];
|
|
31
|
+
function extractOutOfScopeSection(content) {
|
|
32
|
+
const headingPattern = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
|
|
33
|
+
const fencePattern = /^[ \t]{0,3}(`{3,}|~{3,})/;
|
|
34
|
+
let fence = null;
|
|
35
|
+
let found = false;
|
|
36
|
+
let capturing = false;
|
|
37
|
+
const captured = [];
|
|
38
|
+
for (const line of content.split('\n')) {
|
|
39
|
+
const fenceMatch = fencePattern.exec(line);
|
|
40
|
+
if (fenceMatch?.[1]) {
|
|
41
|
+
const marker = fenceMatch[1].charAt(0);
|
|
42
|
+
if (fence === null) {
|
|
43
|
+
fence = { marker, length: fenceMatch[1].length };
|
|
44
|
+
}
|
|
45
|
+
else if (fence.marker === marker && fenceMatch[1].length >= fence.length) {
|
|
46
|
+
fence = null;
|
|
47
|
+
}
|
|
48
|
+
if (capturing) {
|
|
49
|
+
captured.push(line);
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (fence !== null) {
|
|
54
|
+
if (capturing) {
|
|
55
|
+
captured.push(line);
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const heading = headingPattern.exec(line);
|
|
60
|
+
if (heading?.[1] && heading[2] !== undefined) {
|
|
61
|
+
if (capturing) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (!found && heading[2].trim().toLowerCase() === 'out of scope') {
|
|
65
|
+
found = true;
|
|
66
|
+
capturing = true;
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (capturing) {
|
|
71
|
+
captured.push(line);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return found ? captured.join('\n') : null;
|
|
75
|
+
}
|
|
76
|
+
async function resolveDocumentOutOfScope(spec) {
|
|
77
|
+
if (!spec.specPath) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
let fileContent;
|
|
81
|
+
try {
|
|
82
|
+
fileContent = await readFile(spec.specPath, 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const sectionContent = extractOutOfScopeSection(fileContent);
|
|
88
|
+
return sectionContent === null ? [] : extractListItems(sectionContent);
|
|
89
|
+
}
|
|
90
|
+
async function resolveOutOfScopeItems(spec) {
|
|
91
|
+
const documentItems = await resolveDocumentOutOfScope(spec);
|
|
92
|
+
return documentItems.length > 0 ? documentItems : (spec.outOfScope ?? []);
|
|
93
|
+
}
|
|
30
94
|
/**
|
|
31
95
|
* SPEC-615 AC3: detect contradictions between spec criteria and prior decisions.
|
|
32
96
|
* Mutates failureScenarios in place. Best-effort — never throws.
|
|
@@ -150,12 +214,13 @@ export async function handleChallengeSpec(args, server) {
|
|
|
150
214
|
}
|
|
151
215
|
failureScenarios.push(...collectCapabilityScenarios({ spec, specContent, knowledge, focusAreas, capabilities }));
|
|
152
216
|
// SPEC-612: Check for contradictions between outOfScope and acceptance criteria
|
|
153
|
-
|
|
217
|
+
const resolvedOutOfScope = await resolveOutOfScopeItems(spec);
|
|
218
|
+
if (resolvedOutOfScope.length > 0) {
|
|
154
219
|
const criteriaRange = findMarkdownSectionRange(specContent, 'Acceptance Criteria');
|
|
155
220
|
const criteriaTexts = criteriaRange === null
|
|
156
221
|
? []
|
|
157
222
|
: extractListItems(specContent.slice(criteriaRange.contentStart, criteriaRange.end));
|
|
158
|
-
const contradictionResult = checkScopeContradictions(
|
|
223
|
+
const contradictionResult = checkScopeContradictions(resolvedOutOfScope, criteriaTexts);
|
|
159
224
|
for (const c of contradictionResult.contradictions) {
|
|
160
225
|
failureScenarios.push({
|
|
161
226
|
scenario: `Criterion contradicts outOfScope declaration: "${c.contradicts}"`,
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import type { CreateSpecInput, ToolResult } from '../types/index.js';
|
|
2
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { type LeanFileEntry } from '../engine/spec-format/lean-technical-generator.js';
|
|
4
|
+
import { type AutopilotAnalysis } from './create-spec/autopilot-analyzer.js';
|
|
5
|
+
import type { TechnicalReferenceGroundingRecord } from '../types/spec-grounding.js';
|
|
6
|
+
export declare function groundTechnicalFiles(input: {
|
|
7
|
+
files: {
|
|
8
|
+
create: LeanFileEntry[];
|
|
9
|
+
modify: LeanFileEntry[];
|
|
10
|
+
test: LeanFileEntry[];
|
|
11
|
+
};
|
|
12
|
+
projectPath: string;
|
|
13
|
+
userInput: string;
|
|
14
|
+
autopilot: AutopilotAnalysis;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
files: {
|
|
17
|
+
create: LeanFileEntry[];
|
|
18
|
+
modify: LeanFileEntry[];
|
|
19
|
+
test: LeanFileEntry[];
|
|
20
|
+
};
|
|
21
|
+
records: TechnicalReferenceGroundingRecord[];
|
|
22
|
+
advisoryFiles: LeanFileEntry[];
|
|
23
|
+
}>;
|
|
3
24
|
export declare function resolveGroundedVerificationCommands(projectPath: string, testPaths: string[]): Promise<string[]>;
|
|
4
25
|
export declare function handleCreateSpec(inputParams: CreateSpecInput, server?: McpServer): Promise<ToolResult>;
|
|
5
26
|
//# sourceMappingURL=create-spec.d.ts.map
|
|
@@ -102,7 +102,7 @@ async function resolveTechnicalFiles(input) {
|
|
|
102
102
|
}
|
|
103
103
|
return { create: [], modify: [], test: [] };
|
|
104
104
|
}
|
|
105
|
-
async function groundTechnicalFiles(input) {
|
|
105
|
+
export async function groundTechnicalFiles(input) {
|
|
106
106
|
const userInput = input.userInput.toLowerCase();
|
|
107
107
|
const autopilotPaths = new Set([
|
|
108
108
|
...input.autopilot.suggestedFiles.modify.map((file) => file.path),
|
|
@@ -118,7 +118,7 @@ async function groundTechnicalFiles(input) {
|
|
|
118
118
|
const advisoryFiles = [];
|
|
119
119
|
for (const section of ['create', 'modify', 'test']) {
|
|
120
120
|
for (const file of input.files[section]) {
|
|
121
|
-
const pathInUserInput = userInput
|
|
121
|
+
const pathInUserInput = pathSurvivesNegativeClauseCheck(userInput, file.path.toLowerCase());
|
|
122
122
|
const exists = await pathExists(pathJoin(input.projectPath, file.path));
|
|
123
123
|
const fromAutopilot = autopilotPaths.has(file.path);
|
|
124
124
|
const derivedTest = section === 'test' && isDerivedFromGroundedSource(file.path, grounded.modify);
|
|
@@ -146,6 +146,96 @@ async function groundTechnicalFiles(input) {
|
|
|
146
146
|
}
|
|
147
147
|
return { files: grounded, records, advisoryFiles };
|
|
148
148
|
}
|
|
149
|
+
const NEGATIVE_WORK_CLAUSE_RE = /\b(?:remains? unchanged|is not touched|not touched|must not (?:change|be changed|be modified|touch|be touched)|unchanged|out of scope)\b/;
|
|
150
|
+
const CLAUSE_DELIMITERS = ['\n', '. ', '; ', ', and ', ', but '];
|
|
151
|
+
const SENTENCE_DELIMITERS = ['\n', '. '];
|
|
152
|
+
const BACK_REFERENCE_SUBJECT_RE = /^(?:and\s+|but\s+)?(?:it|they|this|that|these|those)\s+(?:must|remains?|stays?|is|are|was|were|should|shall|will|cannot|can|does|do)\b/;
|
|
153
|
+
const PATH_LIKE_TOKEN_RE = /\S+\/\S*\.\w+/;
|
|
154
|
+
const CLAUSE_SPLIT_RE = new RegExp(CLAUSE_DELIMITERS.map((delimiter) => delimiter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
|
|
155
|
+
function computeDelimitedBounds(text, matchIndex, matchLength, delimiters) {
|
|
156
|
+
const startCandidates = delimiters.map((delimiter) => {
|
|
157
|
+
const index = text.lastIndexOf(delimiter, matchIndex);
|
|
158
|
+
return index === -1 ? -1 : index + delimiter.length;
|
|
159
|
+
});
|
|
160
|
+
const start = Math.max(0, ...startCandidates);
|
|
161
|
+
const searchAfter = matchIndex + matchLength;
|
|
162
|
+
const endCandidates = delimiters
|
|
163
|
+
.map((delimiter) => text.indexOf(delimiter, searchAfter))
|
|
164
|
+
.filter((value) => value !== -1);
|
|
165
|
+
const end = endCandidates.length > 0 ? Math.min(...endCandidates) : text.length;
|
|
166
|
+
return [start, end];
|
|
167
|
+
}
|
|
168
|
+
function findClauseBounds(text, matchIndex, matchLength) {
|
|
169
|
+
return computeDelimitedBounds(text, matchIndex, matchLength, CLAUSE_DELIMITERS);
|
|
170
|
+
}
|
|
171
|
+
function findSentenceBounds(text, matchIndex, matchLength) {
|
|
172
|
+
return computeDelimitedBounds(text, matchIndex, matchLength, SENTENCE_DELIMITERS);
|
|
173
|
+
}
|
|
174
|
+
function splitClausesWithOffsets(sentence, sentenceStart) {
|
|
175
|
+
const clauses = [];
|
|
176
|
+
let cursor = 0;
|
|
177
|
+
CLAUSE_SPLIT_RE.lastIndex = 0;
|
|
178
|
+
let delimiterMatch = CLAUSE_SPLIT_RE.exec(sentence);
|
|
179
|
+
while (delimiterMatch !== null) {
|
|
180
|
+
clauses.push([sentence.slice(cursor, delimiterMatch.index), sentenceStart + cursor]);
|
|
181
|
+
cursor = delimiterMatch.index + delimiterMatch[0].length;
|
|
182
|
+
delimiterMatch = CLAUSE_SPLIT_RE.exec(sentence);
|
|
183
|
+
}
|
|
184
|
+
clauses.push([sentence.slice(cursor), sentenceStart + cursor]);
|
|
185
|
+
return clauses;
|
|
186
|
+
}
|
|
187
|
+
function sentenceHasBackReferencedNegation(userInput, matchIndex, matchLength) {
|
|
188
|
+
const [sentenceStart, sentenceEnd] = findSentenceBounds(userInput, matchIndex, matchLength);
|
|
189
|
+
const sentence = userInput.slice(sentenceStart, sentenceEnd);
|
|
190
|
+
const clauses = splitClausesWithOffsets(sentence, sentenceStart);
|
|
191
|
+
const matchEnd = matchIndex + matchLength;
|
|
192
|
+
const currentClauseIndex = clauses.findIndex(([clauseText, clauseStart]) => matchIndex >= clauseStart && matchEnd <= clauseStart + clauseText.length);
|
|
193
|
+
if (currentClauseIndex === -1) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const currentClauseEntry = clauses[currentClauseIndex];
|
|
197
|
+
if (currentClauseEntry === undefined) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
const [currentClauseText, currentClauseStart] = currentClauseEntry;
|
|
201
|
+
const currentClauseTail = currentClauseText.slice(matchEnd - currentClauseStart);
|
|
202
|
+
if (PATH_LIKE_TOKEN_RE.test(currentClauseTail)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
for (let index = currentClauseIndex + 1; index < clauses.length; index += 1) {
|
|
206
|
+
const clauseEntry = clauses[index];
|
|
207
|
+
if (clauseEntry === undefined) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const [clauseText] = clauseEntry;
|
|
211
|
+
if (PATH_LIKE_TOKEN_RE.test(clauseText)) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const trimmedClause = clauseText.replace(/^\s+/, '');
|
|
215
|
+
if (BACK_REFERENCE_SUBJECT_RE.test(trimmedClause) &&
|
|
216
|
+
NEGATIVE_WORK_CLAUSE_RE.test(trimmedClause)) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
function pathSurvivesNegativeClauseCheck(userInput, lowerPath) {
|
|
223
|
+
let searchFrom = 0;
|
|
224
|
+
for (;;) {
|
|
225
|
+
const matchIndex = userInput.indexOf(lowerPath, searchFrom);
|
|
226
|
+
if (matchIndex === -1) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const [clauseStart, clauseEnd] = findClauseBounds(userInput, matchIndex, lowerPath.length);
|
|
230
|
+
const clause = userInput.slice(clauseStart, clauseEnd);
|
|
231
|
+
const ownClauseNegated = NEGATIVE_WORK_CLAUSE_RE.test(clause);
|
|
232
|
+
const backReferencedNegation = sentenceHasBackReferencedNegation(userInput, matchIndex, lowerPath.length);
|
|
233
|
+
if (!ownClauseNegated && !backReferencedNegation) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
searchFrom = matchIndex + lowerPath.length;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
149
239
|
function isDerivedFromGroundedSource(testPath, modifyFiles) {
|
|
150
240
|
return modifyFiles.some((file) => {
|
|
151
241
|
const withoutSrc = file.path.startsWith('src/') ? file.path.slice(4) : file.path;
|
|
@@ -194,6 +194,7 @@ export const ValidateOutputSchema = {
|
|
|
194
194
|
workspaceId: z.string().min(1).max(128),
|
|
195
195
|
state: z.enum(['accepted', 'running', 'checkpointed']),
|
|
196
196
|
submittedAt: z.iso.datetime(),
|
|
197
|
+
attachedAt: z.iso.datetime(),
|
|
197
198
|
cursor: z.string().regex(/^[a-f0-9]{64}$/),
|
|
198
199
|
queue: z.object({
|
|
199
200
|
status: z.enum(['active', 'queued', 'terminal']),
|
package/dist/tools/validate.js
CHANGED
|
@@ -212,7 +212,7 @@ export async function handleValidate(args) {
|
|
|
212
212
|
if (active.state === 'accepted' || active.state === 'checkpointed') {
|
|
213
213
|
scheduleValidateJobExecution(projectId, active.workspaceId);
|
|
214
214
|
}
|
|
215
|
-
return validationAcceptedResult(active, projectId, runtime);
|
|
215
|
+
return validationAcceptedResult(active, projectId, runtime, { justCreated: false });
|
|
216
216
|
}
|
|
217
217
|
const job = createPendingValidationSubmission({
|
|
218
218
|
runtime,
|
|
@@ -228,7 +228,7 @@ export async function handleValidate(args) {
|
|
|
228
228
|
if (job.state === 'accepted' || job.state === 'checkpointed') {
|
|
229
229
|
scheduleValidateJobExecution(projectId, job.workspaceId);
|
|
230
230
|
}
|
|
231
|
-
return validationAcceptedResult(job, projectId, runtime);
|
|
231
|
+
return validationAcceptedResult(job, projectId, runtime, { justCreated: true });
|
|
232
232
|
}
|
|
233
233
|
catch (e_1) {
|
|
234
234
|
env_1.error = e_1;
|
|
@@ -276,7 +276,7 @@ async function acquireValidationSubmissionLock(projectId, specId) {
|
|
|
276
276
|
}
|
|
277
277
|
throw new Error('[Planu] Timed out waiting for the validation submission lock');
|
|
278
278
|
}
|
|
279
|
-
function validationAcceptedResult(job, projectId, runtime) {
|
|
279
|
+
function validationAcceptedResult(job, projectId, runtime, { justCreated }) {
|
|
280
280
|
const observation = runtime.observe(job);
|
|
281
281
|
const accepted = {
|
|
282
282
|
schemaVersion: 1,
|
|
@@ -285,6 +285,7 @@ function validationAcceptedResult(job, projectId, runtime) {
|
|
|
285
285
|
workspaceId: job.workspaceId,
|
|
286
286
|
state: job.state,
|
|
287
287
|
submittedAt: job.createdAt,
|
|
288
|
+
attachedAt: justCreated ? job.createdAt : new Date().toISOString(),
|
|
288
289
|
cursor: publicJobCursor(observation),
|
|
289
290
|
queue: observation.queue,
|
|
290
291
|
terminalStates: ['completed', 'failed', 'cancelled', 'dead-letter'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.24",
|
|
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",
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"packageName": "@planu/core"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@planu/core-darwin-arm64": "5.3.
|
|
40
|
-
"@planu/core-darwin-x64": "5.3.
|
|
41
|
-
"@planu/core-linux-arm64-gnu": "5.3.
|
|
42
|
-
"@planu/core-linux-arm64-musl": "5.3.
|
|
43
|
-
"@planu/core-linux-x64-gnu": "5.3.
|
|
44
|
-
"@planu/core-linux-x64-musl": "5.3.
|
|
45
|
-
"@planu/core-win32-arm64-msvc": "5.3.
|
|
46
|
-
"@planu/core-win32-x64-msvc": "5.3.
|
|
39
|
+
"@planu/core-darwin-arm64": "5.3.24",
|
|
40
|
+
"@planu/core-darwin-x64": "5.3.24",
|
|
41
|
+
"@planu/core-linux-arm64-gnu": "5.3.24",
|
|
42
|
+
"@planu/core-linux-arm64-musl": "5.3.24",
|
|
43
|
+
"@planu/core-linux-x64-gnu": "5.3.24",
|
|
44
|
+
"@planu/core-linux-x64-musl": "5.3.24",
|
|
45
|
+
"@planu/core-win32-arm64-msvc": "5.3.24",
|
|
46
|
+
"@planu/core-win32-x64-msvc": "5.3.24"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=24.0.0"
|
package/planu-native.json
CHANGED
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.
|
|
5
|
+
"version": "5.3.24",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|