atris 3.52.0 → 3.53.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.
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('node:fs');
4
+
5
+ const HTML_TAG_RE = /<!doctype\s+html|<\/?(?:html|head|body|div|section|article|main|table|thead|tbody|tr|td|th|p|br|span|h[1-6]|ul|ol|li|style|script|a|img|strong|em)\b[^>]*>/i;
6
+ const ENCODED_HTML_RE = /&lt;\/?(?:html|head|body|div|table|tr|td|p|span|h[1-6]|ul|ol|li|style|script|svg)\b/i;
7
+ const RENDERED_SOURCE_FENCE_RE = /```\s*(?:html|xml|jsx|tsx|css|svg|mermaid)\b/i;
8
+ const COACH_INTERNAL_RE = /(?:\b(?:CLI|WEB|BE)-\d+\b|\b[0-9A-HJKMNP-TV-Z]{26}\b|\bmission-[a-z0-9-]+|(?:^|\s)--[a-z][a-z-]*\b|(?:^|\s)(?:\.atris|atris\/runs)\/|(?:^|\s)\/(?:Users|home|workspace|tmp)\/\S+)/im;
9
+ const COACH_PRESSURE_RE = /\b(?:asap|urgent|time-sensitive|immediately|friendly reminder|just checking in|you still haven'?t|don'?t forget|why haven'?t you|you need to|productive day|great work|nice work|good job|keep the streak)\b/i;
10
+
11
+ const SLOP_RULES = [
12
+ {
13
+ id: 'copy-slop-corporate-filler',
14
+ re: /\b(?:revolutionary|game[- ]changing|cutting[- ]edge|seamlessly|effortlessly|robust|powerful|comprehensive|leverage|utilize|facilitate|synergy|holistic|pivotal|crucial|unlock|supercharge)\b/i,
15
+ message: 'copy contains corporate filler or hype',
16
+ },
17
+ {
18
+ id: 'copy-slop-ai-tell',
19
+ re: /\b(?:it'?s worth noting that|as you can see|in order to|at the end of the day|great question|absolutely)\b/i,
20
+ message: 'copy contains an AI-tell phrase',
21
+ },
22
+ {
23
+ id: 'copy-slop-punctuation',
24
+ re: /[\u2014\u2728\u{1F680}\u{1F4A1}\u{1F3AF}]/u,
25
+ message: 'copy contains punctuation or decorative symbols blocked by the anti-slop gate',
26
+ },
27
+ ];
28
+
29
+ const VALID_FORMATS = new Set(['plain', 'html', 'markdown', 'visual', 'source']);
30
+ const VALID_COACH_SURFACES = new Set(['morning', 'evening', 'warm-ping']);
31
+
32
+ function usage() {
33
+ return [
34
+ 'Usage:',
35
+ ' node scripts/outbound-artifact-gate.js --channel email --format plain --body-file body.txt',
36
+ ' node scripts/outbound-artifact-gate.js --channel email --format html --body-file body.html --proof-file render.txt',
37
+ '',
38
+ 'Options:',
39
+ ' --channel <email|slack|doc|deck|web|other>',
40
+ ' --format <plain|html|markdown|visual|source>',
41
+ ' --body <text>',
42
+ ' --body-file <path>',
43
+ ' --proof-file <path> Required for html and visual formats',
44
+ ' --coach-surface <morning|evening|warm-ping>',
45
+ ' --signal-proof <path> Required for a warm-ping coach surface',
46
+ ' --visual Require visual proof even when format is not visual',
47
+ ' --allow-slop Skip anti-slop copy checks',
48
+ ].join('\n');
49
+ }
50
+
51
+ function parseArgs(argv) {
52
+ const options = {
53
+ channel: 'other',
54
+ format: 'plain',
55
+ body: '',
56
+ bodyFile: null,
57
+ proofFile: null,
58
+ coachSurface: null,
59
+ signalProof: null,
60
+ visual: false,
61
+ allowSlop: false,
62
+ help: false,
63
+ };
64
+
65
+ for (let i = 0; i < argv.length; i += 1) {
66
+ const arg = argv[i];
67
+ if (!arg.startsWith('--')) {
68
+ throw new Error(`Unexpected argument: ${arg}`);
69
+ }
70
+
71
+ const eq = arg.indexOf('=');
72
+ const key = (eq === -1 ? arg.slice(2) : arg.slice(2, eq)).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
73
+ const inlineValue = eq === -1 ? null : arg.slice(eq + 1);
74
+
75
+ if (key === 'help') {
76
+ options.help = true;
77
+ continue;
78
+ }
79
+ if (key === 'visual' || key === 'allowSlop') {
80
+ options[key] = true;
81
+ continue;
82
+ }
83
+
84
+ const value = inlineValue !== null ? inlineValue : argv[i + 1];
85
+ if (value === undefined || value.startsWith('--')) {
86
+ throw new Error(`Missing value for --${arg.slice(2)}`);
87
+ }
88
+ i += inlineValue === null ? 1 : 0;
89
+
90
+ if (!Object.prototype.hasOwnProperty.call(options, key)) {
91
+ throw new Error(`Unknown option: --${arg.slice(2)}`);
92
+ }
93
+ options[key] = value;
94
+ }
95
+
96
+ return options;
97
+ }
98
+
99
+ function readBody(options) {
100
+ if (options.bodyFile) {
101
+ return fs.readFileSync(options.bodyFile, 'utf8');
102
+ }
103
+ return String(options.body || '');
104
+ }
105
+
106
+ function proofExists(proofFile) {
107
+ if (!proofFile) return false;
108
+ try {
109
+ fs.statSync(proofFile);
110
+ return true;
111
+ } catch (_err) {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ function addError(errors, id, message) {
117
+ errors.push({ id, message });
118
+ }
119
+
120
+ function scanOutboundArtifact(options, body) {
121
+ const errors = [];
122
+ const format = String(options.format || 'plain').toLowerCase();
123
+ const channel = String(options.channel || 'other').toLowerCase();
124
+ const coachSurface = options.coachSurface ? String(options.coachSurface).toLowerCase() : null;
125
+
126
+ if (!VALID_FORMATS.has(format)) {
127
+ addError(errors, 'invalid-format', `format must be one of ${Array.from(VALID_FORMATS).join(', ')}`);
128
+ return errors;
129
+ }
130
+
131
+ if (coachSurface && !VALID_COACH_SURFACES.has(coachSurface)) {
132
+ addError(errors, 'invalid-coach-surface', `coach surface must be one of ${Array.from(VALID_COACH_SURFACES).join(', ')}`);
133
+ }
134
+
135
+ const sendsSource = format === 'source';
136
+ const needsRenderProof = format === 'html' || format === 'visual' || options.visual;
137
+
138
+ if (format === 'plain' && (HTML_TAG_RE.test(body) || ENCODED_HTML_RE.test(body))) {
139
+ addError(errors, 'raw-html-in-plain-body', 'plain body contains HTML source; send rendered HTML or rewrite as plain text');
140
+ }
141
+
142
+ if (!sendsSource && RENDERED_SOURCE_FENCE_RE.test(body)) {
143
+ addError(errors, 'rendered-source-fence', 'body contains source that should be rendered before sending');
144
+ }
145
+
146
+ if (channel === 'email' && format === 'markdown' && RENDERED_SOURCE_FENCE_RE.test(body)) {
147
+ addError(errors, 'markdown-email-source', 'email body contains fenced rendered source; attach source intentionally or render it first');
148
+ }
149
+
150
+ if (needsRenderProof && !proofExists(options.proofFile)) {
151
+ addError(errors, 'render-proof-missing', 'HTML or visual sends need --proof-file with preview, screenshot, PDF, or rendered-email receipt');
152
+ }
153
+
154
+ if (coachSurface) {
155
+ if (COACH_INTERNAL_RE.test(body)) {
156
+ addError(errors, 'coach-internal-language', 'coach copy contains an internal id, flag, or path');
157
+ }
158
+ if (COACH_PRESSURE_RE.test(body)) {
159
+ addError(errors, 'coach-pressure-language', 'coach copy contains urgency, guilt, nagging, or generic productivity praise');
160
+ }
161
+ if (coachSurface === 'warm-ping' && !proofExists(options.signalProof)) {
162
+ addError(errors, 'coach-signal-proof-missing', 'warm pings need --signal-proof for the fresh human event');
163
+ }
164
+ }
165
+
166
+ if (!options.allowSlop) {
167
+ for (const rule of SLOP_RULES) {
168
+ if (rule.re.test(body)) {
169
+ addError(errors, rule.id, rule.message);
170
+ }
171
+ }
172
+ }
173
+
174
+ return errors;
175
+ }
176
+
177
+ function main() {
178
+ let options;
179
+ try {
180
+ options = parseArgs(process.argv.slice(2));
181
+ } catch (err) {
182
+ console.error(err.message);
183
+ console.error(usage());
184
+ process.exitCode = 2;
185
+ return;
186
+ }
187
+
188
+ if (options.help) {
189
+ console.log(usage());
190
+ return;
191
+ }
192
+
193
+ let body;
194
+ try {
195
+ body = readBody(options);
196
+ } catch (err) {
197
+ console.error(`failed to read body: ${err.message}`);
198
+ process.exitCode = 2;
199
+ return;
200
+ }
201
+
202
+ const errors = scanOutboundArtifact(options, body);
203
+ if (errors.length) {
204
+ console.error('outbound artifact gate failed');
205
+ for (const error of errors) {
206
+ console.error(`- ${error.id}: ${error.message}`);
207
+ }
208
+ process.exitCode = 1;
209
+ return;
210
+ }
211
+
212
+ console.log('outbound artifact gate passed');
213
+ }
214
+
215
+ if (require.main === module) {
216
+ main();
217
+ }
218
+
219
+ module.exports = {
220
+ HTML_TAG_RE,
221
+ RENDERED_SOURCE_FENCE_RE,
222
+ SLOP_RULES,
223
+ COACH_INTERNAL_RE,
224
+ COACH_PRESSURE_RE,
225
+ parseArgs,
226
+ scanOutboundArtifact,
227
+ };