mandrel 2.57.0 → 2.59.0
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/.agents/README.md +6 -3
- package/.agents/agents/story-worker.md +12 -11
- package/.agents/docs/SDLC.md +6 -7
- package/.agents/docs/quality-gates.md +1 -1
- package/.agents/instructions.md +2 -3
- package/.agents/runtime-deps.json +7 -2
- package/.agents/schemas/crap-baseline.schema.json +1 -1
- package/.agents/schemas/crap-report.schema.json +1 -1
- package/.agents/scripts/evidence-gate.js +17 -1
- package/.agents/scripts/install-matrix-assert.js +48 -3
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +51 -33
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +0 -8
- package/.agents/scripts/lib/baselines/kinds/crap.js +35 -18
- package/.agents/scripts/lib/crap-engine.js +2 -2
- package/.agents/scripts/lib/crap-utils.js +21 -5
- package/.agents/scripts/lib/escomplex-ast-compat.js +39 -17
- package/.agents/scripts/lib/escomplex-kernel.js +298 -0
- package/.agents/scripts/lib/maintainability-engine.js +3 -3
- package/.agents/scripts/lib/orchestration/code-review.js +7 -3
- package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +41 -27
- package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +6 -1
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +14 -9
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +45 -31
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +8 -9
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-persist/wave-collision-gate.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +15 -5
- package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +25 -209
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +8 -5
- package/.agents/scripts/lib/runtime-deps/dep-resolution.js +155 -0
- package/.agents/scripts/lib/runtime-deps/ensure-installed.js +44 -9
- package/.agents/scripts/lib/runtime-deps/parser-major.js +110 -0
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -25
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +46 -1
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- package/.agents/scripts/lib/story-body/story-body.js +36 -2
- package/.agents/scripts/lib/templates/decomposer-prompts.js +73 -21
- package/.agents/scripts/lib/test-run-credit.js +23 -12
- package/.agents/scripts/plan-persist.js +0 -11
- package/.agents/skills/skills.index.json +1 -11
- package/.agents/workflows/audit-to-stories.md +14 -11
- package/.agents/workflows/helpers/deliver-digest.md +22 -15
- package/.agents/workflows/helpers/deliver-story-reference.md +31 -11
- package/.agents/workflows/helpers/deliver-story.md +6 -5
- package/.agents/workflows/helpers/plan-reference.md +53 -13
- package/.agents/workflows/mandrel-plan.md +19 -14
- package/README.md +3 -3
- package/docs/CHANGELOG.md +21 -0
- package/lib/cli/registry.js +143 -27
- package/package.json +7 -2
- package/.agents/scripts/lib/orchestration/split-policy-validator.js +0 -188
- package/.agents/scripts/lib/templates/spec-author-prompts.js +0 -76
- package/.agents/skills/core/scope-triage/SKILL.md +0 -48
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime-deps/dep-resolution — is a declared runtime dependency actually
|
|
3
|
+
* there, is it the right major, and how do we say so.
|
|
4
|
+
*
|
|
5
|
+
* `.agents/` materializes into the consumer's repository root, so every
|
|
6
|
+
* framework runtime dependency resolves from *their* `node_modules`. A range
|
|
7
|
+
* in `.agents/runtime-deps.json` therefore documents a requirement it cannot
|
|
8
|
+
* enforce, and the preflight guard needs to compare the range against what
|
|
9
|
+
* actually resolved.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately major-only, and deliberately not `semver`. The framework's
|
|
12
|
+
* runtime ranges are all `^`, whose whole contract is "this major"; pulling in
|
|
13
|
+
* a semver implementation to decide one comparison would add a dependency to
|
|
14
|
+
* the very closure this module exists to keep honest.
|
|
15
|
+
*
|
|
16
|
+
* @module lib/runtime-deps/dep-resolution
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Leading major number of a version or a caret/tilde range, or `null`.
|
|
21
|
+
*
|
|
22
|
+
* Module-local: `majorMismatch` is the only question callers have.
|
|
23
|
+
*
|
|
24
|
+
* Anything this cannot read as `<major>.` — `*`, a tag, a git URL, a
|
|
25
|
+
* `>=x <y` span — yields `null` and is treated as "not range-checked". That
|
|
26
|
+
* asymmetry is intentional: a conservative miss is a no-op, while a false
|
|
27
|
+
* positive blocks a working install.
|
|
28
|
+
*
|
|
29
|
+
* @param {string|null|undefined} spec
|
|
30
|
+
* @returns {number|null}
|
|
31
|
+
*/
|
|
32
|
+
function majorOf(spec) {
|
|
33
|
+
if (typeof spec !== 'string') return null;
|
|
34
|
+
const match = /^[\^~]?(\d+)\./.exec(spec.trim());
|
|
35
|
+
return match ? Number(match[1]) : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Does `resolved` sit outside the major `range` names?
|
|
40
|
+
*
|
|
41
|
+
* Module-local: `checkRuntimeDeps` is the only caller, and exporting it only
|
|
42
|
+
* for a test would be a production-dead export.
|
|
43
|
+
*
|
|
44
|
+
* `false` whenever either side is unreadable, so an unparseable range or an
|
|
45
|
+
* unreadable installed version is never reported as a mismatch.
|
|
46
|
+
*
|
|
47
|
+
* `0.x` majors compare as written: `^0.1.0` and `0.2.1` differ in minor, not
|
|
48
|
+
* major, so this does not separate them. Accepted — the `0.x` packages in the
|
|
49
|
+
* closure are terminal, and the range this exists to enforce is
|
|
50
|
+
* `@babel/parser`'s `^7`.
|
|
51
|
+
*
|
|
52
|
+
* @param {string|null|undefined} range
|
|
53
|
+
* @param {string|null|undefined} resolved
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
function majorMismatch(range, resolved) {
|
|
57
|
+
const want = majorOf(range);
|
|
58
|
+
if (want === null) return false;
|
|
59
|
+
const got = majorOf(resolved);
|
|
60
|
+
if (got === null) return false;
|
|
61
|
+
return want !== got;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Is a package present in the resolvable tree?
|
|
66
|
+
*
|
|
67
|
+
* The bare specifier is tried first, then `<name>/package.json`. The fallback
|
|
68
|
+
* is not belt-and-braces: a package with no `main` and no `exports` — which
|
|
69
|
+
* `typhonjs-escomplex-commons` and `babel-runtime` both are — cannot be
|
|
70
|
+
* resolved by name at all, and is reached only by deep path. Probing the bare
|
|
71
|
+
* name alone would report such a package missing while it sits installed, and
|
|
72
|
+
* this guard exits the process on that verdict.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} dep
|
|
75
|
+
* @param {(specifier: string) => string} resolve
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
export function isResolvable(dep, resolve) {
|
|
79
|
+
try {
|
|
80
|
+
resolve(dep);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
// fall through to the manifest probe
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
resolve(`${dep}/package.json`);
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Remediation text for a resolved dependency whose major differs from the
|
|
95
|
+
* range the framework declares.
|
|
96
|
+
*
|
|
97
|
+
* Named separately from the missing-deps message because the remedy differs:
|
|
98
|
+
* the package is installed, so installing again changes nothing. What is
|
|
99
|
+
* wrong is the version the consumer's own tree resolves, which only they can
|
|
100
|
+
* change.
|
|
101
|
+
*
|
|
102
|
+
* @param {{name: string, required: string, resolved: string}[]} mismatched
|
|
103
|
+
* @param {{ root: string }} ctx
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function formatMismatchedDepsMessage(mismatched, { root }) {
|
|
107
|
+
const lines = mismatched.map(
|
|
108
|
+
(m) => ` - ${m.name}: need ${m.required}, resolved ${m.resolved}`,
|
|
109
|
+
);
|
|
110
|
+
return [
|
|
111
|
+
'Mandrel framework runtime dependency version mismatch:',
|
|
112
|
+
...lines,
|
|
113
|
+
'',
|
|
114
|
+
`Resolved from: ${root}`,
|
|
115
|
+
'These packages are resolved from your repository, not from mandrel, so',
|
|
116
|
+
'the version your tree installs is the version the framework gets. Pin a',
|
|
117
|
+
'compatible major in your package.json and reinstall.',
|
|
118
|
+
].join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve each required package via the injected `resolve` seam and collect
|
|
123
|
+
* the ones that fail. `resolve` is typically `require.resolve` bound to the
|
|
124
|
+
* framework module location; it throws `MODULE_NOT_FOUND` when a package is
|
|
125
|
+
* absent from the resolvable `node_modules`.
|
|
126
|
+
*
|
|
127
|
+
* @param {{ required: string[], resolve: (specifier: string) => string }} opts
|
|
128
|
+
* @returns {{ ok: boolean, missing: string[] }}
|
|
129
|
+
*/
|
|
130
|
+
export function checkRuntimeDeps({
|
|
131
|
+
required,
|
|
132
|
+
resolve,
|
|
133
|
+
ranges = null,
|
|
134
|
+
readVersion = null,
|
|
135
|
+
}) {
|
|
136
|
+
const missing = [];
|
|
137
|
+
const mismatched = [];
|
|
138
|
+
for (const dep of required) {
|
|
139
|
+
if (!isResolvable(dep, resolve)) {
|
|
140
|
+
missing.push(dep);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!ranges || !readVersion) continue;
|
|
144
|
+
const range = ranges[dep];
|
|
145
|
+
const resolved = readVersion(dep);
|
|
146
|
+
if (majorMismatch(range, resolved)) {
|
|
147
|
+
mismatched.push({ name: dep, required: range, resolved });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
ok: missing.length === 0 && mismatched.length === 0,
|
|
152
|
+
missing,
|
|
153
|
+
mismatched,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -28,12 +28,13 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { createRequire } from 'node:module';
|
|
31
|
-
import {
|
|
31
|
+
import { resolveDependencyVersion } from '../dependency-version.js';
|
|
32
32
|
import {
|
|
33
33
|
checkRuntimeDeps,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} from './
|
|
34
|
+
formatMismatchedDepsMessage,
|
|
35
|
+
} from './dep-resolution.js';
|
|
36
|
+
import { loadRuntimeDepsManifest } from './manifest.js';
|
|
37
|
+
import { detectPackageManager, formatMissingDepsMessage } from './preflight.js';
|
|
37
38
|
|
|
38
39
|
// `require.resolve` bound to this module's location walks `node_modules`
|
|
39
40
|
// upward from `.agents/scripts/lib/runtime-deps/` to the consumer root —
|
|
@@ -50,7 +51,8 @@ const frameworkRequire = createRequire(import.meta.url);
|
|
|
50
51
|
* cwd?: string,
|
|
51
52
|
* stderr?: { write: (s: string) => void },
|
|
52
53
|
* exit?: (code: number) => void,
|
|
53
|
-
* manifest?: { required: string[] },
|
|
54
|
+
* manifest?: { required: string[], dependencies?: Record<string,string> },
|
|
55
|
+
* readVersion?: (name: string) => string | null,
|
|
54
56
|
* }} [opts]
|
|
55
57
|
* @returns {{ ok: boolean, missing: string[] }}
|
|
56
58
|
*/
|
|
@@ -61,6 +63,7 @@ export function ensureRuntimeDepsInstalled(opts = {}) {
|
|
|
61
63
|
stderr = process.stderr,
|
|
62
64
|
exit = process.exit,
|
|
63
65
|
manifest = safeLoadManifest(),
|
|
66
|
+
readVersion,
|
|
64
67
|
} = opts;
|
|
65
68
|
|
|
66
69
|
// A manifest we cannot read is a packaging defect the drift test owns —
|
|
@@ -70,17 +73,49 @@ export function ensureRuntimeDepsInstalled(opts = {}) {
|
|
|
70
73
|
const result = checkRuntimeDeps({
|
|
71
74
|
required: manifest.required,
|
|
72
75
|
resolve: requireResolve,
|
|
76
|
+
ranges: manifest.dependencies ?? null,
|
|
77
|
+
readVersion: readVersion ?? defaultReadVersion,
|
|
73
78
|
});
|
|
74
79
|
if (result.ok) return result;
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
stderr.write(
|
|
78
|
-
`${formatMissingDepsMessage(result.missing, { root: cwd, packageManager })}\n`,
|
|
79
|
-
);
|
|
81
|
+
stderr.write(`${describeFailure(result, cwd)}\n`);
|
|
80
82
|
exit(1);
|
|
81
83
|
return result;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Remediation text for a failed check.
|
|
88
|
+
*
|
|
89
|
+
* Absence is reported first: a package that is not installed cannot have a
|
|
90
|
+
* version, and installing it is the prerequisite for any version complaint
|
|
91
|
+
* being actionable.
|
|
92
|
+
*
|
|
93
|
+
* @param {{ missing: string[], mismatched: {name: string, required: string, resolved: string}[] }} result
|
|
94
|
+
* @param {string} cwd
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
function describeFailure(result, cwd) {
|
|
98
|
+
if (result.missing.length === 0) {
|
|
99
|
+
return formatMismatchedDepsMessage(result.mismatched, { root: cwd });
|
|
100
|
+
}
|
|
101
|
+
const packageManager = detectPackageManager(cwd);
|
|
102
|
+
return formatMissingDepsMessage(result.missing, {
|
|
103
|
+
root: cwd,
|
|
104
|
+
packageManager,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Read a resolved package's version through the framework's own resolution,
|
|
110
|
+
* so the version checked is the one the framework's imports will load.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} name
|
|
113
|
+
* @returns {string | null}
|
|
114
|
+
*/
|
|
115
|
+
function defaultReadVersion(name) {
|
|
116
|
+
return resolveDependencyVersion(name, frameworkRequire);
|
|
117
|
+
}
|
|
118
|
+
|
|
84
119
|
/**
|
|
85
120
|
* Load the manifest, swallowing a read/parse failure to `null` so the guard
|
|
86
121
|
* stays inert on a packaging defect (see `ensureRuntimeDepsInstalled`).
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime-deps/parser-major — which `@babel/parser` major the complexity
|
|
3
|
+
* kernel can actually parse with, and how to say so when it is wrong.
|
|
4
|
+
*
|
|
5
|
+
* Its own module because two very different callers need the same answer and
|
|
6
|
+
* neither should drag the other in: the kernel asserts it at load, and
|
|
7
|
+
* `mandrel doctor` reports it to a consumer. Importing the kernel into doctor
|
|
8
|
+
* to ask one version question would pull the whole metric core and install the
|
|
9
|
+
* AST compatibility patch as a side effect.
|
|
10
|
+
*
|
|
11
|
+
* `.agents/` materializes into the consumer's repository root, so
|
|
12
|
+
* `@babel/parser` resolves from *their* `node_modules`. A range in
|
|
13
|
+
* `runtime-deps.json` documents the requirement; it cannot enforce it. This is
|
|
14
|
+
* the enforcement.
|
|
15
|
+
*
|
|
16
|
+
* @module lib/runtime-deps/parser-major
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The only `@babel/parser` major the kernel supports.
|
|
24
|
+
*
|
|
25
|
+
* Module-local, with `describeParserMajorError` as the single public door:
|
|
26
|
+
* every caller wants the verdict and the remedy, not the number.
|
|
27
|
+
*
|
|
28
|
+
* 8.x removed several plugin names from the kernel's fixed list (they became
|
|
29
|
+
* default syntax), so it does not merely warn — it throws on the plugin list
|
|
30
|
+
* itself. Adopting it is a deliberate change with a baseline recut attached,
|
|
31
|
+
* not something to absorb from a consumer's resolution.
|
|
32
|
+
*/
|
|
33
|
+
const SUPPORTED_PARSER_MAJOR = 7;
|
|
34
|
+
|
|
35
|
+
/** Package whose resolved major gates the kernel. */
|
|
36
|
+
const PARSER_PACKAGE = '@babel/parser';
|
|
37
|
+
|
|
38
|
+
/** Memoised resolved parser version: `undefined` unread, `null` unresolvable. */
|
|
39
|
+
let parserVersion;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read the resolved `@babel/parser` version from its own manifest.
|
|
43
|
+
*
|
|
44
|
+
* The package exports no version, so its `package.json` is the only source.
|
|
45
|
+
* This is the one non-static resolution in the file and it deliberately
|
|
46
|
+
* targets a manifest rather than code: `@babel/parser` itself is reached by a
|
|
47
|
+
* static import above, so it is declared and preflighted like every other
|
|
48
|
+
* dependency.
|
|
49
|
+
*
|
|
50
|
+
* @returns {string|null} The resolved version, or `null` when the manifest
|
|
51
|
+
* cannot be read (a layout that hides `package.json` behind `exports`, say).
|
|
52
|
+
*/
|
|
53
|
+
function resolveParserVersion() {
|
|
54
|
+
if (parserVersion !== undefined) return parserVersion;
|
|
55
|
+
try {
|
|
56
|
+
const require = createRequire(import.meta.url);
|
|
57
|
+
const manifest = require.resolve(`${PARSER_PACKAGE}/package.json`);
|
|
58
|
+
const parsed = JSON.parse(fs.readFileSync(manifest, 'utf-8'));
|
|
59
|
+
parserVersion = typeof parsed?.version === 'string' ? parsed.version : null;
|
|
60
|
+
} catch {
|
|
61
|
+
parserVersion = null;
|
|
62
|
+
}
|
|
63
|
+
return parserVersion;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The resolved parser's major version.
|
|
68
|
+
*
|
|
69
|
+
* @returns {number|null} `null` when the version could not be resolved or
|
|
70
|
+
* does not lead with an integer.
|
|
71
|
+
*/
|
|
72
|
+
function resolveParserMajor() {
|
|
73
|
+
const version = resolveParserVersion();
|
|
74
|
+
if (version === null) return null;
|
|
75
|
+
const major = Number.parseInt(version, 10);
|
|
76
|
+
return Number.isInteger(major) ? major : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Describe the resolved-parser problem, if there is one.
|
|
81
|
+
*
|
|
82
|
+
* Single-sourced so the load-time assertion below and the preflight guard
|
|
83
|
+
* (`runtime-deps/ensure-installed.js`) emit the *same* named, actionable
|
|
84
|
+
* message — the point of AC-5 is that a consumer never meets this as a
|
|
85
|
+
* plugin-list syntax error mid-scan.
|
|
86
|
+
*
|
|
87
|
+
* An unresolvable version is **not** a problem: a consumer layout that hides
|
|
88
|
+
* the manifest still resolves the parser itself, and refusing to score would
|
|
89
|
+
* be a worse answer than scoring with an unverified parser. Only a
|
|
90
|
+
* *known-wrong* major is reported.
|
|
91
|
+
*
|
|
92
|
+
* @param {{major?: number|null, version?: string|null}} [resolved] Overrides
|
|
93
|
+
* the resolved parser, so the message a consumer on an unsupported major
|
|
94
|
+
* would read is assertable without installing one.
|
|
95
|
+
* @returns {string|null} The message, or `null` when the resolved parser is
|
|
96
|
+
* supported (or its version is unknowable).
|
|
97
|
+
*/
|
|
98
|
+
export function describeParserMajorError(resolved = {}) {
|
|
99
|
+
const { major = resolveParserMajor(), version = resolveParserVersion() } =
|
|
100
|
+
resolved;
|
|
101
|
+
if (major === null || major === SUPPORTED_PARSER_MAJOR) return null;
|
|
102
|
+
return (
|
|
103
|
+
`unsupported ${PARSER_PACKAGE} major: resolved ${version}, ` +
|
|
104
|
+
`the complexity kernel requires ${SUPPORTED_PARSER_MAJOR}.x. It parses ` +
|
|
105
|
+
`with a fixed plugin list that later majors reject, so CRAP and ` +
|
|
106
|
+
`maintainability scoring would fail mid-scan with an opaque plugin-list ` +
|
|
107
|
+
`error. Declare "${PARSER_PACKAGE}": "^${SUPPORTED_PARSER_MAJOR}" in ` +
|
|
108
|
+
`your package.json (see .agents/runtime-deps.json).`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* runtime-deps/preflight — pure helpers for the dependency-presence check
|
|
2
|
+
* runtime-deps/preflight — pure helpers for the dependency-presence check's
|
|
3
|
+
* *messaging* half.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The check itself moved to `dep-resolution.js`, which owns resolving a
|
|
6
|
+
* declared dependency, judging its major, and explaining a mismatch. What is
|
|
7
|
+
* left here holds no side effects and stays unit-testable in isolation:
|
|
8
|
+
* `detectPackageManager` takes an injected `exists` seam and
|
|
7
9
|
* `formatMissingDepsMessage` is a pure string builder. The side-effecting
|
|
8
10
|
* guard that wires them to the real process lives in `ensure-installed.js`.
|
|
9
11
|
*
|
|
@@ -14,27 +16,6 @@
|
|
|
14
16
|
import fs from 'node:fs';
|
|
15
17
|
import { detectPackageManager as detectPm } from '../detect-package-manager.js';
|
|
16
18
|
|
|
17
|
-
/**
|
|
18
|
-
* Resolve each required package via the injected `resolve` seam and collect
|
|
19
|
-
* the ones that fail. `resolve` is typically `require.resolve` bound to the
|
|
20
|
-
* framework module location; it throws `MODULE_NOT_FOUND` when a package is
|
|
21
|
-
* absent from the resolvable `node_modules`.
|
|
22
|
-
*
|
|
23
|
-
* @param {{ required: string[], resolve: (specifier: string) => string }} opts
|
|
24
|
-
* @returns {{ ok: boolean, missing: string[] }}
|
|
25
|
-
*/
|
|
26
|
-
export function checkRuntimeDeps({ required, resolve }) {
|
|
27
|
-
const missing = [];
|
|
28
|
-
for (const dep of required) {
|
|
29
|
-
try {
|
|
30
|
-
resolve(dep);
|
|
31
|
-
} catch {
|
|
32
|
-
missing.push(dep);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return { ok: missing.length === 0, missing };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
19
|
/**
|
|
39
20
|
* Detect the consumer's package manager from lockfile presence so the
|
|
40
21
|
* remediation message names the right install command. Defaults to `npm`.
|
|
@@ -71,6 +71,51 @@ const STATIC_FROM =
|
|
|
71
71
|
const SIDE_EFFECT = /^\s*import\s*['"]([^'"]+)['"]/gm;
|
|
72
72
|
// `require(...)` and dynamic `import(...)` may appear mid-expression.
|
|
73
73
|
const CALL_FORM = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
74
|
+
// Names bound to a `createRequire(...)` result, e.g.
|
|
75
|
+
// `const fromReader = createRequire(x)`. Such a binding is a require function
|
|
76
|
+
// under a different name, so calls through it are real runtime imports that
|
|
77
|
+
// `CALL_FORM` cannot see — it matches the literal callees `require`/`import`.
|
|
78
|
+
// A module reached only that way would be an undeclared, unpreflighted
|
|
79
|
+
// dependency that this scanner reported as absent.
|
|
80
|
+
const REQUIRE_ALIAS =
|
|
81
|
+
/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*createRequire\s*\(/g;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build a matcher for calls through `createRequire`-bound identifiers.
|
|
85
|
+
*
|
|
86
|
+
* Returns `null` when the source binds none, so the common case adds no pass.
|
|
87
|
+
* Aliases named `require` need no entry — `CALL_FORM` already covers them.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} cleaned Comment-stripped source.
|
|
90
|
+
* @returns {RegExp|null}
|
|
91
|
+
*/
|
|
92
|
+
function aliasedRequireMatcher(cleaned) {
|
|
93
|
+
REQUIRE_ALIAS.lastIndex = 0;
|
|
94
|
+
const names = new Set();
|
|
95
|
+
let m = REQUIRE_ALIAS.exec(cleaned);
|
|
96
|
+
while (m !== null) {
|
|
97
|
+
if (m[1] !== 'require') names.add(m[1]);
|
|
98
|
+
m = REQUIRE_ALIAS.exec(cleaned);
|
|
99
|
+
}
|
|
100
|
+
if (names.size === 0) return null;
|
|
101
|
+
const alternation = [...names]
|
|
102
|
+
.map((n) => n.replace(/[$]/g, '\\$$'))
|
|
103
|
+
.join('|');
|
|
104
|
+
return new RegExp(`\\b(?:${alternation})\\s*\\(\\s*['"]([^'"]+)['"]`, 'g');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The specifier patterns to run over one source: the three fixed forms, plus
|
|
109
|
+
* an alias matcher when the source binds a `createRequire` result.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} cleaned Comment-stripped source.
|
|
112
|
+
* @returns {RegExp[]}
|
|
113
|
+
*/
|
|
114
|
+
function specifierMatchers(cleaned) {
|
|
115
|
+
const aliased = aliasedRequireMatcher(cleaned);
|
|
116
|
+
if (!aliased) return [STATIC_FROM, SIDE_EFFECT, CALL_FORM];
|
|
117
|
+
return [STATIC_FROM, SIDE_EFFECT, CALL_FORM, aliased];
|
|
118
|
+
}
|
|
74
119
|
|
|
75
120
|
/**
|
|
76
121
|
* Extract the set of third-party top-level package names imported by a
|
|
@@ -82,7 +127,7 @@ const CALL_FORM = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
|
82
127
|
export function extractThirdPartyImports(source) {
|
|
83
128
|
const found = new Set();
|
|
84
129
|
const cleaned = stripJsComments(source);
|
|
85
|
-
for (const re of
|
|
130
|
+
for (const re of specifierMatchers(cleaned)) {
|
|
86
131
|
re.lastIndex = 0;
|
|
87
132
|
let match = re.exec(cleaned);
|
|
88
133
|
while (match !== null) {
|
|
@@ -41,7 +41,7 @@ export const LOCAL_SKILLS_SEGMENTS = Object.freeze([
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* A skill id is the tier-relative path naming a skill — e.g.
|
|
44
|
-
* `core/
|
|
44
|
+
* `core/test-first` or `stack/qa/playwright`. It is the value that
|
|
45
45
|
* appears in `skills.index.json` minus the root prefix, and the value a
|
|
46
46
|
* `qa.environments.*.signInSeam.skill` seam carries.
|
|
47
47
|
*
|
|
@@ -164,7 +164,14 @@ const HUMANIZED_PATH_ENTRY_RE = /^`([^`]+)`\s+—\s+(\S+)$/;
|
|
|
164
164
|
// AC-<n> presentation prefix on acceptance checkboxes (Story #4600). The
|
|
165
165
|
// numbering is a stable 1-based human handle only — parse() strips it so the
|
|
166
166
|
// top-level acceptance[] machine contract round-trips byte-identical.
|
|
167
|
-
|
|
167
|
+
//
|
|
168
|
+
// The lettered form (`AC-14a:`) is accepted too (Story #5323). Nothing emits
|
|
169
|
+
// one — `serialize()` numbers from the array index — but a Story planned from
|
|
170
|
+
// an existing ticket can copy one out of the source issue's rendered
|
|
171
|
+
// checkboxes, and a body that already carries one must still parse to the
|
|
172
|
+
// handle-free text or the round-trip invariant breaks for it alone. Trailing
|
|
173
|
+
// whitespace is optional so `AC-3:text` normalises as readily as `AC-3: text`.
|
|
174
|
+
const AC_PREFIX_RE = /^AC-\d+[a-z]?:\s*/i;
|
|
168
175
|
|
|
169
176
|
// Machine-managed marker lines a body authored before Story #5312 may still
|
|
170
177
|
// carry: the `> **Wide:** <reason>` rationale line (Story #4600), the
|
|
@@ -560,6 +567,33 @@ function parseTextListSection(lines) {
|
|
|
560
567
|
* @returns {ParseResult}
|
|
561
568
|
* @throws {StoryBodyParseError} When the body is structurally unrecoverable.
|
|
562
569
|
*/
|
|
570
|
+
/**
|
|
571
|
+
* Strip the presentation `AC-<n>:` handle off one acceptance item.
|
|
572
|
+
*
|
|
573
|
+
* The handle belongs to {@link serialize}, which numbers every checkbox from
|
|
574
|
+
* its position in `acceptance[]`; an authored item that already carries one
|
|
575
|
+
* would render doubled (`- [ ] AC-1: AC-1: …`) and a lettered handle copied
|
|
576
|
+
* from a source ticket would survive into the machine contract. Both parse
|
|
577
|
+
* and the persist-side normalisation resolve the grammar here so the two can
|
|
578
|
+
* never disagree about what a handle is (Story #5323).
|
|
579
|
+
*
|
|
580
|
+
* Stacked handles are peeled in full — a body persisted while the doubling
|
|
581
|
+
* was live carries two, and leaving the inner one would normalise to
|
|
582
|
+
* something that still is not the authored text.
|
|
583
|
+
*
|
|
584
|
+
* @param {string} item
|
|
585
|
+
* @returns {{ text: string, stripped: boolean }} The handle-free text, and
|
|
586
|
+
* whether anything was removed.
|
|
587
|
+
*/
|
|
588
|
+
export function stripAcceptanceHandle(item) {
|
|
589
|
+
const original = String(item ?? '');
|
|
590
|
+
let text = original;
|
|
591
|
+
while (AC_PREFIX_RE.test(text)) {
|
|
592
|
+
text = text.replace(AC_PREFIX_RE, '');
|
|
593
|
+
}
|
|
594
|
+
return { text, stripped: text !== original };
|
|
595
|
+
}
|
|
596
|
+
|
|
563
597
|
export function parse(input) {
|
|
564
598
|
if (input === null || input === undefined) {
|
|
565
599
|
throw new StoryBodyParseError('Story body is null or undefined', {
|
|
@@ -618,7 +652,7 @@ export function parse(input) {
|
|
|
618
652
|
// The AC-<n> checkbox prefix is presentation-only (Story #4600): strip it
|
|
619
653
|
// so acceptance[] round-trips byte-identical to the authored array.
|
|
620
654
|
const acceptance = parseTextListSection(sections.get('acceptance') ?? []).map(
|
|
621
|
-
(a) => a.
|
|
655
|
+
(a) => stripAcceptanceHandle(a).text,
|
|
622
656
|
);
|
|
623
657
|
const verify = parseTextListSection(sections.get('verify') ?? []);
|
|
624
658
|
const references = parsePathEntrySection(
|