@ericrisco/rsc 3.0.21 → 3.0.22

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.
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.0.21",
2
+ "version": "3.0.22",
3
3
  "counts": {
4
4
  "skills": 271,
5
5
  "agents": 34,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "3.0.21",
3
+ "version": "3.0.22",
4
4
  "description": "A guided harness builder that turns your goal into a minimal, verified AI-agent setup using strict best practices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,27 +14,40 @@
14
14
  import { readdirSync, readFileSync, statSync } from 'node:fs';
15
15
  import { join, relative } from 'node:path';
16
16
 
17
- const BAN = String.raw`(?:prohib\p{L}*|forbid\p{L}*|banned|ban list|never (?:use|say|write)|nunca (?:uses|usar|digas|decir|escribas)|no (?:uses|digas|escribas)|do not (?:use|say|write)|don'?t (?:use|say|write)|vetad\p{L}*|blocklist|denylist)`;
17
+ // A rule is a DECLARATION, not a mention. 3.0.21 read any sentence that contained "prohibido" and any
18
+ // backticked token near it, and on a real wiki that turned file names, table cells and words like
19
+ // "add" or "review" into banned phrases: documentation ABOUT rules became rules. So the keyword has to
20
+ // open the line or sentence, code spans are never phrases in prose, tables are skipped, and process
21
+ // records (ftd/, sdd/) are not policy.
22
+ const BAN = String.raw`(?:prohib\p{L}*|vetad\p{L}*|forbidden|banned|never (?:use|say|write)|nunca (?:uses|usar|digas|decir|escribas)|no (?:uses|digas|escribas)|do not (?:use|say|write)|don'?t (?:use|say|write)|blocklist|denylist)`;
23
+ const LEAD = String.raw`^[\s>*_¡¿("“«-]*(?:(?:queda|quedan|esta|está|estan|están|it is|it's|is|are|strictly|totalmente|terminantemente|absolutamente|words|phrases|terms|palabras|frases|términos|terminos|expresiones)\s+){0,2}`;
24
+ const DECLARES = new RegExp(LEAD + BAN, 'iu');
18
25
  const BAN_HEADING = new RegExp(BAN, 'iu');
19
- const BAN_SENTENCE = new RegExp(BAN, 'iu');
20
- const BAN_COLON = new RegExp(String.raw`^\s*(?:[-*+]\s+)?\**\s*(?:[\p{L} ]{0,20}\s)?${BAN}[^:]{0,20}:\**\s*(.+)$`, 'iu');
21
- const QUOTED = /"([^"\n]{1,80})"|“([^”\n]{1,80})”|«([^»\n]{1,80})»|`([^`\n]{1,80})`/gu;
26
+ const BAN_COLON = new RegExp(String.raw`^\s*(?:[-*+]\s+)?\**\s*(?:(?:queda|quedan|esta|está|it is|words|phrases|terms|palabras|frases|términos|terminos|expresiones)\s+){0,2}${BAN}[\p{L} ]{0,25}:\**\s*(.*)$`, 'iu');
27
+ const PROSE_QUOTES = /"([^"\n]{1,80})"|“([^”\n]{1,80})”|«([^»\n]{1,80})»/gu;
28
+ const LIST_QUOTES = /"([^"\n]{1,80})"|“([^”\n]{1,80})”|«([^»\n]{1,80})»|`([^`\n]{1,80})`/gu;
29
+ const STOP = new Set(['y', 'o', 'e', 'u', 'a', 'de', 'la', 'el', 'en', 'and', 'or', 'the', 'of', 'to', 'in']);
30
+ const SKIP_DIRS = new Set(['ftd', 'sdd', 'raw']);
31
+ // The guard checks TEXT. A visual-identity ban ("no illustrations of people") read as copy would forbid
32
+ // the word "people" in every answer, so pages and sections about visuals are not copy policy.
33
+ const VISUAL = /visual|design|dise[nñ]o|colou?r|logo|imagen|image|ilustraci|tipograf|typograph|paleta|palette/iu;
22
34
 
