@clossys/writer 0.4.0 → 0.5.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 (109) hide show
  1. package/README.md +587 -2
  2. package/dist/approval-bypass.d.ts +120 -0
  3. package/dist/approval-bypass.d.ts.map +1 -0
  4. package/dist/approval-bypass.js +2318 -0
  5. package/dist/approval-bypass.js.map +1 -0
  6. package/dist/approval.d.ts +62 -0
  7. package/dist/approval.d.ts.map +1 -0
  8. package/dist/approval.js +421 -0
  9. package/dist/approval.js.map +1 -0
  10. package/dist/approve-cli.d.ts +82 -0
  11. package/dist/approve-cli.d.ts.map +1 -0
  12. package/dist/approve-cli.js +430 -0
  13. package/dist/approve-cli.js.map +1 -0
  14. package/dist/cli.d.ts +15 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +48 -13
  17. package/dist/cli.js.map +1 -1
  18. package/dist/field-options.d.ts +59 -0
  19. package/dist/field-options.d.ts.map +1 -0
  20. package/dist/field-options.js +109 -0
  21. package/dist/field-options.js.map +1 -0
  22. package/dist/front-door.check.d.ts +2 -0
  23. package/dist/front-door.check.d.ts.map +1 -0
  24. package/dist/front-door.check.js +8 -0
  25. package/dist/front-door.check.js.map +1 -0
  26. package/dist/front-door.d.ts +82 -0
  27. package/dist/front-door.d.ts.map +1 -0
  28. package/dist/front-door.js +271 -0
  29. package/dist/front-door.js.map +1 -0
  30. package/dist/generated/advisor-plan-contract.generated.d.ts +3 -0
  31. package/dist/generated/advisor-plan-contract.generated.d.ts.map +1 -0
  32. package/dist/generated/advisor-plan-contract.generated.js +430 -0
  33. package/dist/generated/advisor-plan-contract.generated.js.map +1 -0
  34. package/dist/generated/contract-schema.generated.d.ts +97 -0
  35. package/dist/generated/contract-schema.generated.d.ts.map +1 -0
  36. package/dist/generated/contract-schema.generated.js +496 -0
  37. package/dist/generated/contract-schema.generated.js.map +1 -0
  38. package/dist/index.d.ts +14 -2
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +24 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/messaging-kit.d.ts +113 -0
  43. package/dist/messaging-kit.d.ts.map +1 -0
  44. package/dist/messaging-kit.js +121 -0
  45. package/dist/messaging-kit.js.map +1 -0
  46. package/dist/passage.d.ts +2 -1
  47. package/dist/passage.d.ts.map +1 -1
  48. package/dist/passage.js +2 -1
  49. package/dist/passage.js.map +1 -1
  50. package/dist/plan-authority.d.ts +73 -0
  51. package/dist/plan-authority.d.ts.map +1 -0
  52. package/dist/plan-authority.js +126 -0
  53. package/dist/plan-authority.js.map +1 -0
  54. package/dist/plan-contract.d.ts +16 -0
  55. package/dist/plan-contract.d.ts.map +1 -0
  56. package/dist/plan-contract.js +33 -0
  57. package/dist/plan-contract.js.map +1 -0
  58. package/dist/plan-digest.d.ts +23 -0
  59. package/dist/plan-digest.d.ts.map +1 -0
  60. package/dist/plan-digest.js +82 -0
  61. package/dist/plan-digest.js.map +1 -0
  62. package/dist/plan-rules.d.ts +54 -0
  63. package/dist/plan-rules.d.ts.map +1 -0
  64. package/dist/plan-rules.js +145 -0
  65. package/dist/plan-rules.js.map +1 -0
  66. package/dist/resolve.d.ts +40 -6
  67. package/dist/resolve.d.ts.map +1 -1
  68. package/dist/resolve.js +144 -18
  69. package/dist/resolve.js.map +1 -1
  70. package/dist/scan.d.ts +32 -0
  71. package/dist/scan.d.ts.map +1 -1
  72. package/dist/scan.js +67 -6
  73. package/dist/scan.js.map +1 -1
  74. package/dist/schema.d.ts +1 -0
  75. package/dist/schema.d.ts.map +1 -1
  76. package/dist/schema.js +209 -5
  77. package/dist/schema.js.map +1 -1
  78. package/dist/site-identity.d.ts +88 -0
  79. package/dist/site-identity.d.ts.map +1 -0
  80. package/dist/site-identity.js +82 -0
  81. package/dist/site-identity.js.map +1 -0
  82. package/dist/types.d.ts +65 -7
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js +9 -7
  85. package/dist/types.js.map +1 -1
  86. package/package.json +7 -6
  87. package/skill/SKILL.md +2 -2
  88. package/src/approval-bypass.ts +2263 -0
  89. package/src/approval.ts +491 -0
  90. package/src/approve-cli.ts +474 -0
  91. package/src/cli.ts +46 -13
  92. package/src/field-options.ts +134 -0
  93. package/src/front-door.check.ts +10 -0
  94. package/src/front-door.ts +312 -0
  95. package/src/generated/advisor-plan-contract.generated.ts +430 -0
  96. package/src/generated/contract-schema.generated.ts +520 -0
  97. package/src/index.ts +97 -1
  98. package/src/messaging-kit.ts +195 -0
  99. package/src/passage.ts +2 -1
  100. package/src/plan-authority.ts +160 -0
  101. package/src/plan-contract.ts +45 -0
  102. package/src/plan-digest.ts +81 -0
  103. package/src/plan-rules.ts +179 -0
  104. package/src/resolve.ts +185 -19
  105. package/src/scan.ts +97 -6
  106. package/src/schema.ts +223 -5
  107. package/src/site-identity.ts +125 -0
  108. package/src/types.ts +72 -7
  109. package/templates/front-door.en.json +426 -0
