@application-kit/acceptance 0.0.0-stage → 0.1.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.
Files changed (152) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cjs/bin/application-scenarios.d.ts +2 -0
  3. package/dist/cjs/bin/application-scenarios.js +15 -0
  4. package/dist/cjs/bin/index.d.ts +1 -0
  5. package/dist/cjs/bin/index.js +17 -0
  6. package/dist/cjs/cadence/index.d.ts +1 -0
  7. package/dist/cjs/cadence/index.js +17 -0
  8. package/dist/cjs/cadence/resolve-cadence.d.ts +8 -0
  9. package/dist/cjs/cadence/resolve-cadence.js +18 -0
  10. package/dist/cjs/environments/environment.interface.d.ts +5 -0
  11. package/dist/cjs/environments/environment.interface.js +2 -0
  12. package/dist/cjs/environments/index.d.ts +3 -0
  13. package/dist/cjs/environments/index.js +19 -0
  14. package/dist/cjs/environments/resolve-environment.d.ts +10 -0
  15. package/dist/cjs/environments/resolve-environment.js +20 -0
  16. package/dist/cjs/environments/secrets.d.ts +4 -0
  17. package/dist/cjs/environments/secrets.js +14 -0
  18. package/dist/cjs/index.d.ts +2 -0
  19. package/dist/cjs/index.js +18 -0
  20. package/dist/cjs/package.json +3 -0
  21. package/dist/cjs/scenarios/acceptance-class.d.ts +26 -0
  22. package/dist/cjs/scenarios/acceptance-class.js +100 -0
  23. package/dist/cjs/scenarios/board.d.ts +6 -0
  24. package/dist/cjs/scenarios/board.js +98 -0
  25. package/dist/cjs/scenarios/classes.d.ts +16 -0
  26. package/dist/cjs/scenarios/classes.js +48 -0
  27. package/dist/cjs/scenarios/classify.d.ts +35 -0
  28. package/dist/cjs/scenarios/classify.js +108 -0
  29. package/dist/cjs/scenarios/config.d.ts +17 -0
  30. package/dist/cjs/scenarios/config.js +67 -0
  31. package/dist/cjs/scenarios/coverage-data.d.ts +53 -0
  32. package/dist/cjs/scenarios/coverage-data.js +118 -0
  33. package/dist/cjs/scenarios/coverage-json.d.ts +2 -0
  34. package/dist/cjs/scenarios/coverage-json.js +94 -0
  35. package/dist/cjs/scenarios/coverage-open.d.ts +16 -0
  36. package/dist/cjs/scenarios/coverage-open.js +76 -0
  37. package/dist/cjs/scenarios/coverage-page.d.ts +2 -0
  38. package/dist/cjs/scenarios/coverage-page.js +108 -0
  39. package/dist/cjs/scenarios/coverage-removed.d.ts +13 -0
  40. package/dist/cjs/scenarios/coverage-removed.js +30 -0
  41. package/dist/cjs/scenarios/coverage.d.ts +26 -0
  42. package/dist/cjs/scenarios/coverage.js +91 -0
  43. package/dist/cjs/scenarios/dates.d.ts +8 -0
  44. package/dist/cjs/scenarios/dates.js +93 -0
  45. package/dist/cjs/scenarios/declared.d.ts +18 -0
  46. package/dist/cjs/scenarios/declared.js +69 -0
  47. package/dist/cjs/scenarios/features.d.ts +13 -0
  48. package/dist/cjs/scenarios/features.js +112 -0
  49. package/dist/cjs/scenarios/gherkin.d.ts +10 -0
  50. package/dist/cjs/scenarios/gherkin.js +75 -0
  51. package/dist/cjs/scenarios/git.d.ts +15 -0
  52. package/dist/cjs/scenarios/git.js +83 -0
  53. package/dist/cjs/scenarios/index.d.ts +27 -0
  54. package/dist/cjs/scenarios/index.js +43 -0
  55. package/dist/cjs/scenarios/inventory.d.ts +8 -0
  56. package/dist/cjs/scenarios/inventory.js +55 -0
  57. package/dist/cjs/scenarios/links.d.ts +4 -0
  58. package/dist/cjs/scenarios/links.js +99 -0
  59. package/dist/cjs/scenarios/lint-inputs.d.ts +12 -0
  60. package/dist/cjs/scenarios/lint-inputs.js +118 -0
  61. package/dist/cjs/scenarios/lint.d.ts +8 -0
  62. package/dist/cjs/scenarios/lint.js +94 -0
  63. package/dist/cjs/scenarios/projects.d.ts +21 -0
  64. package/dist/cjs/scenarios/projects.js +104 -0
  65. package/dist/cjs/scenarios/python-text.d.ts +11 -0
  66. package/dist/cjs/scenarios/python-text.js +95 -0
  67. package/dist/cjs/scenarios/remote.d.ts +6 -0
  68. package/dist/cjs/scenarios/remote.js +29 -0
  69. package/dist/cjs/scenarios/removed-list.d.ts +15 -0
  70. package/dist/cjs/scenarios/removed-list.js +62 -0
  71. package/dist/cjs/scenarios/report.d.ts +6 -0
  72. package/dist/cjs/scenarios/report.js +10 -0
  73. package/dist/cjs/scenarios/run.d.ts +10 -0
  74. package/dist/cjs/scenarios/run.js +145 -0
  75. package/dist/cjs/scenarios/task-links.d.ts +15 -0
  76. package/dist/cjs/scenarios/task-links.js +54 -0
  77. package/dist/esm/bin/application-scenarios.d.ts +2 -0
  78. package/dist/esm/bin/application-scenarios.js +10 -0
  79. package/dist/esm/bin/index.d.ts +1 -0
  80. package/dist/esm/bin/index.js +1 -0
  81. package/dist/esm/cadence/index.d.ts +1 -0
  82. package/dist/esm/cadence/index.js +1 -0
  83. package/dist/esm/cadence/resolve-cadence.d.ts +8 -0
  84. package/dist/esm/cadence/resolve-cadence.js +14 -0
  85. package/dist/esm/environments/environment.interface.d.ts +5 -0
  86. package/dist/esm/environments/environment.interface.js +1 -0
  87. package/dist/esm/environments/index.d.ts +3 -0
  88. package/dist/esm/environments/index.js +3 -0
  89. package/dist/esm/environments/resolve-environment.d.ts +10 -0
  90. package/dist/esm/environments/resolve-environment.js +16 -0
  91. package/dist/esm/environments/secrets.d.ts +4 -0
  92. package/dist/esm/environments/secrets.js +9 -0
  93. package/dist/esm/index.d.ts +2 -0
  94. package/dist/esm/index.js +2 -0
  95. package/dist/esm/scenarios/acceptance-class.d.ts +26 -0
  96. package/dist/esm/scenarios/acceptance-class.js +93 -0
  97. package/dist/esm/scenarios/board.d.ts +6 -0
  98. package/dist/esm/scenarios/board.js +92 -0
  99. package/dist/esm/scenarios/classes.d.ts +16 -0
  100. package/dist/esm/scenarios/classes.js +42 -0
  101. package/dist/esm/scenarios/classify.d.ts +35 -0
  102. package/dist/esm/scenarios/classify.js +104 -0
  103. package/dist/esm/scenarios/config.d.ts +17 -0
  104. package/dist/esm/scenarios/config.js +58 -0
  105. package/dist/esm/scenarios/coverage-data.d.ts +53 -0
  106. package/dist/esm/scenarios/coverage-data.js +109 -0
  107. package/dist/esm/scenarios/coverage-json.d.ts +2 -0
  108. package/dist/esm/scenarios/coverage-json.js +90 -0
  109. package/dist/esm/scenarios/coverage-open.d.ts +16 -0
  110. package/dist/esm/scenarios/coverage-open.js +71 -0
  111. package/dist/esm/scenarios/coverage-page.d.ts +2 -0
  112. package/dist/esm/scenarios/coverage-page.js +104 -0
  113. package/dist/esm/scenarios/coverage-removed.d.ts +13 -0
  114. package/dist/esm/scenarios/coverage-removed.js +26 -0
  115. package/dist/esm/scenarios/coverage.d.ts +26 -0
  116. package/dist/esm/scenarios/coverage.js +84 -0
  117. package/dist/esm/scenarios/dates.d.ts +8 -0
  118. package/dist/esm/scenarios/dates.js +87 -0
  119. package/dist/esm/scenarios/declared.d.ts +18 -0
  120. package/dist/esm/scenarios/declared.js +62 -0
  121. package/dist/esm/scenarios/features.d.ts +13 -0
  122. package/dist/esm/scenarios/features.js +96 -0
  123. package/dist/esm/scenarios/gherkin.d.ts +10 -0
  124. package/dist/esm/scenarios/gherkin.js +68 -0
  125. package/dist/esm/scenarios/git.d.ts +15 -0
  126. package/dist/esm/scenarios/git.js +72 -0
  127. package/dist/esm/scenarios/index.d.ts +27 -0
  128. package/dist/esm/scenarios/index.js +27 -0
  129. package/dist/esm/scenarios/inventory.d.ts +8 -0
  130. package/dist/esm/scenarios/inventory.js +48 -0
  131. package/dist/esm/scenarios/links.d.ts +4 -0
  132. package/dist/esm/scenarios/links.js +95 -0
  133. package/dist/esm/scenarios/lint-inputs.d.ts +12 -0
  134. package/dist/esm/scenarios/lint-inputs.js +110 -0
  135. package/dist/esm/scenarios/lint.d.ts +8 -0
  136. package/dist/esm/scenarios/lint.js +89 -0
  137. package/dist/esm/scenarios/projects.d.ts +21 -0
  138. package/dist/esm/scenarios/projects.js +93 -0
  139. package/dist/esm/scenarios/python-text.d.ts +11 -0
  140. package/dist/esm/scenarios/python-text.js +81 -0
  141. package/dist/esm/scenarios/remote.d.ts +6 -0
  142. package/dist/esm/scenarios/remote.js +24 -0
  143. package/dist/esm/scenarios/removed-list.d.ts +15 -0
  144. package/dist/esm/scenarios/removed-list.js +57 -0
  145. package/dist/esm/scenarios/report.d.ts +6 -0
  146. package/dist/esm/scenarios/report.js +6 -0
  147. package/dist/esm/scenarios/run.d.ts +10 -0
  148. package/dist/esm/scenarios/run.js +141 -0
  149. package/dist/esm/scenarios/task-links.d.ts +15 -0
  150. package/dist/esm/scenarios/task-links.js +50 -0
  151. package/package.json +65 -3
  152. package/README.md +0 -3