23
35
  export const normalize = (s) => String(s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase();
24
36
 
25
37
  // A trailing * is the explicit wildcard, so only a markdown bold pair is stripped from the end.
26
- const clean = (s) => s.replace(/^[\s"“«`'*_]+/u, '').replace(/\*\*$/u, '').replace(/[\s"”»`'_.;:,]+$/u, '').trim();
38
+ const clean = (s) => s.replace(/\*\*/gu, '').replace(/^[\s"“«`'_]+/u, '').replace(/^(?:y|and|o|or)\s+/iu, '').replace(/[\s"”»`'_.;:,]+$/u, '').trim();
27
39
 
28
- function quotedIn(text) {
40
+ function quotedIn(text, re) {
29
41
  const out = [];
30
- for (const m of text.matchAll(QUOTED)) out.push(m[1] ?? m[2] ?? m[3] ?? m[4]);
42
+ for (const m of text.matchAll(re)) out.push(m[1] ?? m[2] ?? m[3] ?? m[4]);
31
43
  return out;
32
44
  }
33
45
 
34
46
  function listItems(text) {
35
- const q = quotedIn(text);
47
+ const q = quotedIn(text, LIST_QUOTES);
36
48
  if (q.length) return q;
37
- return text.split(/[,;]|\s+(?:y|and|o|or)\s+/u).map(clean).filter(Boolean);
49
+ // Commas only: "cerebros, robots o personas" split on "o" would ban the word "personas".
50
+ return text.split(/[,;]/u).map(clean).filter(Boolean);
38
51
  }
39
52
 
40
53
  /** Banned phrases declared in one markdown document. */
@@ -42,28 +55,34 @@ export function extractRules(markdown, source = '') {
42
55
  const found = [];
43
56
  const add = (phrase, line) => {
44
57
  const p = clean(phrase);
45
- // A ban is a word or a short phrase. Anything longer is a sentence that happened to be quoted.
46
- if (p && p.split(/\s+/).length <= 6 && /\p{L}/u.test(p)) found.push({ phrase: p, source, line });
58
+ const letters = normalize(p).replace(/[^\p{L}]/gu, '');
59
+ // A ban is a word or a short phrase: not a sentence that happened to be quoted, not a stray "y".
60
+ if (p && p.split(/\s+/).length <= 6 && letters.length >= 2 && !STOP.has(normalize(p))) found.push({ phrase: p, source, line });
47
61
  };
48
62
  const lines = String(markdown).split('\n');
49
- let inFence = false, banSection = false, para = [], paraStart = 0;
63
+ let inFence = false, banSection = false, visualSection = false, para = [], paraStart = 0, colonList = null;
50
64
  const flush = () => {
65
+ if (colonList) { for (const item of listItems(colonList.text)) add(item, colonList.line); colonList = null; }
51
66
  if (!para.length) return;
52
67
  const text = para.join(' ');
53
- for (const sentence of text.split(/(?<=[.!?])\s+(?=[\p{Lu}¿¡"“«])/u)) {
54
- if (BAN_SENTENCE.test(sentence)) for (const q of quotedIn(sentence)) add(q, paraStart + 1);
68
+ for (const sentence of text.split(/(?<=[.!?])\s+(?=[\p{Lu}¿¡"“«*])/u)) {
69
+ if (DECLARES.test(sentence)) for (const q of quotedIn(sentence, PROSE_QUOTES)) add(q, paraStart + 1);
55
70
  }
56
71
  para = [];
57
72
  };
58
73
  lines.forEach((raw, i) => {
59
74
  if (/^\s*```/.test(raw)) { flush(); inFence = !inFence; return; }
60
75
  if (inFence) return;
76
+ if (/^\s*\|/.test(raw)) { flush(); return; }
61
77
  const heading = raw.match(/^#{1,6}\s+(.*)$/);
62
- if (heading) { flush(); banSection = BAN_HEADING.test(heading[1]); return; }
78
+ if (heading) { flush(); visualSection = VISUAL.test(heading[1]); banSection = !visualSection && BAN_HEADING.test(heading[1]) && heading[1].split(/\s+/).length <= 6; return; }
79
+ if (visualSection) return;
63
80
  if (!raw.trim()) { flush(); return; }
64
- const colon = raw.match(BAN_COLON);
65
- if (colon && !quotedIn(raw.slice(0, raw.indexOf(':'))).length) { flush(); for (const item of listItems(colon[1])) add(item, i + 1); return; }
66
81
  const bullet = raw.match(/^\s*(?:[-*+]|\d+[.)])\s+(.*)$/);
82
+ // A "Vetado: a, b," list may wrap onto the next lines; it runs until a blank line or a new item.
83
+ if (colonList && !bullet) { colonList.text += ' ' + raw.trim(); const end = colonList.text.search(/[.!?](?=\s+\p{Lu}|\s*$)/u); if (end >= 0) { colonList.text = colonList.text.slice(0, end); flush(); } return; }
84
+ const colon = raw.match(BAN_COLON);
85
+ if (colon) { flush(); colonList = { text: colon[1], line: i + 1 }; const end = colonList.text.search(/[.!?](?=\s+\p{Lu}|\s*$)/u); if (end >= 0) { colonList.text = colonList.text.slice(0, end); flush(); } return; }
67
86
  if (bullet && banSection) { flush(); for (const item of listItems(bullet[1])) add(item, i + 1); return; }
68
87
  if (bullet) { flush(); para = [bullet[1]]; paraStart = i; flush(); return; }
69
88
  if (!para.length) paraStart = i;
@@ -84,8 +103,9 @@ export function loadRules(root, { maxFiles = 3000 } = {}) {
84
103
  if (files.length >= maxFiles || name.startsWith('.')) continue;
85
104
  const full = join(dir, name);
86
105
  let st; try { st = statSync(full); } catch { continue; }
87
- if (st.isDirectory()) walk(full);
88
- else if (name.endsWith('.md')) files.push(full);
106
+ // Archived pages (_archive/, archive/) are history, not the rules in force.
107
+ if (st.isDirectory()) { if ((dir !== base || !SKIP_DIRS.has(name)) && !name.startsWith('_') && !/^archive/i.test(name)) walk(full); }
108
+ else if (name.endsWith('.md') && !VISUAL.test(name)) files.push(full);
89
109
  }
90
110
  };
91
111
  walk(base);