@nurix/etna 0.20.42 → 0.20.44

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "bd1f2b63adef",
2
+ "fingerprint": "192f46e9ad7e",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.20.12",
4
+ "version": "3.20.14",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -29,5 +29,5 @@
29
29
  "origin": "https://cdn.nustack.io",
30
30
  "track": "latest"
31
31
  },
32
- "digest": "16ebb3336e2e"
32
+ "digest": "a1ccbb9247d0"
33
33
  }
@@ -455,10 +455,15 @@ function principleNumbers(lines) {
455
455
  // The body's sentences long enough to state a rule, as content-word sets; headings, tables and the footer drop out.
456
456
  function sentencesOf(lines) {
457
457
  const out = [];
458
+ // The opening states the document's boundary and points at its neighbours; a sentence there is a pointer, not a rule.
459
+ let opened = false;
458
460
  for (const { n, line } of lines) {
461
+ if (/^##\s+/.test(line)) opened = true;
462
+ if (!opened) continue;
459
463
  if (DOCTRINE_CLOSING.test(line)) break;
460
464
  if (/^\s*(#|\|)/.test(line) || !line.trim()) continue;
461
- const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '');
465
+ // A citation's words name where a rule lives; two sentences citing one heading share no rule by it.
466
+ const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '').replace(/\([^()]*(?:principles?\s+\d|§)[^()]*\)/g, '');
462
467
  for (const sentence of plain.split(/(?<=[.;])\s+(?=[A-Z(])/)) {
463
468
  if (SPEC_SENTENCE.test(sentence)) continue;
464
469
  const words = new Set((sentence.toLowerCase().match(/[a-z][a-z'-]+/g) || []).filter((w) => w.length > 2 && !STOPWORDS.has(w)));
@@ -520,12 +525,17 @@ function lintDoctrine(path, text, lines, disabled, findings) {
520
525
  const words = bare.replace(/\*\*/g, '').replace(/^\s*([-*]|\d+\.)\s+/, '');
521
526
  if (!disabled.has('modal-verb') && /\b(must|should|shall)\b/i.test(words)) findings.push({ path, n, severity: 'warn', id: 'modal-verb', message: 'a modal in a specification — state what happens ("the request is rejected"); being in this document makes it a requirement', excerpt: line.trim().slice(0, 90) });
522
527
  if (!disabled.has('question') && /\?(\s|$)/.test(words)) findings.push({ path, n, severity: 'warn', id: 'question', message: 'a question in prose — state the rule declaratively', excerpt: line.trim().slice(0, 90) });
528
+ // A term the set governs (any sibling's objects:, defined in its Terms table) is a term of art, never a vague synonym.
529
+ const ownTerms = [...new Set([...siblings.flatMap((d) => d.objects), ...listValue((frontmatter(text) || new Map()).get('objects')).map((o) => o.toLowerCase())])];
530
+ const unowned = ownTerms.reduce((acc, term) => acc.replace(new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}s?\\b`, 'gi'), 'X'), words);
523
531
  if (!disabled.has('vague-term')) for (const [pattern, use] of VAGUE_TERMS) {
524
- const m = pattern.exec(words);
532
+ const m = pattern.exec(unowned);
525
533
  if (m) { findings.push({ path, n, severity: 'warn', id: 'vague-term', message: `"${m[0]}" — use the standard term: ${use} (references/style-guide.md § Vocabulary)`, excerpt: line.trim().slice(0, 90) }); break; }
526
534
  }
527
535
  if (!disabled.has('long-sentence')) {
528
- for (const sentence of words.split(/(?<=[.;!])\s+(?=[A-Z(])/)) {
536
+ // A bullet's bold label names the rule; the sentence it leads is counted without it.
537
+ const unlabelled = bare.replace(/^\s*([-*]|\d+\.)\s+\*\*[^*]+\*\*:?\s*/, '').replace(/\*\*/g, '');
538
+ for (const sentence of unlabelled.split(/(?<=[.;!])\s+(?=[A-Z(])/)) {
529
539
  const list = /:\s/.test(sentence) && (sentence.split(/:\s/).slice(1).join(' ').match(/[,;]/g) || []).length >= 2;
530
540
  const count = (sentence.match(/[\w'’-]+/g) || []).length;
531
541
  if (count > 25 && !list) { findings.push({ path, n, severity: 'warn', id: 'long-sentence', message: `${count} words in one sentence — about twenty-five at most, one idea each; a colon list is exempt`, excerpt: sentence.trim().slice(0, 90) }); break; }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "delivery",
3
3
  "name": "delivery",
4
- "version": "0.2.47",
4
+ "version": "0.2.49",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -14,5 +14,5 @@
14
14
  "origin": "https://cdn.nustack.io",
15
15
  "track": "latest"
16
16
  },
17
- "digest": "0e79c085c86d"
17
+ "digest": "ab77b0fbab3a"
18
18
  }
@@ -455,10 +455,15 @@ function principleNumbers(lines) {
455
455
  // The body's sentences long enough to state a rule, as content-word sets; headings, tables and the footer drop out.
456
456
  function sentencesOf(lines) {
457
457
  const out = [];
458
+ // The opening states the document's boundary and points at its neighbours; a sentence there is a pointer, not a rule.
459
+ let opened = false;
458
460
  for (const { n, line } of lines) {
461
+ if (/^##\s+/.test(line)) opened = true;
462
+ if (!opened) continue;
459
463
  if (DOCTRINE_CLOSING.test(line)) break;
460
464
  if (/^\s*(#|\|)/.test(line) || !line.trim()) continue;
461
- const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '');
465
+ // A citation's words name where a rule lives; two sentences citing one heading share no rule by it.
466
+ const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '').replace(/\([^()]*(?:principles?\s+\d|§)[^()]*\)/g, '');
462
467
  for (const sentence of plain.split(/(?<=[.;])\s+(?=[A-Z(])/)) {
463
468
  if (SPEC_SENTENCE.test(sentence)) continue;
464
469
  const words = new Set((sentence.toLowerCase().match(/[a-z][a-z'-]+/g) || []).filter((w) => w.length > 2 && !STOPWORDS.has(w)));
@@ -520,12 +525,17 @@ function lintDoctrine(path, text, lines, disabled, findings) {
520
525
  const words = bare.replace(/\*\*/g, '').replace(/^\s*([-*]|\d+\.)\s+/, '');
521
526
  if (!disabled.has('modal-verb') && /\b(must|should|shall)\b/i.test(words)) findings.push({ path, n, severity: 'warn', id: 'modal-verb', message: 'a modal in a specification — state what happens ("the request is rejected"); being in this document makes it a requirement', excerpt: line.trim().slice(0, 90) });
522
527
  if (!disabled.has('question') && /\?(\s|$)/.test(words)) findings.push({ path, n, severity: 'warn', id: 'question', message: 'a question in prose — state the rule declaratively', excerpt: line.trim().slice(0, 90) });
528
+ // A term the set governs (any sibling's objects:, defined in its Terms table) is a term of art, never a vague synonym.
529
+ const ownTerms = [...new Set([...siblings.flatMap((d) => d.objects), ...listValue((frontmatter(text) || new Map()).get('objects')).map((o) => o.toLowerCase())])];
530
+ const unowned = ownTerms.reduce((acc, term) => acc.replace(new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}s?\\b`, 'gi'), 'X'), words);
523
531
  if (!disabled.has('vague-term')) for (const [pattern, use] of VAGUE_TERMS) {
524
- const m = pattern.exec(words);
532
+ const m = pattern.exec(unowned);
525
533
  if (m) { findings.push({ path, n, severity: 'warn', id: 'vague-term', message: `"${m[0]}" — use the standard term: ${use} (references/style-guide.md § Vocabulary)`, excerpt: line.trim().slice(0, 90) }); break; }
526
534
  }
527
535
  if (!disabled.has('long-sentence')) {
528
- for (const sentence of words.split(/(?<=[.;!])\s+(?=[A-Z(])/)) {
536
+ // A bullet's bold label names the rule; the sentence it leads is counted without it.
537
+ const unlabelled = bare.replace(/^\s*([-*]|\d+\.)\s+\*\*[^*]+\*\*:?\s*/, '').replace(/\*\*/g, '');
538
+ for (const sentence of unlabelled.split(/(?<=[.;!])\s+(?=[A-Z(])/)) {
529
539
  const list = /:\s/.test(sentence) && (sentence.split(/:\s/).slice(1).join(' ').match(/[,;]/g) || []).length >= 2;
530
540
  const count = (sentence.match(/[\w'’-]+/g) || []).length;
531
541
  if (count > 25 && !list) { findings.push({ path, n, severity: 'warn', id: 'long-sentence', message: `${count} words in one sentence — about twenty-five at most, one idea each; a colon list is exempt`, excerpt: sentence.trim().slice(0, 90) }); break; }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.5.37",
4
+ "version": "1.5.39",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -14,5 +14,5 @@
14
14
  "origin": "https://cdn.nustack.io",
15
15
  "track": "latest"
16
16
  },
17
- "digest": "d2ee878e5057"
17
+ "digest": "04e9b6671929"
18
18
  }
@@ -455,10 +455,15 @@ function principleNumbers(lines) {
455
455
  // The body's sentences long enough to state a rule, as content-word sets; headings, tables and the footer drop out.
456
456
  function sentencesOf(lines) {
457
457
  const out = [];
458
+ // The opening states the document's boundary and points at its neighbours; a sentence there is a pointer, not a rule.
459
+ let opened = false;
458
460
  for (const { n, line } of lines) {
461
+ if (/^##\s+/.test(line)) opened = true;
462
+ if (!opened) continue;
459
463
  if (DOCTRINE_CLOSING.test(line)) break;
460
464
  if (/^\s*(#|\|)/.test(line) || !line.trim()) continue;
461
- const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '');
465
+ // A citation's words name where a rule lives; two sentences citing one heading share no rule by it.
466
+ const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '').replace(/\([^()]*(?:principles?\s+\d|§)[^()]*\)/g, '');
462
467
  for (const sentence of plain.split(/(?<=[.;])\s+(?=[A-Z(])/)) {
463
468
  if (SPEC_SENTENCE.test(sentence)) continue;
464
469
  const words = new Set((sentence.toLowerCase().match(/[a-z][a-z'-]+/g) || []).filter((w) => w.length > 2 && !STOPWORDS.has(w)));
@@ -520,12 +525,17 @@ function lintDoctrine(path, text, lines, disabled, findings) {
520
525
  const words = bare.replace(/\*\*/g, '').replace(/^\s*([-*]|\d+\.)\s+/, '');
521
526
  if (!disabled.has('modal-verb') && /\b(must|should|shall)\b/i.test(words)) findings.push({ path, n, severity: 'warn', id: 'modal-verb', message: 'a modal in a specification — state what happens ("the request is rejected"); being in this document makes it a requirement', excerpt: line.trim().slice(0, 90) });
522
527
  if (!disabled.has('question') && /\?(\s|$)/.test(words)) findings.push({ path, n, severity: 'warn', id: 'question', message: 'a question in prose — state the rule declaratively', excerpt: line.trim().slice(0, 90) });
528
+ // A term the set governs (any sibling's objects:, defined in its Terms table) is a term of art, never a vague synonym.
529
+ const ownTerms = [...new Set([...siblings.flatMap((d) => d.objects), ...listValue((frontmatter(text) || new Map()).get('objects')).map((o) => o.toLowerCase())])];
530
+ const unowned = ownTerms.reduce((acc, term) => acc.replace(new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}s?\\b`, 'gi'), 'X'), words);
523
531
  if (!disabled.has('vague-term')) for (const [pattern, use] of VAGUE_TERMS) {
524
- const m = pattern.exec(words);
532
+ const m = pattern.exec(unowned);
525
533
  if (m) { findings.push({ path, n, severity: 'warn', id: 'vague-term', message: `"${m[0]}" — use the standard term: ${use} (references/style-guide.md § Vocabulary)`, excerpt: line.trim().slice(0, 90) }); break; }
526
534
  }
527
535
  if (!disabled.has('long-sentence')) {
528
- for (const sentence of words.split(/(?<=[.;!])\s+(?=[A-Z(])/)) {
536
+ // A bullet's bold label names the rule; the sentence it leads is counted without it.
537
+ const unlabelled = bare.replace(/^\s*([-*]|\d+\.)\s+\*\*[^*]+\*\*:?\s*/, '').replace(/\*\*/g, '');
538
+ for (const sentence of unlabelled.split(/(?<=[.;!])\s+(?=[A-Z(])/)) {
529
539
  const list = /:\s/.test(sentence) && (sentence.split(/:\s/).slice(1).join(' ').match(/[,;]/g) || []).length >= 2;
530
540
  const count = (sentence.match(/[\w'’-]+/g) || []).length;
531
541
  if (count > 25 && !list) { findings.push({ path, n, severity: 'warn', id: 'long-sentence', message: `${count} words in one sentence — about twenty-five at most, one idea each; a colon list is exempt`, excerpt: sentence.trim().slice(0, 90) }); break; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.20.42",
3
+ "version": "0.20.44",
4
4
  "description": "Bootstrap a project with a Nurix harness — writes the committed contract (CLAUDE.md, AGENTS.md, .claude/harness.json, settings.json, the mount hook) and mounts the harness itself, gitignored, from a bundled payload; sessions keep it current from the CDN.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,5 +46,5 @@
46
46
  "agent",
47
47
  "plugin"
48
48
  ],
49
- "etnaFingerprint": "bd1f2b63adef"
49
+ "etnaFingerprint": "192f46e9ad7e"
50
50
  }