@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,204 @@
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
+ import { spawnSync } from 'node:child_process';
24
+ import type { SpawnSyncReturns } from 'node:child_process';
25
+
26
+ /**
27
+ * The environment variable marking the child, so that it does the work
28
+ * rather than starting a child of its own.
29
+ */
30
+ export const SUPERVISED_VARIABLE: string = 'ECR_SUPERVISED';
31
+
32
+ /** The limits a supervised run is given. */
33
+ export interface SupervisionLimits {
34
+ /** Milliseconds the child may run for; `0` for no limit. */
35
+ readonly timeoutMs: number;
36
+ /**
37
+ * Mebibytes the child's V8 old-generation heap may grow to; `0` leaves
38
+ * Node's own default. This bounds the heap where a parse accumulates, not
39
+ * the process's total memory: stacks, buffers and the young generation sit
40
+ * outside it, so it is a brake rather than a ceiling.
41
+ */
42
+ readonly memoryMib: number;
43
+ /** Bytes of output the child may produce before it is stopped. */
44
+ readonly maxOutputBytes: number;
45
+ }
46
+
47
+ /** A child that finished by itself: its output and exit code stand. */
48
+ export interface CompletedRun {
49
+ /** Distinguishes this from a run that was stopped. */
50
+ readonly kind: 'completed';
51
+ /** Everything the child wrote to standard output. */
52
+ readonly stdout: string;
53
+ /** Everything the child wrote to standard error. */
54
+ readonly stderr: string;
55
+ /** The code the child exited with. */
56
+ readonly exitCode: number;
57
+ }
58
+
59
+ /** A child that was stopped, or never ran; nothing it printed is usable. */
60
+ export interface StoppedRun {
61
+ /** Distinguishes this from a run that completed. */
62
+ readonly kind: 'stopped';
63
+ /** Why it did not finish, as a phrase completing "the run did not finish because...". */
64
+ readonly reason: string;
65
+ }
66
+
67
+ /** What a supervised run produced. */
68
+ export type SupervisedOutcome = CompletedRun | StoppedRun;
69
+
70
+ /**
71
+ * Default limits: generous for a real corpus, finite for a hostile one.
72
+ *
73
+ * The heap limit is V8's old-generation size. It bounds where a parse
74
+ * accumulates, not the process: a child can still be stopped for reasons
75
+ * this number does not govern, and a child given too little cannot start at
76
+ * all, which the command reports rather than passing on.
77
+ */
78
+ export const DEFAULT_SUPERVISION_LIMITS: SupervisionLimits = {
79
+ timeoutMs: 120_000,
80
+ memoryMib: 2048,
81
+ maxOutputBytes: 64 * 1024 * 1024,
82
+ };
83
+
84
+ /**
85
+ * Runs a command in a child process that can be stopped.
86
+ */
87
+ export class Supervisor {
88
+ /** The script the child runs: this package's executable. */
89
+ private readonly entryPoint: string;
90
+
91
+ /** The limits placed on the child. */
92
+ private readonly limits: SupervisionLimits;
93
+
94
+ /**
95
+ * The directory the child runs in. Relative paths in the arguments, and the
96
+ * project whose `.ecrignore` applies, are resolved against it, so it must be
97
+ * the directory the command was configured with rather than whichever
98
+ * directory this process happens to be in.
99
+ */
100
+ private readonly workingDirectory: string;
101
+
102
+ /**
103
+ * Creates a supervisor.
104
+ *
105
+ * @param entryPoint - Path of the script the child runs
106
+ * @param limits - The limits to place on it
107
+ * @param workingDirectory - The directory the child runs in
108
+ */
109
+ public constructor(
110
+ entryPoint: string,
111
+ limits: SupervisionLimits = DEFAULT_SUPERVISION_LIMITS,
112
+ workingDirectory: string = process.cwd(),
113
+ ) {
114
+ this.entryPoint = entryPoint;
115
+ this.limits = limits;
116
+ this.workingDirectory = workingDirectory;
117
+ }
118
+
119
+ /**
120
+ * Runs one command in a child process and waits for it.
121
+ *
122
+ * @param argv - The arguments to pass on, exactly as they were given
123
+ * @returns What the child produced, or why it was stopped
124
+ */
125
+ public run(argv: readonly string[]): SupervisedOutcome {
126
+ const nodeOptions: readonly string[] =
127
+ this.limits.memoryMib > 0 ? [`--max-old-space-size=${String(this.limits.memoryMib)}`] : [];
128
+
129
+ let child: SpawnSyncReturns<string>;
130
+
131
+ try {
132
+ child = spawnSync(process.execPath, [...nodeOptions, this.entryPoint, ...argv], {
133
+ encoding: 'utf8',
134
+ cwd: this.workingDirectory,
135
+ maxBuffer: this.limits.maxOutputBytes,
136
+ ...(this.limits.timeoutMs > 0 ? { timeout: this.limits.timeoutMs } : {}),
137
+ // A child busy in synchronous JavaScript cannot run a handler for a
138
+ // signal, but an idle one can, and a handled SIGTERM would leave this
139
+ // process waiting past the limit it just imposed. Nothing here needs
140
+ // the child's cooperation, so it is not asked for.
141
+ killSignal: 'SIGKILL',
142
+ env: { ...process.env, [SUPERVISED_VARIABLE]: '1' },
143
+ });
144
+ } catch (error: unknown) {
145
+ // Starting a process can fail outright -- a limit node will not accept,
146
+ // a directory that is not there -- and that is a run that did not happen.
147
+ return {
148
+ kind: 'stopped',
149
+ reason: `it could not be started (${error instanceof Error ? error.message : String(error)})`,
150
+ };
151
+ }
152
+
153
+ if (child.error !== undefined) {
154
+ return { kind: 'stopped', reason: this.showFailure(child.error) };
155
+ }
156
+
157
+ // A child killed by a signal reports no exit code. Whatever reached this
158
+ // process before it died says nothing about the corpus as a whole.
159
+ if (child.status === null) {
160
+ return {
161
+ kind: 'stopped',
162
+ reason: `it was stopped (${child.signal ?? 'unknown signal'}) before it finished`,
163
+ };
164
+ }
165
+
166
+ return {
167
+ kind: 'completed',
168
+ stdout: child.stdout,
169
+ stderr: child.stderr,
170
+ exitCode: child.status,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Describes why a child did not finish, in terms of the limit it met.
176
+ *
177
+ * @param error - What `spawnSync` reported
178
+ * @returns The reason, as a phrase completing "the run did not finish because..."
179
+ */
180
+ private showFailure(error: Error): string {
181
+ const code: string | undefined = (error as NodeJS.ErrnoException).code;
182
+
183
+ if (code === 'ETIMEDOUT') {
184
+ const seconds: string = (this.limits.timeoutMs / 1000).toFixed(0);
185
+
186
+ return (
187
+ `it reached its time limit of ${seconds} second(s). One document can take that long ` +
188
+ `to parse when its Markdown is deeply nested. Raise the limit with --timeout <seconds>, ` +
189
+ `or narrow the corpus`
190
+ );
191
+ }
192
+
193
+ if (code === 'ENOBUFS') {
194
+ const mebibytes: string = (this.limits.maxOutputBytes / (1024 * 1024)).toFixed(0);
195
+
196
+ return (
197
+ `it produced more than ${mebibytes} MiB of output, which cannot be passed on whole. ` +
198
+ `Part of a report is not a report, so none of it was printed`
199
+ );
200
+ }
201
+
202
+ return `it could not be run (${code ?? error.message})`;
203
+ }
204
+ }