@piagent/platform 1.51.1 → 1.51.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/docs/command-reference-vietnamese.md +2 -2
  3. package/docs/distribution-standard.md +6 -6
  4. package/docs/operator-manual-vietnamese.md +7 -7
  5. package/docs/publishing-for-teams.md +1 -0
  6. package/docs/release-install-policy.md +2 -2
  7. package/docs/team-onboarding.md +2 -2
  8. package/docs-site/architecture.html +1 -1
  9. package/docs-site/assets/search-en.json +1 -1
  10. package/docs-site/assets/search-vi.json +1 -1
  11. package/docs-site/benchmark.html +1 -1
  12. package/docs-site/commands.html +1 -1
  13. package/docs-site/context-engine.html +1 -1
  14. package/docs-site/ecosystem.html +1 -1
  15. package/docs-site/en/architecture.html +1 -1
  16. package/docs-site/en/benchmark.html +1 -1
  17. package/docs-site/en/commands.html +1 -1
  18. package/docs-site/en/context-engine.html +1 -1
  19. package/docs-site/en/ecosystem.html +1 -1
  20. package/docs-site/en/index.html +9 -9
  21. package/docs-site/en/mcp-approval.html +1 -1
  22. package/docs-site/en/mcp-auth.html +1 -1
  23. package/docs-site/en/mcp-servers.html +1 -1
  24. package/docs-site/en/mcp.html +1 -1
  25. package/docs-site/en/permissions.html +1 -1
  26. package/docs-site/en/profiles.html +1 -1
  27. package/docs-site/en/quickstart.html +1 -1
  28. package/docs-site/en/runtime.html +1 -1
  29. package/docs-site/en/scope.html +1 -1
  30. package/docs-site/en/security.html +1 -1
  31. package/docs-site/en/team.html +1 -1
  32. package/docs-site/en/update.html +3 -3
  33. package/docs-site/en/whats-new.html +28 -18
  34. package/docs-site/en/windows.html +1 -1
  35. package/docs-site/en/workflows.html +1 -1
  36. package/docs-site/index.html +9 -9
  37. package/docs-site/mcp-approval.html +1 -1
  38. package/docs-site/mcp-auth.html +1 -1
  39. package/docs-site/mcp-servers.html +1 -1
  40. package/docs-site/mcp.html +1 -1
  41. package/docs-site/permissions.html +1 -1
  42. package/docs-site/profiles.html +1 -1
  43. package/docs-site/quickstart.html +1 -1
  44. package/docs-site/releases.json +26 -0
  45. package/docs-site/runtime.html +1 -1
  46. package/docs-site/scope.html +1 -1
  47. package/docs-site/security.html +1 -1
  48. package/docs-site/team.html +1 -1
  49. package/docs-site/update.html +3 -3
  50. package/docs-site/whats-new.html +28 -18
  51. package/docs-site/windows.html +1 -1
  52. package/docs-site/workflows.html +1 -1
  53. package/evals/private-holdout-v1/public-exposure.v2.json +1 -1
  54. package/package.json +1 -1
  55. package/packages/piagent-core/managed/check-questions.mjs +2 -46
  56. package/packages/piagent-core/managed/test-scope.mjs +170 -0
  57. package/packages/piagent-core/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { memberQuestions } from './member-questions.mjs';
3
3
  import { alwaysBypass, effectivePermission } from './permission.mjs';
4
+ import { checkScope, suitePart, unquoted } from './test-scope.mjs';
4
5
  import { digestsOnce, knownFast, markCheckStart, missingSuites, pushQuestion, pushRepositories, reviewBeforePush } from './check-evidence.mjs';
5
6
 
6
7
  // How much to test is the member's decision: a one-line fix should not cost a
