@holdyourvoice/hyv 3.3.4 → 3.3.5

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/Readme.md CHANGED
@@ -2,77 +2,45 @@
2
2
 
3
3
  [![npm downloads](https://img.shields.io/npm/dt/%40holdyourvoice%2Fhyv?label=npm%20downloads&color=2f81f7)](https://www.npmjs.com/package/@holdyourvoice/hyv)
4
4
 
5
+ Hold Your Voice is a local-first writing gate. It helps you use AI without losing the parts of your writing that make it yours.
5
6
 
6
- Hold Your Voice is an MIT-licensed, local-first writing gate for people who want AI help without losing the parts of their writing that make it theirs.
7
+ It runs two checks on a draft, with separate scores and pass states. A strong result in one engine leaves the other engine's finding unchanged.
7
8
 
8
- It checks a draft through two separate programs:
9
+ - **VoiceDNA** compares your draft with observable elements from your own writing samples.
10
+ - **AI Editor** flags a versioned set of editorial patterns that make writing sound generic or formulaic.
9
11
 
10
- - **VoiceDNA** compares the draft with 13 observable elements from your own local writing samples.
11
- - **AI Editor** flags a reviewed, versioned set of editorial patterns that can make writing generic, formulaic, or inflated.
12
+ Everything runs from local files. There are no accounts, no telemetry, and no runtime network requests. The optional Claude extension is a local adapter around the same engine.
12
13
 
13
- Those programs keep separate findings, scores, and pass states. A strong result from one never cancels a failure in the other. The tool creates a tiered editing brief, then checks the candidate again before you accept it.
14
-
15
- Everything in the CLI runs from local files: accounts, API calls, telemetry, payment collection, and runtime network requests stay out of the core path. The optional Claude extension adds a local stdio MCP adapter around that same engine; it is not a hosted service.
16
-
17
- > **Status:** [`@holdyourvoice/hyv`](https://www.npmjs.com/package/@holdyourvoice/hyv) **3.3.4** is the public founder-aware rewrite. It runs locally and makes no runtime network requests. The package includes Profile v3 policy, pre-edit SHIP/EDIT/REBUILD judgments, contiguous range edits, authorized rebuild, and a signed semantic lifecycle.
18
-
19
- ## Why it exists
20
-
21
- A draft can avoid obvious AI-shaped writing and still sound unlike its author. It can also match a writer’s short sentences while leaning on empty persuasion templates. Those are different problems. Treating them as one generic score hides the useful signal.
22
-
23
- Hold Your Voice keeps the work visible:
24
-
25
- | Question | Program | Result |
26
- | --- | --- | --- |
27
- | Does the draft still resemble this writer’s observable mechanics? | VoiceDNA | A profile-based score, findings, and pass state. |
28
- | Does the draft contain a configured editorial pattern worth inspecting? | AI Editor | A rule-based score, sentence findings, and pass state. |
29
- | Did the rewrite introduce a new blocker or replace too much? | Verification | Regressions, preservation score, and a release decision. |
30
- | Should this draft ship, take a bounded edit, or rebuild? | Judgment | A SHIP, EDIT, or REBUILD recommendation bound to the draft and profile. |
31
-
32
- Its scope is a local writing gate. It includes a source-consistency fact linter, not a truth engine: it checks a final draft against the evidence you provide. Authorship detection, plagiarism review, and hosted generation each need their own tools.
14
+ > **Status:** [`@holdyourvoice/hyv`](https://www.npmjs.com/package/@holdyourvoice/hyv) runs locally and makes no runtime network requests.
33
15
 
34
16
  ## Start here
35
17
 
36
- ### Requirements
18
+ **Requirements:** Node.js 20+, npm, and at least two writing samples you have the right to use.
37
19
 
38
- - Node.js 20 or newer.
39
- - npm.
40
- - At least two local writing samples you have the right to use.
41
-
42
- ### Install and verify
43
-
44
- ```bash
45
- npx @holdyourvoice/hyv patterns
46
- ```
47
-
48
- Run any command without a global install with `npx @holdyourvoice/hyv`. To use the short `hyv` command repeatedly:
20
+ ### Install
49
21
 
50
22
  ```bash
51
23
  npm install --global @holdyourvoice/hyv
52
24
  hyv patterns
53
25
  ```
54
26
 
55
- To contribute, clone this repository, run `npm install`, then run `npm test` and `npm run check:release`.
56
-
57
- ### Use it in Claude Desktop
27
+ No global install? Run any command with `npx @holdyourvoice/hyv`.
58
28
 
59
- Build the fully local Claude Desktop extension with `npm run pack:claude`, then install `dist/hold-your-voice.mcpb` from **Settings → Extensions → Advanced settings → Install Extension**. The extension accepts text and portable profile JSON in the current conversation only. Verification is read-only. Learning requires an explicit learning command or an approved lifecycle transition; neither path retains writing text or makes network requests. See the [Claude Desktop guide](docs/CLAUDE-DESKTOP.md).
60
-
61
- ### Build a local VoiceDNA profile
29
+ ### Build a profile
62
30
 
63
31
  ```bash
64
- npx @holdyourvoice/hyv profile profile.json samples/one.md samples/two.md --avoid=overused-phrase
32
+ hyv profile profile.json samples/one.md samples/two.md --avoid=overused-phrase
65
33
  ```
66
34
 
67
- Use writing by one person, with a similar audience and format where possible. The command needs at least two samples. It creates a portable JSON profile and keeps the samples on your machine. Repeat `--avoid=phrase` for each local phrase that must block a candidate.
35
+ Use writing by one person, with a similar audience and format. The command needs at least two samples. It writes a portable JSON profile and keeps your samples on your machine. Repeat `--avoid=phrase` for each phrase that must block a candidate.
68
36
 
69
- ### Inspect a draft
37
+ ### Analyze a draft
70
38
 
71
39
  ```bash
72
- npx @holdyourvoice/hyv analyze draft.md profile.json
40
+ hyv analyze draft.md profile.json
73
41
  ```
74
42
 
75
- The result is JSON with independent reports:
43
+ The result is JSON with independent reports. The outer `passed` field is true only when each engine passes.
76
44
 
77
45
  ```json
78
46
  {
@@ -83,177 +51,39 @@ The result is JSON with independent reports:
83
51
  }
84
52
  ```
85
53
 
86
- Read both scored reports and the separate hygiene inspection. The outer `passed` field means each engine passed. Unicode hygiene never changes either score or the release decision.
54
+ ### Edit and verify
55
+
56
+ ```bash
57
+ hyv rewrite-prompt draft.md profile.json > rewrite-brief.md
58
+ hyv verify draft.md candidate.md profile.json
59
+ ```
87
60
 
88
- ### Gate every final text output
61
+ Give the brief and draft to a human editor or any model you trust. Ask for replacement sentences keyed by sentence number, then save them as a separate candidate file. `verify` compares the original and candidate, reports new findings, and exits `2` when the candidate fails the gate.
89
62
 
90
- `final-check` is a profile-free last-mile boundary for text from any model, tool, agent, API, template, CLI, or GUI:
63
+ ### Gate final output
91
64
 
92
65
  ```bash
93
66
  producer | hyv final-check -
94
67
  hyv final-check final-response.md
95
68
  ```
96
69
 
97
- Clean text is written to stdout byte-for-byte. The gate removes only non-semantic ASCII controls and byte-order marks. When any other hidden Unicode remains, stdout stays empty, the report goes to stderr, and the command exits `2`. Put this command immediately before display, copy, export, posting, or an API response. The producer and the presence of a VoiceDNA profile do not change the policy.
98
-
99
- This is an integration boundary, not a background interceptor. HYV applies the same gate by default during rewrite and rebuild verification; unresolved output is withheld from their CLI and MCP evaluation result. A GUI, agent host, or external tool must still pass any later changed text to `hyv final-check -` or the read-only `hyv_final_check` MCP tool and deliver only accepted output. Run it after the last rewrite, formatter, template expansion, or manual edit; checking an earlier draft does not cover later changes.
100
-
101
- ### Inspect and clean hidden Unicode
70
+ `final-check` is the last step before text reaches a user. It needs no profile. Clean text goes to stdout byte-for-byte. If hidden Unicode remains, stdout stays empty and the command exits `2`. Run it after the last edit, formatter, or template expansion.
102
71
 
103
- Use the profile-free hygiene command to inspect zero-width characters, bidirectional controls, Unicode tag characters, and unusual spaces:
72
+ ### Clean hidden Unicode
104
73
 
105
74
  ```bash
106
75
  hyv hygiene draft.md
107
- ```
108
-
109
- Add `--fix` to create `draft.cleaned.md` while keeping `draft.md` unchanged:
110
-
111
- ```bash
112
76
  hyv hygiene draft.md --fix
113
- hyv hygiene draft.md --fix --output=review-copy.md
114
- ```
115
-
116
- The fix receipt lists every changed UTF-16 offset and code point. The conservative cleaner removes ASCII controls and byte-order marks. It reports other zero-width characters, unusual spaces, bidirectional controls, and tag characters without changing them because they can carry legitimate language, typography, or emoji behavior. Existing output files are never overwritten.
117
-
118
- For a deliberately narrow, policy-backed cleanup of non-semantic ASCII controls and mid-document byte-order marks, inspect first and write a separate result:
119
-
120
- ```bash
121
- hyv inspect-hidden-text draft.md policy.json
122
- hyv apply-hidden-text-policy draft.md policy.json draft.sanitized.md
123
- ```
124
-
125
- The receipt carries the input/output hashes, exact changes, remaining review findings, and an idempotence result. It does not label Unicode findings as watermarks or claim that any provider watermark was removed.
126
-
127
- ### Add contextual editorial guidance
128
-
129
- Use an optional local WritingBrief when the same writer needs different guidance for a social post, deck, outreach note, blog, audit, or website. A brief activates only the relevant advisory format checks and can block explicitly prohibited local terms. It never changes your VoiceDNA profile or the default two-engine analysis.
130
-
131
- ```json
132
- {
133
- "version": "1",
134
- "audience": "technical founders",
135
- "intent": "start a useful discussion",
136
- "format": "social",
137
- "readerKnowsAuthor": false,
138
- "vocabulary": ["deployment", "incident"],
139
- "prohibitedTerms": ["internal contract value"],
140
- "evidenceStatus": "attributed",
141
- "argumentMap": {
142
- "observation": "A worker fails during a live request.",
143
- "mechanism": "Its in-memory state is lost.",
144
- "consequence": "The request restarts instead of resuming.",
145
- "readerValue": "Avoid the cost of a cold restart."
146
- }
147
- }
148
- ```
149
-
150
- ```bash
151
- hyv analyze draft.md profile.json writing-brief.json
152
- hyv rewrite-prompt draft.md profile.json writing-brief.json > rewrite-brief.md
153
- hyv verify original.md candidate.md profile.json writing-brief.json
154
- ```
155
-
156
- Format checks, an `unverified` evidence state, and a missing configured reader-value cue are yellow review cues. `argumentMap` is a soft editorial contract surfaced to the rewrite prompt; it does not become a universal formula. Explicit `prohibitedTerms` are red release blockers. Keep client-specific briefs outside public repositories unless you have the right to publish them.
157
-
158
- ### Inspect a batch
159
-
160
- Use batch analysis to catch exact repeated opening or closing sentences across two or more drafts. It is advisory and keeps all drafts local.
161
-
162
- ```bash
163
- hyv batch-analyze posts/one.md posts/two.md posts/three.md
164
- ```
165
-
166
- ### Create an editing brief
167
-
168
- ```bash
169
- npx @holdyourvoice/hyv rewrite-prompt draft.md profile.json > rewrite-brief.md
170
- ```
171
-
172
- Give the brief and draft to a human editor or any model you trust. This repository stays out of that provider call. Ask for replacement sentences keyed by sentence number, then save the output as a separate candidate file.
173
-
174
- ### Verify the candidate
175
-
176
- ```bash
177
- npx @holdyourvoice/hyv verify draft.md candidate.md profile.json
178
- ```
179
-
180
- `verify` returns the original and candidate reports, identifies newly introduced findings, calculates a coarse preservation score, and exits with status `2` when the candidate fails the dual gate. It does not mutate learning state. It exits with `1` for a usage or runtime error. Treat status `2` as a release signal in scripts or CI.
181
-
182
- ### Lock factual claims with a CopySpec
183
-
184
- Use `verify-spec` when a draft has claims that must remain verbatim unless they declare atomic facts. A local CopySpec records each immutable claim alongside its evidence, then blocks a candidate if its required text or atoms are absent, or if it is joined by a prohibited claim.
185
-
186
- ```json
187
- {
188
- "version": "1",
189
- "audience": "operators",
190
- "intent": "explain a launch date",
191
- "channel": "email",
192
- "claims": [
193
- {
194
- "id": "launch-date",
195
- "text": "The launch is on 14 August.",
196
- "atoms": ["The launch is on 14 August."],
197
- "evidence": "Release calendar, checked 7 August."
198
- }
199
- ],
200
- "prohibitedClaims": ["The launch is guaranteed to double revenue."]
201
- }
202
- ```
203
-
204
- ```bash
205
- hyv verify-spec original.md candidate.md profile.json copy-spec.json
206
- ```
207
-
208
- The check is deterministic. Without `atoms`, an immutable claim remains a verbatim sentence check. With `atoms`, every declared phrase must remain somewhere in the candidate, allowing independent facts to be split or reordered. Atoms are lexical-presence checks, not factual validation.
209
-
210
- ### Check factual consistency with supplied sources
211
-
212
- `fact-lint` compares a final draft with local evidence. It extracts claims with sentence and UTF-16 offsets, checks dates, names, quotes, capabilities, causal/comparative escalation, and draft contradictions, then returns JSON with exact local evidence.
213
-
214
- ```bash
215
- hyv fact-lint final.md --source=release:release-notes.md --source=research:research.md
216
- hyv fact-lint final.md --source=release:release-notes.md --human
217
- hyv fact-lint final.md --source=release:release-notes.md --strict
218
- ```
219
-
220
- The default is report-only and exits `0`; `--strict` exits `2` for error findings. Known conflicts such as a CSV-to-PDF change are errors. A new or unclear capability, or weak evidence such as “exists” versus “grows”, becomes `needs_human_review`. No source text leaves the process by default. The linter checks consistency with supplied evidence; it does not prove the sources are true. See the [fact linter guide](docs/wiki/Fact-Linter.md).
221
-
222
- When a `WritingBrief` includes `factSources`, HYV runs the same local fact lint automatically during `verify`, `verify-spec`, rewrite evaluation, and their MCP equivalents. Error findings block verification. Source-free flows remain unchanged.
223
-
224
- Use `requiredFacts` for facts that must appear in the final draft. Each required fact must be supported by its source text or declared atoms in `factSources`; HYV fails verification if it is missing, negated, or denied. It does not assume every fact from every source belongs in every output.
225
-
226
- ```json
227
- {
228
- "version": "1",
229
- "audience": "founders",
230
- "intent": "write a post",
231
- "format": "social",
232
- "factSources": [{ "id": "bio", "text": "Shashank is a LinkedIn Top Voice." }],
233
- "requiredFacts": [{ "id": "linkedin-top-voice", "text": "Shashank is a LinkedIn Top Voice." }]
234
- }
235
77
  ```
236
78
 
237
- HYV does not infer trusted evidence from ordinary prompt prose. Pass source material through `factSources`, then mark only the inclusion-critical statements in `requiredFacts`. Run source-backed `verify` after the last substantive edit. `final-check` is a hygiene gate; it does not re-run evidence checks. Use source material that you are allowed to include in a rewrite task; task handoff is controlled by the calling host.
238
-
239
- ### Local voice memory
240
-
241
- Learning changes are explicit. Use the learning commands below, or complete the separately authorized semantic-review and final-approval lifecycle before recording approved learning. State lives under `~/.hyv/learning/`, scoped to the portable profile, and stores no draft or candidate text. The next `rewrite-prompt` uses a bounded list of approved repairs.
242
-
243
- ```bash
244
- hyv learning show profile.json
245
- hyv learning add profile.json "Keep the direct opening."
246
- hyv learning clear profile.json
247
- ```
248
-
249
- `show` lets you inspect the exact local preferences. `clear` removes only that profile's learning file. Set `HYV_HOME` to place this local state elsewhere.
79
+ Inspect zero-width characters, bidirectional controls, tag characters, and unusual spaces. Add `--fix` to write a cleaned copy while leaving the original untouched. The cleaner only removes non-semantic ASCII controls and byte-order marks; everything else is reported for review because it can carry real meaning.
250
80
 
251
81
  ## The editing loop
252
82
 
253
83
  ```mermaid
254
84
  flowchart LR
255
- S[Your local samples] --> P[Build profile]
256
- P --> D[VoiceDNA JSON]
85
+ S[Your samples] --> P[Build profile]
86
+ P --> D[VoiceDNA profile]
257
87
  T[Draft] --> A[Analyze]
258
88
  D --> A
259
89
  A --> V[VoiceDNA report]
@@ -264,178 +94,63 @@ flowchart LR
264
94
  T --> G[Verify candidate]
265
95
  C --> G
266
96
  D --> G
267
- F[Optional factSources + requiredFacts] --> L[Local fact lint]
268
- L --> G
269
- G --> R[Errors block; review findings stay visible]
270
- R --> H[final-check: hygiene before output]
271
- ```
272
-
273
- The tool never applies changes to your draft. You decide which findings are valid, apply replacement sentences or an authorized rebuild deliberately, and run the final check.
274
-
275
- ## Founder-aware rewrite
276
-
277
- 3.3.0 keeps the original analyze → brief → verify loop and adds a structured rewrite path.
278
-
279
- 1. Prepare a pre-edit judgment. Findings reduce to **SHIP**, bounded **EDIT**, or **REBUILD**.
280
- 2. **SHIP** returns the original bytes. No model call.
281
- 3. **EDIT** applies eligible sentence replacements or contiguous range edits through `prepare-rewrite` / `apply-rewrite`. Clean and unflagged text stays in place. Overlapping, out-of-order, or partly locked ranges fail before a candidate is built.
282
- 4. **REBUILD** prepares a whole-document candidate only after a matching REBUILD recommendation, a CopySpec, and a signed `hyv.rebuild-authorization` capability. `prepare-rebuild` / `apply-rebuild` re-check that capability and the bound profile. Claim, polarity, hygiene, and semantic gates stay in force. Edit and rebuild responses are mutually incompatible.
283
-
284
- For a meaning-first recomposition, pass an explicit lexical-residual policy to `prepare-rebuild`. HYV gives the external writer structured facts and constraints rather than automatically including the source draft in its prompt, then measures shared wording after the candidate returns. A passed residual report means only that the candidate meets the configured overlap policy. It does not detect, remove, or prove the absence of a provider watermark, and it does not establish authorship.
285
-
286
- ```bash
287
- hyv prepare-judgment pre-edit argument draft.md profile.json task.json
288
- hyv reduce-judgment envelope-a.json envelope-b.json envelope-c.json
289
- hyv prepare-rewrite draft.md profile.json task.json
290
- hyv apply-rewrite task.json response.json profile.json
291
- hyv prepare-rebuild draft.md profile.json reduction.json copy-spec.json task.json --recomposition-policy policy.json --capability-file capability.json
292
- hyv apply-rebuild task.json response.json profile.json --capability-file capability.json
293
- ```
294
-
295
- CLI and MCP expose the same contracts. The engine never calls a model. An editor or chosen model still sits outside the package.
296
-
297
- ## The five rewrite tiers
298
-
299
- The prompt has an order. Lower tiers can refine a higher tier; they cannot override it.
300
-
301
- 1. **Tier 0: preservation.** Keep facts, names, numbers, claims, and every unflagged sentence exactly. Keep the response within the supplied sentences.
302
- 2. **Tier 1: release blockers.** Resolve profile avoid-list phrases and red findings.
303
- 3. **Tier 2: VoiceDNA.** Use the 13 profile elements as a writer-specific target.
304
- 4. **Tier 3: AI Editor.** Inspect yellow findings. Change a line only when the repair helps.
305
- 5. **Tier 4: output.** Return replacement sentences keyed by sentence number.
306
-
307
- This order protects meaning before style. Rebuild is a separate whole-document contract; it does not use sentence-number replacements. Read the complete [prompt contract](docs/PROMPT-CONTRACT.md) before changing either path.
308
-
309
- ## VoiceDNA: 13 observable elements
310
-
311
- VoiceDNA is a local profile of writing mechanics drawn from the samples you choose. It makes no claim about personality. Its structural measurements support Unicode writing; the current point-of-view and transition lists are English-specific evidence.
312
-
313
- | # | Element | What it captures | Current gate behavior |
314
- | --- | --- | --- | --- |
315
- | 1 | Sentence length | Mean words per sentence | Yellow finding outside the profile band. |
316
- | 2 | Sentence variation | Spread of sentence lengths | Used to set the sentence-length tolerance. |
317
- | 3 | Sentence structure | Frequent three-word openings | Profile evidence. |
318
- | 4 | Rhythm | Change between neighbouring sentence lengths | Profile evidence. |
319
- | 5 | Paragraph length | Mean sentences per paragraph | Profile evidence. |
320
- | 6 | Opening moves | Frequent first words | Profile evidence. |
321
- | 7 | Vocabulary | Frequent non-stop words | Profile evidence and rewrite context. |
322
- | 8 | Lexical density | Share of non-stop words | Profile evidence. |
323
- | 9 | Point of view | First, second, third, or mixed | Yellow finding for a dominant-profile mismatch. |
324
- | 10 | Punctuation | Counts of selected marks | Profile evidence. |
325
- | 11 | Case style | Lowercase, standard, or mixed | Yellow finding on mismatch. |
326
- | 12 | Question rate | Sentences ending in questions | Yellow finding for a material difference. |
327
- | 13 | Transitions | Frequent recognised connectors | Profile evidence and rewrite context. |
328
-
329
- An explicit profile avoid list creates red findings. The other evidence-only elements are calculated and shown. Future enforcement needs a written policy, counterexamples, and tests.
330
-
331
- Read the full [VoiceDNA reference](docs/VOICE-DNA.md) and [Wiki guide](https://github.com/shashank-sn/holdyourvoice/wiki/VoiceDNA).
332
-
333
- ## AI Editor: inspectable rules
334
-
335
- AI Editor uses a local, deterministic ruleset. The current `3.2.0-reconciled.1` ruleset contains 148 stable catalog entries: the inherited catalog plus en-dash and performative-sincerity coverage. Applied profile policy determines whether a match blocks, advises, requires judgment, or is disabled. Duplicate legacy expressions remain cataloged for ID compatibility but emit one canonical finding. Most rules inspect sentences; selected inherited rules inspect one physical line to preserve multi-sentence and line-start behavior.
336
-
337
- Run this command to see the rules and ruleset version that actually execute in the published CLI:
338
-
339
- ```bash
340
- npx @holdyourvoice/hyv patterns
97
+ G --> R[Errors block; findings stay visible]
98
+ R --> H[final-check before output]
341
99
  ```
342
100
 
343
- From a built source checkout, run `node dist/cli.js patterns` instead.
344
-
345
- Red findings are release blockers. Yellow findings are a request to inspect a sentence in context. A match never proves who wrote the text, and a clean scan never proves the text is good.
346
-
347
- The repository also includes a public [220-pattern editorial catalog](docs/patterns/AI-WRITING-PATTERNS-1-220.md). It is broader editorial guidance, not a claim that all 220 entries execute. A catalog entry becomes executable only after the project has defined its counterexamples, reviewed public provenance, written tests, and decided the rule is narrow enough to help without creating noise. Neither the executable rules nor the editorial catalog can prove authorship.
101
+ The tool never edits your draft. You decide which findings are valid, apply the changes yourself, and run the final check.
348
102
 
349
- ## Verification contract
103
+ ## A few more commands
350
104
 
351
- Verification analyzes the original and candidate with the same profile and ruleset. It passes only when:
352
-
353
- - VoiceDNA passes.
354
- - AI Editor passes.
355
- - The candidate introduces zero new red findings.
356
- - The lexical preservation score is at least 70.
357
-
358
- The preservation score is a guardrail based on retained original words longer than four characters. A human still needs to review facts, source links, intent, and reader value.
359
-
360
- ## Commands
361
-
362
- | Command | Input | Output | Use it when |
363
- | --- | --- | --- | --- |
364
- | `hyv profile <profile.json> <sample...>` | Two or more text files | Profile JSON | You need a new local reference. |
365
- | `hyv analyze <draft> <profile.json>` | Draft and profile | Analysis JSON | You need both reports before editing. |
366
- | `hyv hygiene <draft> [--fix] [--output=path]` | Draft | Hygiene report or cleaned copy plus receipt | You need to inspect or conservatively clean hidden Unicode. |
367
- | `hyv final-check <path\|->` | Any final text | Exact accepted text on stdout or a withheld-output report on stderr | Text is about to cross a user-facing boundary. |
368
- | `hyv rewrite-prompt <draft> <profile.json>` | Draft and profile | Markdown editing brief | You need a constrained request for an editor or model. |
369
- | `hyv prepare-rewrite <draft> <profile.json> <task.json>` | Draft and profile | Versioned task file plus metadata | A host needs a fingerprint-bound sentence-edit or range-edit task. |
370
- | `hyv apply-rewrite <task.json> <response.json> <profile.json>` | Task, response, and profile | Candidate evaluation JSON | A host needs to apply and recheck eligible sentence replacements. |
371
- | `hyv prepare-judgment <pre-edit\|post-candidate> <kind> <draft> <profile.json> <task.json> [candidate.md]` | Draft, profile, and optional candidate | Versioned judgment task | Findings need a SHIP, EDIT, or REBUILD recommendation. |
372
- | `hyv reduce-judgment <envelope.json> <envelope.json> [envelope.json...]` | Signed judgment envelopes | Recommendation JSON | Multiple judgment envelopes must reduce to one decision. |
373
- | `hyv prepare-rebuild <draft> <profile.json> <reduction.json> <copy-spec.json> <task.json> [--recomposition-policy policy.json]` | Draft, recommendation, CopySpec, capability, and optional policy | Versioned rebuild task | An upstream REBUILD recommendation needs a whole-document candidate; an optional policy makes it meaning-first and adds lexical-residual evidence. |
374
- | `hyv apply-rebuild <task.json> <response.json> <profile.json>` | Task, response, profile, and capability | Candidate evaluation JSON | A host needs to apply and recheck an authorized rebuild. |
375
- | `hyv verify <original> <candidate> <profile.json>` | Original, candidate, profile | Verification JSON and exit code | You need the candidate gate. |
376
- | `hyv verify-spec <original> <candidate> <profile.json> <copy-spec.json>` | Original, candidate, profile, CopySpec | Verification JSON with hard claim gate | A brief contains locked facts or prohibited claims. |
377
- | `hyv learning <show\|inspect\|add\|record\|ratify\|supersede\|migrate\|clear> ...` | Profile, operation value, and bounded metadata options | Preferences or a text-free mutation receipt | You need to inspect, migrate, or manage profile-scoped learning. |
378
- | `hyv lifecycle <prepare-semantic\|submit-verdict\|inspect\|validate-final-approval\|finalize> ...` | Versioned lifecycle artifacts | Canonical lifecycle artifact or metadata | A normal-policy semantic review or human decision must advance through the shared reducer. |
379
- | `hyv patterns` | None | Ruleset JSON | You need the exact enabled rules. |
380
-
381
- Every file argument can be `-` when the command accepts text input from standard input. Profile output is always written to the path you give it. Use `npx @holdyourvoice/hyv <command>` in place of `hyv <command>` when you have not installed the CLI globally.
382
-
383
- Profile v3 learning is keyed by its stable local profile ID, so compatible history survives profile revisions. `record`, `ratify`, and `supersede` accept bounded `--mutation-id`, `--authority`, `--provenance`, `--weight`, and `--compatibility` options. `ratify` and `supersede` require Profile v3. `migrate` explicitly copies compatible legacy Profile v2 learning into one Profile v3 identity. Replaying an identical mutation is idempotent; reusing its ID for a different operation returns a conflict. Inspection and receipts expose event metadata only, never stored instructions or draft text.
384
-
385
- The standalone CLI supports normal-policy semantic review. High-assurance review requires a trusted embedding and is rejected by the CLI. Approval and rebuild capabilities are accepted only through `--capability-stdin` or a permission-checked `--capability-file`; adapters validate capabilities but never mint them. Rejection needs no capability. Approval and `learning record-approved` require the matching signed final-approval capability. `apply-rewrite`, `apply-rebuild`, `lifecycle submit-verdict`, and `lifecycle finalize` exit `2` when the candidate or transition is not accepted, while usage and runtime failures exit `1`.
386
-
387
- ## Project map
388
-
389
- | Path | Responsibility |
105
+ | Command | Use it when |
390
106
  | --- | --- |
391
- | `src/contracts.ts` | Profiles, findings, reports, analysis, and verification data shapes. |
392
- | `src/text.ts` | Sentence, paragraph, word, and basic statistics helpers. |
393
- | `src/hygiene.ts` | Profile-free hidden Unicode inspection and conservative cleaning. |
394
- | `src/voice-dna.ts` | Builds profiles and runs VoiceDNA checks. |
395
- | `src/ai-editor.ts` | Owns the versioned deterministic editorial rules. |
396
- | `src/editorial-packs.ts` | Parses WritingBrief context and runs format and batch checks. |
397
- | `src/learning.ts` | Stores text-free, profile-scoped verified repairs and composes bounded local preferences. |
398
- | `src/pipeline.ts` | Combines scored pass states, makes briefs, and verifies candidates. |
399
- | `src/rewrite-task.ts` | Prepares and evaluates fingerprint-bound sentence-replacement and range-edit tasks. |
400
- | `src/judgment-task.ts` | Reduces pre-edit SHIP/EDIT/REBUILD recommendations and post-candidate clearance. |
401
- | `src/rebuild-task.ts` | Prepares whole-document rebuild after a matching recommendation, CopySpec, and signed capability. |
402
- | `src/recomposition.ts` | Builds meaning-first rebuild briefs and measures declared lexical-residual evidence. |
403
- | `src/semantic-review.ts` | Defines and reduces semantic and human-review lifecycle artifacts. |
404
- | `src/approval-capability.ts` | Verifies canonical signed approval capabilities. |
405
- | `src/approval-context.ts` | Loads permission-checked trust roots and evaluator authorization. |
406
- | `src/lifecycle-adapter.ts` | Shares lifecycle operations across CLI and MCP adapters. |
407
- | `src/cli.ts` | Local file and standard-input command adapter. |
408
- | `src/mcp.ts` | Local stdio MCP registration and host-capability gating. |
409
- | `src/pipeline.test.ts` | Contract and regression tests. |
410
- | `CONTRIBUTING.md` | Public-safety rules and the contributor model. |
411
- | `scripts/release-audit.mjs` | Checks source files for credential and network markers. |
412
-
413
- `pipeline.ts` is the sole scored output-composition point. It combines pass states and preserves each engine’s separate score. Rewrite-task and lifecycle modules compose their own versioned, non-scoring artifacts.
414
-
415
- ## Privacy and data rights
416
-
417
- The runtime uses files on your machine. Samples, drafts, profiles, candidates, and client data stay there. Verification is read-only. Explicit learning commands and approved lifecycle recording can write text-free local events under `~/.hyv/learning/`: profile fingerprint, finding IDs, severities, counts, timestamp, and an opaque one-way candidate digest for retry deduplication. An instruction added through `hyv learning add` is stored as entered.
107
+ | `hyv verify-spec` | A draft has facts that must stay verbatim (CopySpec). |
108
+ | `hyv fact-lint` | Check a draft against local evidence sources. |
109
+ | `hyv batch-analyze` | Catch exact repeated sentences across drafts. |
110
+ | `hyv prepare-rewrite` / `apply-rewrite` | Fingerprint-bound sentence or range edits. |
111
+ | `hyv prepare-rebuild` / `apply-rebuild` | Whole-document rebuild after an authorized recommendation. |
112
+ | `hyv prepare-judgment` / `reduce-judgment` | Reduce findings to SHIP, EDIT, or REBUILD. |
113
+ | `hyv lifecycle` | Semantic and human-review lifecycle steps. |
114
+ | `hyv learning show` | Inspect local voice-memory preferences. |
418
115
 
419
- The package does not upload writing, use embeddings, or make runtime network requests. Keep writing samples, edit histories, client text, local learning files, and datasets out of public commits unless you hold explicit rights and a provenance record. A profile is aggregated JSON and can still reveal vocabulary and preferences. Store private profiles outside public repositories.
420
-
421
- See the [privacy guide](https://github.com/shashank-sn/holdyourvoice/wiki/Privacy-and-Data-Rights) for maintainer and contributor boundaries.
422
-
423
- ## Benchmarks and claims
424
-
425
- The repository preserves historical product material in [docs/BENCHMARKS.md](docs/BENCHMARKS.md), with related public articles about the [Voice Memory Composer](https://holdyourvoice.com/blog/voice-memory-composer) and [Hold Your Voice vs GPT-5.6 writing](https://holdyourvoice.com/blog/hold-your-voice-vs-gpt-5-6-writing).
116
+ ## Commands
426
117
 
427
- Treat those as dated reference material. A reproducible benchmark needs rights-cleared data, frozen settings, a published rubric, separate dimensions, and visible limitations. The [benchmark guide](https://github.com/shashank-sn/holdyourvoice/wiki/Benchmarks-and-Research) explains the standard.
118
+ | Command | Input | Output |
119
+ | --- | --- | --- |
120
+ | `hyv profile <profile.json> <sample...>` | Two or more text files | Profile JSON |
121
+ | `hyv analyze <draft> <profile.json>` | Draft and profile | Analysis JSON |
122
+ | `hyv hygiene <draft> [--fix] [--output=path]` | Draft | Hygiene report or cleaned copy plus receipt |
123
+ | `hyv final-check <path\|->` | Any final text | Accepted text on stdout or a withheld-output report |
124
+ | `hyv rewrite-prompt <draft> <profile.json>` | Draft and profile | Markdown editing brief |
125
+ | `hyv prepare-rewrite <draft> <profile.json> <task.json>` | Draft and profile | Versioned task file |
126
+ | `hyv apply-rewrite <task.json> <response.json> <profile.json>` | Task, response, profile | Candidate evaluation JSON |
127
+ | `hyv prepare-judgment <pre-edit\|post-candidate> <kind> <draft> <profile.json> <task.json> [candidate.md]` | Draft, profile, optional candidate | Versioned judgment task |
128
+ | `hyv reduce-judgment <envelope.json> ...` | Signed judgment envelopes | Recommendation JSON |
129
+ | `hyv prepare-rebuild <draft> <profile.json> <reduction.json> <copy-spec.json> <task.json> [--recomposition-policy policy.json]` | Draft, recommendation, CopySpec, capability, optional policy | Versioned rebuild task |
130
+ | `hyv apply-rebuild <task.json> <response.json> <profile.json>` | Task, response, profile, capability | Candidate evaluation JSON |
131
+ | `hyv verify <original> <candidate> <profile.json>` | Original, candidate, profile | Verification JSON and exit code |
132
+ | `hyv verify-spec <original> <candidate> <profile.json> <copy-spec.json>` | Original, candidate, profile, CopySpec | Verification JSON with hard claim gate |
133
+ | `hyv learning <show\|inspect\|add\|record\|ratify\|supersede\|migrate\|clear> ...` | Profile, operation, bounded metadata | Preferences or a text-free receipt |
134
+ | `hyv lifecycle <prepare-semantic\|submit-verdict\|inspect\|validate-final-approval\|finalize> ...` | Versioned lifecycle artifacts | Lifecycle artifact or metadata |
135
+ | `hyv patterns` | None | Ruleset JSON |
136
+
137
+ Every file argument can be `-` when the command accepts input on standard input. Use `npx @holdyourvoice/hyv <command>` if you have not installed the CLI globally.
138
+
139
+ ## Privacy
140
+
141
+ Your samples, drafts, profiles, and candidates stay on your machine. Verification is read-only. Learning commands can write text-free local events under `~/.hyv/learning/` — profile fingerprint, finding IDs, counts, and an opaque digest. No writing text is uploaded, and the package makes no runtime network requests.
142
+
143
+ Keep writing samples, edit histories, and client text out of public commits unless you hold the rights and a provenance record. A profile is aggregated JSON and can still reveal vocabulary, so store private profiles outside public repositories.
428
144
 
429
145
  ## Documentation
430
146
 
431
147
  | Read this | When you need |
432
148
  | --- | --- |
433
- | [The complete Wiki](https://github.com/shashank-sn/holdyourvoice/wiki) | Product, workflow, and contributor documentation. |
434
- | [Thesis](docs/THESIS.md) | The design argument for two independent engines. |
149
+ | [The Wiki](https://github.com/shashank-sn/holdyourvoice/wiki) | Product and contributor docs. |
435
150
  | [Architecture](docs/ARCHITECTURE.md) | Source boundaries and extension rules. |
436
- | [Local voice memory](docs/wiki/Local-Voice-Memory.md) | What default local learning stores, uses, and never changes. |
437
151
  | [Prompt contract](docs/PROMPT-CONTRACT.md) | The tier order and editing constraints. |
438
- | [Pattern taxonomy](docs/PATTERN-TAXONOMY.md) | The catalog/executable-rule boundary. |
152
+ | [VoiceDNA](docs/VOICE-DNA.md) | The 13 profile elements. |
153
+ | [Fact linter](docs/wiki/Fact-Linter.md) | The source-consistency checker. |
439
154
  | [Support](SUPPORT.md) | Funding without a feature gate. |
440
155
 
441
156
  ## Contribute
@@ -445,16 +160,10 @@ npm test
445
160
  npm run check:release
446
161
  ```
447
162
 
448
- Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. Keep changes narrow. Add tests when code behavior changes. Separate current executable behavior, editorial guidance, historical research, and proposals. Keep private, client, secret, and unlicensed material out of issues, fixtures, tests, and documentation.
449
-
450
- ## npm releases
451
-
452
- `@holdyourvoice/hyv` is published automatically after a change to the package source reaches `main`. The workflow publishes only when the version in `package.json` is not already on npm, so bump that version in the same pull request as a release-worthy change. It runs the tests and release audit before publishing, then verifies that npm reports the package as MIT licensed.
453
-
454
- ## Support
163
+ Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. Keep changes narrow, add tests when behavior changes, and keep private or unlicensed material out of the repo.
455
164
 
456
- Hold Your Voice stays fully open source. Read the [funding policy](FUNDING.md) or [sponsor maintenance on GitHub](https://github.com/sponsors/shashank-sn) with a one-time or recurring sponsorship. Sponsorship funds maintenance while the feature set and local privacy contract remain the same for everyone.
165
+ `@holdyourvoice/hyv` publishes automatically when a change reaches `main`. Bump the version in `package.json` in the same pull request as a release-worthy change; the workflow publishes only if that version is not already on npm.
457
166
 
458
167
  ## License
459
168
 
460
- [MIT](LICENSE). You can use, modify, and distribute the code under its terms. Third-party writing and data retain their own rights.
169
+ [MIT](LICENSE). Third-party writing and data retain their own rights.
@@ -1,14 +1,11 @@
1
1
  import { createHash, createPublicKey, verify } from 'node:crypto';
2
2
  import { parseCanonicalJson } from './canonical-json.js';
3
+ import { exactKeys, isPlainObject as plain } from './internal.js';
3
4
  const DIGEST = /^[a-f0-9]{64}$/;
4
5
  const BASE64URL = /^[A-Za-z0-9_-]+$/;
5
6
  const CLAIM_KEYS = ['version', 'purpose', 'issuer', 'audience', 'subjectArtifactFingerprint', 'sourceHash', 'candidateHash', 'profileId', 'profileRevisionDigest', 'keyId', 'issuedAt', 'notBefore', 'expiresAt', 'nonce'];
6
7
  const STORE_KEYS = ['version', 'audience', 'maxCapabilityLifetimeSeconds', 'keys'];
7
8
  function fail(error) { return { ok: false, error }; }
8
- function plain(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype; }
9
- function exactKeys(value, required, optional = []) {
10
- return required.every((key) => key in value) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
11
- }
12
9
  function bounded(value) { return typeof value === 'string' && value.length > 0 && value.length <= 128; }
13
10
  function safeTime(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; }
14
11
  function validClaims(value) {
@@ -2,7 +2,7 @@ import { closeSync, constants, fstatSync, openSync, readSync } from 'node:fs';
2
2
  import { userInfo } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { parseApprovalTrustStore } from './approval-capability.js';
5
- const MAX_BYTES = 1024 * 1024;
5
+ import { MAX_JSON_BYTES as MAX_BYTES } from './internal.js';
6
6
  const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
7
7
  function readBounded(descriptor) {
8
8
  const chunks = [];
package/dist/cli.js CHANGED
@@ -13,13 +13,13 @@ import { evaluateRewriteResponse, parseRewriteTask, prepareRewriteTask } from '.
13
13
  import { parseJudgmentEnvelope, preparePostCandidateJudgment, preparePreEditJudgment, reducePostCandidate, reducePreEdit } from './judgment-task.js';
14
14
  import { evaluateRebuildResponse, parseRebuildTask, prepareRebuildTask, writerRequestForRebuild } from './rebuild-task.js';
15
15
  import { canonicalJson, parseCanonicalJson } from './canonical-json.js';
16
+ import { MAX_JSON_BYTES } from './internal.js';
16
17
  import { finalizeLifecycle, inspectLifecycle, prepareLifecycle, recordApprovedLearning, submitSemanticVerdict, validateFinalApproval } from './lifecycle-adapter.js';
17
18
  import { buildProfile } from './voice-dna.js';
18
19
  import { loadApprovalContext } from './approval-context.js';
19
20
  import { formatFactLintReport, lintFacts } from './fact-linter.js';
20
21
  import { lintLogic } from './logic-linter.js';
21
22
  const usage = 'Commands: profile, analyze, hygiene, inspect-hidden-text, apply-hidden-text-policy, final-check, fact-lint, logic-lint, batch-analyze, rewrite-prompt, prepare-rewrite, apply-rewrite, prepare-judgment, reduce-judgment, prepare-rebuild, rebuild-writer-request, apply-rebuild, verify, verify-spec, lifecycle, learning, patterns, mcp';
22
- const MAX_JSON_BYTES = 1024 * 1024;
23
23
  function input(path) {
24
24
  return path === '-' ? readFileSync(0, 'utf8') : readFileSync(path, 'utf8');
25
25
  }
package/dist/copy-spec.js CHANGED
@@ -1,19 +1,14 @@
1
1
  import { sentences } from './text.js';
2
+ import { escaped, isText } from './internal.js';
2
3
  function normalized(value) {
3
4
  return value.toLowerCase().replace(/\s+/g, ' ').trim();
4
5
  }
5
- function escaped(value) {
6
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
- }
8
6
  function atomMatches(value, atom) {
9
7
  return new RegExp(`(?<![\\p{L}\\p{N}\\p{M}])${escaped(atom)}(?![\\p{L}\\p{N}\\p{M}])`, 'u').test(value);
10
8
  }
11
9
  function isAtom(value) {
12
10
  return isText(value, 500) && /[\p{L}\p{N}]/u.test(normalized(value));
13
11
  }
14
- function isText(value, limit) {
15
- return typeof value === 'string' && value.trim().length > 0 && value.length <= limit;
16
- }
17
12
  function isClaim(value) {
18
13
  if (!value || typeof value !== 'object')
19
14
  return false;
@@ -1,9 +1,7 @@
1
1
  import { paragraphs, sentences, words } from './text.js';
2
+ import { isText } from './internal.js';
2
3
  const formats = ['general', 'social', 'deck', 'outreach', 'blog', 'audit', 'website'];
3
4
  const evidenceStatuses = ['primary', 'attributed', 'internal', 'unverified'];
4
- function isText(value, limit) {
5
- return typeof value === 'string' && value.trim().length > 0 && value.length <= limit;
6
- }
7
5
  function isTerms(value) {
8
6
  return Array.isArray(value) && value.length <= 100 && value.every((term) => isText(term, 200));
9
7
  }
@@ -1,10 +1,8 @@
1
- import { createHash } from 'node:crypto';
2
- import { canonicalJson } from './canonical-json.js';
1
+ import { fingerprint as hash } from './internal.js';
3
2
  const ACKNOWLEDGEMENT = 'Removes only listed non-semantic controls; all other findings remain review-only.';
4
3
  export const minimalHiddenTextPolicy = {
5
4
  version: '1', name: 'minimal-text-control-cleanup', approvedRemovals: ['ascii_control', 'mid_document_bom'], acknowledgement: ACKNOWLEDGEMENT,
6
5
  };
7
- function hash(value) { return createHash('sha256').update(typeof value === 'string' ? value : canonicalJson(value)).digest('hex'); }
8
6
  function codepoint(value) { return `U+${value.toString(16).toUpperCase().padStart(4, '0')}`; }
9
7
  export function parseHiddenTextPolicy(value) {
10
8
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -50,15 +48,7 @@ export function inspectHiddenText(text, policy = minimalHiddenTextPolicy) {
50
48
  }
51
49
  export function applyHiddenTextPolicy(text, policy = minimalHiddenTextPolicy) {
52
50
  const report = inspectHiddenText(text, policy);
53
- const offsets = new Set(report.proposedChanges.map((item) => item.offset));
54
- let output = '';
55
- for (let offset = 0; offset < text.length;) {
56
- const value = text.codePointAt(offset);
57
- const character = String.fromCodePoint(value);
58
- if (!offsets.has(offset))
59
- output += character;
60
- offset += character.length;
61
- }
51
+ const output = applyOnce(text, policy);
62
52
  const remaining = inspectHiddenText(output, policy).findings;
63
53
  const again = applyOnce(output, policy);
64
54
  return { ...report, outputHash: hash(output), output, remaining, idempotent: again === output };
@@ -0,0 +1,30 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalJson } from './canonical-json.js';
3
+ export const MAX_JSON_BYTES = 1024 * 1024;
4
+ export function sha256(value) {
5
+ return createHash('sha256').update(value).digest('hex');
6
+ }
7
+ export function digestCanonical(value) {
8
+ return sha256(canonicalJson(value));
9
+ }
10
+ export function fingerprint(value) {
11
+ return sha256(typeof value === 'string' ? value : canonicalJson(value));
12
+ }
13
+ export function profileIdentity(profile) {
14
+ if (profile.version === '3')
15
+ return { profileId: profile.id, profileRevisionDigest: profile.revisionDigest };
16
+ const legacy = `legacy-v2:${digestCanonical(profile)}`;
17
+ return { profileId: legacy, profileRevisionDigest: legacy };
18
+ }
19
+ export function isPlainObject(value) {
20
+ return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
21
+ }
22
+ export function exactKeys(value, required, optional = []) {
23
+ return required.every((key) => key in value) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
24
+ }
25
+ export function isText(value, maximum = 256) {
26
+ return typeof value === 'string' && value.trim().length > 0 && value.length <= maximum;
27
+ }
28
+ export function escaped(value) {
29
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
30
+ }
@@ -1,21 +1,9 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { canonicalJson } from './canonical-json.js';
3
2
  import { sentences } from './text.js';
4
3
  import { HYV_VERSION } from './version.js';
4
+ import { profileIdentity, sha256 as digest } from './internal.js';
5
5
  const PRE_EDIT_KINDS = ['triage', 'argument', 'form'];
6
6
  const POST_CANDIDATE_KINDS = ['argument', 'polarity', 'form', 'flatness', 'semantic'];
7
- function digest(value) {
8
- return createHash('sha256').update(value).digest('hex');
9
- }
10
- function digestCanonical(value) {
11
- return digest(canonicalJson(value));
12
- }
13
- function profileIdentity(profile) {
14
- if (profile.version === '3')
15
- return { profileId: profile.id, profileRevisionDigest: profile.revisionDigest };
16
- const legacy = `legacy-v2:${digestCanonical(profile)}`;
17
- return { profileId: legacy, profileRevisionDigest: legacy };
18
- }
19
7
  function fingerprintTask(task) {
20
8
  return digest(`hyv:judgment-task:v1\0${canonicalJson(task)}`);
21
9
  }
@@ -3,6 +3,7 @@ import { verifyApprovalCapability } from './approval-capability.js';
3
3
  import { createInitialLifecycleArtifact, isValidLifecycleArtifact, parseSemanticVerdict, prepareSemanticReviewTask, reduceRewriteLifecycle } from './semantic-review.js';
4
4
  import { verifyDeterministically } from './pipeline.js';
5
5
  import { recordVerifiedCandidate } from './learning.js';
6
+ import { MAX_JSON_BYTES } from './internal.js';
6
7
  export function prepareLifecycle(deterministic, binding, receipt, policy, allowedViolations) {
7
8
  const task = prepareSemanticReviewTask(binding, policy, receipt, allowedViolations);
8
9
  return { task, artifact: createInitialLifecycleArtifact(task, deterministic) };
@@ -58,7 +59,7 @@ export function finalizeLifecycle(artifact, decision, context, capability) {
58
59
  }
59
60
  export function recordApprovedLearning(request) {
60
61
  const { ready, approved, decision, capability, source, candidate, profile, context, copySpec, writingBrief } = request;
61
- if (Buffer.byteLength(source, 'utf8') > 1024 * 1024 || Buffer.byteLength(candidate, 'utf8') > 1024 * 1024)
62
+ if (Buffer.byteLength(source, 'utf8') > MAX_JSON_BYTES || Buffer.byteLength(candidate, 'utf8') > MAX_JSON_BYTES)
62
63
  throw new Error('Approved learning text exceeds the byte limit.');
63
64
  inspectLifecycle(ready);
64
65
  inspectLifecycle(approved);
package/dist/mcp-tools.js CHANGED
@@ -12,6 +12,7 @@ import { inspectHiddenText, applyHiddenTextPolicy, parseHiddenTextPolicy } from
12
12
  import { buildProfile } from './voice-dna.js';
13
13
  import { finalOutputCheck, inspectHygiene } from './hygiene.js';
14
14
  import { finalizeLifecycle, inspectLifecycle, prepareLifecycle, recordApprovedLearning, submitSemanticVerdict, validateFinalApproval } from './lifecycle-adapter.js';
15
+ import { MAX_JSON_BYTES } from './internal.js';
15
16
  function profileFromJson(profileJson) {
16
17
  try {
17
18
  return parseProfile(JSON.parse(profileJson));
@@ -103,7 +104,7 @@ export function logicLintForMcp(draft, writingBriefJson) {
103
104
  return lintLogic(draft, writingBriefFromJson(writingBriefJson));
104
105
  }
105
106
  function parsed(json, label) {
106
- if (Buffer.byteLength(json, 'utf8') > 1024 * 1024)
107
+ if (Buffer.byteLength(json, 'utf8') > MAX_JSON_BYTES)
107
108
  throw new Error(`${label} exceeds the byte limit.`);
108
109
  try {
109
110
  return JSON.parse(json);
package/dist/pipeline.js CHANGED
@@ -1,4 +1,3 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { canonicalJson } from './canonical-json.js';
3
2
  import { HYV_VERSION } from './version.js';
4
3
  import { analyzeAiEditor } from './ai-editor.js';
@@ -6,10 +5,11 @@ import { verifyClaims } from './copy-spec.js';
6
5
  import { analyzeEditorial } from './editorial-packs.js';
7
6
  import { finalOutputCheck, inspectHygiene } from './hygiene.js';
8
7
  import { analyzeVoiceDna } from './voice-dna.js';
9
- import { legacySetPreservation } from './preservation.js';
8
+ import { legacySetPreservation, LEGACY_SET_PRESERVATION_VERSION } from './preservation.js';
10
9
  import { lintFacts } from './fact-linter.js';
11
10
  import { lintLogic } from './logic-linter.js';
12
11
  import { sentences } from './text.js';
12
+ import { digestCanonical, escaped as escapeRegex, profileIdentity, sha256 as digest } from './internal.js';
13
13
  export function analyze(text, profile, brief) {
14
14
  const voiceDna = analyzeVoiceDna(text, profile);
15
15
  const aiEditor = analyzeAiEditor(text, profile);
@@ -101,7 +101,7 @@ function verifyRequiredFacts(candidate, brief) {
101
101
  const reversed = brief.requiredFacts.filter((fact) => {
102
102
  const terms = fact.atoms?.length ? fact.atoms : [fact.text];
103
103
  return terms.some((term) => {
104
- const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
104
+ const escaped = escapeRegex(term);
105
105
  const quotedDenial = new RegExp(`${escaped}(?:["']|\\s)*(?:is|was|are|were)?\\s*(?:not|false|untrue|incorrect)`, 'i');
106
106
  return quotedDenial.test(candidate) || draftSentences.some((sentence, index) => sentence.text.toLowerCase().includes(term.toLowerCase()) && (/\b(?:not|false|untrue|incorrect)\b/i.test(sentence.text) || /^(?:that|this) (?:statement|claim|fact|assertion|point) (?:is|was) (?:not|false|untrue|incorrect)\b/i.test(draftSentences[index + 1]?.text.trim() ?? '')));
107
107
  });
@@ -153,19 +153,11 @@ export function verifyRebuildWithCopySpec(original, candidate, profile, spec, br
153
153
  passed: checked.passed && !regressions.some(isBlockingFinding) && claims.passed && finalCheck.accepted && logicLint.passed && !factLint?.findings.some((finding) => finding.severity === 'error') && (requiredFacts?.passed ?? true),
154
154
  };
155
155
  }
156
- function digest(value) { return createHash('sha256').update(value).digest('hex'); }
157
- function digestCanonical(value) { return digest(canonicalJson(value)); }
158
- function profileIdentity(profile) {
159
- if (profile.version === '3')
160
- return { profileId: profile.id, profileRevisionDigest: profile.revisionDigest };
161
- const legacy = `legacy-v2:${digestCanonical(profile)}`;
162
- return { profileId: legacy, profileRevisionDigest: legacy };
163
- }
164
156
  function projectDeterministicVerificationArtifact(source, candidate, profile, verification, copySpec, writingBrief, verificationKind = 'claims' in verification ? 'copy_spec' : 'standard') {
165
157
  const identity = profileIdentity(profile);
166
158
  const base = {
167
159
  version: '1', verificationKind, passed: verification.passed, analysisVersion: verification.candidate.version,
168
- rulesetVersion: HYV_VERSION, preservationMetricVersion: 'legacy-set-v1', preservationScore: verification.preservationScore,
160
+ rulesetVersion: HYV_VERSION, preservationMetricVersion: LEGACY_SET_PRESERVATION_VERSION, preservationScore: verification.preservationScore,
169
161
  sourceHash: digest(source), candidateHash: digest(candidate), ...identity,
170
162
  ...(copySpec ? { copySpecHash: digestCanonical(copySpec) } : {}), ...(writingBrief ? { writingBriefHash: digestCanonical(writingBrief) } : {}),
171
163
  regressionKeys: verification.regressions.map((finding) => `${finding.engine}:${finding.id}:${finding.sentence}`).sort(),
package/dist/profile.js CHANGED
@@ -1,12 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { canonicalJson } from './canonical-json.js';
3
+ import { isPlainObject } from './internal.js';
3
4
  const METRICS_KEYS = ['sentenceLength', 'sentenceVariation', 'sentenceStructure', 'rhythm', 'paragraphLength', 'openingMoves', 'vocabulary', 'lexicalDensity', 'pointOfView', 'punctuation', 'caseStyle', 'questionRate', 'transitions'];
4
5
  const PROFILE_V3_KEYS = ['version', 'id', 'revision', 'revisionDigest', 'sampleCount', 'metrics', 'avoid', 'provenance', 'rulePolicy', 'fingerprint', 'tolerances', 'metricFixtures'];
5
6
  const FINGERPRINT_METRICS = ['contractionRate', 'sentenceLengthDistribution', 'bulletRate', 'enDashRate'];
6
7
  const STABLE_ID = /^[a-z0-9](?:[a-z0-9._-]{0,127})$/;
7
- function isPlainObject(value) {
8
- return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
9
- }
10
8
  function hasKnownKeys(value, keys) {
11
9
  return Object.keys(value).every((key) => keys.includes(key)) && keys.every((key) => key in value);
12
10
  }
@@ -1,4 +1,3 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { canonicalJson } from './canonical-json.js';
3
2
  import { parseCopySpec } from './copy-spec.js';
4
3
  import { parseWritingBrief } from './editorial-packs.js';
@@ -10,29 +9,15 @@ import { sentences } from './text.js';
10
9
  import { HYV_VERSION } from './version.js';
11
10
  import { buildRecompositionBrief, measureLexicalResidual, parseRecompositionPolicy } from './recomposition.js';
12
11
  import { provenanceStatusForRebuild, writerRequestForRebuild } from './provenance-status.js';
12
+ import { fingerprint, profileIdentity, sha256 as digest } from './internal.js';
13
13
  const MAX_RESPONSE_BYTES = 100_000;
14
14
  const MAX_CANDIDATE_CHARACTERS = 100_000;
15
- function fingerprint(value) {
16
- return createHash('sha256').update(typeof value === 'string' ? value : canonicalJson(value)).digest('hex');
17
- }
18
- function digest(value) {
19
- return createHash('sha256').update(value).digest('hex');
20
- }
21
- function digestCanonical(value) {
22
- return digest(canonicalJson(value));
23
- }
24
15
  function failure(code, message, path) {
25
16
  return { code, message, ...(path ? { path } : {}) };
26
17
  }
27
18
  function isFailure(value) {
28
19
  return typeof value === 'object' && value !== null && 'code' in value && 'message' in value;
29
20
  }
30
- function profileIdentity(profile) {
31
- if (profile.version === '3')
32
- return { profileId: profile.id, profileRevisionDigest: profile.revisionDigest };
33
- const legacy = `legacy-v2:${digestCanonical(profile)}`;
34
- return { profileId: legacy, profileRevisionDigest: legacy };
35
- }
36
21
  function parseJson(value) {
37
22
  if (Buffer.byteLength(value) > MAX_RESPONSE_BYTES)
38
23
  return failure('response_too_large', `Response exceeds ${MAX_RESPONSE_BYTES} bytes.`);
@@ -174,7 +174,7 @@ test('CLI and MCP rebuild helpers share fingerprints', () => {
174
174
  version: '1', audience: 'operators', intent: 'explain', format: 'outreach',
175
175
  });
176
176
  assert.match(briefTask.prompt, /# WritingBrief/);
177
- assert.equal(HYV_VERSION, '3.3.4');
177
+ assert.equal(HYV_VERSION, '3.3.5');
178
178
  });
179
179
  test('apply rejects forged tasks, missing capability, and substituted profiles', () => {
180
180
  const reduction = rebuildRecommendation();
@@ -1,15 +1,11 @@
1
- import { createHash } from 'node:crypto';
2
- import { canonicalJson } from './canonical-json.js';
3
1
  import { parseWritingBrief } from './editorial-packs.js';
4
2
  import { finalOutputCheck, hygieneSourceFindings } from './hygiene.js';
5
3
  import { analyze, deriveEditScope, renderRewritePrompt, verifyDeterministically } from './pipeline.js';
6
4
  import { sentences } from './text.js';
5
+ import { fingerprint } from './internal.js';
7
6
  const MAX_RESPONSE_BYTES = 100_000;
8
7
  const MAX_REPLACEMENTS = 100;
9
8
  const MAX_REPLACEMENT_CHARACTERS = 10_000;
10
- function fingerprint(value) {
11
- return createHash('sha256').update(typeof value === 'string' ? value : canonicalJson(value)).digest('hex');
12
- }
13
9
  function failure(code, message, path) {
14
10
  return { code, message, ...(path ? { path } : {}) };
15
11
  }
@@ -1,13 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { canonicalJson } from './canonical-json.js';
3
3
  import { verifyApprovalCapability } from './approval-capability.js';
4
+ import { exactKeys as exact, isPlainObject as plain } from './internal.js';
4
5
  const violations = new Set(['action_change', 'dropped_object', 'unsupported_claim', 'constraint_weakened', 'clarity_regression']);
5
6
  const HEX = /^[a-f0-9]{64}$/;
6
7
  const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
7
8
  function hash(domain, value) { return createHash('sha256').update(`${domain}\0`).update(canonicalJson(value)).digest('hex'); }
8
9
  function same(left, right) { return canonicalJson(left) === canonicalJson(right); }
9
- function exact(value, required, optional = []) { return required.every((key) => key in value) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key)); }
10
- function plain(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype; }
11
10
  function bindingValid(binding) {
12
11
  return binding.schemaVersion === '1' && [binding.rewriteTaskFingerprint, binding.rewriteResponseFingerprint, binding.deterministicArtifactFingerprint, binding.sourceHash, binding.candidateHash].every((item) => HEX.test(item))
13
12
  && ID.test(binding.profileId) && ID.test(binding.profileRevisionDigest) && ID.test(binding.rulesetVersion);
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const HYV_VERSION = '3.3.4';
1
+ export const HYV_VERSION = '3.3.5';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holdyourvoice/hyv",
3
- "version": "3.3.4",
3
+ "version": "3.3.5",
4
4
  "description": "A local-first dual-engine writing gate that protects voice and catches generic AI patterns.",
5
5
  "type": "module",
6
6
  "bin": { "hyv": "dist/cli.js" },