package/src/resolve.ts CHANGED
@@ -7,16 +7,29 @@
7
7
  * source provenance, and an approved entry. The result exposes every one of
8
8
  * those inputs so `@example/surface` can place them in its output
9
9
  * manifest without knowing how a consumer stores copy.
10
+ *
11
+ * Target policy: an owner-approved entry resolves on both `"preview"` and
12
+ * `"production"` (the default target). A delegate-approved entry resolves
13
+ * freely on `"preview"`, but on `"production"` is refused
14
+ * (`delegate-approval-refused`) unless the caller opts in with
15
+ * `acceptDelegateInProduction: true`, or passes `approvalPlan`, the bytes of
16
+ * a plan whose latest approval names that plan's own digest and which
17
+ * declares `delegatedCopyApproval` covering the entry — a delegate's sign-off
18
+ * is not, by itself, sufficient evidence to publish, only to preview.
10
19
  */
11
20
 
12
21
  import type {
13
22
  CopyRef,
14
23
  CopyRegistry,
15
24
  CopyResolution,
25
+ CopyResolutionApproval,
16
26
  CopyResolver,
17
27
  CopyValue,
18
28
  } from "./types.js";
19
29
  import { validateCopyRegistryShape } from "./schema.js";
30
+ import { isApprovalExpired, isApprovalStale, isEntryInDelegateScope } from "./approval.js";
31
+ import { isPlanBytes, planDelegateCopyAuthority } from "./plan-authority.js";
32
+ import type { PlanDelegateCopyRefusal } from "./plan-authority.js";
20
33
 
21
34
  export type CopyResolveIssueReason =
22
35
  | "invalid-registry"
@@ -25,13 +38,43 @@ export type CopyResolveIssueReason =
25
38
  | "unknown-copy-id"
26
39
  | "copy-not-approved"
27
40
  | "missing-placeholder-value"