@@ -12,53 +13,8 @@ import { digestsOnce, knownFast, markCheckStart, missingSuites, pushQuestion, pu
12
13
  // overnight) is taken back and the cheap answer goes on: only the tests that
13
14
  // cover the change, and no push. Before this a turn waited until morning.
14
15
  export const UNANSWERED_MS = 10 * 60_000;
16
+ export { checkScope, suitePart };
15
17
 
16
- // How much of the suite a test command covers: 'partial' (a filter, one file, one
17
- // package), 'suite' (the whole test command) or null when it runs no tests.
18
- const VERSION_ONLY = /(?:^|\s)(?:--version|-V|--help)(?:\s|$)/;
19
- const FILTERED = /(?:^|\s)(?:-k|-t|--grep|--testNamePattern|--test-name-pattern|--filter|--only|--testPathPattern)(?:[\s=]|$)|\bgo\s+test\b.*\s-run[\s=]|\bpytest\b.*\s::|\.only\b/;
20
- const TEST_RUNNER = /\b(npm|pnpm|yarn|bun)\s+(run\s+)?test(:\S+)?(?=\s|$)|\bnode\s+--test\b|\b(vitest|jest|mocha|pytest|rspec|phpunit)\b|\bplaywright\s+test\b|\bpython3?\s+-m\s+(pytest|unittest)\b|\b(go|cargo|swift|dotnet|deno)\s+test\b|\bmake\s+test\b|\bxcodebuild\b.*\btest\b|\b(gradle|gradlew|mvn|mvnw)\b.*\b(test|verify)\b/;
21
- // Test commands start a command: `npm install -D vitest` or `rm jest.config.ts` only name one.
22
- const TEST_START = new RegExp(`^(?:${TEST_RUNNER.source})`);
23
- // A function body, a group or a loop body opens a part as well.
24
- const LAUNCHER = /^(?:[\w-]+\s*\(\)\s*\{?\s*|\{\s+|\(\s*|(?:do|then|else)\s+|\w+=\S*\s+|time\s+|nohup\s+|timeout\s+\S+\s+|npx\s+(?:-\S+\s+)*|bunx\s+|pnpm\s+(?:exec|dlx)\s+|uv\s+run\s+|poetry\s+run\s+|yarn\s+(?=jest|vitest|mocha|playwright)|node_modules\/\.bin\/|\.\/)+/;
25
- const partsOf = bare => bare.split(/\s*(?:\|\|?|&&|;)\s*/).map(part => part.replace(/\s\d*>.*$/, '').replace(/[\s)}]+$/, '').trim());
26
- const NOT_TESTS = /\b(lint|typecheck|type-check|tsc|eslint|biome|ruff|mypy|pyright|vet|clippy|format|build)\b/;
27
- const TARGET_FLAG = /^(-p|--package|-w|--workspace|--tests)$|^(-Dtest=|-only-testing:|--workspace=|--package=)/;
28
- // `go test -p 4` sets how many packages run at once; for cargo -p names a package.
29
- const GO_TEST = /^go\s+test\b/;
30
- const VALUE_FLAG = /^(-c|--config|--reporter|--project|--workers|--timeout|--rootDir|--root|-f|--file|-o|--output)$/;
31
- // A file or folder named on the command line; a flag's own value names none.
32
- const targetsPart = (args, go = false) => args.some((a, i) => {
33
- // A variable: a function's arguments, a loop's file, $(go list …). The caller chose them, so this is not the whole command.
34
- if (a.startsWith('$')) return true;
35
- if (go && a === '-p') return false;
36
- if (TARGET_FLAG.test(a)) return true;
37
- if (go && args[i - 1] === '-p') return false;
38
- if (a.startsWith('-') || VALUE_FLAG.test(args[i - 1] ?? '') || a.includes('*') || a.endsWith('...')) return false;
39
- return a.includes('/') || /\.(test|spec)\.\w+$|_test\.\w+$|^test_\w+\.\w+$/.test(a);
40
- });
41
- // What a command runs, without the text it merely quotes (a commit message, an echo).
42
- const unquoted = text => text.replace(/"[^"]*"|'[^']*'/g, quoted => quoted.startsWith('"$') ? '$' : '""');
43
- export function checkScope(command, declared = []) {
44
- const text = String(command ?? '').replace(/\s+/g, ' ').trim();
45
- if (!text || /^(echo|printf|grep|rg|cat|sed|awk|head|tail|less|git|ls|find)\b/.test(text) || VERSION_ONLY.test(text) || /--collect-only/.test(text)) return null;
46
- const bare = unquoted(text);
47
- for (const part of partsOf(bare)) {
48
- const own = part.replace(LAUNCHER, ''), runner = own.match(TEST_START);
49
- if (!runner) continue;
50
- if (FILTERED.test(own)) return 'partial';
51
- return !declared.includes(text) && targetsPart(own.slice(runner[0].length).split(' ').filter(Boolean), GO_TEST.test(own)) ? 'partial' : 'suite';
52
- }
53
- // A script the repository declares: it cannot be narrowed from here.
54
- const named = declared.find(d => partsOf(bare).some(part => part.startsWith(unquoted(d))));
55
- return named && !NOT_TESTS.test(named) ? 'suite' : null;
56
- }
57
- // The test step of a command, without what surrounds it: the same step costs the same.
58
- export function suitePart(command) {
59
- for (const part of partsOf(unquoted(String(command ?? '').replace(/\s+/g, ' ').trim()))) { const own = part.replace(LAUNCHER, ''); if (TEST_START.test(own)) return own; }
60
- return String(command ?? '').trim();
61
- }
62
18
  // The member's own words asking for the whole suite: nothing to ask them then.
63
19
  const RUN_ALL = String.raw`(?:full|all(?:\s+the)?|whole(?:\s+the)?|entire(?:\s+the)?|the\s+(?:full|whole|entire))`;
64
20
  const FULL_WORDS = new RegExp([
@@ -0,0 +1,170 @@
1
+ // How much of the suite a test command covers: 'partial' (a filter, one file, one
2
+ // package or module), 'suite' (the whole test command) or null when it runs no tests.
3
+ // A member who chose "only the related part" is stuck when a part reads as the
4
+ // whole suite, so each tool says how its own commands narrow: the flags that pick
5
+ // tests, the flags that take a value, and the words that still run everything
6
+ // (a Maven goal, `vitest run`). Any other word on the line names a part.
7
+
8
+ // What a command runs, without the text it merely quotes (a commit message, an
9
+ // echo): a quoted word stays one word with nothing in it that splits a command.
10
+ export const unquoted = text => text.replace(/"[^"]*"|'[^']*'/g, quoted => quoted.startsWith('"$') ? '$' : quoted.length > 2 ? quoted.slice(1, -1).replace(/[\s;&|<>()]/g, '_') : '""');
11
+ export const partsOf = bare => bare.split(/\s*(?:\|\|?|&&|;)\s*/).map(part => part.replace(/\s\d*[<>].*$/, '').replace(/[\s)}]+$/, '').trim());
12
+
13
+ const VERSION_ONLY = /(?:^|\s)(?:--version|-V|--help)(?:\s|$)/;
14
+ // Listing the tests runs none of them.
15
+ const LIST_ONLY = /(?:^|\s)(?:--collect-only|--co|--listTests|--list-tests|--list|-list|--dry-run|--dryRun|-showTestPlans)(?=[\s=]|$)/;
16
+ const NOT_A_RUN = /^(echo|printf|grep|rg|cat|sed|awk|head|tail|less|git|ls|find)\b/;
17
+ const NOT_TESTS = /\b(lint|typecheck|type-check|tsc|eslint|biome|ruff|mypy|pyright|vet|clippy|format|build)\b/;
18
+ // What starts a command before the tool itself: a function body, a group, a loop body, env, a runner of binaries.
19
+ const LAUNCHER = /^(?:[\w-]+\s*\(\)\s*\{?\s*|\{\s+|\(\s*|(?:do|then|else)\s+|\w+=\S*\s+|env\s+|time\s+|nohup\s+|timeout\s+\S+\s+|npx\s+(?:-\S+\s+)*|bunx\s+|bun\s+x\s+|pnpm\s+(?:exec|dlx)\s+|(?:yarn|pnpm|bun)\s+(?=(?:jest|vitest|mocha|ava|playwright|cypress|nx|turbo|lerna)\b)|uv\s+run\s+|poetry\s+run\s+|pipenv\s+run\s+|hatch\s+run\s+|bundle\s+exec\s+|php\s+(?=\S*(?:phpunit|pest)\b)|node_modules\/\.bin\/|(?:vendor\/)?bin\/|\.\/)+/;
20
+
21
+ const NONE = /$^/;
22
+ // A word that names everything there is: the test folder, every package, a glob, a count.
23
+ const ALL_OF_IT = /^(?:\.\/)?(?:tests?|specs?|__tests__|e2e|src)\/?$|^(?:\.\/)?\.\.\.$|\*|^\d+[smh]?$|^auto$/;
24
+ // Does this command pick part of the tests? `narrow` flags pick tests (by name before
25
+ // `=`), `value` flags take the next word, `whole` words still run everything; any
26
+ // other word names a part, unless the tool takes none (`words: false`).
27
+ function narrows(args, { narrow = NONE, value = NONE, whole = NONE, words = true }) {
28
+ for (let i = 0; i < args.length; i++) {
29
+ const a = args[i];
30
+ // A variable: a function's arguments, a loop's file. The caller chose them, so this is not the whole command.
31
+ if (a.startsWith('$')) return true;
32
+ if (a === '--') continue;
33
+ if (a.startsWith('-')) {
34
+ const name = a.split('=')[0];
35
+ if (narrow.test(name)) return true;
36
+ if (!a.includes('=') && value.test(name)) i++;
37
+ continue;
38
+ }
39
+ if (words && !whole.test(a) && !ALL_OF_IT.test(a)) return true;
40
+ }
41
+ return false;
42
+ }
43
+
44
+ const JS = {
45
+ narrow: /^(?:-t|--testNamePattern|--test-name-pattern|-g|--grep|--fgrep|--testPathPattern|--testPathPatterns|--filter|--spec|--project|--selectProjects|--findRelatedTests|--onlyChanged|-o|--changed|--changedSince|--lastCommit|--only|--test-only|--match|-m)$/,
46
+ value: /^(?:-w|--maxWorkers|--workers|-j|--jobs|--timeout|--reporter|-r|--require|--import|--loader|--config|-c|--rootDir|--root|--outputFile|--coverageDirectory|--test-concurrency|--test-reporter|--test-reporter-destination|--retries|--repeat-each|--env|--browser|--shard|--seed|--slow|--file|--mode|--environment|--pool|--dir)$/,
47
+ whole: /^(?:run|watch|dev|test)$/,
48
+ };
49
+ const PYTEST = { narrow: /^(?:-k|-m|--lf|--last-failed|--sw|--stepwise|--deselect|--pyargs)$/, value: /^(?:-n|--numprocesses|-c|-p|--maxfail|--rootdir|--durations|-o|--junitxml|--cov|--timeout|--dist|-W|--basetemp|--log-level|--confcutdir|--ignore|--ignore-glob|--tb|-r|--reruns)$/ };
50
+ const tool = (start, spec, test = () => true) => ({ start, scope: args => test(args) ? (narrows(args, spec) ? 'partial' : 'suite') : null });
51
+
52
+ // npm/pnpm/yarn/bun: the workspace or filter before the script, the script, then its own arguments (Jest, Vitest, …).
53
+ function packageScript(args) {
54
+ let picked = false;
55
+ for (let i = 0; i < args.length; i++) {
56
+ const a = args[i];
57
+ if (/^(?:-w|--workspace|--filter|-F)$/.test(a)) { picked = true; i++; continue; }
58
+ if (/^(?:--workspace|--filter)=/.test(a)) { picked = true; continue; }
59
+ if (a.startsWith('-')) continue;
60
+ if (a === 'workspace') { picked = true; i++; continue; }
61
+ // `npm t` is `npm test`; `npm run t` is a script of that name.
62
+ const run = /^(?:run|run-script)$/.test(a), script = run ? args[++i] : a;
63
+ if (!(run ? /^test(?::\S+)?$/ : /^(?:test|t|tst)(?::\S+)?$/).test(script ?? '')) return null;
64
+ // Before `--`, `-w web` is still npm's workspace; a number is Jest's worker count.
65
+ const rest = args.slice(i + 1), end = rest.includes('--') ? rest.indexOf('--') : rest.length;
66
+ picked ||= rest.slice(0, end).some((a, j) => /^--workspace=/.test(a) || (/^(?:-w|--workspace)$/.test(a) && !/^\d+$/.test(rest[j + 1] ?? '0')));
67
+ return picked || narrows(rest, JS) ? 'partial' : 'suite';
68
+ }
69
+ return null;
70
+ }
71
+ // Maven: every word is a goal; it tests when a goal reaches `test`. The flags pick classes, groups or modules.
72
+ function maven(args) {
73
+ if (!args.some(a => /^(?:[\w.-]+:)*(?:test|verify|integration-test)$/.test(a))) return null;
74
+ if (args.some(a => /^-D(?:skipTests|maven\.test\.skip)(?:=true)?$/.test(a))) return null;
75
+ const file = args.findIndex(a => a === '-f' || a === '--file');
76
+ if (file >= 0 && args[file + 1]?.includes('/')) return 'partial';
77
+ return narrows(args, { narrow: /^(?:-Dtest|-Dit\.test|-Dgroups|-Dcucumber\.filter\.tags|-Dcucumber\.options|-pl|--projects|-rf|--resume-from)$/, value: /^(?:-T|--threads|-s|--settings|-gs|--global-settings|-P|--activate-profiles|-l|--log-file|-t|--toolchains|-b|--builder|-f|--file)$/, words: false }) ? 'partial' : 'suite';
78
+ }
79
+ // Gradle: the words are tasks; `:cart:test` is one module's.
80
+ function gradle(args) {
81
+ const value = /^(?:-x|--exclude-task|--max-workers|-I|--init-script|-c|--settings-file|--console|-g|--gradle-user-home)$/;
82
+ const tasks = args.filter((a, i) => !a.startsWith('-') && !value.test(args[i - 1] ?? ''));
83
+ const testTasks = tasks.filter(t => /^(?:test|check|[a-zA-Z]*Test)$/.test(t.split(':').pop()));
84
+ if (!testTasks.length) return null;
85
+ if (testTasks.some(t => /^:?[\w-]+(?::[\w-]+)*:\w+$/.test(t) && t.replace(/^:/, '').includes(':'))) return 'partial';
86
+ return narrows(args, { narrow: /^(?:--tests|-p|--project-dir|-b|--build-file)$/, value, words: false }) ? 'partial' : 'suite';
87
+ }
88
+ // Go: the words are packages; none is the current one, `./...` is all of them.
89
+ function goTest(args) {
90
+ const end = args.indexOf('-args');
91
+ const own = end >= 0 ? args.slice(0, end) : args;
92
+ const spec = { narrow: /^-{1,2}run$/, value: /^-{1,2}(?:p|count|timeout|parallel|cpu|tags|exec|covermode|coverpkg|coverprofile|o|bench|benchtime|shuffle|vet|C|fuzz|fuzztime|skip|json)$/ };
93
+ const packages = own.filter((a, i) => !a.startsWith('-') && !(spec.value.test(own[i - 1] ?? '') && !own[i - 1].includes('=')));
94
+ return !packages.length || narrows(own, spec) ? 'partial' : 'suite';
95
+ }
96
+ // make: a test target; a variable naming tests narrows it (`make test TEST=./cart`).
97
+ function make(args) {
98
+ if (!args.some(a => /^(?:test|check)(?:[-_:]\w+)*$/.test(a))) return null;
99
+ return args.some(a => /^\w*(?:TEST|RUN|FILTER|PKG|SPEC|ONLY|PATTERN|FILE)\w*=/i.test(a) || a.startsWith('$')) ? 'partial' : 'suite';
100
+ }
101
+ // Nx, Turborepo, Lerna run a target across projects; naming projects or `affected` narrows.
102
+ function monorepo(name, args) {
103
+ const words = args.filter(a => !a.startsWith('-'));
104
+ const target = args.find((a, i) => /^(?:-t|--targets?)$/.test(args[i - 1] ?? '')) ?? args.find(a => /^--targets?=/.test(a))?.split('=')[1];
105
+ if (name === 'nx') {
106
+ const [command, project] = words;
107
+ if (command === 'run-many') return /test|e2e/.test(target ?? '') ? (args.some(a => /^(?:-p|--projects)(?:=|$)/.test(a)) ? 'partial' : 'suite') : null;
108
+ if (command === 'affected') return /test|e2e/.test(target ?? words[1] ?? '') ? 'partial' : null;
109
+ if (command === 'run') return /:(?:\w*test|e2e)\w*$/.test(project ?? '') ? 'partial' : null;
110
+ return /test|e2e/.test(command ?? '') ? (project ? 'partial' : 'suite') : null;
111
+ }
112
+ const tasks = words[0] === 'run' ? words.slice(1) : words;
113
+ if (!tasks.some(t => /test|e2e/.test(t))) return null;
114
+ return args.some(a => /^(?:--filter|-F|--scope|--since|--affected)(?:=|$)/.test(a)) ? 'partial' : 'suite';
115
+ }
116
+
117
+ const TOOLS = [
118
+ { start: /^(?:npm|pnpm|yarn|bun)(?=\s|$)/, scope: packageScript },
119
+ tool(/^(?:jest|vitest|mocha|ava|tap|uvu)(?=\s|$)/, JS),
120
+ tool(/^node(?=\s)/, JS, args => args.includes('--test')),
121
+ tool(/^playwright\s+test(?=\s|$)/, JS),
122
+ tool(/^cypress\s+run(?=\s|$)/, JS),
123
+ tool(/^(?:pytest|py\.test|python[\d.]*\s+-m\s+pytest)(?=\s|$)/, PYTEST),
124
+ tool(/^python[\d.]*\s+-m\s+unittest(?=\s|$)/, { narrow: /^(?:-k|-p|--pattern)$/, value: /^(?:-s|--start-directory|-t|--top-level-directory)$/, whole: /^discover$/ }),
125
+ tool(/^python[\d.]*\s+manage\.py\s+test(?=\s|$)/, { narrow: /^(?:-k|--tag)$/, value: /^(?:--parallel|--settings|-v|--verbosity|--exclude-tag|--testrunner)$/ }),
126
+ tool(/^tox(?=\s|$)/, { value: /^(?:-e|-c|--conf|-p|--parallel|-x|--override)$/ }),
127
+ { start: /^go\s+test(?=\s|$)/, scope: goTest },
128
+ tool(/^cargo\s+test(?=\s|$)/, { narrow: /^(?:-p|--package|--test|--lib|--bin|--bins|--example|--examples|--doc|--bench|--manifest-path|--ignored|--exact)$/, value: /^(?:-j|--jobs|--features|-F|--target|--profile|--test-threads|--color|-Z|--target-dir|--message-format|--config|--exclude)$/ }),
129
+ tool(/^cargo\s+nextest\s+run(?=\s|$)/, { narrow: /^(?:-E|--filterset|--filter-expr|-p|--package|--test|--lib|--bin)$/, value: /^(?:-j|--jobs|--profile|--test-threads|--retries|--partition|--features|-F)$/ }),
130
+ { start: /^mvnw?(?=\s|$)/, scope: maven },
131
+ { start: /^gradlew?(?=\s|$)/, scope: gradle },
132
+ tool(/^dotnet\s+test(?=\s|$)/, { narrow: /^--filter$/, value: /^(?:-c|--configuration|-f|--framework|-r|--runtime|-l|--logger|-s|--settings|-v|--verbosity|--results-directory|-a|--test-adapter-path|--collect|-d|--diag|-o|--output|--arch|--os|--blame-hang-timeout|--blame-crash-dump-type|-e|--environment)$/, whole: /\.slnx?$/ }),
133
+ tool(/^swift\s+test(?=\s|$)/, { narrow: /^(?:--filter|--package-path)$/, value: /^(?:-c|--configuration|-j|--jobs|--num-workers|--scratch-path|--skip)$/ }),
134
+ tool(/^xcodebuild(?=\s)/, { narrow: /^-only-testing(?::|$)/, value: /^-(?:scheme|destination|project|workspace|sdk|configuration|derivedDataPath|resultBundlePath|testPlan|skip-testing|parallel-testing-worker-count|xctestrun|target|arch|toolchain|destination-timeout|maximum-parallel-testing-workers|test-iterations|testLanguage|testRegion|only-test-configuration|skip-test-configuration)$/, words: false }, args => args.some(a => /^test(?:-without-building)?$/.test(a))),
135
+ tool(/^rspec(?=\s|$)/, { narrow: /^(?:-t|--tag|-e|--example|-E|--example-matches|--only-failures|--next-failure|-P|--pattern)$/, value: /^(?:-f|--format|-o|--out|--seed|--order|-r|--require|-I|--default-path|-O|--options)$/ }),
136
+ tool(/^(?:rails|rake)\s+test(?::\w+)?(?=\s|$)/, { narrow: /^(?:-n|--name)$/, value: /^(?:--seed|-s|-e|--environment|-j)$/ }),
137
+ tool(/^(?:phpunit|pest|php\s+artisan\s+test)(?=\s|$)/, { narrow: /^(?:--filter|--group|--testsuite)$/, value: /^(?:-c|--configuration|--bootstrap|--log-junit|--exclude-group|--coverage-html|--coverage-clover|--order-by|--cache-directory|--processes|-d)$/ }),
138
+ tool(/^mix\s+test(?=\s|$)/, { narrow: /^(?:--only|--failed|--stale)$/, value: /^(?:--seed|--max-failures|--exclude|--include|--formatter|--partitions|--max-cases)$/ }),
139
+ tool(/^(?:flutter|dart)\s+test(?=\s|$)/, { narrow: /^(?:--name|-n|--plain-name|-N|--tags|-t)$/, value: /^(?:-j|--concurrency|-r|--reporter|-p|--platform|-x|--exclude-tags|--file-reporter|--coverage-path|--flavor|--total-shards|--shard-index)$/ }),
140
+ tool(/^deno\s+test(?=\s|$)/, { narrow: /^--filter$/, value: /^(?:-c|--config|--import-map|--reporter|--seed)$/ }),
141
+ { start: /^make(?=\s)/, scope: make },
142
+ { start: /^(?:nx|turbo|lerna)(?=\s)/, scope: (args, name) => monorepo(name, args) },
143
+ ];
144
+ function testRun(part) {
145
+ const own = part.replace(LAUNCHER, '');
146
+ for (const { start, scope } of TOOLS) {
147
+ const hit = own.match(start);
148
+ if (!hit) continue;
149
+ const result = scope(own.slice(hit[0].length).split(' ').filter(Boolean), hit[0]);
150
+ return result ? { own, result } : null;
151
+ }
152
+ return null;
153
+ }
154
+
155
+ export function checkScope(command, declared = []) {
156
+ const text = String(command ?? '').replace(/\s+/g, ' ').trim();
157
+ if (!text || NOT_A_RUN.test(text) || VERSION_ONLY.test(text) || LIST_ONLY.test(text)) return null;
158
+ const bare = unquoted(text), runs = partsOf(bare).map(testRun).filter(Boolean);
159
+ // A command the repository declares is its whole check, however it is written;
160
+ // a line that also runs a whole suite is a whole run.
161
+ if (runs.length) return declared.includes(text) || runs.some(run => run.result === 'suite') ? 'suite' : 'partial';
162
+ // A script the repository declares: it cannot be narrowed from here.
163
+ const named = declared.find(d => partsOf(bare).some(part => part.startsWith(unquoted(d))));
164
+ return named && !NOT_TESTS.test(named) ? 'suite' : null;
165
+ }
166
+ // The test step of a command, without what surrounds it: the same step costs the same.
167
+ export function suitePart(command) {
168
+ const runs = partsOf(unquoted(String(command ?? '').replace(/\s+/g, ' ').trim())).map(testRun).filter(Boolean);
169
+ return (runs.find(run => run.result === 'suite') ?? runs[0])?.own ?? String(command ?? '').trim();
170
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.51.1",
3
+ "version": "1.51.2",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",