@maestroagora/agora 1.9.0 → 1.10.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maestro-agora",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "displayName": "Maestro: Agora",
5
5
  "description": "Use /agora for persuasive writing, technical explanation, case studies, investment analysis, spoken or written copy, and explicit publication artifact audits.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maestro-agora",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Maestro: Agora writes clear persuasion, technical explanations, case studies, and investment communication, then audits local publication artifacts when requested.",
5
5
  "author": {
6
6
  "name": "Mark Laursen",
@@ -38,7 +38,7 @@
38
38
  "composerIcon": "./assets/icon.png",
39
39
  "defaultPrompt": [
40
40
  "/agora sell Write the strongest hero from this brief and destination.",
41
- "/agora science Explain this technical subject with the certainty and framing I specify.",
41
+ "/agora technical Explain this system for the technical reader I specify.",
42
42
  "/agora case study Turn this project material into the case study I describe.",
43
43
  "/agora invest Build this fundraising asset from my thesis, claims, urgency, and plan.",
44
44
  "/agora publication audit Inspect this local file for hidden Unicode, metadata, and provenance."
package/README.md CHANGED
@@ -24,10 +24,12 @@ Use it for landing pages, heroes, ads, product copy, sales outreach, investor co
24
24
  |---|---|
25
25
  | Persuasion | Argument, consequence, reason to believe, CTA, channel fit, and rhetorical force |
26
26
  | Heroes and short sales copy | Awareness, claim saturation, traffic source, promise grammar, destination fidelity, and the complete first-screen composition |
27
- | `SCIENCE` | Scientific certainty, causal language, statistics, mechanisms, uncertainty, analogies, visuals, sources, and limits |
27
+ | `SCIENCE` | Research findings, methods, statistics, uncertainty, scientific explanation, sources, and limits |
28
+ | `TECHNICAL` | Documented system behavior, architecture, APIs, implementation detail, and technical evaluation |
28
29
  | `CASE_STUDY` | Real projects, fictional mocks, and concept portfolios shaped around the story and status you choose |
29
30
  | `INVEST` | Fundraising, diligence, and capital-allocation communication shaped around your thesis, claims, urgency, and next decision |
30
- | `VOICE` | A measured voice profile built from the corpus you choose |
31
+ | `VOICE` | A measured persistent profile or a task-only sketch from samples supplied with one task |
32
+ | Human-voice editing | One exact canonical standard for output bans, structural cleanup, meaning preservation, genre fit, and detector limits |
31
33
  | Written GEO/AEO | Clear entities, self-contained passages, source transparency, and relevant publication checks |
32
34
 
33
35
  Agora returns one ready-to-use result by default. Alternatives, internal routes, and rationale stay out of the final copy unless requested.
@@ -111,7 +113,7 @@ Choose a primary mode when you want to override inference:
111
113
  Add modifiers when the subject or asset needs them:
112
114
 
113
115
  ```text
114
- /agora sell science Write a technical product hero with this certainty and framing.
116
+ /agora sell technical Write a product section for engineers with this documented behavior.
115
117
  /agora inform science Explain this study for a general audience.
116
118
  /agora sell case study Write a customer success case from this approved project record.
117
119
  /agora inform science case study Explain this engineering implementation and its measured limits.
@@ -122,6 +124,23 @@ Add modifiers when the subject or asset needs them:
122
124
 
123
125
  In Codex, `$agora` and the skills picker can also select the installed skill. Other hosts may expose skills through a picker or mention syntax. Asking the agent to use the Agora skill remains portable.
124
126
 
127
+ ## Human-voice editing standard
128
+
129
+ Agora ships the attached source unchanged as `human-voice-editing-reference.md`. It is the single canonical authority for banned vocabulary, connectives, templates, significance tails, punctuation, prompt leakage, structural tells, author samples, meaning preservation, genre fit, detector limits, and privacy cautions.
130
+
131
+ The standard removes generated stock vocabulary, connective phrases, templates, significance tails, prompt leakage, repeated structural patterns, raw channel residue, fake human texture, and detector-driven corruption. It preserves the current user's facts, required wording, technical terms, genre, and intended meaning. Measured voice profiles document author habits but do not automatically exempt banned vocabulary.
132
+
133
+ Plain professional writing is the default. AI, software, data, security, engineering, and infrastructure products stay in plain language for ordinary readers. A specialized register activates only when the deliverable needs scientific findings, technical detail, legal wording, or formal review.
134
+
135
+ Run the deterministic style review on a file:
136
+
137
+ ```sh
138
+ npx -y -p @maestroagora/agora@latest agora-style-audit ./draft.md
139
+ npx -y -p @maestroagora/agora@latest agora-style-audit ./draft.md --register technical --json
140
+ ```
141
+
142
+ Hard findings cover banned typography, canonical vocabulary and templates, prompt leakage, and significance tails. Review warnings cover plain-register control words, long or joined sentences, repeated openings and paragraph shapes, noun and preposition stacks, false reframes, question fragments, corporate helper phrases, and legalistic leakage. The tool does not rewrite text, validate facts, infer authorship, or promise detector evasion.
143
+
125
144
  ## Publication privacy and provenance
126
145
 
127
146
  Agora writes copy. It does not create SVG, PNG, JPEG, PDF, DOCX, or PPTX files by itself. Other document, presentation, PDF, site, design, or image tools may place Agora copy into those artifacts.
@@ -149,7 +168,7 @@ Model-level text watermarks are statistical generation signals, not hidden chara
149
168
 
150
169
  ## Routing model
151
170
 
152
- Agora chooses a primary mode first, then the publication surface, then any domain or asset modifiers. `VOICE` enters afterward and preserves your required language and content choices.
171
+ Agora resolves the audience, genre, writing register, and voice before it plans the argument. It then chooses a primary mode, publication surface, and any domain or asset modifiers. Voice shapes expression from the first sentence while preserving required facts and wording.
153
172
 
154
173
  ### Primary modes
155
174
 
@@ -167,11 +186,14 @@ Agora chooses a primary mode first, then the publication surface, then any domai
167
186
 
168
187
  | Modifier | Function |
169
188
  |---|---|
170
- | `SCIENCE` | Adds empirical or technical explanation and optional claim-review tools |
189
+ | `SCIENCE` | Adds research findings, methods, statistics, uncertainty, and optional scientific review |
190
+ | `TECHNICAL` | Adds documented system behavior, architecture, APIs, implementation detail, and technical evaluation |
171
191
  | `CASE_STUDY` | Adds case-study structure, result framing, and optional attribution or permission review |
172
- | `VOICE` | Applies a measured voice profile from the corpus you select |
192
+ | `VOICE` | Applies a measured persistent profile or a task-only sketch |
173
193
 
174
- The modifiers can compose. A technical fundraising case may use `INVEST + SCIENCE + CASE_STUDY`. A founder-voiced scientific product video may use `SELL + SCIENCE + VOICE + HYBRID`.
194
+ The modifiers can compose. A technical fundraising case may use `INVEST + TECHNICAL + CASE_STUDY`. A founder-voiced scientific product video may use `SELL + SCIENCE + VOICE + HYBRID`.
195
+
196
+ Subject matter alone does not activate `SCIENCE` or `TECHNICAL`. A nontechnical homepage for an AI or security product remains `SELL + PLAIN`.
175
197
 
176
198
  ### Surfaces
177
199
 
@@ -221,12 +243,12 @@ Promise grammar matters. `See X`, `Learn how to X`, `We help you X`, `Do X more
221
243
 
222
244
  ## Scientific and technical communication
223
245
 
224
- `SCIENCE` supports three internal routes:
246
+ Specialized explanation supports three internal routes:
225
247
 
226
248
  | Route | Subject |
227
249
  |---|---|
228
250
  | `EMPIRICAL` | Studies, experiments, observations, measurements, and findings |
229
- | `TECHNICAL` | Systems, interfaces, architecture, mechanisms, dependencies, and failure behavior |
251
+ | `TECHNICAL` | Systems, interfaces, architecture, APIs, implementation, dependencies, and failure behavior |
230
252
  | `MIXED` | Measured findings plus technical mechanism |
231
253
 
232
254
  When you request scientific claim review, Agora can classify material as observation, established fact or consensus, model, interpretation, implication, recommendation, hypothesis, speculation, or unknown. Without that request, it follows the certainty and framing in your brief.
@@ -285,7 +307,7 @@ Legal, securities, offering, solicitation, eligibility, and disclosure review is
285
307
 
286
308
  ## Measured voice profiles
287
309
 
288
- `VOICE` modifies another mode rather than replacing it. Profiles are measured from the corpus you select, not improvised from adjectives.
310
+ `VOICE` modifies another mode rather than replacing it. Persistent profiles are measured from the corpus you select, not improvised from adjectives.
289
311
 
290
312
  Build and inspect a profile with the shipped engine:
291
313
 
@@ -304,6 +326,10 @@ A profile records sentence and paragraph distributions, function words, punctuat
304
326
 
305
327
  Profiles live at `~/.agora/voices/`, outside the replaceable skill directory. A default profile can apply across modes. Use `--no-voice`, `neutral`, or `--voice <name>` per request. Your required wording overrides habitual profile tendencies.
306
328
 
329
+ Task-only voice sketches are separate. When you supply authentic samples with one task, Agora can use three to ten same-genre samples without the 5,000-word persistent-profile floor. One or two samples support only cautious local observations. The sketch records visible habits, stays inside the task, is never certified or stored as a profile, and never claims authorship or statistical identity. Agora does not copy distinctive phrases, facts, examples, metaphors, slogans, or anecdotes from the samples. With no samples, it uses plain professional writing and preserves credible choices from your draft.
330
+
331
+ Sentence and paragraph measurements remain available in `voice check`. They are diagnostics, not generation quotas. Agora does not force a 25-word sentence, alternate sentence lengths, or write toward a target distribution.
332
+
307
333
  Agora builds or applies the profile you request. You are responsible for corpus rights, identity use, attribution, endorsements, disclosure, and publication.
308
334
 
309
335
  ## User control and responsibility
@@ -342,6 +368,11 @@ skills/agora/
342
368
  |-- scripts/
343
369
  | `-- publication-audit.mjs
344
370
  `-- references/
371
+ |-- agora-writing-runtime.md
372
+ |-- agora-marketing-runtime.md
373
+ |-- agora-conversion-runtime.md
374
+ |-- agora-case-study-runtime.md
375
+ |-- human-voice-editing-reference.md
345
376
  |-- agora-case-studies.md
346
377
  |-- agora-conversion.md
347
378
  |-- agora-craft.md
@@ -352,9 +383,12 @@ skills/agora/
352
383
  `-- agora-voice.md
353
384
  ```
354
385
 
355
- `SKILL.md` contains routing and the concise operating contract. Ordinary work loads only the reference sections it needs.
386
+ `SKILL.md` contains routing. Ordinary work loads the compact writing and job runtimes first. Deep research stays available for explicit research, source review, scientific, technical, legal, audit, diligence, and maintainer work.
356
387
 
357
- - `agora-marketing.md` is the canonical doctrine for user authority, argument, channels, optional claim review, GEO/AEO, AI-writing-tell controls, examples, research grades, and conflict handling.
388
+ - `human-voice-editing-reference.md` is the exact canonical human-voice editing authority.
389
+ - `agora-writing-runtime.md` is the small plain-language writing contract loaded for every writing task.
390
+ - `agora-marketing-runtime.md`, `agora-conversion-runtime.md`, and `agora-case-study-runtime.md` keep ordinary drafting out of the research register.
391
+ - `agora-marketing.md` preserves deep doctrine, source material, research grades, and maintainer guidance.
358
392
  - `agora-craft.md` adds headlines, heroes, awareness and sophistication, emotion, and prose rhythm.
359
393
  - `agora-science.md` adds empirical and technical explanation plus optional scientific claim review.
360
394
  - `agora-case-studies.md` adds case structure, results, and optional attribution, permission, and confidentiality review.
@@ -370,13 +404,13 @@ Research informed Agora, but research custody is separate from distribution.
370
404
 
371
405
  The public repository and npm package must not contain raw or corrected transcripts, caption files, supplied PDFs or office documents, audio, video, private source identities, model-output scratch, research working files, local paths, or assigned secrets.
372
406
 
373
- `npm run check` runs validation, deterministic tests, the exact package allowlist, and release hygiene. `npm run release:check` is the proportional mandatory release gate and runs those same checks. `npm pack` and `npm publish` invoke it through `prepack` and `prepublishOnly`.
407
+ `npm run check` runs validation, deterministic tests, the exact package allowlist, and release hygiene. `npm run release:check` adds the current frozen blind-evaluation evidence gate. `npm pack` and `npm publish` invoke it through `prepack` and `prepublishOnly`.
374
408
 
375
- Behavioral evaluation is optional research, not a pack or publish blocker. `npm run eval:release` preserves the version-specific v1.7 evidence audit and must be run from a matching v1.7 checkout. Existing evaluation artifacts remain frozen for reproducibility; new blind or confirmatory generations are not required for later releases.
409
+ `evals/releases/current.json` is the explicit release contract. Its version must match `package.json`, the frozen manifest, schemas, release gates, adjudications, records, custody hashes, and external artifact manifests. Missing or stale human-writing evidence blocks a publishable release. Live generation does not run inside unit tests.
376
410
 
377
411
  The versioned directories under `evals/blind/` are public pairwise-release artifacts, not permanently secret holdouts. `v1.2.0`, `v1.4.0`, `v1.5.0`, and `v1.7.0` are frozen by exact tree hashes in `evals/releases/locks.json`; validation fails on additions, deletions, or edits.
378
412
 
379
- `evals/prospective/conversion-context-v1.0.0/` remains the development source for conversion-context cases. Earlier fixture sets are preserved under `evals/regression/` for regression analysis and historical reproducibility. `evals/blind/v1.7.0/` contains 25 independently authored conversion scenarios with versioned judging and reduction tooling.
413
+ `evals/prospective/human-writing-v1.0.0/` is the development source for the human-writing partition. The frozen current manifest lives under `evals/blind/` after prospective generation and blind pairwise review. Earlier fixture sets remain frozen for regression analysis and historical reproducibility.
380
414
 
381
415
  Deterministic tests verify instruction structure, routing contracts, static invariants, and evaluation-record shape. Versioned blind-evaluation tooling remains available for repeatable development analysis without turning model preference into a universal conversion claim.
382
416
 
@@ -390,12 +424,13 @@ npm pack --dry-run --json
390
424
  npx -y @maestroagora/agora --dry-run
391
425
  ```
392
426
 
393
- The mandatory release gate checks skill structure, routing contracts, user-authority boundaries, modifiers, typography, metadata, reference links, full-tree installer parity, exact npm contents, frozen evaluation-tree locks, public-tree hygiene, and deterministic tests. It does not require model generation or blind adjudication.
427
+ The mandatory release gate checks skill structure, routing contracts, factual and user-authority boundaries, typography, metadata, reference links, installer parity, exact npm contents, frozen evaluation-tree locks, public-tree hygiene, deterministic tests, and checked current blind evidence. Generation and judging happen before evidence is frozen, not during routine tests.
394
428
 
395
429
  ## Change record
396
430
 
397
431
  | Version | What changed |
398
432
  |---|---|
433
+ | 1.10.0 | Makes plain professional writing the default, separates compact runtime rules from maintainer research, ships the exact canonical human-voice source, adds task-only voice sketches and a deterministic style audit, routes technical language by job and audience, removes forced rhythm targets, and binds release checks to current human-writing evidence. |
399
434
  | 1.9.0 | Makes first-read clarity automatic. Agora now drafts for factual completeness, rewrites for literal clarity, preserves qualifiers across short passages, names concrete actors and observable results, and rejects vague referents, hidden metaphors, noun stacks, and revisions that only rename ambiguity. Adds rendered label-clearance guidance for technical diagrams. |
400
435
  | 1.8.0 | Adds opt-in publication privacy and provenance review plus a packaged read-only audit CLI. Reports configured hidden Unicode, document and image metadata, Office review material, and C2PA carrier or validation signals without changing source files. Redacts sensitive values and paths by default, preserves unknown coverage, and makes no watermark-removal or authorship claim. |
401
436
  | 1.7.0 | Adds a progressively loaded conversion-context reference with bounded conversion priors, downstream outcome matching, self-serve and enterprise route design, pricing decision contracts, proof placement, and contradiction handling. Tightens closed-world fact preservation and limits written GEO/AEO requirements to indexable public work. |
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@maestroagora/agora",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Install Maestro: Agora for persuasive writing, technical explanation, case studies, investment communication, and publication artifact review.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "agora": "scripts/install.mjs",
8
8
  "agora-publication-audit": "skills/agora/scripts/publication-audit.mjs",
9
+ "agora-style-audit": "scripts/style-audit.mjs",
9
10
  "agora-voice": "scripts/voice-measure.mjs"
10
11
  },
11
12
  "files": [
@@ -21,6 +22,9 @@
21
22
  "assets/icon.png",
22
23
  "assets/maestro-agora-banner.png",
23
24
  "scripts/install.mjs",
25
+ "scripts/style-audit-core.mjs",
26
+ "scripts/style-audit.mjs",
27
+ "scripts/task-voice-sketch.mjs",
24
28
  "scripts/voice-measure.mjs",
25
29
  "scripts/voice",
26
30
  "skills/agora"
@@ -32,8 +36,8 @@
32
36
  "prepack": "npm run release:check",
33
37
  "prepublishOnly": "npm run release:check",
34
38
  "eval:release": "node scripts/release-evidence-check.mjs",
35
- "release:check": "npm run check",
36
- "test": "node --test tests/behavior-contract.test.mjs tests/hero-contract.test.mjs tests/conversion-context-contract.test.mjs tests/science-contract.test.mjs tests/case-study-contract.test.mjs tests/invest-contract.test.mjs tests/publication-audit.test.mjs tests/blind-summary.test.mjs tests/blind-judge-prompt.test.mjs tests/blind-judge-materialize.test.mjs tests/blind-judgment-ingest.test.mjs tests/adjudication-reducer.test.mjs tests/eval-provenance-check.test.mjs tests/release-evidence-check.test.mjs tests/release-hygiene.test.mjs tests/install.test.mjs tests/voice-measure.test.mjs",
39
+ "release:check": "npm run check && npm run eval:release",
40
+ "test": "node --test tests/behavior-contract.test.mjs tests/human-writing-contract.test.mjs tests/style-audit.test.mjs tests/task-voice-sketch.test.mjs tests/release-contract.test.mjs tests/hero-contract.test.mjs tests/conversion-context-contract.test.mjs tests/science-contract.test.mjs tests/case-study-contract.test.mjs tests/invest-contract.test.mjs tests/publication-audit.test.mjs tests/blind-summary.test.mjs tests/blind-judge-prompt.test.mjs tests/blind-judge-materialize.test.mjs tests/blind-judgment-ingest.test.mjs tests/adjudication-reducer.test.mjs tests/eval-provenance-check.test.mjs tests/release-evidence-check.test.mjs tests/release-hygiene.test.mjs tests/install.test.mjs tests/voice-measure.test.mjs",
37
41
  "validate": "node scripts/validate.mjs"
38
42
  },
39
43
  "engines": {
@@ -252,6 +252,11 @@ async function verifySource() {
252
252
  "references/agora-craft.md",
253
253
  "references/agora-invest.md",
254
254
  "references/agora-marketing.md",
255
+ "references/agora-case-study-runtime.md",
256
+ "references/agora-conversion-runtime.md",
257
+ "references/agora-marketing-runtime.md",
258
+ "references/agora-writing-runtime.md",
259
+ "references/human-voice-editing-reference.md",
255
260
  "references/agora-publication.md",
256
261
  "references/agora-science.md",
257
262
  "references/agora-voice.md",
@@ -0,0 +1,267 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { GENERIC_AI_VOCABULARY } from "./voice/lexicon.mjs";
4
+ import { segmentParagraphs, segmentSentences, tokenize } from "./voice/pipeline.mjs";
5
+
6
+ export const REGISTERS = new Set(["plain", "technical", "scientific", "legal", "audit"]);
7
+
8
+ export const CANONICAL_CONNECTIVES = [
9
+ "moreover",
10
+ "furthermore",
11
+ "additionally",
12
+ "in addition",
13
+ "notably",
14
+ "importantly",
15
+ "indeed",
16
+ "in essence",
17
+ "in summary",
18
+ "in conclusion",
19
+ "ultimately",
20
+ "that said",
21
+ "on the other hand",
22
+ "on one hand",
23
+ ];
24
+
25
+ export const CANONICAL_TEMPLATE_PATTERNS = [
26
+ ["important-note", /\bit(?:'s| is) important to note that\b/giu],
27
+ ["worth-noting", /\bit(?:'s| is) worth (?:noting|mentioning) that\b/giu],
28
+ ["fast-paced-world", /\bin today(?:'s|s) fast-paced world\b/giu],
29
+ ["ever-evolving-landscape", /\bin the ever-evolving landscape of\b/giu],
30
+ ["realm", /\bin the realm of\b/giu],
31
+ ["testament", /\ba testament to\b/giu],
32
+ ["stands-as", /\bstands as a\b/giu],
33
+ ["serves-as", /\bserves as a\b/giu],
34
+ ["pivotal-role", /\bplays a (?:crucial|pivotal|vital) role in\b/giu],
35
+ ["not-only", /\bnot only\b[^.!?\n]{0,180}\bbut also\b/giu],
36
+ ["whether-you", /\bwhether you(?:'re| are)\b[^.!?\n]{0,120}\bor\b/giu],
37
+ ["from-to", /\bfrom\b[^,!?.\n]{1,80}\bto\b[^,!?.\n]{1,80},[^.!?\n]{0,120}\bhas\b/giu],
38
+ ["at-core", /\bat its core\b/giu],
39
+ ["when-it-comes", /\bwhen it comes to\b/giu],
40
+ ["navigating-complexities", /\bnavigating the complexities of\b/giu],
41
+ ["unlocking-potential", /\bunlocking the potential of\b/giu],
42
+ ["harnessing-power", /\bharnessing the power of\b/giu],
43
+ ["paving-way", /\bpaving the way for\b/giu],
44
+ ["setting-stage", /\bsetting the stage for\b/giu],
45
+ ["bringing-forefront", /\bbringing\b[^.!?\n]{1,100}\bto the forefront\b/giu],
46
+ ];
47
+
48
+ export const CANONICAL_SIGNIFICANCE_TAILS = [
49
+ "emphasizing the significance of",
50
+ "reflecting the continued relevance of",
51
+ "underscoring the importance of",
52
+ "highlighting its role in",
53
+ "demonstrating its impact on",
54
+ "marking a turning point in",
55
+ "cementing its place as",
56
+ "solidifying its reputation for",
57
+ "showcasing its commitment to",
58
+ ];
59
+
60
+ export const CANONICAL_PROMPT_LEAKAGE = [
61
+ /\bcertainly!\b/giu,
62
+ /\bof course!\b/giu,
63
+ /\babsolutely!\b/giu,
64
+ /\bgreat question!\b/giu,
65
+ /\bhere is your (?:article|blog post|essay) on\b/giu,
66
+ /\bas an ai language model\b/giu,
67
+ /\bup to my last training update\b/giu,
68
+ /\bi hope this helps!\b/giu,
69
+ /\blet me know if you(?:'d| would) like\b/giu,
70
+ /\bcertainly! here(?:'s| is) a\b/giu,
71
+ /\bsure, i can help with that\b/giu,
72
+ ];
73
+
74
+ export const CONTROL_ROOM_TERMS = [
75
+ "evidence", "evidentiary", "proof", "verified", "verification", "claim",
76
+ "substantiation", "provenance", "methodology", "framework", "mechanism",
77
+ "criterion", "criteria", "qualification", "qualifier", "causal", "entailment",
78
+ "proposition", "decision surface", "route", "artifact", "register", "hierarchy",
79
+ "gate", "audit", "ledger", "boundary", "scope", "operational",
80
+ ];
81
+
82
+ export const FALSE_REFRAMES = [
83
+ /\bthis is not just\b[^.!?\n]{1,120}[.!?]\s*it is\b/giu,
84
+ /\bthis is not\b[^.!?\n]{1,120}[.!?]\s*it is\b/giu,
85
+ /\b\w[\w -]{0,80} is more than \w/giu,
86
+ /\bthe real issue is not\b[^.!?\n]{1,140}[.!?]\s*it is\b/giu,
87
+ /\bthe question is not whether\b[^.!?\n]{1,160}\bbut how\b/giu,
88
+ ];
89
+
90
+ export const QUESTION_FRAGMENT_THEATER = /\b(?:the result|the answer|the problem|the difference|the bottom line)\?/giu;
91
+ export const CONVERSATIONAL_THEATER = /\b(?:here is the thing|let us be honest|think about it|imagine this|picture this|you know the feeling|the bottom line is)\b/giu;
92
+ export const CORPORATE_HELPERS = /\b(?:provides the ability to|is designed to enable|helps to facilitate|allows users to|offers a way to|serves to|works to|aims to|seeks to|has the potential to)\b/giu;
93
+ export const LEGALISTIC_LEAKAGE = /\b(?:with respect to|in relation to|insofar as|pursuant to|herein|therein|whereby|for the avoidance of doubt|where applicable|subject to the foregoing|constitutes|shall)\b/giu;
94
+ export const EMPTY_ANALYTICAL_ENDINGS = /\bthis (?:demonstrates that|indicates the importance of|provides a strong foundation for|creates a clear pathway to|supports the broader objective of|aligns with|reinforces|reflects)\b/giu;
95
+
96
+ const HARD = "hard";
97
+ const WARNING = "warning";
98
+ const EXACT_START = "<!-- agora-style-audit: exact-start -->";
99
+ const EXACT_END = "<!-- agora-style-audit: exact-end -->";
100
+
101
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
102
+
103
+ const lineAndColumn = (text, index) => {
104
+ const before = text.slice(0, index);
105
+ const lines = before.split("\n");
106
+ return { line: lines.length, column: lines.at(-1).length + 1 };
107
+ };
108
+
109
+ const finding = (text, match, severity, category, rule, message) => ({
110
+ severity,
111
+ category,
112
+ rule,
113
+ message,
114
+ excerpt: match[0],
115
+ ...lineAndColumn(text, match.index),
116
+ });
117
+
118
+ const maskRange = (characters, start, end) => {
119
+ for (let index = Math.max(0, start); index < Math.min(characters.length, end); index += 1) {
120
+ if (characters[index] !== "\n") characters[index] = " ";
121
+ }
122
+ };
123
+
124
+ export function maskExactText(text, exactTextRanges = []) {
125
+ const characters = [...text];
126
+ const patterns = [
127
+ /```[\s\S]*?```/g,
128
+ /~~~[\s\S]*?~~~/g,
129
+ /`[^`\n]+`/g,
130
+ /^\s*>.*$/gm,
131
+ new RegExp(`${escapeRegExp(EXACT_START)}[\\s\\S]*?${escapeRegExp(EXACT_END)}`, "g"),
132
+ ];
133
+ for (const pattern of patterns) {
134
+ for (const match of text.matchAll(pattern)) maskRange(characters, match.index, match.index + match[0].length);
135
+ }
136
+ for (const rangeValue of exactTextRanges) maskRange(characters, rangeValue.start, rangeValue.end);
137
+ return characters.join("");
138
+ }
139
+
140
+ const scanRegex = (text, masked, regex, severity, category, rule, message, findings) => {
141
+ regex.lastIndex = 0;
142
+ for (const match of masked.matchAll(regex)) findings.push(finding(text, match, severity, category, rule, message));
143
+ };
144
+
145
+ const sentenceEntries = (text) => {
146
+ const entries = [];
147
+ let searchFrom = 0;
148
+ for (const paragraph of segmentParagraphs(text)) {
149
+ for (const sentence of segmentSentences(paragraph)) {
150
+ const index = text.indexOf(sentence, searchFrom);
151
+ entries.push({ sentence, index: index < 0 ? searchFrom : index, tokens: tokenize(sentence) });
152
+ if (index >= 0) searchFrom = index + sentence.length;
153
+ }
154
+ }
155
+ return entries;
156
+ };
157
+
158
+ const sentenceWarnings = (text, masked, findings) => {
159
+ const entries = sentenceEntries(masked);
160
+ const joinWords = new Set(["and", "but", "or", "because", "although", "while", "whereas", "which", "that", "whereby", "if", "when", "since"]);
161
+ const prepositions = new Set(["of", "for", "with", "in", "through"]);
162
+ const nounSuffix = /(?:tion|sion|ment|ness|ity|ance|ence|al|ure|ism|ship)$/u;
163
+ for (const entry of entries) {
164
+ const match = { 0: entry.sentence, index: entry.index };
165
+ if (entry.tokens.length > 28) findings.push(finding(text, match, WARNING, "sentence", "sentence-over-28-words", `Ordinary sentence has ${entry.tokens.length} words; review whether a split preserves meaning.`));
166
+ const joins = entry.tokens.filter((token) => joinWords.has(token)).length;
167
+ if (joins >= 3) findings.push(finding(text, match, WARNING, "sentence", "three-or-more-joined-clauses", `Sentence contains ${joins} clause-join proxies.`));
168
+ const prepCount = entry.tokens.filter((token) => prepositions.has(token)).length;
169
+ if (prepCount >= 5) findings.push(finding(text, match, WARNING, "sentence", "preposition-stack", `Sentence contains ${prepCount} common prepositions.`));
170
+ let nounRun = 0;
171
+ let longest = 0;
172
+ for (const token of entry.tokens) {
173
+ nounRun = nounSuffix.test(token) ? nounRun + 1 : 0;
174
+ longest = Math.max(longest, nounRun);
175
+ }
176
+ if (longest >= 3) findings.push(finding(text, match, WARNING, "sentence", "noun-stack", "Sentence contains a possible abstract noun stack."));
177
+ }
178
+
179
+ for (let index = 2; index < entries.length; index += 1) {
180
+ const openings = entries.slice(index - 2, index + 1).map((entry) => entry.tokens.slice(0, 2).join(" "));
181
+ if (openings[0] && openings.every((opening) => opening === openings[0])) {
182
+ const entry = entries[index - 2];
183
+ findings.push(finding(text, { 0: openings[0], index: entry.index }, WARNING, "structure", "repeated-openings", "Three consecutive sentences share the same two-word opening."));
184
+ }
185
+ }
186
+ };
187
+
188
+ const paragraphWarnings = (text, masked, findings) => {
189
+ const paragraphs = segmentParagraphs(masked).map((paragraph) => ({
190
+ paragraph,
191
+ words: tokenize(paragraph).length,
192
+ sentences: segmentSentences(paragraph).length,
193
+ }));
194
+ for (let index = 2; index < paragraphs.length; index += 1) {
195
+ const group = paragraphs.slice(index - 2, index + 1);
196
+ const sameSentenceCount = group.every((entry) => entry.sentences === group[0].sentences);
197
+ const smallest = Math.min(...group.map((entry) => entry.words));
198
+ const largest = Math.max(...group.map((entry) => entry.words));
199
+ if (sameSentenceCount && smallest > 0 && largest / smallest <= 1.15) {
200
+ const start = text.indexOf(group[0].paragraph);
201
+ findings.push(finding(text, { 0: group[0].paragraph.slice(0, 80), index: Math.max(0, start) }, WARNING, "structure", "repeated-paragraph-shape", "Three consecutive paragraphs have nearly identical shapes."));
202
+ }
203
+ }
204
+ };
205
+
206
+ export function auditText(text, { register = "plain", exactTextRanges = [] } = {}) {
207
+ if (!REGISTERS.has(register)) throw new Error(`unknown register: ${register}`);
208
+ const masked = maskExactText(text, exactTextRanges);
209
+ const findings = [];
210
+
211
+ scanRegex(text, masked, /\u2014/gu, HARD, "punctuation", "u+2014", "Unicode U+2014 is forbidden outside exact text.", findings);
212
+ scanRegex(text, masked, /[\u2018\u2019\u201c\u201d]/gu, HARD, "punctuation", "smart-quote", "Generated smart quotes are forbidden outside exact text.", findings);
213
+
214
+ for (const word of GENERIC_AI_VOCABULARY) {
215
+ scanRegex(text, masked, new RegExp(`\\b${escapeRegExp(word)}\\b`, "giu"), HARD, "canonical", `banned-vocabulary:${word}`, "Canonical banned vocabulary requires an explicit exact-text exception.", findings);
216
+ }
217
+ for (const phrase of CANONICAL_CONNECTIVES) {
218
+ scanRegex(text, masked, new RegExp(`\\b${escapeRegExp(phrase)}\\b`, "giu"), HARD, "canonical", `banned-connective:${phrase}`, "Canonical banned connective requires an explicit formal-genre or exact-text exception.", findings);
219
+ }
220
+ for (const [id, pattern] of CANONICAL_TEMPLATE_PATTERNS) {
221
+ scanRegex(text, masked, pattern, HARD, "canonical", `banned-template:${id}`, "Canonical stock template is forbidden.", findings);
222
+ }
223
+ for (const phrase of CANONICAL_SIGNIFICANCE_TAILS) {
224
+ scanRegex(text, masked, new RegExp(`,?\\s*${escapeRegExp(phrase)}\\b`, "giu"), HARD, "canonical", `significance-tail:${phrase}`, "Canonical significance tail is forbidden.", findings);
225
+ }
226
+ for (const [index, pattern] of CANONICAL_PROMPT_LEAKAGE.entries()) {
227
+ scanRegex(text, masked, pattern, HARD, "canonical", `prompt-leakage:${index + 1}`, "Prompt or task meta-commentary is forbidden.", findings);
228
+ }
229
+
230
+ if (register === "plain") {
231
+ for (const term of CONTROL_ROOM_TERMS) {
232
+ scanRegex(text, masked, new RegExp(`\\b${escapeRegExp(term)}\\b`, "giu"), WARNING, "register", `control-room:${term}`, "Review whether the reader needs this internal control term.", findings);
233
+ }
234
+ }
235
+
236
+ for (const [index, pattern] of FALSE_REFRAMES.entries()) scanRegex(text, masked, pattern, WARNING, "structure", `false-reframe:${index + 1}`, "Review generic false-reframe construction.", findings);
237
+ scanRegex(text, masked, QUESTION_FRAGMENT_THEATER, WARNING, "structure", "question-fragment-theater", "Review generic question-fragment theater.", findings);
238
+ scanRegex(text, masked, CONVERSATIONAL_THEATER, WARNING, "structure", "conversational-theater", "Review conversational theater against the active voice.", findings);
239
+ scanRegex(text, masked, CORPORATE_HELPERS, WARNING, "wording", "corporate-helper", "Replace a corporate helper phrase with a direct verb when meaning permits.", findings);
240
+ if (register !== "legal" && register !== "audit") scanRegex(text, masked, LEGALISTIC_LEAKAGE, WARNING, "register", "legalistic-leakage", "Review legalistic wording outside a legal or audit register.", findings);
241
+ scanRegex(text, masked, EMPTY_ANALYTICAL_ENDINGS, WARNING, "structure", "empty-analytical-ending", "Replace an empty analytical ending with the fact or needed inference.", findings);
242
+
243
+ sentenceWarnings(text, masked, findings);
244
+ paragraphWarnings(text, masked, findings);
245
+
246
+ findings.sort((left, right) => left.line - right.line || left.column - right.column || left.rule.localeCompare(right.rule));
247
+ const hardFailures = findings.filter((item) => item.severity === HARD);
248
+ const warnings = findings.filter((item) => item.severity === WARNING);
249
+ return {
250
+ schema_version: 1,
251
+ register,
252
+ pass: hardFailures.length === 0,
253
+ hard_failure_count: hardFailures.length,
254
+ warning_count: warnings.length,
255
+ findings,
256
+ limits: {
257
+ authorship: "not assessed",
258
+ detector_evasion: "not promised",
259
+ factual_validation: "outside this lexical and structural tool",
260
+ rewriting: "not performed",
261
+ },
262
+ };
263
+ }
264
+
265
+ export async function auditFile(path, options = {}) {
266
+ return auditText(await readFile(path, "utf8"), options);
267
+ }
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve } from "node:path";
4
+
5
+ import { auditFile, REGISTERS } from "./style-audit-core.mjs";
6
+
7
+ export function parseArgs(argv) {
8
+ const options = { file: null, register: "plain", json: false };
9
+ for (let index = 0; index < argv.length; index += 1) {
10
+ const value = argv[index];
11
+ if (value === "--json") options.json = true;
12
+ else if (value === "--register") options.register = argv[++index];
13
+ else if (value.startsWith("--register=")) options.register = value.slice("--register=".length);
14
+ else if (value.startsWith("-")) throw new Error(`unknown option: ${value}`);
15
+ else if (options.file) throw new Error("supply exactly one file");
16
+ else options.file = value;
17
+ }
18
+ if (!options.file) throw new Error("missing file");
19
+ if (!REGISTERS.has(options.register)) throw new Error(`unknown register: ${options.register}`);
20
+ return options;
21
+ }
22
+
23
+ export function renderHuman(path, report) {
24
+ const status = report.pass ? "PASS" : "FAIL";
25
+ const lines = [
26
+ `${status} ${path}`,
27
+ `Register: ${report.register}`,
28
+ `Hard failures: ${report.hard_failure_count}`,
29
+ `Review warnings: ${report.warning_count}`,
30
+ ];
31
+ for (const item of report.findings) {
32
+ lines.push(`${item.severity.toUpperCase()} ${item.line}:${item.column} ${item.rule}: ${item.message}`);
33
+ }
34
+ lines.push("This audit does not rewrite text, validate facts, infer authorship, or promise detector evasion.");
35
+ return `${lines.join("\n")}\n`;
36
+ }
37
+
38
+ export async function main(argv = process.argv.slice(2)) {
39
+ const options = parseArgs(argv);
40
+ const path = resolve(options.file);
41
+ const report = await auditFile(path, { register: options.register });
42
+ process.stdout.write(options.json ? `${JSON.stringify(report, null, 2)}\n` : renderHuman(options.file, report));
43
+ if (!report.pass) process.exitCode = 1;
44
+ return report;
45
+ }
46
+
47
+ if (import.meta.url === new URL(`file://${resolve(process.argv[1]).replaceAll("\\", "/")}`).href) {
48
+ try {
49
+ await main();
50
+ } catch (error) {
51
+ process.stderr.write(`agora-style-audit: ${error.message}\n`);
52
+ process.exitCode = 2;
53
+ }
54
+ }