28
- | "unexpected-placeholder-value";
41
+ | "unexpected-placeholder-value"
42
+ | "invalid-options"
43
+ | "approval-stale"
44
+ | "approval-expired"
45
+ | "delegate-approval-refused";
46
+
47
+ /** Which audience a resolution is for — see this file's top doc comment for the policy difference. */
48
+ export type CopyResolveTarget = "preview" | "production";
49
+
50
+ /** Options controlling how strictly `resolveCopyRef`/`createCopyResolver` treat an entry's approval record. */
51
+ export interface CopyResolveOptions {
52
+ /** Defaults to `"production"`. */
53
+ target?: CopyResolveTarget;
54
+ /** Defaults to `false`. Only meaningful when `target` is `"production"`. */
55
+ acceptDelegateInProduction?: boolean;
56
+ /** Defaults to `new Date()`, evaluated per call. */
57
+ now?: Date;
58
+ /**
59
+ * The bytes of an Advisor plan record (a `Uint8Array`; Node's `Buffer` is
60
+ * one), read by the caller. Consulted only for a delegate-approved entry on
61
+ * `"production"` when `acceptDelegateInProduction` is not `true`: see
62
+ * `planDelegateCopyAuthority()`. Not a `Uint8Array`, or given together with
63
+ * `acceptDelegateInProduction: true`, is `"invalid-options"`.
64
+ */
65
+ approvalPlan?: Uint8Array;
66
+ }
67
+
68
+ /** Why an `approvalPlan` did not authorize an entry: the plan's own refusal, or that the entry is outside the scopes the plan declares. */
69
+ export type CopyResolvePlanRefusal = PlanDelegateCopyRefusal | "entry-outside-plan-scopes";
29
70
 
30
71
  export interface CopyResolveIssue {
31
72
  reason: CopyResolveIssueReason;
32
73
  message: string;
33
74
  id?: string;
34
75
  placeholder?: string;
76
+ /** Only on `"delegate-approval-refused"` when an `approvalPlan` was given: why it did not authorize this entry. */
77
+ planRefusal?: CopyResolvePlanRefusal;
35
78
  }
36
79
 
37
80
  export interface CopyResolveResult {
@@ -56,13 +99,60 @@ function interpolate(text: string, values: Readonly<Record<string, CopyValue>>):
56
99
  return text.replace(/\{([^{}]+)\}/g, (_match, name: string) => String(values[name]!));
57
100
  }
58
101
 
102
+ /** The options a call runs with, each read from the caller's object exactly once. */
103
+ interface ResolveSettings {
104
+ target: CopyResolveTarget;
105
+ acceptDelegateInProduction: boolean;
106
+ now: Date;
107
+ approvalPlan: Uint8Array | undefined;
108
+ }
109
+
110
+ /**
111
+ * Reads a well-formed `CopyResolveOptions`, or returns null — checked before
112
+ * anything else so a malformed caller-supplied options object fails closed
113
+ * rather than silently falling back to defaults. Each property is read
114
+ * exactly once, into a local that is validated and then used, so a getter
115
+ * cannot answer differently at validation and at use.
116
+ */
117
+ function readResolveOptions(value: unknown): ResolveSettings | null {
118
+ if (value === undefined) return { target: "production", acceptDelegateInProduction: false, now: new Date(), approvalPlan: undefined };
119
+ if (!isPlainObject(value)) return null;
120
+ const { target, acceptDelegateInProduction, now, approvalPlan } = value;
121
+ if (target !== undefined && target !== "preview" && target !== "production") return null;
122
+ if (acceptDelegateInProduction !== undefined && typeof acceptDelegateInProduction !== "boolean") return null;
123
+ if (now !== undefined && !(now instanceof Date && Number.isFinite(now.getTime()))) return null;
124
+ if (approvalPlan !== undefined && !isPlanBytes(approvalPlan)) return null;
125
+ // Two authorities for one decision is an ambiguity, so it is refused: no precedence needs defining.
126
+ if (approvalPlan !== undefined && acceptDelegateInProduction === true) return null;
127
+ return {
128
+ target: target ?? "production",
129
+ acceptDelegateInProduction: acceptDelegateInProduction ?? false,
130
+ now: now ?? new Date(),
131
+ approvalPlan,
132
+ };
133
+ }
134
+
59
135
  /**
60
136
  * Resolves a `CopyRef` against one locale-specific registry. Any absence,
61
- * lifecycle failure, locale mismatch, or placeholder mismatch produces no
62
- * text and a non-complete result. Consumers may log `issues`; renderers must
63
- * treat a missing `resolution` as an unresolved required input.
137
+ * lifecycle failure, locale mismatch, staleness/expiry, delegate-in-production
138
+ * refusal, or placeholder mismatch produces no text and a non-complete
139
+ * result. Consumers may log `issues`; renderers must treat a missing
140
+ * `resolution` as an unresolved required input.
64
141
  */