@@ -0,0 +1,5 @@
1
+ export interface EnvironmentInterface {
2
+ readonly admissionTag?: string;
3
+ readonly name: string;
4
+ readonly secretsPrefix: string;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from './environment.interface.js';
2
+ export * from './resolve-environment.js';
3
+ export * from './secrets.js';
@@ -0,0 +1,3 @@
1
+ export * from './environment.interface.js';
2
+ export * from './resolve-environment.js';
3
+ export * from './secrets.js';
@@ -0,0 +1,10 @@
1
+ import type { EnvironmentInterface } from './environment.interface.js';
2
+ export interface EnvironmentRegistryInterface {
3
+ readonly environments: readonly EnvironmentInterface[];
4
+ readonly fallback: string;
5
+ }
6
+ export interface ResolvedEnvironmentInterface extends EnvironmentInterface {
7
+ readonly baseUrl: string | undefined;
8
+ }
9
+ export type VariablesType = Readonly<Record<string, string | undefined>>;
10
+ export declare const resolveEnvironment: (registry: EnvironmentRegistryInterface, variables: VariablesType) => ResolvedEnvironmentInterface;
@@ -0,0 +1,16 @@
1
+ const findEnvironment = (registry, name) => {
2
+ const environment = registry.environments.find((candidate) => candidate.name === name);
3
+ if (environment === undefined) {
4
+ const known = registry.environments.map((candidate) => candidate.name).join(', ');
5
+ throw new Error(`ACCEPTANCE_ENV: окружения «${name}» нет в реестре (есть: ${known})`);
6
+ }
7
+ return environment;
8
+ };
9
+ const baseUrlOf = (variables) => {
10
+ const baseUrl = variables.ACCEPTANCE_BASE_URL ?? '';
11
+ return baseUrl === '' ? undefined : new URL(baseUrl).href;
12
+ };
13
+ export const resolveEnvironment = (registry, variables) => {
14
+ const environment = findEnvironment(registry, variables.ACCEPTANCE_ENV ?? registry.fallback);
15
+ return { ...environment, baseUrl: baseUrlOf(variables) };
16
+ };
@@ -0,0 +1,4 @@
1
+ import type { EnvironmentInterface } from './environment.interface.js';
2
+ import type { VariablesType } from './resolve-environment.js';
3
+ export declare const secretVariable: (environment: EnvironmentInterface, name: string) => string;
4
+ export declare const readSecret: (environment: EnvironmentInterface, name: string, variables: VariablesType) => string;
@@ -0,0 +1,9 @@
1
+ export const secretVariable = (environment, name) => `${environment.secretsPrefix}${name}`;
2
+ export const readSecret = (environment, name, variables) => {
3
+ const variable = secretVariable(environment, name);
4
+ const value = variables[variable] ?? '';
5
+ if (value === '') {
6
+ throw new Error(`${variable}: переменная с учётными данными не задана`);
7
+ }
8
+ return value;
9
+ };
@@ -0,0 +1,2 @@
1
+ export * from './cadence/index.js';
2
+ export * from './environments/index.js';
@@ -0,0 +1,2 @@
1
+ export * from './cadence/index.js';
2
+ export * from './environments/index.js';
@@ -0,0 +1,26 @@
1
+ import type { ConfigInterface } from './config.js';
2
+ import type { ScenarioInterface } from './gherkin.js';
3
+ export interface AcceptanceFieldInterface {
4
+ readonly kind: string;
5
+ readonly kinds: readonly AcceptanceKindType[];
6
+ readonly line: number;
7
+ readonly problem: string | undefined;
8
+ readonly reason: string;
9
+ }
10
+ export type AcceptanceKindType = (typeof acceptanceKinds)[number];
11
+ export type ScenarioStateType = 'без сценария' | 'все сняты' | 'с тестом' | 'только долг';
12
+ export interface StoryClassInterface {
13
+ readonly defects: readonly string[];
14
+ readonly kinds: readonly AcceptanceKindType[] | undefined;
15
+ }
16
+ export interface StoryInputInterface {
17
+ readonly config: ConfigInterface;
18
+ readonly location: string;
19
+ readonly text: string;
20
+ }
21
+ declare const acceptanceKinds: readonly ["e2e", "code-tests", "none"];
22
+ export declare const acceptanceFields: (text: string) => AcceptanceFieldInterface[];
23
+ export declare const storyLinks: (text: string, config: ConfigInterface) => (readonly [number, string])[];
24
+ export declare const storyClass: (input: StoryInputInterface) => StoryClassInterface;
25
+ export declare const scenarioState: (storyIds: readonly string[], ids: ReadonlyMap<string, readonly ScenarioInterface[]>, removed: ReadonlyMap<string, number>) => ScenarioStateType;
26
+ export {};
@@ -0,0 +1,93 @@
1
+ import { isProjectCovered, markTodo } from './config.js';
2
+ import { pyRepr, splitLines } from './python-text.js';
3
+ import { linkToken } from './task-links.js';
4
+ const acceptanceField = /\*\*Acceptance tests:\*\*[\t ]*(?<value>.*?)(?=\s+·\s+\*\*|$)/gu;
5
+ const acceptanceKinds = ['e2e', 'code-tests', 'none'];
6
+ const legacyKinds = {
7
+ 'n/a': 'none',
8
+ ниже: 'code-tests',
9
+ сценарий: 'e2e',
10
+ };
11
+ const kindNames = acceptanceKinds;
12
+ const isKind = (name) => kindNames.includes(name);
13
+ const partition = (text, separator) => {
14
+ const index = text.indexOf(separator);
15
+ return index === -1 ? [text, ''] : [text.slice(0, index), text.slice(index + separator.length)];
16
+ };
17
+ const namesOf = (kindPart) => kindPart.split(',').map((name) => name.trim());
18
+ const setProblem = (kindPart, kinds) => {
19
+ const field = `\`**Acceptance tests:** ${kindPart}\``;
20
+ if (new Set(kinds).size < kinds.length) {
21
+ return `${field}: класс в наборе дважды`;
22
+ }
23
+ return kinds.includes('none') && kinds.length > 1 ? `${field}: none в наборе с другим классом` : undefined;
24
+ };
25
+ const nameProblem = (kindPart, names, value) => {
26
+ for (const name of names) {
27
+ const renamed = legacyKinds[name];
28
+ if (renamed !== undefined) {
29
+ return `\`**Acceptance tests:** ${kindPart}\`: старое имя класса ${name} — теперь ${renamed}`;
30
+ }
31
+ }
32
+ if (names.every((name) => isKind(name))) {
33
+ return undefined;
34
+ }
35
+ const shown = pyRepr([...value].slice(0, 60).join(''));
36
+ return `\`**Acceptance tests:**\` вне набора ${acceptanceKinds.join(' | ')} (через запятую): ${shown}`;
37
+ };
38
+ const reasonProblem = (kindPart, kinds, reason) => {
39
+ const isUnexplained = kinds.some((kind) => kind !== 'e2e') && reason === '';
40
+ return isUnexplained ? `\`**Acceptance tests:** ${kindPart}\` без причины после тире` : undefined;
41
+ };
42
+ const problemOf = (kindPart, reason, value) => {
43
+ const names = namesOf(kindPart);
44
+ return nameProblem(kindPart, names, value) ?? reasonProblem(kindPart, names, reason) ?? setProblem(kindPart, names);
45
+ };
46
+ const fieldOf = (rawValue, line) => {
47
+ const value = rawValue.trim();
48
+ const [kindPart, reasonPart] = partition(value, '—');
49
+ const kind = kindPart.trim();
50
+ const reason = reasonPart.trim();
51
+ const problem = problemOf(kind, reason, value);
52
+ const kinds = problem === undefined ? namesOf(kind).filter((name) => isKind(name)) : [];
53
+ return { kind, kinds, line, problem, reason };
54
+ };
55
+ const lineFields = (line, index) => [...line.matchAll(acceptanceField)]
56
+ .map((match) => fieldOf(match.groups?.value ?? '', index + 1));
57
+ export const acceptanceFields = (text) => {
58
+ const [header] = partition(text, '\n## ');
59
+ return splitLines(header).flatMap((line, index) => lineFields(line, index));
60
+ };
61
+ export const storyLinks = (text, config) => splitLines(text)
62
+ .flatMap((line, index) => [...line.matchAll(linkToken)]
63
+ .filter((match) => isProjectCovered(config, match.groups?.project ?? ''))
64
+ .map((match) => [index + 1, match[0].slice(1)]));
65
+ const repeatedField = (fields, line, location) => {
66
+ const lines = fields.map((item) => String(item.line)).join(', ');
67
+ return `поле \`**Acceptance tests:**\` в шапке ${fields.length} раза (строки ${lines}): ${location}:${line}`;
68
+ };
69
+ const contradiction = (input, field) => {
70
+ const [link] = storyLinks(input.text, input.config);
71
+ return link === undefined || field.kinds.includes('e2e') ? [] : [`класс ${field.kind}, а story ссылается на сценарий @${link[1]}: ${input.location}:${link[0]}`];
72
+ };
73
+ const classOf = (input, field) => (field.problem === undefined ? { defects: contradiction(input, field), kinds: field.kinds } : { defects: [`${field.problem}: ${input.location}:${field.line}`], kinds: undefined });
74
+ export const storyClass = (input) => {
75
+ const fields = acceptanceFields(input.text);
76
+ const [field, second] = fields;
77
+ if (field === undefined) {
78
+ return { defects: [`закрытая story без поля \`**Acceptance tests:**\`: ${input.location}`], kinds: undefined };
79
+ }
80
+ return second === undefined
81
+ ? classOf(input, field)
82
+ : { defects: [repeatedField(fields, second.line, input.location)], kinds: undefined };
83
+ };
84
+ export const scenarioState = (storyIds, ids, removed) => {
85
+ const alive = storyIds.filter((id) => !removed.has(id)).flatMap((id) => ids.get(id)?.slice(0, 1) ?? []);
86
+ if (alive.some((scenario) => !scenario.tags.includes(markTodo))) {
87
+ return 'с тестом';
88
+ }
89
+ if (alive.length > 0) {
90
+ return 'только долг';
91
+ }
92
+ return storyIds.some((id) => removed.has(id)) ? 'все сняты' : 'без сценария';
93
+ };
@@ -0,0 +1,6 @@
1
+ type RecordType = Readonly<Record<string, unknown>>;
2
+ export declare const acceptanceKey = "acceptance_tests";
3
+ export declare const isRecord: (value: unknown) => value is RecordType;
4
+ export declare const parseBoard: (text: string) => unknown;
5
+ export declare const boardStatuses: (text: string, where: string) => Map<string, string>;
6
+ export {};
@@ -0,0 +1,92 @@
1
+ import { parse } from 'yaml';
2
+ import { ConfigError } from './config.js';
3
+ import { messageOf, pyText } from './python-text.js';
4
+ const boardFormat = 6;
5
+ const epicFields = new Set(['retro', 'status', 'tasks', 'version']);
6
+ const legacyStatuses = {
7
+ contexted: 'in_progress',
8
+ drafted: 'ready',
9
+ 'in-progress': 'in_progress',
10
+ 'ready-for-dev': 'ready',
11
+ };
12
+ const epicKey = /^epic-\d+[a-z]?$/u;
13
+ const integer = /^[+-]?\d+$/u;
14
+ const yamlLogLevel = 'error';
15
+ export const acceptanceKey = 'acceptance_tests';
16
+ export const isRecord = (value) => {
17
+ const isObject = typeof value === 'object' && value !== null;
18
+ return isObject && !Array.isArray(value);
19
+ };
20
+ export const parseBoard = (text) => parse(text, {
21
+ logLevel: yamlLogLevel,
22
+ maxAliasCount: -1,
23
+ uniqueKeys: false,
24
+ version: '1.1',
25
+ });
26
+ const isMissing = (value) => value === undefined || value === null;
27
+ const formatOf = (data, where) => {
28
+ const raw = isMissing(data.version) ? data.board_format : data.version;
29
+ const text = isMissing(raw) ? '1' : pyText(raw).trim();
30
+ if (!integer.test(text)) {
31
+ throw new ConfigError(`board ${where}: version: ${text} — ожидалось целое число версии формата`);
32
+ }
33
+ return Number(text);
34
+ };
35
+ const normalize = (value) => {
36
+ const status = pyText(value);
37
+ return legacyStatuses[status] ?? status;
38
+ };
39
+ const statusField = (value) => ('status' in value ? value.status : 'backlog');
40
+ const taskStatus = (value) => normalize(isRecord(value) ? statusField(value) : value);
41
+ const isEpicBlock = (block) => {
42
+ const keys = isRecord(block) ? Object.keys(block) : undefined;
43
+ return keys?.every((key) => epicFields.has(key)) === true;
44
+ };
45
+ const tasksOf = (epic, block, where) => {
46
+ const tasks = block.tasks ?? {};
47
+ if (!isRecord(tasks)) {
48
+ throw new ConfigError(`board ${where}: development_status[${epic}].tasks — ожидался маппинг задач`);
49
+ }
50
+ return tasks;
51
+ };
52
+ const epicTasks = (epic, block, where) => {
53
+ if (isMissing(block)) {
54
+ return {};
55
+ }
56
+ if (!isEpicBlock(block)) {
57
+ throw new ConfigError(`board ${where}: development_status[${epic}] — не блок эпика формата ${boardFormat}`);
58
+ }
59
+ return tasksOf(epic, block, where);
60
+ };
61
+ const isStory = (key) => !key.endsWith('-retrospective') && !epicKey.test(key);
62
+ const parseOrFail = (text, where) => {
63
+ try {
64
+ return parseBoard(text) ?? {};
65
+ }
66
+ catch (error) {
67
+ throw new ConfigError(`board ${where}: невалидный YAML: ${messageOf(error)}`);
68
+ }
69
+ };
70
+ const loadBoard = (text, where) => {
71
+ const data = parseOrFail(text, where);
72
+ if (!isRecord(data)) {
73
+ throw new ConfigError(`board ${where}: ожидался маппинг`);
74
+ }
75
+ return data;
76
+ };
77
+ const epicsOf = (data, where) => {
78
+ const format = formatOf(data, where);
79
+ if (format !== boardFormat) {
80
+ throw new ConfigError(`board ${where}: формат ${format}, инструмент понимает ${boardFormat}`);
81
+ }
82
+ const epics = data.development_status ?? {};
83
+ if (!isRecord(epics)) {
84
+ throw new ConfigError(`board ${where}: development_status — ожидался маппинг эпиков`);
85
+ }
86
+ return epics;
87
+ };
88
+ export const boardStatuses = (text, where) => {
89
+ const epics = epicsOf(loadBoard(text, where), where);
90
+ const tasks = Object.entries(epics).flatMap(([epic, block]) => Object.entries(epicTasks(epic, block, where)));
91
+ return new Map(tasks.filter(([key]) => isStory(key)).map(([key, value]) => [key, taskStatus(value)]));
92
+ };
@@ -0,0 +1,16 @@
1
+ import type { ClassificationInterface, ScenariosInterface } from './classify.js';
2
+ import type { DeclaredInputInterface } from './declared.js';
3
+ import type { ProjectInterface } from './projects.js';
4
+ import type { ReportInterface } from './report.js';
5
+ export interface ClassifiedAllInterface {
6
+ readonly classified: readonly ClassifiedInterface[];
7
+ readonly defects: readonly string[];
8
+ readonly notes: readonly string[];
9
+ }
10
+ export interface ClassifiedInterface extends ClassificationInterface {
11
+ readonly project: ProjectInterface;
12
+ }
13
+ export declare const noProjects = "\u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432, \u0447\u0435\u0439 board \u043E\u0431\u044A\u044F\u0432\u043B\u044F\u0435\u0442 \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439, \u043D\u0435\u0442";
14
+ export declare const classifyDeclared: (scenarios: ScenariosInterface, input: DeclaredInputInterface) => Promise<ClassifiedAllInterface>;
15
+ export declare const sourcesLine: (projects: readonly ProjectInterface[]) => string;
16
+ export declare const classesCommand: (input: DeclaredInputInterface, isListed: boolean) => Promise<ReportInterface>;
@@ -0,0 +1,42 @@
1
+ import { classify } from './classify.js';
2
+ import { markTodo } from './config.js';
3
+ import { declaredProjects } from './declared.js';
4
+ import { scenarioIds, worktreeFeatures } from './features.js';
5
+ import { sourceOf } from './projects.js';
6
+ import { parseRemoved } from './removed-list.js';
7
+ export const noProjects = 'проектов, чей board объявляет этот репозиторий, нет';
8
+ const readScenarios = async (input) => {
9
+ const files = await worktreeFeatures(input.cwd, input.config);
10
+ const removed = await parseRemoved(input.cwd, input.config.removed);
11
+ return { config: input.config, cwd: input.cwd, ids: scenarioIds(input.config, files), removed: removed.entries };
12
+ };
13
+ const classifyProject = async (scenarios, project) => {
14
+ const classification = await classify(scenarios, project);
15
+ return { ...classification, project };
16
+ };
17
+ export const classifyDeclared = async (scenarios, input) => {
18
+ const { declared, notes } = await declaredProjects(input);
19
+ const results = await Promise.all(declared.map(async ({ defect, project }) => {
20
+ const item = await classifyProject(scenarios, project);
21
+ return { defects: [defect ?? [], item.defects].flat(), item };
22
+ }));
23
+ return { classified: results.map(({ item }) => item), defects: results.flatMap(({ defects }) => defects), notes };
24
+ };
25
+ const projectLine = ({ closed, counts, project }) => {
26
+ const e2e = `e2e ${counts.e2e} (с тестом ${counts['с тестом']}, только долг @${markTodo} `
27
+ + `${counts['только долг']}, все сняты ${counts['все сняты']}, без сценария ${counts['без сценария']})`;
28
+ const others = `code-tests ${counts['code-tests']}, none ${counts.none}, без класса ${counts['без класса']}`;
29
+ return `${project.name} — закрытых ${closed}: ${e2e}, ${others}, знаменатель покрытия ${counts.e2e - counts['все сняты']}`;
30
+ };
31
+ export const sourcesLine = (projects) => {
32
+ const sources = projects.map((project) => sourceOf(project)).join('; ');
33
+ return `задачи: ${sources || 'нет'}`;
34
+ };
35
+ export const classesCommand = async (input, isListed) => {
36
+ const scenarios = await readScenarios(input);
37
+ const { classified, defects, notes } = await classifyDeclared(scenarios, input);
38
+ const parts = classified.map((item) => projectLine(item)).join(' · ');
39
+ const transfers = isListed ? classified.flatMap((item) => item.transfers) : [];
40
+ const summary = [`классы закрытых story: ${parts || noProjects}`, sourcesLine(classified.map((item) => item.project))];
41
+ return { defects, notes, summary: [...summary, ...transfers].join('\n') };
42
+ };
@@ -0,0 +1,35 @@
1
+ import type { AcceptanceKindType, ScenarioStateType } from './acceptance-class.js';
2
+ import type { ConfigInterface } from './config.js';
3
+ import type { ScenarioInterface } from './gherkin.js';
4
+ import type { ProjectInterface } from './projects.js';
5
+ export interface ClassificationInterface {
6
+ readonly closed: number;
7
+ readonly counts: CountsType;
8
+ readonly defects: readonly string[];
9
+ readonly open: readonly OpenStoryInterface[];
10
+ readonly stories: readonly ScenarioStoryInterface[];
11
+ readonly transfers: readonly string[];
12
+ }
13
+ export interface OpenStoryInterface {
14
+ readonly file: string | undefined;
15
+ readonly ids: readonly string[];
16
+ readonly key: string;
17
+ readonly kinds: readonly AcceptanceKindType[] | undefined;
18
+ readonly status: string;
19
+ }
20
+ export interface ScenariosInterface {
21
+ readonly config: ConfigInterface;
22
+ readonly cwd: string;
23
+ readonly ids: ReadonlyMap<string, readonly ScenarioInterface[]>;
24
+ readonly removed: ReadonlyMap<string, number>;
25
+ }
26
+ export interface ScenarioStoryInterface {
27
+ readonly file: string;
28
+ readonly ids: readonly string[];
29
+ readonly key: string;
30
+ readonly state: ScenarioStateType;
31
+ }
32
+ type CountsType = Readonly<Record<CountType, number>>;
33
+ type CountType = 'без класса' | AcceptanceKindType | ScenarioStateType;
34
+ export declare const classify: (scenarios: ScenariosInterface, project: ProjectInterface) => Promise<ClassificationInterface>;
35
+ export {};
@@ -0,0 +1,104 @@
1
+ import { scenarioState, storyClass, storyLinks } from './acceptance-class.js';
2
+ import { ConfigError } from './config.js';
3
+ import { git, readAtRevision } from './git.js';
4
+ import { projectStatuses, storyKey } from './projects.js';
5
+ import { compareText, sortedText } from './python-text.js';
6
+ const storyNumber = /^(?<epic>\d+[a-z]*)-(?<story>\d+[a-z]*)(?:-|$)/u;
7
+ const revisionStoryFiles = async (cwd, project, revision) => {
8
+ const listed = await git(cwd, project.repo, ['ls-tree', '-r', '-z', '--name-only', revision, '--', 'tasks']);
9
+ if (listed === undefined) {
10
+ throw new ConfigError(`git ls-tree в ${project.repo} на ${project.revision} не прошёл`);
11
+ }
12
+ const files = sortedText(listed.split('\0')).filter((file) => file.endsWith('.md'))
13
+ .toReversed();
14
+ return new Map(files.map((file) => [storyKey(file), file]));
15
+ };
16
+ const storyPrefix = (story) => {
17
+ const groups = storyNumber.exec(story.key)?.groups;
18
+ return groups === undefined ? undefined : `${story.project}:AC-${groups.epic}-${groups.story}.`;
19
+ };
20
+ const storyIds = (scenarios, story) => {
21
+ const prefix = storyPrefix(story);
22
+ const linked = storyLinks(story.text, scenarios.config).map(([, tag]) => tag);
23
+ const known = [...scenarios.ids.keys(), ...scenarios.removed.keys()];
24
+ const prefixed = prefix === undefined ? [] : known.filter((id) => id.startsWith(prefix));
25
+ return sortedText(new Set([...linked, ...prefixed]));
26
+ };
27
+ const scenarioOutcome = (scenarios, story, found) => {
28
+ const ids = storyIds(scenarios, story);
29
+ const state = scenarioState(ids, scenarios.ids, scenarios.removed);
30
+ const { defects, kinds } = found;
31
+ const scenarioStory = { file: story.file, ids, key: story.key, state };
32
+ return { counted: [...kinds, state], defects, story: scenarioStory };
33
+ };
34
+ const fileOutcome = (scenarios, story) => {
35
+ const location = `${story.project}/${story.file}`;
36
+ const found = storyClass({ config: scenarios.config, location, text: story.text });
37
+ const { defects, kinds } = found;
38
+ return kinds?.includes('e2e') === true
39
+ ? scenarioOutcome(scenarios, story, { defects, kinds })
40
+ : { counted: kinds ?? ['без класса'], defects };
41
+ };
42
+ const isOpen = (status) => status !== 'done' && status !== 'rejected';
43
+ const openStory = (scenarios, story, status) => {
44
+ const { kinds } = storyClass({ config: scenarios.config, location: story.file, text: story.text });
45
+ const ids = kinds?.includes('e2e') === true ? storyIds(scenarios, story) : [];
46
+ return { file: story.file, ids, key: story.key, kinds, status };
47
+ };
48
+ const missingOutcome = (project, key) => ({
49
+ counted: ['без класса'],
50
+ defects: [`закрытая story без файла в tasks/ на ${project.revision}: ${project.name}:${key}`],
51
+ });
52
+ const countOf = (outcomes) => {
53
+ const counted = outcomes.flatMap((outcome) => outcome.counted);
54
+ const count = (name) => counted.filter((item) => item === name).length;
55
+ return {
56
+ 'code-tests': count('code-tests'),
57
+ e2e: count('e2e'),
58
+ none: count('none'),
59
+ 'без класса': count('без класса'),
60
+ 'без сценария': count('без сценария'),
61
+ 'все сняты': count('все сняты'),
62
+ 'с тестом': count('с тестом'),
63
+ 'только долг': count('только долг'),
64
+ };
65
+ };
66
+ const transferLines = (project, stories) => stories
67
+ .filter((story) => story.state === 'без сценария')
68
+ .map((story) => `перенести: ${project.name}:${story.key} — ${story.file}`);
69
+ const locateStories = async (cwd, project, revision, keys) => {
70
+ const files = await revisionStoryFiles(cwd, project, revision);
71
+ const paths = keys.flatMap((key) => files.get(key) ?? []);
72
+ const texts = await readAtRevision({ cwd, repo: project.repo }, revision, paths);
73
+ return new Map([...texts].map(([file, text]) => [
74
+ storyKey(file),
75
+ { file, key: storyKey(file), project: project.name, text },
76
+ ]));
77
+ };
78
+ export const classify = async (scenarios, project) => {
79
+ const statuses = await projectStatuses(scenarios.cwd, project);
80
+ const revision = String(project.revision);
81
+ const sorted = [...statuses].toSorted(([left], [right]) => compareText(left, right));
82
+ const closed = sorted.filter(([, status]) => status === 'done').map(([key]) => key);
83
+ const open = sorted.filter(([, status]) => isOpen(status));
84
+ const located = await locateStories(scenarios.cwd, project, revision, [...closed, ...open.map(([key]) => key)]);
85
+ const outcomes = closed.map((key) => {
86
+ const story = located.get(key);
87
+ return story === undefined ? missingOutcome(project, key) : fileOutcome(scenarios, story);
88
+ });
89
+ const stories = outcomes.flatMap((outcome) => outcome.story ?? []);
90
+ const openStories = open.map(([key, status]) => {
91
+ const story = located.get(key);
92
+ return story === undefined
93
+ ? { file: undefined, ids: [], key, kinds: undefined, status }
94
+ : openStory(scenarios, story, status);
95
+ });
96
+ return {
97
+ closed: closed.length,
98
+ counts: countOf(outcomes),
99
+ defects: outcomes.flatMap((outcome) => outcome.defects),
100
+ open: openStories,
101
+ stories,
102
+ transfers: transferLines(project, stories),
103
+ };
104
+ };
@@ -0,0 +1,17 @@
1
+ export interface ConfigInterface {
2
+ readonly blocking: readonly string[];
3
+ readonly features: string;
4
+ readonly groups: readonly string[];
5
+ readonly id: RegExp;
6
+ readonly pending: string | undefined;
7
+ readonly removed: string;
8
+ }
9
+ export declare const markPending = "not-implemented";
10
+ export declare const markQuarantine = "quarantine";
11
+ export declare const markTodo = "todo";
12
+ export declare const marks: readonly string[];
13
+ export declare class ConfigError extends Error {
14
+ readonly name = "ConfigError";
15
+ }
16
+ export declare const loadConfig: (file: string, cwd: string) => Promise<ConfigInterface>;
17
+ export declare const isProjectCovered: (config: ConfigInterface, project: string) => boolean;
@@ -0,0 +1,58 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { messageOf } from './python-text.js';
4
+ export const markPending = 'not-implemented';
5
+ export const markQuarantine = 'quarantine';
6
+ export const markTodo = 'todo';
7
+ export const marks = [markPending, markQuarantine, markTodo];
8
+ export class ConfigError extends Error {
9
+ name = 'ConfigError';
10
+ }
11
+ const emptyValues = new Set(['', 0, false, null, undefined]);
12
+ const isFilled = (value) => (Array.isArray(value) ? value.length > 0 : !emptyValues.has(value));
13
+ const isRawConfig = (value) => {
14
+ const isObject = typeof value === 'object' && value !== null;
15
+ return isObject && !Array.isArray(value);
16
+ };
17
+ const parseRaw = (text) => {
18
+ const parsed = JSON.parse(text);
19
+ if (!isRawConfig(parsed)) {
20
+ throw new TypeError('ждём объект JSON');
21
+ }
22
+ return parsed;
23
+ };
24
+ const readRaw = async (file, cwd) => {
25
+ try {
26
+ return parseRaw(await readFile(path.resolve(cwd, file), 'utf8'));
27
+ }
28
+ catch (error) {
29
+ throw new ConfigError(`конфиг ${file} не читается: ${messageOf(error)}`);
30
+ }
31
+ };
32
+ const compileId = (source) => {
33
+ try {
34
+ return new RegExp(source, 'u');
35
+ }
36
+ catch (error) {
37
+ throw new ConfigError(`«id» конфига — не regex: ${messageOf(error)}`);
38
+ }
39
+ };
40
+ const textList = (value) => (Array.isArray(value) ? value.map(String) : []);
41
+ const textOr = (value, fallback) => (typeof value === 'string' ? value : fallback);
42
+ const pendingOf = (value) => (isFilled(value) ? String(value) : undefined);
43
+ export const loadConfig = async (file, cwd) => {
44
+ const raw = await readRaw(file, cwd);
45
+ const missing = ['id', 'groups'].find((key) => !isFilled(raw[key]));
46
+ if (missing !== undefined) {
47
+ throw new ConfigError(`в конфиге ${file} нет ключа «${missing}»`);
48
+ }
49
+ return {
50
+ blocking: textList(raw.blocking),
51
+ features: textOr(raw.features, 'features'),
52
+ groups: textList(raw.groups),
53
+ id: compileId(String(raw.id)),
54
+ pending: pendingOf(raw.pending),
55
+ removed: textOr(raw.removed, 'removed-scenarios.md'),
56
+ };
57
+ };
58
+ export const isProjectCovered = (config, project) => config.id.test(`${project}:AC-1-1.1`);
@@ -0,0 +1,53 @@
1
+ import type { ClassifiedInterface } from './classes.js';
2
+ import type { ScenariosInterface } from './classify.js';
3
+ import type { FilesType } from './features.js';
4
+ import type { ScenarioInterface } from './gherkin.js';
5
+ import type { LinkRowInterface } from './task-links.js';
6
+ export interface CoverageInterface {
7
+ readonly projects: readonly CoverageProjectInterface[];
8
+ readonly uncovered: readonly UncoveredStoryInterface[];
9
+ }
10
+ export interface CoverageProjectInterface {
11
+ readonly allRemoved: number;
12
+ readonly closed: number;
13
+ readonly codeTests: number;
14
+ readonly commit: string;
15
+ readonly covered: number;
16
+ readonly denominator: number;
17
+ readonly e2e: number;
18
+ readonly none: number;
19
+ readonly project: string;
20
+ readonly revision: string;
21
+ readonly share: number | undefined;
22
+ readonly unclassified: number;
23
+ readonly uncovered: Readonly<Record<UncoveredType, number>>;
24
+ }
25
+ export interface PendingInterface {
26
+ readonly file: string;
27
+ readonly id: string;
28
+ readonly line: number;
29
+ readonly stories: readonly ReferrerInterface[];
30
+ readonly title: string;
31
+ readonly url: string | undefined;
32
+ }
33
+ export interface UncoveredStoryInterface {
34
+ readonly file: string;
35
+ readonly project: string;
36
+ readonly scenarios: readonly string[];
37
+ readonly state: UncoveredType;
38
+ readonly story: string;
39
+ readonly url: string | undefined;
40
+ }
41
+ export type UncoveredType = (typeof uncoveredOrder)[number];
42
+ interface ReferrerInterface {
43
+ readonly file: string;
44
+ readonly line: number;
45
+ readonly project: string;
46
+ readonly status: string;
47
+ }
48
+ export declare const uncoveredOrder: readonly ["off-run", "todo-only", "no-scenario"];
49
+ export declare const uncoveredLabels: Readonly<Record<UncoveredType, string>>;
50
+ export declare const liveScenarios: (ids: readonly string[], scenarios: ScenariosInterface) => ScenarioInterface[];
51
+ export declare const coverageOf: (classified: readonly ClassifiedInterface[], scenarios: ScenariosInterface) => Promise<CoverageInterface>;
52
+ export declare const pendingScenarios: (scenarios: ScenariosInterface, files: FilesType, rows: readonly LinkRowInterface[]) => Promise<PendingInterface[]>;
53
+ export {};