@timiagama/ecr 0.0.1 → 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 (138) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +16 -0
  3. package/README.md +411 -7
  4. package/dist/bin.d.ts +20 -0
  5. package/dist/bin.d.ts.map +1 -0
  6. package/dist/bin.js +26 -0
  7. package/dist/bin.js.map +1 -0
  8. package/dist/cli/corpus-loader.d.ts +198 -0
  9. package/dist/cli/corpus-loader.d.ts.map +1 -0
  10. package/dist/cli/corpus-loader.js +351 -0
  11. package/dist/cli/corpus-loader.js.map +1 -0
  12. package/dist/cli/corpus-statistics.d.ts +113 -0
  13. package/dist/cli/corpus-statistics.d.ts.map +1 -0
  14. package/dist/cli/corpus-statistics.js +119 -0
  15. package/dist/cli/corpus-statistics.js.map +1 -0
  16. package/dist/cli/diagnostic-reporter.d.ts +103 -0
  17. package/dist/cli/diagnostic-reporter.d.ts.map +1 -0
  18. package/dist/cli/diagnostic-reporter.js +254 -0
  19. package/dist/cli/diagnostic-reporter.js.map +1 -0
  20. package/dist/cli/project-ignore.d.ts +113 -0
  21. package/dist/cli/project-ignore.d.ts.map +1 -0
  22. package/dist/cli/project-ignore.js +199 -0
  23. package/dist/cli/project-ignore.js.map +1 -0
  24. package/dist/cli/safe-text.d.ts +25 -0
  25. package/dist/cli/safe-text.d.ts.map +1 -0
  26. package/dist/cli/safe-text.js +34 -0
  27. package/dist/cli/safe-text.js.map +1 -0
  28. package/dist/cli/supervisor.d.ts +108 -0
  29. package/dist/cli/supervisor.d.ts.map +1 -0
  30. package/dist/cli/supervisor.js +139 -0
  31. package/dist/cli/supervisor.js.map +1 -0
  32. package/dist/cli.d.ts +247 -0
  33. package/dist/cli.d.ts.map +1 -0
  34. package/dist/cli.js +591 -0
  35. package/dist/cli.js.map +1 -0
  36. package/dist/corpus-validator.d.ts +183 -0
  37. package/dist/corpus-validator.d.ts.map +1 -0
  38. package/dist/corpus-validator.js +479 -0
  39. package/dist/corpus-validator.js.map +1 -0
  40. package/dist/document-identity-rule.d.ts +198 -0
  41. package/dist/document-identity-rule.d.ts.map +1 -0
  42. package/dist/document-identity-rule.js +244 -0
  43. package/dist/document-identity-rule.js.map +1 -0
  44. package/dist/ecr.d.ts +103 -0
  45. package/dist/ecr.d.ts.map +1 -0
  46. package/dist/ecr.js +216 -0
  47. package/dist/ecr.js.map +1 -0
  48. package/dist/glob-pattern.d.ts +94 -0
  49. package/dist/glob-pattern.d.ts.map +1 -0
  50. package/dist/glob-pattern.js +188 -0
  51. package/dist/glob-pattern.js.map +1 -0
  52. package/dist/heading-source-form.d.ts +77 -0
  53. package/dist/heading-source-form.d.ts.map +1 -0
  54. package/dist/heading-source-form.js +129 -0
  55. package/dist/heading-source-form.js.map +1 -0
  56. package/dist/identifier-grammar.d.ts +212 -0
  57. package/dist/identifier-grammar.d.ts.map +1 -0
  58. package/dist/identifier-grammar.js +273 -0
  59. package/dist/identifier-grammar.js.map +1 -0
  60. package/dist/index.d.ts +19 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +11 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/inline-reference-rule.d.ts +557 -0
  65. package/dist/inline-reference-rule.d.ts.map +1 -0
  66. package/dist/inline-reference-rule.js +802 -0
  67. package/dist/inline-reference-rule.js.map +1 -0
  68. package/dist/meta-documents.d.ts +91 -0
  69. package/dist/meta-documents.d.ts.map +1 -0
  70. package/dist/meta-documents.js +133 -0
  71. package/dist/meta-documents.js.map +1 -0
  72. package/dist/per-document-visitor.d.ts +510 -0
  73. package/dist/per-document-visitor.d.ts.map +1 -0
  74. package/dist/per-document-visitor.js +1032 -0
  75. package/dist/per-document-visitor.js.map +1 -0
  76. package/dist/references-section-rule.d.ts +482 -0
  77. package/dist/references-section-rule.d.ts.map +1 -0
  78. package/dist/references-section-rule.js +777 -0
  79. package/dist/references-section-rule.js.map +1 -0
  80. package/dist/section-hierarchy-rule.d.ts +299 -0
  81. package/dist/section-hierarchy-rule.d.ts.map +1 -0
  82. package/dist/section-hierarchy-rule.js +482 -0
  83. package/dist/section-hierarchy-rule.js.map +1 -0
  84. package/dist/source-alignment.d.ts +41 -0
  85. package/dist/source-alignment.d.ts.map +1 -0
  86. package/dist/source-alignment.js +166 -0
  87. package/dist/source-alignment.js.map +1 -0
  88. package/dist/source-lines.d.ts +69 -0
  89. package/dist/source-lines.d.ts.map +1 -0
  90. package/dist/source-lines.js +104 -0
  91. package/dist/source-lines.js.map +1 -0
  92. package/dist/spec-version.d.ts +19 -0
  93. package/dist/spec-version.d.ts.map +1 -0
  94. package/dist/spec-version.js +19 -0
  95. package/dist/spec-version.js.map +1 -0
  96. package/dist/types.d.ts +219 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +11 -0
  99. package/dist/types.js.map +1 -0
  100. package/examples/docs/0. Orientation/0.0 - System Overview.md +34 -0
  101. package/examples/docs/0. Orientation/0.0.1 - Glossary.md +21 -0
  102. package/examples/docs/0. Orientation/0.0.2 - Quality Gates.md +22 -0
  103. package/examples/docs/3. Ingestion/3.1 - Ingestion - Validation Rules.md +33 -0
  104. package/examples/docs/4. Payments/4.10 - Settlement.md +22 -0
  105. package/examples/docs/4. Payments/4.2 - Payment Processing Contract.md +33 -0
  106. package/examples/docs/8. Orchestration/8.1 - Workflow Orchestration Contract.md +38 -0
  107. package/examples/docs/8. Orchestration/8.1.3 - Retry Policy.md +17 -0
  108. package/examples/docs/8. Orchestration/8.10 - Dead Letter Queue.md +20 -0
  109. package/examples/docs/README.md +7 -0
  110. package/package.json +65 -7
  111. package/protocol/navigation-protocol.md +223 -0
  112. package/spec/v2/1 - ECR - Structural Specification.md +1467 -0
  113. package/spec/v2/2 - ECR - User Guide.md +845 -0
  114. package/spec/v2/3 - Design Rationale - The Section Separator.md +243 -0
  115. package/src/bin.ts +30 -0
  116. package/src/cli/corpus-loader.ts +482 -0
  117. package/src/cli/corpus-statistics.ts +196 -0
  118. package/src/cli/diagnostic-reporter.ts +344 -0
  119. package/src/cli/project-ignore.ts +238 -0
  120. package/src/cli/safe-text.ts +39 -0
  121. package/src/cli/supervisor.ts +204 -0
  122. package/src/cli.ts +836 -0
  123. package/src/corpus-validator.ts +642 -0
  124. package/src/document-identity-rule.ts +422 -0
  125. package/src/ecr.ts +298 -0
  126. package/src/glob-pattern.ts +242 -0
  127. package/src/heading-source-form.ts +173 -0
  128. package/src/identifier-grammar.ts +380 -0
  129. package/src/index.ts +78 -0
  130. package/src/inline-reference-rule.ts +1235 -0
  131. package/src/meta-documents.ts +159 -0
  132. package/src/per-document-visitor.ts +1528 -0
  133. package/src/references-section-rule.ts +1182 -0
  134. package/src/section-hierarchy-rule.ts +705 -0
  135. package/src/source-alignment.ts +209 -0
  136. package/src/source-lines.ts +123 -0
  137. package/src/spec-version.ts +19 -0
  138. package/src/types.ts +267 -0
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Project Ignore File
3
+ *
4
+ * `.ecrignore` holds a project's ignore patterns, so they need not be repeated
5
+ * with `--ignore` on every command. Like `.gitignore` it lives at the project
6
+ * root, which the CLI takes to be the working directory, as Prettier and
7
+ * ESLint do; it does not search upwards.
8
+ *
9
+ * Patterns use the same glob rules as `--ignore`, one per line, with blank
10
+ * lines and `#` comments allowed. They are relative to the working directory,
11
+ * whereas `--ignore` patterns are relative to the directory being linted.
12
+ */
13
+ /** The file's name, at the project root. */
14
+ export declare const PROJECT_IGNORE_FILENAME: string;
15
+ /** What reading the project ignore file found. */
16
+ export type ProjectIgnoreRead = {
17
+ readonly kind: 'patterns';
18
+ readonly patterns: readonly string[];
19
+ } | {
20
+ readonly kind: 'missing';
21
+ } | {
22
+ readonly kind: 'unreadable';
23
+ readonly reason: string;
24
+ };
25
+ /** The `.ecrignore` entry an installation needs, or why none can be made. */
26
+ export type InstallExclusion = {
27
+ readonly kind: 'pattern';
28
+ readonly pattern: string;
29
+ } | {
30
+ readonly kind: 'outside';
31
+ } | {
32
+ readonly kind: 'invalid';
33
+ readonly reason: string;
34
+ };
35
+ /**
36
+ * Reads, parses and updates a project's `.ecrignore`.
37
+ */
38
+ export declare class ProjectIgnoreFile {
39
+ /** Path of the `.ecrignore` file. */
40
+ private readonly path;
41
+ /**
42
+ * Creates a handle on the ignore file of a project.
43
+ *
44
+ * @param workingDirectory - The project root: the directory the command runs from
45
+ */
46
+ constructor(workingDirectory: string);
47
+ /**
48
+ * Reads the ignore file's patterns. A missing file is normal; one that
49
+ * exists but cannot be read is reported, not treated as empty.
50
+ *
51
+ * @returns The patterns, or that there is no file, or why it could not be read
52
+ */
53
+ read(): ProjectIgnoreRead;
54
+ /**
55
+ * Adds a pattern unless the file already holds it, keeping everything else
56
+ * in the file exactly as it was. Creates the file if it does not exist.
57
+ *
58
+ * The new contents are written to a temporary file beside it, which then
59
+ * replaces it, so a write that fails part-way leaves the existing patterns
60
+ * untouched rather than truncated.
61
+ *
62
+ * @param pattern - The pattern to add
63
+ * @returns Whether the pattern was added or was already there
64
+ * @throws When the file cannot be read or replaced; it is then unchanged
65
+ */
66
+ addPattern(pattern: string): 'added' | 'present';
67
+ /**
68
+ * Removes a temporary file this class created, ignoring any failure to do
69
+ * so: the error being reported is the write, and a leftover temporary file
70
+ * does not change what the user must do about it.
71
+ *
72
+ * @param path - Path of the temporary file
73
+ */
74
+ private static removeQuietly;
75
+ /**
76
+ * Writes the replacement contents to a temporary file beside the ignore
77
+ * file, which this call creates itself.
78
+ *
79
+ * The name is unpredictable and the file is created exclusively, so nothing
80
+ * already at that path is opened: a link planted there, pointing anywhere on
81
+ * the disk, makes creation fail rather than be written through. That also
82
+ * means only a file this call created is ever removed.
83
+ *
84
+ * @param contents - The complete new contents of the ignore file
85
+ * @returns Path of the temporary file, which now holds those contents
86
+ * @throws When no temporary file could be created
87
+ */
88
+ private writeTemporaryFile;
89
+ /**
90
+ * Splits an ignore file into its patterns.
91
+ *
92
+ * @param text - The file's contents
93
+ * @returns Each pattern, trimmed, without blank lines or `#` comments
94
+ */
95
+ static parse(text: string): readonly string[];
96
+ /**
97
+ * Works out the exact `.ecrignore` pattern that excludes an installation
98
+ * directory, and nothing else.
99
+ *
100
+ * @param workingDirectory - The project root
101
+ * @param destination - The directory being installed into
102
+ * @returns The pattern; or that the destination lies outside the project and needs none; or why no safe pattern exists
103
+ */
104
+ static readInstallExclusion(workingDirectory: string, destination: string): InstallExclusion;
105
+ /**
106
+ * Describes a file-system error briefly.
107
+ *
108
+ * @param error - What was thrown
109
+ * @returns The error's code, such as `EACCES`, or its message
110
+ */
111
+ static showReason(error: unknown): string;
112
+ }
113
+ //# sourceMappingURL=project-ignore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-ignore.d.ts","sourceRoot":"","sources":["../../src/cli/project-ignore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,4CAA4C;AAC5C,eAAO,MAAM,uBAAuB,EAAE,MAAqB,CAAC;AAE5D,kDAAkD;AAClD,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACnE;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D,6EAA6E;AAC7E,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAkB1D;;GAEG;AACH,qBAAa,iBAAiB;IAC5B,qCAAqC;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAE9B;;;;OAIG;gBACgB,gBAAgB,EAAE,MAAM;IAI3C;;;;;OAKG;IACI,IAAI,IAAI,iBAAiB;IAYhC;;;;;;;;;;;OAWG;IACI,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAqBvD;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAQ5B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,kBAAkB;IA2B1B;;;;;OAKG;WACW,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE;IAOpD;;;;;;;OAOG;WACW,oBAAoB,CAAC,gBAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,gBAAgB;IAgCnG;;;;;OAKG;WACW,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM;CAQjD"}
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Project Ignore File
3
+ *
4
+ * `.ecrignore` holds a project's ignore patterns, so they need not be repeated
5
+ * with `--ignore` on every command. Like `.gitignore` it lives at the project
6
+ * root, which the CLI takes to be the working directory, as Prettier and
7
+ * ESLint do; it does not search upwards.
8
+ *
9
+ * Patterns use the same glob rules as `--ignore`, one per line, with blank
10
+ * lines and `#` comments allowed. They are relative to the working directory,
11
+ * whereas `--ignore` patterns are relative to the directory being linted.
12
+ */
13
+ import { randomBytes } from 'node:crypto';
14
+ import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
15
+ import { isAbsolute, join, relative } from 'node:path';
16
+ /** The file's name, at the project root. */
17
+ export const PROJECT_IGNORE_FILENAME = '.ecrignore';
18
+ /**
19
+ * Characters that would change a destination's meaning if written into
20
+ * `.ecrignore` as a literal path: glob wildcards, and line breaks.
21
+ */
22
+ const UNSAFE_PATH_CHARACTERS = /[*?\r\n]/;
23
+ /** A relative path that leaves the directory it is relative to. */
24
+ const PARENT_PREFIX = /^\.\.(?:[\\/]|$)/;
25
+ /**
26
+ * How many names to try when creating the temporary file. A name collision
27
+ * needs 16 random hexadecimal digits to repeat, so one retry would do; a few
28
+ * cost nothing and keep a wedged directory from failing on chance alone.
29
+ */
30
+ const TEMPORARY_NAME_ATTEMPTS = 5;
31
+ /**
32
+ * Reads, parses and updates a project's `.ecrignore`.
33
+ */
34
+ export class ProjectIgnoreFile {
35
+ /** Path of the `.ecrignore` file. */
36
+ path;
37
+ /**
38
+ * Creates a handle on the ignore file of a project.
39
+ *
40
+ * @param workingDirectory - The project root: the directory the command runs from
41
+ */
42
+ constructor(workingDirectory) {
43
+ this.path = join(workingDirectory, PROJECT_IGNORE_FILENAME);
44
+ }
45
+ /**
46
+ * Reads the ignore file's patterns. A missing file is normal; one that
47
+ * exists but cannot be read is reported, not treated as empty.
48
+ *
49
+ * @returns The patterns, or that there is no file, or why it could not be read
50
+ */
51
+ read() {
52
+ if (!existsSync(this.path)) {
53
+ return { kind: 'missing' };
54
+ }
55
+ try {
56
+ return { kind: 'patterns', patterns: ProjectIgnoreFile.parse(readFileSync(this.path, 'utf8')) };
57
+ }
58
+ catch (error) {
59
+ return { kind: 'unreadable', reason: ProjectIgnoreFile.showReason(error) };
60
+ }
61
+ }
62
+ /**
63
+ * Adds a pattern unless the file already holds it, keeping everything else
64
+ * in the file exactly as it was. Creates the file if it does not exist.
65
+ *
66
+ * The new contents are written to a temporary file beside it, which then
67
+ * replaces it, so a write that fails part-way leaves the existing patterns
68
+ * untouched rather than truncated.
69
+ *
70
+ * @param pattern - The pattern to add
71
+ * @returns Whether the pattern was added or was already there
72
+ * @throws When the file cannot be read or replaced; it is then unchanged
73
+ */
74
+ addPattern(pattern) {
75
+ const existing = existsSync(this.path) ? readFileSync(this.path, 'utf8') : '';
76
+ if (ProjectIgnoreFile.parse(existing).includes(pattern)) {
77
+ return 'present';
78
+ }
79
+ const separator = existing === '' || existing.endsWith('\n') ? '' : '\n';
80
+ const temporaryPath = this.writeTemporaryFile(`${existing}${separator}${pattern}\n`);
81
+ try {
82
+ renameSync(temporaryPath, this.path);
83
+ }
84
+ catch (error) {
85
+ ProjectIgnoreFile.removeQuietly(temporaryPath);
86
+ throw error;
87
+ }
88
+ return 'added';
89
+ }
90
+ /**
91
+ * Removes a temporary file this class created, ignoring any failure to do
92
+ * so: the error being reported is the write, and a leftover temporary file
93
+ * does not change what the user must do about it.
94
+ *
95
+ * @param path - Path of the temporary file
96
+ */
97
+ static removeQuietly(path) {
98
+ try {
99
+ rmSync(path, { force: true });
100
+ }
101
+ catch {
102
+ // Nothing useful can be done, and nothing more needs saying.
103
+ }
104
+ }
105
+ /**
106
+ * Writes the replacement contents to a temporary file beside the ignore
107
+ * file, which this call creates itself.
108
+ *
109
+ * The name is unpredictable and the file is created exclusively, so nothing
110
+ * already at that path is opened: a link planted there, pointing anywhere on
111
+ * the disk, makes creation fail rather than be written through. That also
112
+ * means only a file this call created is ever removed.
113
+ *
114
+ * @param contents - The complete new contents of the ignore file
115
+ * @returns Path of the temporary file, which now holds those contents
116
+ * @throws When no temporary file could be created
117
+ */
118
+ writeTemporaryFile(contents) {
119
+ let lastError;
120
+ for (let attempt = 0; attempt < TEMPORARY_NAME_ATTEMPTS; attempt += 1) {
121
+ const candidate = `${this.path}.${randomBytes(8).toString('hex')}.tmp`;
122
+ try {
123
+ writeFileSync(candidate, contents, { encoding: 'utf8', flag: 'wx' });
124
+ return candidate;
125
+ }
126
+ catch (error) {
127
+ if (error.code === 'EEXIST') {
128
+ // Something is already at that name. It is not this call's to
129
+ // read, write or remove: try another name.
130
+ lastError = error;
131
+ continue;
132
+ }
133
+ // Anything else means the name was free, so whatever is there now was
134
+ // created by the call that just failed, perhaps written only in part.
135
+ ProjectIgnoreFile.removeQuietly(candidate);
136
+ throw error;
137
+ }
138
+ }
139
+ throw lastError;
140
+ }
141
+ /**
142
+ * Splits an ignore file into its patterns.
143
+ *
144
+ * @param text - The file's contents
145
+ * @returns Each pattern, trimmed, without blank lines or `#` comments
146
+ */
147
+ static parse(text) {
148
+ return text
149
+ .split(/\r?\n/)
150
+ .map((line) => line.trim())
151
+ .filter((line) => line !== '' && !line.startsWith('#'));
152
+ }
153
+ /**
154
+ * Works out the exact `.ecrignore` pattern that excludes an installation
155
+ * directory, and nothing else.
156
+ *
157
+ * @param workingDirectory - The project root
158
+ * @param destination - The directory being installed into
159
+ * @returns The pattern; or that the destination lies outside the project and needs none; or why no safe pattern exists
160
+ */
161
+ static readInstallExclusion(workingDirectory, destination) {
162
+ const relativePath = relative(workingDirectory, destination);
163
+ if (relativePath === '') {
164
+ return {
165
+ kind: 'invalid',
166
+ reason: 'it is the working directory itself, and excluding it would exclude the whole project',
167
+ };
168
+ }
169
+ // Another drive on Windows gives an absolute path; a parent gives `..`.
170
+ if (isAbsolute(relativePath) || PARENT_PREFIX.test(relativePath)) {
171
+ return { kind: 'outside' };
172
+ }
173
+ const path = relativePath.replaceAll('\\', '/');
174
+ if (UNSAFE_PATH_CHARACTERS.test(path)) {
175
+ return { kind: 'invalid', reason: 'its name contains `*`, `?` or a line break, which .ecrignore would read as a pattern' };
176
+ }
177
+ if (path.startsWith('#')) {
178
+ return { kind: 'invalid', reason: 'its name starts with `#`, which .ecrignore would read as a comment' };
179
+ }
180
+ if (path !== path.trim()) {
181
+ return { kind: 'invalid', reason: 'its name starts or ends with a space, which .ecrignore would drop' };
182
+ }
183
+ return { kind: 'pattern', pattern: `${path}/**` };
184
+ }
185
+ /**
186
+ * Describes a file-system error briefly.
187
+ *
188
+ * @param error - What was thrown
189
+ * @returns The error's code, such as `EACCES`, or its message
190
+ */
191
+ static showReason(error) {
192
+ if (error instanceof Error) {
193
+ const code = error.code;
194
+ return typeof code === 'string' ? code : error.message;
195
+ }
196
+ return String(error);
197
+ }
198
+ }
199
+ //# sourceMappingURL=project-ignore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-ignore.js","sourceRoot":"","sources":["../../src/cli/project-ignore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Terminal-Safe Text
3
+ *
4
+ * The CLI prints text it did not write: titles and headings quoted back from
5
+ * documents, and the paths of files it found. A terminal reads some characters
6
+ * as commands rather than as text — an escape sequence can clear the screen,
7
+ * move the cursor, recolour what is already there or overwrite a line — so a
8
+ * document could otherwise decide what the report of it appears to say.
9
+ *
10
+ * Anything printed as text is therefore written with those characters shown
11
+ * rather than obeyed. The JSON form needs no such treatment, because a
12
+ * consumer of it is not a terminal, and escaping there would change the data.
13
+ */
14
+ /**
15
+ * Shows the characters a terminal would act on, rather than letting it act on
16
+ * them, writing each as the `\uXXXX` escape that names it.
17
+ *
18
+ * Tab and newline are left alone: the report is made of lines, and neither
19
+ * can misrepresent what it says.
20
+ *
21
+ * @param text - Text about to be printed to a terminal
22
+ * @returns The same text with every acted-on character shown
23
+ */
24
+ export declare function showControlCharacters(text: string): string;
25
+ //# sourceMappingURL=safe-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-text.d.ts","sourceRoot":"","sources":["../../src/cli/safe-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAUH;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM1D"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Terminal-Safe Text
3
+ *
4
+ * The CLI prints text it did not write: titles and headings quoted back from
5
+ * documents, and the paths of files it found. A terminal reads some characters
6
+ * as commands rather than as text — an escape sequence can clear the screen,
7
+ * move the cursor, recolour what is already there or overwrite a line — so a
8
+ * document could otherwise decide what the report of it appears to say.
9
+ *
10
+ * Anything printed as text is therefore written with those characters shown
11
+ * rather than obeyed. The JSON form needs no such treatment, because a
12
+ * consumer of it is not a terminal, and escaping there would change the data.
13
+ */
14
+ /**
15
+ * Characters a terminal acts on rather than displays: the C0 controls apart
16
+ * from tab and newline, delete, the C1 controls, and the bidirectional
17
+ * formatting characters, which reorder the text around them.
18
+ */
19
+ // eslint-disable-next-line no-control-regex -- Matching control characters is the point.
20
+ const ACTED_ON_BY_TERMINALS = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F‪-‮⁦-⁩]/g;
21
+ /**
22
+ * Shows the characters a terminal would act on, rather than letting it act on
23
+ * them, writing each as the `\uXXXX` escape that names it.
24
+ *
25
+ * Tab and newline are left alone: the report is made of lines, and neither
26
+ * can misrepresent what it says.
27
+ *
28
+ * @param text - Text about to be printed to a terminal
29
+ * @returns The same text with every acted-on character shown
30
+ */
31
+ export function showControlCharacters(text) {
32
+ return text.replace(ACTED_ON_BY_TERMINALS, (character) => `\\u${character.codePointAt(0)?.toString(16).padStart(4, '0') ?? ''}`);
33
+ }
34
+ //# sourceMappingURL=safe-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-text.js","sourceRoot":"","sources":["../../src/cli/safe-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;GAIG;AACH,yFAAyF;AACzF,MAAM,qBAAqB,GAAW,kDAAkD,CAAC;AAEzF;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO,IAAI,CAAC,OAAO,CACjB,qBAAqB,EACrB,CAAC,SAAiB,EAAU,EAAE,CAC5B,MAAM,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE,CACxE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Bounded Execution
3
+ *
4
+ * Parsing Markdown is where a corpus stops being data and starts being work
5
+ * the command cannot predict. The parser is recursive and, on some inputs,
6
+ * quadratic: four thousand nested bold spans inside a link label -- 16 KB --
7
+ * exhaust the call stack, and four thousand nested images -- 35 KB -- parse
8
+ * for the best part of a minute. Neither is a fault in a document anyone
9
+ * wrote on purpose, and neither can be found by inspecting a document before
10
+ * parsing it, since the cost is the parse.
11
+ *
12
+ * So `lint` and `stats` do their work in a child process this one starts and
13
+ * can stop. The child is this same executable, run once for the whole corpus,
14
+ * with a time limit and a heap limit; whatever it prints is relayed. If it
15
+ * has to be stopped, nothing it printed is relayed, because a report of a
16
+ * corpus that was never finished is worse than no report: half a JSON
17
+ * document still parses as JSON.
18
+ *
19
+ * This bounds the command, not the library. A host calling `lintDocument`
20
+ * directly runs the parser in its own process and must bound it itself.
21
+ */
22
+ /**
23
+ * The environment variable marking the child, so that it does the work
24
+ * rather than starting a child of its own.
25
+ */
26
+ export declare const SUPERVISED_VARIABLE: string;
27
+ /** The limits a supervised run is given. */
28
+ export interface SupervisionLimits {
29
+ /** Milliseconds the child may run for; `0` for no limit. */
30
+ readonly timeoutMs: number;
31
+ /**
32
+ * Mebibytes the child's V8 old-generation heap may grow to; `0` leaves
33
+ * Node's own default. This bounds the heap where a parse accumulates, not
34
+ * the process's total memory: stacks, buffers and the young generation sit
35
+ * outside it, so it is a brake rather than a ceiling.
36
+ */
37
+ readonly memoryMib: number;
38
+ /** Bytes of output the child may produce before it is stopped. */
39
+ readonly maxOutputBytes: number;
40
+ }
41
+ /** A child that finished by itself: its output and exit code stand. */
42
+ export interface CompletedRun {
43
+ /** Distinguishes this from a run that was stopped. */
44
+ readonly kind: 'completed';
45
+ /** Everything the child wrote to standard output. */
46
+ readonly stdout: string;
47
+ /** Everything the child wrote to standard error. */
48
+ readonly stderr: string;
49
+ /** The code the child exited with. */
50
+ readonly exitCode: number;
51
+ }
52
+ /** A child that was stopped, or never ran; nothing it printed is usable. */
53
+ export interface StoppedRun {
54
+ /** Distinguishes this from a run that completed. */
55
+ readonly kind: 'stopped';
56
+ /** Why it did not finish, as a phrase completing "the run did not finish because...". */
57
+ readonly reason: string;
58
+ }
59
+ /** What a supervised run produced. */
60
+ export type SupervisedOutcome = CompletedRun | StoppedRun;
61
+ /**
62
+ * Default limits: generous for a real corpus, finite for a hostile one.
63
+ *
64
+ * The heap limit is V8's old-generation size. It bounds where a parse
65
+ * accumulates, not the process: a child can still be stopped for reasons
66
+ * this number does not govern, and a child given too little cannot start at
67
+ * all, which the command reports rather than passing on.
68
+ */
69
+ export declare const DEFAULT_SUPERVISION_LIMITS: SupervisionLimits;
70
+ /**
71
+ * Runs a command in a child process that can be stopped.
72
+ */
73
+ export declare class Supervisor {
74
+ /** The script the child runs: this package's executable. */
75
+ private readonly entryPoint;
76
+ /** The limits placed on the child. */
77
+ private readonly limits;
78
+ /**
79
+ * The directory the child runs in. Relative paths in the arguments, and the
80
+ * project whose `.ecrignore` applies, are resolved against it, so it must be
81
+ * the directory the command was configured with rather than whichever
82
+ * directory this process happens to be in.
83
+ */
84
+ private readonly workingDirectory;
85
+ /**
86
+ * Creates a supervisor.
87
+ *
88
+ * @param entryPoint - Path of the script the child runs
89
+ * @param limits - The limits to place on it
90
+ * @param workingDirectory - The directory the child runs in
91
+ */
92
+ constructor(entryPoint: string, limits?: SupervisionLimits, workingDirectory?: string);
93
+ /**
94
+ * Runs one command in a child process and waits for it.
95
+ *
96
+ * @param argv - The arguments to pass on, exactly as they were given
97
+ * @returns What the child produced, or why it was stopped
98
+ */
99
+ run(argv: readonly string[]): SupervisedOutcome;
100
+ /**
101
+ * Describes why a child did not finish, in terms of the limit it met.
102
+ *
103
+ * @param error - What `spawnSync` reported
104
+ * @returns The reason, as a phrase completing "the run did not finish because..."
105
+ */
106
+ private showFailure;
107
+ }
108
+ //# sourceMappingURL=supervisor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../src/cli/supervisor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAyB,CAAC;AAE5D,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,sDAAsD;IACtD,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,4EAA4E;AAC5E,MAAM,WAAW,UAAU;IACzB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,sCAAsC;AACtC,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,UAAU,CAAC;AAE1D;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,EAAE,iBAIxC,CAAC;AAEF;;GAEG;AACH,qBAAa,UAAU;IACrB,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAEpC,sCAAsC;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAE3C;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C;;;;;;OAMG;gBAED,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,iBAA8C,EACtD,gBAAgB,GAAE,MAAsB;IAO1C;;;;;OAKG;IACI,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,iBAAiB;IAiDtD;;;;;OAKG;IACH,OAAO,CAAC,WAAW;CAwBpB"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Bounded Execution
3
+ *
4
+ * Parsing Markdown is where a corpus stops being data and starts being work
5
+ * the command cannot predict. The parser is recursive and, on some inputs,
6
+ * quadratic: four thousand nested bold spans inside a link label -- 16 KB --
7
+ * exhaust the call stack, and four thousand nested images -- 35 KB -- parse
8
+ * for the best part of a minute. Neither is a fault in a document anyone
9
+ * wrote on purpose, and neither can be found by inspecting a document before
10
+ * parsing it, since the cost is the parse.
11
+ *
12
+ * So `lint` and `stats` do their work in a child process this one starts and
13
+ * can stop. The child is this same executable, run once for the whole corpus,
14
+ * with a time limit and a heap limit; whatever it prints is relayed. If it
15
+ * has to be stopped, nothing it printed is relayed, because a report of a
16
+ * corpus that was never finished is worse than no report: half a JSON
17
+ * document still parses as JSON.
18
+ *
19
+ * This bounds the command, not the library. A host calling `lintDocument`
20
+ * directly runs the parser in its own process and must bound it itself.
21
+ */
22
+ import { spawnSync } from 'node:child_process';
23
+ /**
24
+ * The environment variable marking the child, so that it does the work
25
+ * rather than starting a child of its own.
26
+ */
27
+ export const SUPERVISED_VARIABLE = 'ECR_SUPERVISED';
28
+ /**
29
+ * Default limits: generous for a real corpus, finite for a hostile one.
30
+ *
31
+ * The heap limit is V8's old-generation size. It bounds where a parse
32
+ * accumulates, not the process: a child can still be stopped for reasons
33
+ * this number does not govern, and a child given too little cannot start at
34
+ * all, which the command reports rather than passing on.
35
+ */
36
+ export const DEFAULT_SUPERVISION_LIMITS = {
37
+ timeoutMs: 120_000,
38
+ memoryMib: 2048,
39
+ maxOutputBytes: 64 * 1024 * 1024,
40
+ };
41
+ /**
42
+ * Runs a command in a child process that can be stopped.
43
+ */
44
+ export class Supervisor {
45
+ /** The script the child runs: this package's executable. */
46
+ entryPoint;
47
+ /** The limits placed on the child. */
48
+ limits;
49
+ /**
50
+ * The directory the child runs in. Relative paths in the arguments, and the
51
+ * project whose `.ecrignore` applies, are resolved against it, so it must be
52
+ * the directory the command was configured with rather than whichever
53
+ * directory this process happens to be in.
54
+ */
55
+ workingDirectory;
56
+ /**
57
+ * Creates a supervisor.
58
+ *
59
+ * @param entryPoint - Path of the script the child runs
60
+ * @param limits - The limits to place on it
61
+ * @param workingDirectory - The directory the child runs in
62
+ */
63
+ constructor(entryPoint, limits = DEFAULT_SUPERVISION_LIMITS, workingDirectory = process.cwd()) {
64
+ this.entryPoint = entryPoint;
65
+ this.limits = limits;
66
+ this.workingDirectory = workingDirectory;
67
+ }
68
+ /**
69
+ * Runs one command in a child process and waits for it.
70
+ *
71
+ * @param argv - The arguments to pass on, exactly as they were given
72
+ * @returns What the child produced, or why it was stopped
73
+ */
74
+ run(argv) {
75
+ const nodeOptions = this.limits.memoryMib > 0 ? [`--max-old-space-size=${String(this.limits.memoryMib)}`] : [];
76
+ let child;
77
+ try {
78
+ child = spawnSync(process.execPath, [...nodeOptions, this.entryPoint, ...argv], {
79
+ encoding: 'utf8',
80
+ cwd: this.workingDirectory,
81
+ maxBuffer: this.limits.maxOutputBytes,
82
+ ...(this.limits.timeoutMs > 0 ? { timeout: this.limits.timeoutMs } : {}),
83
+ // A child busy in synchronous JavaScript cannot run a handler for a
84
+ // signal, but an idle one can, and a handled SIGTERM would leave this
85
+ // process waiting past the limit it just imposed. Nothing here needs
86
+ // the child's cooperation, so it is not asked for.
87
+ killSignal: 'SIGKILL',
88
+ env: { ...process.env, [SUPERVISED_VARIABLE]: '1' },
89
+ });
90
+ }
91
+ catch (error) {
92
+ // Starting a process can fail outright -- a limit node will not accept,
93
+ // a directory that is not there -- and that is a run that did not happen.
94
+ return {
95
+ kind: 'stopped',
96
+ reason: `it could not be started (${error instanceof Error ? error.message : String(error)})`,
97
+ };
98
+ }
99
+ if (child.error !== undefined) {
100
+ return { kind: 'stopped', reason: this.showFailure(child.error) };
101
+ }
102
+ // A child killed by a signal reports no exit code. Whatever reached this
103
+ // process before it died says nothing about the corpus as a whole.
104
+ if (child.status === null) {
105
+ return {
106
+ kind: 'stopped',
107
+ reason: `it was stopped (${child.signal ?? 'unknown signal'}) before it finished`,
108
+ };
109
+ }
110
+ return {
111
+ kind: 'completed',
112
+ stdout: child.stdout,
113
+ stderr: child.stderr,
114
+ exitCode: child.status,
115
+ };
116
+ }
117
+ /**
118
+ * Describes why a child did not finish, in terms of the limit it met.
119
+ *
120
+ * @param error - What `spawnSync` reported
121
+ * @returns The reason, as a phrase completing "the run did not finish because..."
122
+ */
123
+ showFailure(error) {
124
+ const code = error.code;
125
+ if (code === 'ETIMEDOUT') {
126
+ const seconds = (this.limits.timeoutMs / 1000).toFixed(0);
127
+ return (`it reached its time limit of ${seconds} second(s). One document can take that long ` +
128
+ `to parse when its Markdown is deeply nested. Raise the limit with --timeout <seconds>, ` +
129
+ `or narrow the corpus`);
130
+ }
131
+ if (code === 'ENOBUFS') {
132
+ const mebibytes = (this.limits.maxOutputBytes / (1024 * 1024)).toFixed(0);
133
+ return (`it produced more than ${mebibytes} MiB of output, which cannot be passed on whole. ` +
134
+ `Part of a report is not a report, so none of it was printed`);
135
+ }
136
+ return `it could not be run (${code ?? error.message})`;
137
+ }
138
+ }
139
+ //# sourceMappingURL=supervisor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../../src/cli/supervisor.ts"],"names":[],"mappings":"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"}