65
- export function resolveCopyRef(registry: CopyRegistry | unknown, ref: CopyRef | unknown): CopyResolveResult {
142
+ export function resolveCopyRef(
143
+ registry: CopyRegistry | unknown,
144
+ ref: CopyRef | unknown,
145
+ options?: CopyResolveOptions | unknown,
146
+ ): CopyResolveResult {
147
+ const settings = readResolveOptions(options);
148
+ if (settings === null) {
149
+ return {
150
+ issues: [{ reason: "invalid-options", message: "CopyResolveOptions is malformed." }],
151
+ complete: false,
152
+ };
153
+ }
154
+ const { target, acceptDelegateInProduction, now, approvalPlan } = settings;
155
+
66
156
  const registryFindings = validateCopyRegistryShape(registry);
67
157
  if (registryFindings.length > 0) {
68
158
  return {
@@ -119,6 +209,69 @@ export function resolveCopyRef(registry: CopyRegistry | unknown, ref: CopyRef |
119
209
  };
120
210
  }
121
211
 
212
+ const approval = entry.approval;
213
+ // Set only when an approvalPlan, not the flag, authorized a delegate entry on production.
214
+ let authorizingPlanDigest: string | undefined;
215
+ if (approval) {
216
+ if (isApprovalStale(entry)) {
217
+ return {
218
+ issues: [
219
+ {
220
+ reason: "approval-stale",
221
+ id: validRef.id,
222
+ message: `CopyRef "${validRef.id}"'s approval record no longer matches its current text.`,
223
+ },
224
+ ],
225
+ complete: false,
226
+ };
227
+ }
228
+ if (isApprovalExpired(approval, now)) {
229
+ return {
230
+ issues: [
231
+ {
232
+ reason: "approval-expired",
233
+ id: validRef.id,
234
+ message: `CopyRef "${validRef.id}"'s approval expired at ${approval.expiresAt}.`,
235
+ },
236
+ ],
237
+ complete: false,
238
+ };
239
+ }
240
+ if (approval.approvedBy === "delegate" && target === "production" && acceptDelegateInProduction !== true) {
241
+ // The flag did not authorize this entry, so only a plan can. The plan is
242
+ // read here and nowhere else: never for preview, an owner entry, a
243
+ // record-less entry, or once the flag is true, and never before the
244
+ // entry's own staleness and expiry, which outrank it.
245
+ if (approvalPlan === undefined) {
246
+ return {
247
+ issues: [
248
+ {
249
+ reason: "delegate-approval-refused",
250
+ id: validRef.id,
251
+ message: `CopyRef "${validRef.id}" was approved by a delegate; production resolution requires acceptDelegateInProduction: true.`,
252
+ },
253
+ ],
254
+ complete: false,
255
+ };
256
+ }
257
+ const planRefused = (code: CopyResolvePlanRefusal): CopyResolveResult => ({
258
+ issues: [
259
+ {
260
+ reason: "delegate-approval-refused",
261
+ id: validRef.id,
262
+ planRefusal: code,
263
+ message: `CopyRef "${validRef.id}" was approved by a delegate; the approvalPlan given does not authorize it on production (${code}).`,
264
+ },
265
+ ],
266
+ complete: false,
267
+ });
268
+ const authority = planDelegateCopyAuthority(approvalPlan);
269
+ if (!authority.authorized) return planRefused(authority.refusal);
270
+ if (authority.scopes !== undefined && !isEntryInDelegateScope(entry.id, authority.scopes)) return planRefused("entry-outside-plan-scopes");
271
+ authorizingPlanDigest = authority.planDigest;
272
+ }
273
+ }
274
+
122
275
  const values = validRef.values ?? {};
123
276
  const issues: CopyResolveIssue[] = [];
124
277
  const declared = new Set(entry.placeholders ?? []);
@@ -151,22 +304,35 @@ export function resolveCopyRef(registry: CopyRegistry | unknown, ref: CopyRef |
151
304
  }
152
305
  if (issues.length > 0) return { issues, complete: false };
153
306
 
154
- return {
155
- resolution: {
156
- ref: validRef as unknown as CopyRef,
157
- text: interpolate(entry.text, values as Readonly<Record<string, CopyValue>>),
158
- recordId: validRegistry.id,
159
- revision: validRegistry.revision,
160
- locale: validRegistry.locale,
161
- source: validRegistry.source,
162
- entryId: entry.id,
163
- },
164
- issues: [],
165
- complete: true,
307
+ const resolution: CopyResolution = {
308
+ ref: validRef as unknown as CopyRef,
309
+ text: interpolate(entry.text, values as Readonly<Record<string, CopyValue>>),
310
+ recordId: validRegistry.id,
311
+ revision: validRegistry.revision,
312
+ locale: validRegistry.locale,
313
+ source: validRegistry.source,
314
+ entryId: entry.id,
166
315
  };
316
+ // Only set when the entry actually carries a record — an approved entry
317
+ // with no record resolves exactly as it did before this field existed,
318
+ // with no `approval` key on the resolution at all.
319
+ if (approval) {
320
+ // Built explicitly, so the digest key exists only when a plan authorized the entry.
321
+ const facts: CopyResolutionApproval =
322
+ approval.approvedBy === "owner"
323
+ ? { approvedBy: "owner", pendingOwnerReview: false }
324
+ : {
325
+ approvedBy: "delegate",
326
+ pendingOwnerReview: approval.pendingOwnerReview === true,
327
+ ...(authorizingPlanDigest === undefined ? {} : { authorizingPlanDigest }),
328
+ };
329
+ resolution.approval = facts;
330
+ }
331
+
332
+ return { resolution, issues: [], complete: true };
167
333
  }
168
334
 
169
335
  /** Creates the narrow resolver callback for presentation code. */
170
- export function createCopyResolver(registry: CopyRegistry | unknown): CopyResolver {
171
- return (ref) => resolveCopyRef(registry, ref).resolution;
336
+ export function createCopyResolver(registry: CopyRegistry | unknown, options?: CopyResolveOptions | unknown): CopyResolver {
337
+ return (ref) => resolveCopyRef(registry, ref, options).resolution;
172
338
  }
package/src/scan.ts CHANGED
@@ -634,6 +634,14 @@ interface TokenizeResult {
634
634
  literals: Literal[];
635
635
  jsxTexts: JsxTextRun[];
636
636
  unchecked: RawUnchecked[];
637
+ /**
638
+ * Half-open `[start, end)` ranges of `content` that are NOT code: every
639
+ * comment, every string literal, every template literal's static segments
640
+ * (never its `${...}` interpolations, which are code), every regex literal
641
+ * body, and every JSX text run. Read only by `maskNonCode` below; every
642
+ * other caller ignores it.
643
+ */
644
+ opaque: Array<[number, number]>;
637
645
  /** Set when the scanner could not reach a consistent end-of-file state (unterminated string/template/comment, or a regex heuristic that desynced quote tracking). */
638
646
  failure?: string;
639
647
  }
@@ -687,6 +695,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
687
695
  const literals: Literal[] = [];
688
696
  const jsxTexts: JsxTextRun[] = [];
689
697
  const unchecked: RawUnchecked[] = [];
698
+ const opaque: Array<[number, number]> = [];
690
699
  const n = content.length;
691
700
  let i = 0;
692
701
  let line = 1;
@@ -745,6 +754,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
745
754
  }
746
755
  if (ck === '"' || ck === "'") {
747
756
  const q = ck;
757
+ const stringStart = j;
748
758
  j++;
749
759
  while (j < n && content[j] !== q) {
750
760
  if (content[j] === "\\") j++;
@@ -752,6 +762,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
752
762
  j++;
753
763
  }
754
764
  j++; // consume closing quote, or run past EOF — caught by the outer unterminated-template check
765
+ opaque.push([stringStart, Math.min(j, n)]);
755
766
  continue;
756
767
  }
757
768
  if (ck === "`") {
@@ -762,6 +773,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
762
773
  }
763
774
  if (ck === "/" && content[j + 1] === "/") {
764
775
  const nl = content.indexOf("\n", j);
776
+ opaque.push([j, nl === -1 ? n : nl]);
765
777
  j = nl === -1 ? n : nl;
766
778
  continue;
767
779
  }
@@ -769,6 +781,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
769
781
  const close = content.indexOf("*/", j + 2);
770
782
  if (close === -1) return -1;
771
783
  advanceLine(j, close + 2);
784
+ opaque.push([j, close + 2]);
772
785
  j = close + 2;
773
786
  continue;
774
787
  }
@@ -780,22 +793,28 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
780
793
  /** Skips an entire template literal starting at `pos` (`content[pos] === "\`"`), including every interpolation it contains, recursively. Returns the index one past its closing backtick, or `-1` if unterminated. */
781
794
  function skipTemplateFrom(pos: number): number {
782
795
  let j = pos + 1;
796
+ let segmentStart = pos;
783
797
  while (j < n) {
784
798
  const cj = content[j] as string;
785
799
  if (cj === "\\" && j + 1 < n) {
786
800
  j += 2;
787
801
  continue;
788
802
  }
789
- if (cj === "`") return j + 1;
803
+ if (cj === "`") {
804
+ opaque.push([segmentStart, j + 1]);
805
+ return j + 1;
806
+ }
790
807
  if (cj === "\n") {
791
808
  line++;
792
809
  j++;
793
810
  continue;
794
811
  }
795
812
  if (cj === "$" && content[j + 1] === "{") {
813
+ opaque.push([segmentStart, j]);
796
814
  const next = skipInterpolation(j + 2);
797
815
  if (next === -1) return -1;
798
816
  j = next;
817
+ segmentStart = j;
799
818
  continue;
800
819
  }
801
820
  j++;
@@ -858,6 +877,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
858
877
  literalFailure = `unterminated string starting at line ${startLine}`;
859
878
  return -1;
860
879
  }
880
+ opaque.push([start, j]);
861
881
  literals.push({
862
882
  kind: "string",
863
883
  line: startLine,
@@ -892,6 +912,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
892
912
  let j = pos + 1;
893
913
  let current = "";
894
914
  let closed = false;
915
+ let segmentStart = pos;
895
916
  while (j < n) {
896
917
  const cj = content[j] as string;
897
918
  if (cj === "\\" && j + 1 < n) {
@@ -905,6 +926,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
905
926
  break;
906
927
  }
907
928
  if (cj === "$" && content[j + 1] === "{") {
929
+ opaque.push([segmentStart, j]);
908
930
  staticParts.push(unescape(current));
909
931
  current = "";
910
932
  placeholderCount++;
@@ -920,6 +942,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
920
942
  return -1;
921
943
  }
922
944
  j = next;
945
+ segmentStart = j;
923
946
  continue;
924
947
  }
925
948
  if (cj === "\n") line++;
@@ -931,6 +954,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
931
954
  return -1;
932
955
  }
933
956
  staticParts.push(unescape(current));
957
+ opaque.push([segmentStart, j]);
934
958
  literals.push({
935
959
  kind: "template",
936
960
  line: startLine,
@@ -981,6 +1005,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
981
1005
  }
982
1006
  if (cj === "/" && content[j + 1] === "/") {
983
1007
  const nl = content.indexOf("\n", j);
1008
+ opaque.push([j, nl === -1 ? n : nl]);
984
1009
  j = nl === -1 ? n : nl;
985
1010
  continue;
986
1011
  }
@@ -988,6 +1013,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
988
1013
  const close = content.indexOf("*/", j + 2);
989
1014
  if (close === -1) return -1;
990
1015
  advanceLine(j, close + 2);
1016
+ opaque.push([j, close + 2]);
991
1017
  j = close + 2;
992
1018
  continue;
993
1019
  }
@@ -1079,6 +1105,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
1079
1105
  if (found !== -1) {
1080
1106
  let m = found + 1;
1081
1107
  while (m < n && /[a-z]/i.test(content[m] as string)) m++;
1108
+ opaque.push([j, m]);
1082
1109
  j = m;
1083
1110
  localLast = "value";
1084
1111
  continue;
@@ -1116,7 +1143,10 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
1116
1143
  const flush = (): void => {
1117
1144
  if (j > runStart) {
1118
1145
  const raw = content.slice(runStart, j);
1119
- if (raw.trim().length > 0) jsxTexts.push({ line: runStartLine, raw });
1146
+ if (raw.trim().length > 0) {
1147
+ jsxTexts.push({ line: runStartLine, raw });
1148
+ opaque.push([runStart, j]);
1149
+ }
1120
1150
  }
1121
1151
  };
1122
1152
 
@@ -1268,12 +1298,14 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
1268
1298
  const entryLiteralsCount = literals.length;
1269
1299
  const entryJsxTextsCount = jsxTexts.length;
1270
1300
  const entryUncheckedCount = unchecked.length;
1301
+ const entryOpaqueCount = opaque.length;
1271
1302
 
1272
1303
  function backtrack(): null {
1273
1304
  line = entryLine;
1274
1305
  literals.length = entryLiteralsCount;
1275
1306
  jsxTexts.length = entryJsxTextsCount;
1276
1307
  unchecked.length = entryUncheckedCount;
1308
+ opaque.length = entryOpaqueCount;
1277
1309
  return null;
1278
1310
  }
1279
1311
 
@@ -1339,6 +1371,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
1339
1371
  // above.
1340
1372
  if (cj === "/" && content[j + 1] === "/") {
1341
1373
  const nl = content.indexOf("\n", j);
1374
+ opaque.push([j, nl === -1 ? n : nl]);
1342
1375
  j = nl === -1 ? n : nl;
1343
1376
  continue;
1344
1377
  }
@@ -1346,6 +1379,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
1346
1379
  const close = content.indexOf("*/", j + 2);
1347
1380
  if (close === -1) return backtrack(); // unterminated block comment — not committed, silently not-JSX
1348
1381
  advanceLine(j, close + 2);
1382
+ opaque.push([j, close + 2]);
1349
1383
  j = close + 2;
1350
1384
  continue;
1351
1385
  }
@@ -1456,6 +1490,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
1456
1490
  // line comment
1457
1491
  if (c === "/" && content[i + 1] === "/") {
1458
1492
  const nl = content.indexOf("\n", i);
1493
+ opaque.push([i, nl === -1 ? n : nl]);
1459
1494
  i = nl === -1 ? n : nl;
1460
1495
  continue;
1461
1496
  }
@@ -1463,8 +1498,9 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
1463
1498
  // block comment
1464
1499
  if (c === "/" && content[i + 1] === "*") {
1465
1500
  const close = content.indexOf("*/", i + 2);
1466
- if (close === -1) return { literals, jsxTexts, unchecked, failure: `unterminated block comment starting at line ${line}` };
1501
+ if (close === -1) return { literals, jsxTexts, unchecked, opaque, failure: `unterminated block comment starting at line ${line}` };
1467
1502
  advanceLine(i, close + 2);
1503
+ opaque.push([i, close + 2]);
1468
1504
  i = close + 2;
1469
1505
  continue;
1470
1506
  }
@@ -1472,7 +1508,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
1472
1508
  // string literal
1473
1509
  if (c === '"' || c === "'") {
1474
1510
  const end = scanStringLiteral(i);
1475
- if (end === -1) return { literals, jsxTexts, unchecked, failure: literalFailure };
1511
+ if (end === -1) return { literals, jsxTexts, unchecked, opaque, failure: literalFailure };
1476
1512
  lastTokenType = "value";
1477
1513
  i = end;
1478
1514
  continue;
@@ -1481,7 +1517,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
1481
1517
  // template literal
1482
1518
  if (c === "`") {
1483
1519
  const end = scanTemplateLiteral(i);
1484
- if (end === -1) return { literals, jsxTexts, unchecked, failure: literalFailure };
1520
+ if (end === -1) return { literals, jsxTexts, unchecked, opaque, failure: literalFailure };
1485
1521
  lastTokenType = "value";
1486
1522
  i = end;
1487
1523
  continue;
@@ -1540,6 +1576,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
1540
1576
  if (found !== -1) {
1541
1577
  let k = found + 1;
1542
1578
  while (k < n && /[a-z]/i.test(content[k] as string)) k++;
1579
+ opaque.push([i, k]);
1543
1580
  i = k;
1544
1581
  lastTokenType = "value";
1545
1582
  continue;
@@ -1581,7 +1618,7 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
1581
1618
  i++;
1582
1619
  }
1583
1620
 
1584
- return { literals, jsxTexts, unchecked };
1621
+ return { literals, jsxTexts, unchecked, opaque };
1585
1622
  }
1586
1623
 
1587
1624
  // ---------------------------------------------------------- classification
@@ -1988,3 +2025,57 @@ export function extractCopyCandidates(content: string, filePath: string): Extrac
1988
2025
 
1989
2026
  return { candidates, excluded, citations, unchecked };
1990
2027
  }
2028
+
2029
+ /** One string or template literal `maskNonCode` found, with its position in the original content. */
2030
+ export interface MaskedLiteral {
2031
+ kind: "string" | "template";
2032
+ line: number;
2033
+ /** Index of the opening quote/backtick. */
2034
+ start: number;
2035
+ /** Index one past the closing quote/backtick. */
2036
+ end: number;
2037
+ raw: string;
2038
+ /** The literal's static text, unescaped; a template's interpolations are dropped. */
2039
+ text: string;
2040
+ }
2041
+
2042
+ /** `maskNonCode`'s result. `code` has the same length and line breaks as the input. */
2043
+ export interface CodeMask {
2044
+ code: string;
2045
+ literals: MaskedLiteral[];
2046
+ failure?: string;
2047
+ }
2048
+
2049
+ /**
2050
+ * Pure. Runs this file's tokenizer over `content` and returns a copy of it in
2051
+ * which every comment, string literal, template static segment, regex literal
2052
+ * and (in a `.tsx`/`.jsx` file) JSX text run is replaced by spaces, keeping
2053
+ * every newline and every character offset. A caller can then match code
2054
+ * patterns against `code` without a comment or a string ever matching, and
2055
+ * look up the literal at a given offset through `literals`. A template's
2056
+ * `${...}` interpolations stay visible in `code`, because they are code.
2057
+ *
2058
+ * Exported for `approval-bypass.ts` so it delegates literal and comment
2059
+ * boundaries to this tokenizer instead of growing a second one. Deliberately
2060
+ * not re-exported from `index.ts`: it is an internal helper, not package API.
2061
+ */
2062
+ export function maskNonCode(content: string, filePath: string): CodeMask {
2063
+ const { literals, opaque, failure } = tokenize(content, JSX_FILE_RE.test(filePath));
2064
+ const masked = content.split("");
2065
+ if (!failure) {
2066
+ for (const [start, end] of opaque) {
2067
+ for (let k = start; k < end && k < masked.length; k++) {
2068
+ if (masked[k] !== "\n") masked[k] = " ";
2069
+ }
2070
+ }
2071
+ }
2072
+ const out: MaskedLiteral[] = literals.map((lit) => ({
2073
+ kind: lit.kind,
2074
+ line: lit.line,
2075
+ start: lit.start,
2076
+ end: lit.end,
2077
+ raw: lit.raw,
2078
+ text: lit.staticParts.join(""),
2079
+ }));
2080
+ return failure ? { code: masked.join(""), literals: out, failure } : { code: masked.join(""), literals: out };
2081
+ }