@planu/cli 5.0.0 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -1
- package/dist/cli/commands/spec.js +10 -1
- package/dist/core/spec-validator.js +32 -18
- package/dist/engine/evidence-gates/artifact-reader.d.ts +2 -0
- package/dist/engine/evidence-gates/artifact-reader.js +59 -2
- package/dist/engine/evidence-gates/evidence-autofill.d.ts +10 -0
- package/dist/engine/evidence-gates/evidence-autofill.js +148 -0
- package/dist/engine/evidence-gates/evidence-skeletons.d.ts +19 -0
- package/dist/engine/evidence-gates/evidence-skeletons.js +69 -0
- package/dist/engine/execution/operation-journal.js +10 -4
- package/dist/engine/minimality/policy-loader.js +247 -6
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +14 -14
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +14 -14
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +14 -14
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +14 -14
- package/dist/engine/reverse-engineer/api-detector.js +2 -13
- package/dist/engine/reverse-engineer/complexity-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/config-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/dependency-graph.js +2 -13
- package/dist/engine/reverse-engineer/test-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/walk-ignore.d.ts +3 -0
- package/dist/engine/reverse-engineer/walk-ignore.js +26 -0
- package/dist/engine/spec-format/acceptance-criteria.js +13 -12
- package/dist/engine/spec-format/text-fences.js +20 -2
- package/dist/engine/spec-state-syncer.js +1 -1
- package/dist/engine/timing/budget.js +5 -1
- package/dist/server/routes/specs.js +7 -5
- package/dist/tools/challenge-spec-helpers.d.ts +10 -1
- package/dist/tools/challenge-spec-helpers.js +63 -22
- package/dist/tools/challenge-spec.js +18 -3
- package/dist/tools/check-readiness.js +37 -13
- package/dist/tools/create-spec/spec-builder.d.ts +7 -0
- package/dist/tools/create-spec/spec-builder.js +19 -4
- package/dist/tools/create-spec.js +156 -86
- package/dist/tools/register-spec-tools/core-spec-tools.js +13 -12
- package/dist/tools/sync-spec-state-handler.js +49 -1
- package/dist/tools/update-status/batch.d.ts +6 -2
- package/dist/tools/update-status/batch.js +58 -1
- package/dist/tools/update-status/dod-gates.d.ts +16 -1
- package/dist/tools/update-status/dod-gates.js +191 -1
- package/dist/tools/update-status/done-receipt-verifier.d.ts +8 -0
- package/dist/tools/update-status/done-receipt-verifier.js +37 -2
- package/dist/tools/update-status/evidence-gate.d.ts +4 -0
- package/dist/tools/update-status/evidence-gate.js +67 -2
- package/dist/tools/update-status/file-sync.d.ts +2 -2
- package/dist/tools/update-status/index.d.ts +23 -1
- package/dist/tools/update-status/index.js +201 -24
- package/dist/tools/update-status/transition-guard.js +13 -1
- package/dist/tools/workspace-dashboard-handler.js +38 -0
- package/dist/types/evidence-autofill.d.ts +34 -0
- package/dist/types/evidence-autofill.js +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/spec/core.d.ts +6 -0
- package/dist/types/spec/inputs.d.ts +7 -2
- package/dist/types/spec-format.d.ts +1 -1
- package/dist/types/transition-log.d.ts +1 -1
- package/dist/types/validation.d.ts +8 -2
- package/package.json +11 -10
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"version": 1,
|
|
5
5
|
"metadata": {
|
|
6
6
|
"component": {
|
|
7
|
-
"bom-ref": "pkg:cargo/planu-core@5.
|
|
7
|
+
"bom-ref": "pkg:cargo/planu-core@5.1.1",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.
|
|
10
|
+
"version": "5.1.1",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "301863177ab4858bbbcd6f1d5ac86af8f98c424512a54c28b5343c01c1bd377b"
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -291,9 +291,9 @@
|
|
|
291
291
|
},
|
|
292
292
|
{
|
|
293
293
|
"type": "library",
|
|
294
|
-
"bom-ref": "pkg:cargo/globset@0.4.
|
|
294
|
+
"bom-ref": "pkg:cargo/globset@0.4.20",
|
|
295
295
|
"name": "globset",
|
|
296
|
-
"version": "0.4.
|
|
296
|
+
"version": "0.4.20",
|
|
297
297
|
"externalReferences": [
|
|
298
298
|
{
|
|
299
299
|
"type": "distribution",
|
|
@@ -529,9 +529,9 @@
|
|
|
529
529
|
},
|
|
530
530
|
{
|
|
531
531
|
"type": "library",
|
|
532
|
-
"bom-ref": "pkg:cargo/regex-automata@0.4.
|
|
532
|
+
"bom-ref": "pkg:cargo/regex-automata@0.4.18",
|
|
533
533
|
"name": "regex-automata",
|
|
534
|
-
"version": "0.4.
|
|
534
|
+
"version": "0.4.18",
|
|
535
535
|
"externalReferences": [
|
|
536
536
|
{
|
|
537
537
|
"type": "distribution",
|
|
@@ -1046,12 +1046,12 @@
|
|
|
1046
1046
|
]
|
|
1047
1047
|
},
|
|
1048
1048
|
{
|
|
1049
|
-
"ref": "pkg:cargo/globset@0.4.
|
|
1049
|
+
"ref": "pkg:cargo/globset@0.4.20",
|
|
1050
1050
|
"dependsOn": [
|
|
1051
1051
|
"pkg:cargo/aho-corasick@1.1.5",
|
|
1052
1052
|
"pkg:cargo/bstr@1.13.0",
|
|
1053
1053
|
"pkg:cargo/log@0.4.33",
|
|
1054
|
-
"pkg:cargo/regex-automata@0.4.
|
|
1054
|
+
"pkg:cargo/regex-automata@0.4.18",
|
|
1055
1055
|
"pkg:cargo/regex-syntax@0.8.11"
|
|
1056
1056
|
]
|
|
1057
1057
|
},
|
|
@@ -1069,10 +1069,10 @@
|
|
|
1069
1069
|
"ref": "pkg:cargo/ignore@0.4.25",
|
|
1070
1070
|
"dependsOn": [
|
|
1071
1071
|
"pkg:cargo/crossbeam-deque@0.8.7",
|
|
1072
|
-
"pkg:cargo/globset@0.4.
|
|
1072
|
+
"pkg:cargo/globset@0.4.20",
|
|
1073
1073
|
"pkg:cargo/log@0.4.33",
|
|
1074
1074
|
"pkg:cargo/memchr@2.8.3",
|
|
1075
|
-
"pkg:cargo/regex-automata@0.4.
|
|
1075
|
+
"pkg:cargo/regex-automata@0.4.18",
|
|
1076
1076
|
"pkg:cargo/same-file@1.0.6",
|
|
1077
1077
|
"pkg:cargo/walkdir@2.5.0",
|
|
1078
1078
|
"pkg:cargo/winapi-util@0.1.11"
|
|
@@ -1106,7 +1106,7 @@
|
|
|
1106
1106
|
"dependsOn": []
|
|
1107
1107
|
},
|
|
1108
1108
|
{
|
|
1109
|
-
"ref": "pkg:cargo/planu-core@5.
|
|
1109
|
+
"ref": "pkg:cargo/planu-core@5.1.1",
|
|
1110
1110
|
"dependsOn": [
|
|
1111
1111
|
"pkg:cargo/core-foundation@0.10.1",
|
|
1112
1112
|
"pkg:cargo/hmac@0.12.1",
|
|
@@ -1151,7 +1151,7 @@
|
|
|
1151
1151
|
]
|
|
1152
1152
|
},
|
|
1153
1153
|
{
|
|
1154
|
-
"ref": "pkg:cargo/regex-automata@0.4.
|
|
1154
|
+
"ref": "pkg:cargo/regex-automata@0.4.18",
|
|
1155
1155
|
"dependsOn": [
|
|
1156
1156
|
"pkg:cargo/aho-corasick@1.1.5",
|
|
1157
1157
|
"pkg:cargo/memchr@2.8.3",
|
|
@@ -1167,7 +1167,7 @@
|
|
|
1167
1167
|
"dependsOn": [
|
|
1168
1168
|
"pkg:cargo/aho-corasick@1.1.5",
|
|
1169
1169
|
"pkg:cargo/memchr@2.8.3",
|
|
1170
|
-
"pkg:cargo/regex-automata@0.4.
|
|
1170
|
+
"pkg:cargo/regex-automata@0.4.18",
|
|
1171
1171
|
"pkg:cargo/regex-syntax@0.8.11"
|
|
1172
1172
|
]
|
|
1173
1173
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { shouldIgnoreDir } from './walk-ignore.js';
|
|
3
4
|
const ROUTE_PATTERNS = [
|
|
4
5
|
// Express/Koa/Hono: app.get('/path', handler) or router.post(...)
|
|
5
6
|
{
|
|
@@ -69,18 +70,6 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
69
70
|
'.kt',
|
|
70
71
|
'.rb',
|
|
71
72
|
]);
|
|
72
|
-
const IGNORE_DIRS = new Set([
|
|
73
|
-
'node_modules',
|
|
74
|
-
'.git',
|
|
75
|
-
'dist',
|
|
76
|
-
'build',
|
|
77
|
-
'out',
|
|
78
|
-
'.next',
|
|
79
|
-
'.nuxt',
|
|
80
|
-
'coverage',
|
|
81
|
-
'vendor',
|
|
82
|
-
'__pycache__',
|
|
83
|
-
]);
|
|
84
73
|
function collectSourceFiles(dir) {
|
|
85
74
|
const results = [];
|
|
86
75
|
const queue = [dir];
|
|
@@ -97,7 +86,7 @@ function collectSourceFiles(dir) {
|
|
|
97
86
|
continue;
|
|
98
87
|
}
|
|
99
88
|
for (const entry of entries) {
|
|
100
|
-
if (
|
|
89
|
+
if (shouldIgnoreDir(entry.name)) {
|
|
101
90
|
continue;
|
|
102
91
|
}
|
|
103
92
|
const full = path.join(current, entry.name);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { shouldIgnoreDir } from './walk-ignore.js';
|
|
3
4
|
const SOURCE_EXTENSIONS = new Set([
|
|
4
5
|
'.ts',
|
|
5
6
|
'.tsx',
|
|
@@ -14,18 +15,6 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
14
15
|
'.rs',
|
|
15
16
|
'.rb',
|
|
16
17
|
]);
|
|
17
|
-
const IGNORE_DIRS = new Set([
|
|
18
|
-
'node_modules',
|
|
19
|
-
'.git',
|
|
20
|
-
'dist',
|
|
21
|
-
'build',
|
|
22
|
-
'out',
|
|
23
|
-
'.next',
|
|
24
|
-
'.nuxt',
|
|
25
|
-
'coverage',
|
|
26
|
-
'vendor',
|
|
27
|
-
'__pycache__',
|
|
28
|
-
]);
|
|
29
18
|
// Patterns that increase cyclomatic complexity
|
|
30
19
|
const BRANCH_PATTERNS = [
|
|
31
20
|
/\bif\s*\(/g,
|
|
@@ -73,7 +62,7 @@ function collectSourceFiles(dir) {
|
|
|
73
62
|
continue;
|
|
74
63
|
}
|
|
75
64
|
for (const entry of entries) {
|
|
76
|
-
if (
|
|
65
|
+
if (shouldIgnoreDir(entry.name)) {
|
|
77
66
|
continue;
|
|
78
67
|
}
|
|
79
68
|
const full = path.join(current, entry.name);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { technologyValue } from '../technology-registry.js';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { shouldIgnoreDir } from './walk-ignore.js';
|
|
4
5
|
const ENV_VAR_PATTERNS = [
|
|
5
6
|
// JS/TS: process.env.VAR_NAME
|
|
6
7
|
/process\.env\.([A-Z_][A-Z0-9_]*)/g,
|
|
@@ -55,18 +56,6 @@ const INFRA_INDICATORS = {
|
|
|
55
56
|
'azure-pipelines.yml': 'Azure Pipelines',
|
|
56
57
|
'cloudbuild.yaml': 'Google Cloud Build',
|
|
57
58
|
};
|
|
58
|
-
const IGNORE_DIRS = new Set([
|
|
59
|
-
'node_modules',
|
|
60
|
-
'.git',
|
|
61
|
-
'dist',
|
|
62
|
-
'build',
|
|
63
|
-
'out',
|
|
64
|
-
'.next',
|
|
65
|
-
'.nuxt',
|
|
66
|
-
'coverage',
|
|
67
|
-
'vendor',
|
|
68
|
-
'__pycache__',
|
|
69
|
-
]);
|
|
70
59
|
const SOURCE_EXTENSIONS = new Set([
|
|
71
60
|
'.ts',
|
|
72
61
|
'.tsx',
|
|
@@ -97,7 +86,7 @@ function collectAllFiles(dir) {
|
|
|
97
86
|
continue;
|
|
98
87
|
}
|
|
99
88
|
for (const entry of entries) {
|
|
100
|
-
if (
|
|
89
|
+
if (shouldIgnoreDir(entry.name)) {
|
|
101
90
|
continue;
|
|
102
91
|
}
|
|
103
92
|
const full = path.join(current, entry.name);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { shouldIgnoreDir } from './walk-ignore.js';
|
|
3
4
|
const IMPORT_PATTERNS = [
|
|
4
5
|
// ES import: import ... from './foo'
|
|
5
6
|
/(?:import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"])/g,
|
|
@@ -22,18 +23,6 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
22
23
|
'.rs',
|
|
23
24
|
'.rb',
|
|
24
25
|
]);
|
|
25
|
-
const IGNORE_DIRS = new Set([
|
|
26
|
-
'node_modules',
|
|
27
|
-
'.git',
|
|
28
|
-
'dist',
|
|
29
|
-
'build',
|
|
30
|
-
'out',
|
|
31
|
-
'.next',
|
|
32
|
-
'.nuxt',
|
|
33
|
-
'coverage',
|
|
34
|
-
'vendor',
|
|
35
|
-
'__pycache__',
|
|
36
|
-
]);
|
|
37
26
|
const HUB_THRESHOLD = 10;
|
|
38
27
|
function collectSourceFiles(dir) {
|
|
39
28
|
const results = [];
|
|
@@ -51,7 +40,7 @@ function collectSourceFiles(dir) {
|
|
|
51
40
|
continue;
|
|
52
41
|
}
|
|
53
42
|
for (const entry of entries) {
|
|
54
|
-
if (
|
|
43
|
+
if (shouldIgnoreDir(entry.name)) {
|
|
55
44
|
continue;
|
|
56
45
|
}
|
|
57
46
|
const full = path.join(current, entry.name);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { shouldIgnoreDir } from './walk-ignore.js';
|
|
3
4
|
const TEST_PATTERNS = [
|
|
4
5
|
/\.test\.\w+$/,
|
|
5
6
|
/\.spec\.\w+$/,
|
|
@@ -25,18 +26,6 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
25
26
|
'.c',
|
|
26
27
|
'.swift',
|
|
27
28
|
]);
|
|
28
|
-
const IGNORE_DIRS = new Set([
|
|
29
|
-
'node_modules',
|
|
30
|
-
'.git',
|
|
31
|
-
'dist',
|
|
32
|
-
'build',
|
|
33
|
-
'out',
|
|
34
|
-
'.next',
|
|
35
|
-
'.nuxt',
|
|
36
|
-
'coverage',
|
|
37
|
-
'vendor',
|
|
38
|
-
'__pycache__',
|
|
39
|
-
]);
|
|
40
29
|
export function isTestFile(filePath) {
|
|
41
30
|
const basename = path.basename(filePath);
|
|
42
31
|
if (TEST_PATTERNS.some((p) => p.test(basename))) {
|
|
@@ -61,7 +50,7 @@ function collectFiles(dir) {
|
|
|
61
50
|
continue;
|
|
62
51
|
}
|
|
63
52
|
for (const entry of entries) {
|
|
64
|
-
if (
|
|
53
|
+
if (shouldIgnoreDir(entry.name)) {
|
|
65
54
|
continue;
|
|
66
55
|
}
|
|
67
56
|
const full = path.join(current, entry.name);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// engine/reverse-engineer/walk-ignore.ts — shared directory-walk ignore list
|
|
2
|
+
// Consumed by all reverse-engineer analyzers (dependency-graph, test-analyzer,
|
|
3
|
+
// api-detector, config-analyzer, complexity-analyzer) so the ignore rules stay
|
|
4
|
+
// in one place instead of five duplicated copies.
|
|
5
|
+
export const WALK_IGNORE_DIRS = new Set([
|
|
6
|
+
'node_modules',
|
|
7
|
+
'.git',
|
|
8
|
+
'dist',
|
|
9
|
+
'build',
|
|
10
|
+
'out',
|
|
11
|
+
'.next',
|
|
12
|
+
'.nuxt',
|
|
13
|
+
'coverage',
|
|
14
|
+
'vendor',
|
|
15
|
+
'__pycache__',
|
|
16
|
+
'.worktrees',
|
|
17
|
+
'.claude',
|
|
18
|
+
'planu',
|
|
19
|
+
'website',
|
|
20
|
+
'data',
|
|
21
|
+
'benchmarks',
|
|
22
|
+
]);
|
|
23
|
+
export function shouldIgnoreDir(name) {
|
|
24
|
+
return WALK_IGNORE_DIRS.has(name);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=walk-ignore.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseFrontmatterScenarios } from '../validator/spec-compliance-runner.js';
|
|
2
2
|
import { stripFrontmatter } from '../frontmatter-parser.js';
|
|
3
3
|
import { extractListItems, extractSection } from './markdown-sections.js';
|
|
4
|
+
import { stripFencedBlocks } from './text-fences.js';
|
|
4
5
|
import { createCriterionIdentity, normalizeCriterionText } from '../criterion-identity.js';
|
|
5
6
|
const BDD_STEP = /^(GIVEN|WHEN|THEN|AND|DADO|CUANDO|ENTONCES|Y)\b/i;
|
|
6
7
|
const BDD_START = /^(GIVEN|DADO)\b/i;
|
|
@@ -91,7 +92,7 @@ function extractSectionCriteria(section) {
|
|
|
91
92
|
return extractBddBlocks(section);
|
|
92
93
|
}
|
|
93
94
|
function extractBodyCriteria(raw, options) {
|
|
94
|
-
const body = stripFrontmatter(raw);
|
|
95
|
+
const body = stripFencedBlocks(stripFrontmatter(raw));
|
|
95
96
|
const acceptanceCriteria = extractSection(body, 'acceptance criteria', 'criterios de aceptaci');
|
|
96
97
|
if (acceptanceCriteria) {
|
|
97
98
|
const sectionCriteria = extractSectionCriteria(acceptanceCriteria);
|
|
@@ -99,18 +100,18 @@ function extractBodyCriteria(raw, options) {
|
|
|
99
100
|
return sectionCriteria;
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
|
-
if (options.allowLegacyBodyFallback
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return
|
|
103
|
+
if (options.allowLegacyBodyFallback !== false) {
|
|
104
|
+
const checkboxCriteria = body
|
|
105
|
+
.split('\n')
|
|
106
|
+
.map((line) => line.trim())
|
|
107
|
+
.filter((line) => /^-\s*\[[ x]\]/i.test(line))
|
|
108
|
+
.map((line) => line.replace(/^-\s*\[[ x]\]\s*/i, '').trim());
|
|
109
|
+
if (checkboxCriteria.length > 0) {
|
|
110
|
+
return checkboxCriteria;
|
|
111
|
+
}
|
|
112
|
+
return extractBddBlocks(body);
|
|
112
113
|
}
|
|
113
|
-
return
|
|
114
|
+
return [];
|
|
114
115
|
}
|
|
115
116
|
function buildCriterion(text, source, index) {
|
|
116
117
|
const trimmed = text.trim();
|
|
@@ -4,7 +4,25 @@
|
|
|
4
4
|
* Returns the remaining prose only, preserving line structure.
|
|
5
5
|
*/
|
|
6
6
|
export function stripFencedBlocks(md) {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const lines = md.split('\n');
|
|
8
|
+
let openFence;
|
|
9
|
+
return lines
|
|
10
|
+
.map((line) => {
|
|
11
|
+
if (openFence !== undefined) {
|
|
12
|
+
const close = new RegExp(`^ {0,3}${openFence.marker}{${String(openFence.length)},}[ \\t]*\\r?$`);
|
|
13
|
+
if (close.test(line)) {
|
|
14
|
+
openFence = undefined;
|
|
15
|
+
}
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
const opening = /^ {0,3}(`{3,}|~{3,})[^\r\n]*\r?$/.exec(line);
|
|
19
|
+
if (opening?.[1]) {
|
|
20
|
+
const marker = opening[1][0];
|
|
21
|
+
openFence = { marker, length: opening[1].length };
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
return line;
|
|
25
|
+
})
|
|
26
|
+
.join('\n');
|
|
9
27
|
}
|
|
10
28
|
//# sourceMappingURL=text-fences.js.map
|
|
@@ -72,7 +72,7 @@ async function processSingleSpec(filePath, dataStatusMap, report) {
|
|
|
72
72
|
* The caller (tool layer) is responsible for persisting updates to storage.
|
|
73
73
|
*/
|
|
74
74
|
export async function detectSpecStateDivergences(projectPath, dataEntries) {
|
|
75
|
-
const report = { synced: 0, divergences: [], errors: [], rejected: [] };
|
|
75
|
+
const report = { synced: 0, divergences: [], errors: [], rejected: [], ghosts: [] };
|
|
76
76
|
const updates = [];
|
|
77
77
|
const dataStatusMap = new Map(dataEntries.map((s) => [s.id, s.status]));
|
|
78
78
|
let specFiles;
|
|
@@ -107,7 +107,11 @@ export async function withTotalBudget(operationName, ceilingMs, fn) {
|
|
|
107
107
|
timedOut: false,
|
|
108
108
|
category: 'operation_failed',
|
|
109
109
|
elapsedMs,
|
|
110
|
-
|
|
110
|
+
// SPEC-1351: keep the thrown error's message — the production bug this
|
|
111
|
+
// fixed surfaced only as an opaque "operation_failed" with no way to
|
|
112
|
+
// tell what actually broke — but never leak the raw call stack into
|
|
113
|
+
// this user-facing warning string.
|
|
114
|
+
warning: `[SPEC-713] Operation "${operationName}" failed after ${String(elapsedMs)}ms (operation_failed): ${outcome.error.message}`,
|
|
111
115
|
};
|
|
112
116
|
}
|
|
113
117
|
return {
|
|
@@ -26,9 +26,7 @@ const VALID_STATUSES = [
|
|
|
26
26
|
'discarded',
|
|
27
27
|
];
|
|
28
28
|
function containsReconciliationAuthorityFields(body) {
|
|
29
|
-
return
|
|
30
|
-
'expectedImplementingTransitionId' in body ||
|
|
31
|
-
'implementationReviewDigest' in body);
|
|
29
|
+
return 'reconciliationRequestId' in body || 'expectedImplementingTransitionId' in body;
|
|
32
30
|
}
|
|
33
31
|
/** Resolve projectPath from the request Host header or fallback to process.cwd(). */
|
|
34
32
|
function getProjectPath(ctx) {
|
|
@@ -192,10 +190,13 @@ export async function handleUpdateSpecStatus(ctx) {
|
|
|
192
190
|
sendBadRequest(ctx.res, 'Request body must be a JSON object');
|
|
193
191
|
return;
|
|
194
192
|
}
|
|
195
|
-
const { status } = body;
|
|
193
|
+
const { status, implementationReviewDigest } = body;
|
|
196
194
|
if (containsReconciliationAuthorityFields(body)) {
|
|
197
195
|
ctx.res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
198
|
-
ctx.res.end(JSON.stringify({
|
|
196
|
+
ctx.res.end(JSON.stringify({
|
|
197
|
+
error: 'Reconciliation is available only through local MCP.',
|
|
198
|
+
code: 'RECONCILIATION_UNTRUSTED_SURFACE',
|
|
199
|
+
}));
|
|
199
200
|
return;
|
|
200
201
|
}
|
|
201
202
|
if (typeof status !== 'string' || !VALID_STATUSES.includes(status)) {
|
|
@@ -219,6 +220,7 @@ export async function handleUpdateSpecStatus(ctx) {
|
|
|
219
220
|
projectPath,
|
|
220
221
|
trigger: 'http',
|
|
221
222
|
actor: 'http-api',
|
|
223
|
+
implementationReviewDigest: implementationReviewDigest,
|
|
222
224
|
});
|
|
223
225
|
if (result.isError) {
|
|
224
226
|
const reason = result.content[0]?.type === 'text' ? result.content[0].text : 'Gate blocked transition';
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { FailureScenario, ConcurrencyAnalysis, RiskLevel, Spec, ProjectKnowledge } from '../types/index.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Result of generateConcurrencyAnalysis. `suppressedCount` tracks candidates
|
|
4
|
+
* that matched a domain keyword (counter/status/file/session/cache) but were
|
|
5
|
+
* dropped because the spec content contains no corroborating concurrency
|
|
6
|
+
* context (e.g. "concurrent", "simultaneously", "multiple users") — SPEC-1350.
|
|
7
|
+
*/
|
|
8
|
+
export interface ConcurrencyAnalysisResult extends ConcurrencyAnalysis {
|
|
9
|
+
suppressedCount: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function generateConcurrencyAnalysis(_spec: Spec, content: string, _knowledge: ProjectKnowledge): ConcurrencyAnalysisResult;
|
|
3
12
|
export declare function buildScalabilityAssessment(spec: Spec, knowledge: ProjectKnowledge, scenarios: FailureScenario[]): string;
|
|
4
13
|
export declare function calculateOverallRisk(scenarios: FailureScenario[], concurrency: ConcurrencyAnalysis): RiskLevel;
|
|
5
14
|
export declare function readSpecContent(spec: Spec): Promise<string>;
|
|
@@ -1,55 +1,96 @@
|
|
|
1
1
|
// tools/challenge-spec-helpers.ts — Concurrency, risk, and spec-reader helpers for challenge-spec
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { readSpecTechnicalSection } from '../engine/spec-format/read-technical-section.js';
|
|
4
|
-
|
|
4
|
+
import { hasAffirmedMatch, stripMetaAnalysisText, stripNonContractText, } from '../engine/text-signal-boundaries.js';
|
|
5
|
+
// Bare keyword matches (counter, status, draft, ...) are common in unrelated
|
|
6
|
+
// prose (variable names, file paths, section headers, bug narratives). They
|
|
7
|
+
// only ground a race-condition/shared-state finding when the spec content
|
|
8
|
+
// also affirms an explicit concurrency context — otherwise they are boilerplate.
|
|
9
|
+
const CONCURRENCY_CONTEXT_PATTERN = /\b(?:concurrent(?:ly)?|simultaneous(?:ly)?|in\s+parallel|parallel\s+writes?|race\s+condition|multiple\s+users?|at\s+the\s+same\s+time|two\s+users|shared\s+state|atomic(?:ally)?|thread-safe|synchroniz(?:e|es|ed|ing|ation)|multiple\s+(?:tabs|devices|requests|clients))\b/i;
|
|
10
|
+
const AUTH_PATTERN = /\b(?:login|auth)\b/i;
|
|
11
|
+
const LIST_SEARCH_PATTERN = /\b(?:list|search|feed)\b/i;
|
|
12
|
+
const WRITE_PATTERN = /\b(?:submit|create|update)\b/i;
|
|
13
|
+
const COUNTER_PATTERN = /\b(?:counter|increment|balance|stock|quantity|vote)\b/i;
|
|
14
|
+
const STATUS_PATTERN = /\bstatus\b/i;
|
|
15
|
+
const TRANSITION_PATTERN = /\b(?:update|transition)\b/i;
|
|
16
|
+
const FILE_PATTERN = /\b(?:file|upload|resource)\b/i;
|
|
17
|
+
const SESSION_PATTERN = /\b(?:session|cart|draft)\b/i;
|
|
18
|
+
const CACHE_PATTERN = /\b(?:cache|global|config)\b/i;
|
|
19
|
+
/** Grounded, boundary-aware, negation-aware match against contract prose only. */
|
|
20
|
+
function affirms(content, pattern) {
|
|
21
|
+
return hasAffirmedMatch(content, pattern);
|
|
22
|
+
}
|
|
5
23
|
export function generateConcurrencyAnalysis(_spec, content, _knowledge) {
|
|
6
|
-
|
|
24
|
+
// Drop frontmatter, code fences, and quoted/backticked examples before
|
|
25
|
+
// keyword inference — mirrors detectChallengeCapabilities so this generator
|
|
26
|
+
// reads the same "contract prose" the capability gate already reads.
|
|
27
|
+
const contract = stripMetaAnalysisText(stripNonContractText(content));
|
|
7
28
|
const hotPaths = [];
|
|
8
29
|
const raceConditions = [];
|
|
9
30
|
const sharedState = [];
|
|
10
31
|
const recommendations = [];
|
|
11
|
-
|
|
12
|
-
|
|
32
|
+
let suppressedCount = 0;
|
|
33
|
+
// Hot paths are informational only (they do not feed overallRisk), so a
|
|
34
|
+
// plain boundary-aware match is sufficient here — no context gate needed.
|
|
35
|
+
if (affirms(contract, AUTH_PATTERN)) {
|
|
13
36
|
hotPaths.push('Authentication endpoint (high-frequency, shared session state)');
|
|
14
37
|
}
|
|
15
|
-
if (
|
|
38
|
+
if (affirms(contract, LIST_SEARCH_PATTERN)) {
|
|
16
39
|
hotPaths.push('List/search endpoints (high read frequency)');
|
|
17
40
|
}
|
|
18
|
-
if (
|
|
41
|
+
if (affirms(contract, WRITE_PATTERN)) {
|
|
19
42
|
hotPaths.push('Write endpoints (mutation-heavy, lock-prone)');
|
|
20
43
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
lower.includes('balance') ||
|
|
25
|
-
lower.includes('stock') ||
|
|
26
|
-
lower.includes('quantity') ||
|
|
27
|
-
lower.includes('vote')) {
|
|
44
|
+
const hasConcurrencyContext = affirms(contract, CONCURRENCY_CONTEXT_PATTERN);
|
|
45
|
+
const counterMatched = affirms(contract, COUNTER_PATTERN);
|
|
46
|
+
if (counterMatched && hasConcurrencyContext) {
|
|
28
47
|
raceConditions.push('Counter/balance updates may lose increments under concurrent writes');
|
|
29
48
|
recommendations.push('Use database-level atomic increments (UPDATE SET count = count + 1) instead of read-modify-write');
|
|
30
49
|
}
|
|
31
|
-
if (
|
|
50
|
+
else if (counterMatched) {
|
|
51
|
+
suppressedCount += 1;
|
|
52
|
+
}
|
|
53
|
+
const statusMatched = affirms(contract, STATUS_PATTERN) && affirms(contract, TRANSITION_PATTERN);
|
|
54
|
+
if (statusMatched && hasConcurrencyContext) {
|
|
32
55
|
raceConditions.push('Status transitions may conflict when two users update simultaneously');
|
|
33
56
|
recommendations.push('Use optimistic locking (version column) or pessimistic locking (SELECT FOR UPDATE)');
|
|
34
57
|
}
|
|
35
|
-
if (
|
|
58
|
+
else if (statusMatched) {
|
|
59
|
+
suppressedCount += 1;
|
|
60
|
+
}
|
|
61
|
+
const fileMatched = affirms(contract, FILE_PATTERN);
|
|
62
|
+
if (fileMatched && hasConcurrencyContext) {
|
|
36
63
|
raceConditions.push('Concurrent file writes to same resource may corrupt data');
|
|
37
64
|
recommendations.push('Use advisory locks or rename-and-swap pattern for file operations');
|
|
38
65
|
}
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
else if (fileMatched) {
|
|
67
|
+
suppressedCount += 1;
|
|
68
|
+
}
|
|
69
|
+
const sessionMatched = affirms(contract, SESSION_PATTERN);
|
|
70
|
+
if (sessionMatched && hasConcurrencyContext) {
|
|
41
71
|
sharedState.push('User session state (may be accessed from multiple tabs/devices)');
|
|
42
72
|
}
|
|
43
|
-
if (
|
|
73
|
+
else if (sessionMatched) {
|
|
74
|
+
suppressedCount += 1;
|
|
75
|
+
}
|
|
76
|
+
const cacheMatched = affirms(contract, CACHE_PATTERN);
|
|
77
|
+
if (cacheMatched && hasConcurrencyContext) {
|
|
44
78
|
sharedState.push('Global cache or configuration (requires synchronized access)');
|
|
45
79
|
}
|
|
46
|
-
|
|
80
|
+
else if (cacheMatched) {
|
|
81
|
+
suppressedCount += 1;
|
|
82
|
+
}
|
|
83
|
+
// General recommendations — only emitted once a race condition is actually
|
|
84
|
+
// grounded, otherwise a formatting-only spec would still surface generic
|
|
85
|
+
// "use database transactions" boilerplate (SPEC-1350 AC1).
|
|
47
86
|
if (raceConditions.length === 0) {
|
|
48
87
|
recommendations.push('No obvious race conditions detected, but test concurrent scenarios in integration tests');
|
|
49
88
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
89
|
+
else {
|
|
90
|
+
recommendations.push('Use database transactions for multi-step operations');
|
|
91
|
+
recommendations.push('Implement idempotency keys for all mutation endpoints');
|
|
92
|
+
}
|
|
93
|
+
return { hotPaths, raceConditions, sharedState, recommendations, suppressedCount };
|
|
53
94
|
}
|
|
54
95
|
// --- Scalability and risk ---
|
|
55
96
|
export function buildScalabilityAssessment(spec, knowledge, scenarios) {
|
|
@@ -171,9 +171,24 @@ export async function handleChallengeSpec(args, server) {
|
|
|
171
171
|
// SPEC-615 AC3: Check criteria against prior decisions for technology contradictions
|
|
172
172
|
await runPriorDecisionContradictions(projectId, spec.title, spec.tags, specContent, failureScenarios);
|
|
173
173
|
// 6. Generate concurrency analysis
|
|
174
|
-
const
|
|
174
|
+
const concurrencyResult = focusAreas.includes('concurrency') && capabilities.concurrency
|
|
175
175
|
? generateConcurrencyAnalysis(spec, specContent, knowledge)
|
|
176
|
-
: {
|
|
176
|
+
: {
|
|
177
|
+
hotPaths: [],
|
|
178
|
+
raceConditions: [],
|
|
179
|
+
sharedState: [],
|
|
180
|
+
recommendations: [],
|
|
181
|
+
suppressedCount: 0,
|
|
182
|
+
};
|
|
183
|
+
// SPEC-1350: concurrency candidates that matched a domain keyword but lacked
|
|
184
|
+
// a corroborating concurrency context are suppressed before risk/summary math.
|
|
185
|
+
const concurrencySuppressedCount = concurrencyResult.suppressedCount;
|
|
186
|
+
const concurrencyAnalysis = {
|
|
187
|
+
hotPaths: concurrencyResult.hotPaths,
|
|
188
|
+
raceConditions: concurrencyResult.raceConditions,
|
|
189
|
+
sharedState: concurrencyResult.sharedState,
|
|
190
|
+
recommendations: concurrencyResult.recommendations,
|
|
191
|
+
};
|
|
177
192
|
// Unsupported domain templates are removed before prioritization and risk calculation.
|
|
178
193
|
const actionableFailureScenarios = failureScenarios.filter((scenario) => isScenarioSupportedByCapabilities(scenario, capabilities));
|
|
179
194
|
const suppressedScenarioCount = failureScenarios.length - actionableFailureScenarios.length;
|
|
@@ -219,7 +234,7 @@ export async function handleChallengeSpec(args, server) {
|
|
|
219
234
|
summary: {
|
|
220
235
|
totalScenarios: failureScenarios.length,
|
|
221
236
|
actionableScenarios: failureScenariosScored.length,
|
|
222
|
-
suppressedLowRelevanceScenarios: suppressedScenarioCount,
|
|
237
|
+
suppressedLowRelevanceScenarios: suppressedScenarioCount + concurrencySuppressedCount,
|
|
223
238
|
shownByDefault: 3,
|
|
224
239
|
mustAddressBeforeCoding: prioritizedSummary,
|
|
225
240
|
criticalImpact: actionableFailureScenarios.filter((s) => s.impact === 'critical').length,
|