@holdyourvoice/hyv 3.6.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/Readme.md +93 -154
  2. package/dist/ai-editor-rules.js +1 -0
  3. package/dist/ai-editor.js +23 -33
  4. package/dist/analysis.js +33 -0
  5. package/dist/cli/agents.js +88 -0
  6. package/dist/cli/checks.js +226 -0
  7. package/dist/cli/io.js +141 -0
  8. package/dist/cli/lifecycle.js +162 -0
  9. package/dist/cli/profiles.js +246 -0
  10. package/dist/cli/rewriting.js +111 -0
  11. package/dist/cli.js +5 -974
  12. package/dist/copy-spec.js +6 -6
  13. package/dist/editorial-packs.js +4 -3
  14. package/dist/fact-linter.js +45 -27
  15. package/dist/hidden-text.js +6 -6
  16. package/dist/hold-your-voice.mcpb +0 -0
  17. package/dist/internal.js +8 -0
  18. package/dist/judgment-task.js +10 -14
  19. package/dist/learning.js +77 -71
  20. package/dist/lifecycle-adapter.js +2 -5
  21. package/dist/local-eval.js +16 -31
  22. package/dist/mcp-server.js +272 -0
  23. package/dist/mcp-tools.js +10 -19
  24. package/dist/mcp.js +2 -267
  25. package/dist/pipeline.js +18 -105
  26. package/dist/profile-compose.js +18 -11
  27. package/dist/profile-score.js +6 -6
  28. package/dist/profile.js +17 -19
  29. package/dist/provenance-status.js +1 -3
  30. package/dist/rebuild-task.js +18 -52
  31. package/dist/recomposition.js +18 -13
  32. package/dist/rewrite-prompt.js +82 -0
  33. package/dist/rewrite-response.js +27 -0
  34. package/dist/rewrite-task.js +73 -98
  35. package/dist/rule-allowances.js +6 -10
  36. package/dist/semantic-review.js +71 -47
  37. package/dist/stage1-evaluation.js +29 -14
  38. package/dist/strict-quality.js +10 -18
  39. package/dist/version.js +1 -1
  40. package/dist/voice-dna.js +34 -21
  41. package/dist/writing-examples.js +2 -7
  42. package/package.json +6 -3
  43. package/skills/hyv-prepare-judgment/SKILL.md +4 -0
  44. package/dist/agents/catalog.test.js +0 -60
  45. package/dist/agents/emit.test.js +0 -64
  46. package/dist/agents/load.test.js +0 -149
  47. package/dist/ai-editor.test.js +0 -265
  48. package/dist/approval-capability.test.js +0 -52
  49. package/dist/approval-context.test.js +0 -38
  50. package/dist/backtest.test.js +0 -20
  51. package/dist/benchmark.test.js +0 -328
  52. package/dist/canonical-json.test.js +0 -24
  53. package/dist/cli.test.js +0 -731
  54. package/dist/editorial-packs.test.js +0 -94
  55. package/dist/fact-linter.test.js +0 -85
  56. package/dist/hidden-text.test.js +0 -26
  57. package/dist/hygiene.test.js +0 -83
  58. package/dist/judgment-task.test.js +0 -162
  59. package/dist/learning.test.js +0 -325
  60. package/dist/lifecycle-adapter.test.js +0 -56
  61. package/dist/local-eval.test.js +0 -20
  62. package/dist/logic-linter-corpus.test.js +0 -22
  63. package/dist/logic-linter.test.js +0 -39
  64. package/dist/mcp-tools.test.js +0 -286
  65. package/dist/mcp.test.js +0 -312
  66. package/dist/mirror-refs.test.js +0 -63
  67. package/dist/pipeline.test.js +0 -247
  68. package/dist/preservation.test.js +0 -22
  69. package/dist/production-gates.test.js +0 -34
  70. package/dist/profile-compose.test.js +0 -32
  71. package/dist/profile-score.test.js +0 -22
  72. package/dist/profile-watch.test.js +0 -23
  73. package/dist/profile.test.js +0 -141
  74. package/dist/provenance-status.test.js +0 -22
  75. package/dist/rebuild-task.test.js +0 -206
  76. package/dist/recomposition.test.js +0 -34
  77. package/dist/release-audit.test.js +0 -292
  78. package/dist/rewrite-task.test.js +0 -166
  79. package/dist/rule-allowances.test.js +0 -17
  80. package/dist/rule-reconciliation.test.js +0 -50
  81. package/dist/sample-ingest.test.js +0 -52
  82. package/dist/semantic-review.test.js +0 -101
  83. package/dist/stage1-dry-run.test.js +0 -39
  84. package/dist/stage1-evaluation.test.js +0 -184
  85. package/dist/stage1-human-packet.test.js +0 -102
  86. package/dist/stage1-schema-contract.test.js +0 -95
  87. package/dist/stage2-human-packet.test.js +0 -81
  88. package/dist/strict-quality.test.js +0 -62
  89. package/dist/text-provenance.feature.test.js +0 -45
  90. package/dist/text.test.js +0 -16
  91. package/dist/voice-dna.test.js +0 -121
  92. package/dist/writing-examples.test.js +0 -35
package/Readme.md CHANGED
@@ -1,200 +1,139 @@
1
- # Hold Your Voice
1
+ # hold your voice
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 (`hyv`) is a local writing checker. It helps you edit AI-assisted writing without losing your own writing patterns. It is not an AI-authorship detector: it provides local, inspectable writing evidence while a human still decides what to publish.
6
-
7
- It runs two independent checks:
8
-
9
- - VoiceDNA compares a draft with a profile built from your writing samples.
10
- - AI Editor finds deterministic patterns that often make writing generic or formulaic.
11
-
12
- The package also checks hidden Unicode, source-backed facts, document logic, and protected claims. All checks run locally, without model calls, automatic draft changes, or runtime network requests.
13
-
14
- ## how it works
15
-
16
- ```mermaid
17
- flowchart TD
18
- samples["Writing samples"] --> profile["Local VoiceDNA profile"]
19
- draft["Draft"] --> analyze["hyv analyze"]
20
- profile --> analyze
21
- brief["Optional WritingBrief"] -.-> analyze
22
-
23
- subgraph inspect["1 · inspect the draft"]
24
- analyze --> voice["VoiceDNA check"]
25
- analyze --> patterns["AI pattern lint"]
26
- analyze --> hidden["Hidden-text / Unicode check"]
27
- voice --> local{"Local result"}
28
- patterns --> local
29
- hidden -.-> local
30
- end
31
-
32
- local -->|No blocking change| candidate["Candidate text"]
33
- local -->|Blocking edit scope| editTask["Prepare fingerprint-bound edit task"]
34
- local -->|Judgment required| judgment["Prepare and reduce judgments"]
35
- judgment --> route{"SHIP, EDIT, or REBUILD?"}
36
- route -->|SHIP| candidate
37
- route -->|EDIT| editTask
38
- route -->|REBUILD| authorization["REBUILD recommendation + CopySpec + signed authorization"]
39
- authorization --> rebuildTask["Prepare fingerprint-bound rebuild task"]
40
- editTask --> editor["Human editor or model you choose"]
41
- rebuildTask --> editor
42
- editor --> response["Bound response"]
43
-
44
- brief -.-> logic
45
- sources["Optional fact sources in WritingBrief"] -.-> facts
46
- spec["Optional for verify-spec; required for rebuild"] -.-> authorization
47
- spec -.-> standard
48
-
49
- subgraph verification["2 · verification gate"]
50
- candidate --> standard["hyv verify / verify-spec"]
51
- response --> mode{"Bound task mode"}
52
- mode -->|EDIT| editApply["apply-rewrite + standard verification"]
53
- mode -->|REBUILD| rebuildApply["apply-rebuild + rebuild verification"]
54
- standard --> standardRules["Preservation gate + CopySpec claims when supplied"]
55
- editApply --> standardRules
56
- rebuildApply --> rebuildRules["CopySpec claims; preservation reported"]
57
- standardRules --> engines["VoiceDNA + AI Editor checks and blocking regressions"]
58
- rebuildRules --> engines
59
- engines --> logic["Logic lint"]
60
- logic --> facts["Fact lint when sources are supplied"]
61
- facts --> outputGate["Hidden-text + final-output gate"]
62
- outputGate --> passed{"All required checks pass?"}
63
- end
64
-
65
- passed -->|No| repair["Repair externally or prepare a new task"]
66
- repair --> analyze
67
- passed -->|Yes| review["Semantic review and human approval, when required"]
68
- review --> final["Run final-check after the last change"]
69
- final --> output["Exact accepted text"]
70
- ```
5
+ hold your voice (`hyv`) checks writing locally. it compares a draft with your writing samples, flags generic writing patterns, and checks an edited candidate before you use it. you supply the writing and the edits; hyv supplies inspectable findings and verification.
71
6
 
72
- HYV keeps draft inspection, candidate verification, and final delivery separate. Standard verification reruns VoiceDNA and AI Editor, rejects blocking regressions, enforces preservation, runs logic lint, applies fact lint when a WritingBrief supplies sources, and withholds hidden-text failures. `verify-spec` adds CopySpec claim checks. Authorized rebuilds require an upstream REBUILD recommendation, a CopySpec, and signed authorization; their verification reports preservation without using the standard preservation threshold. Run `final-check` again after the last human, model, formatter, or template change. HYV never calls a model; a human editor or model you choose supplies edits and judgments.
7
+ it works through a command-line tool, an mcp server, and portable agent contracts. it needs node.js 20 or newer. it makes no model calls and needs no account or api key.
73
8
 
74
- ## install
9
+ ## what it does
75
10
 
76
- You need Node.js 20 or newer and at least two writing samples you have the right to use.
11
+ | check or workflow | result |
12
+ | --- | --- |
13
+ | voice dna | compares sentence structure, rhythm, vocabulary, punctuation, and other measurements with a local profile. |
14
+ | ai editor | flags deterministic wording and structural patterns, with sentence locations and repair guidance. |
15
+ | profiles | builds v2 or v3 profiles, assesses sample readiness, composes profiles, watches local sample files, and scores against held-out samples. |
16
+ | fact and logic checks | compares claims with supplied source files and checks document structure, required facts, and claim polarity. |
17
+ | hidden-text checks | inspects unicode controls, applies an explicit removal policy, and gates the exact final text. |
18
+ | delivery checks | optionally checks placeholders, likely secrets, local links, and supplied citation ids offline. |
19
+ | editing and rebuilding | prepares fingerprint-bound tasks, accepts a supplied response, preserves locked text or required claims, and verifies the candidate. |
20
+ | review and approval | binds semantic judgments and signed approval capabilities to the exact candidate. |
21
+ | local learning | records explicit instructions or verified repairs and applies profile revision and authority rules. |
22
+ | evaluation | runs local backtests, synthetic comparisons, and evidence-bound benchmark workflows. |
23
+
24
+ voice fit, generic-pattern findings, factual evidence, and approval stay separate. a passing check does not prove authorship, truth, or publication quality.
25
+
26
+ ## install
77
27
 
78
28
  ```bash
79
29
  npm install --global @holdyourvoice/hyv
80
30
  ```
81
31
 
82
- For a one-off command, replace `hyv` with `npx @holdyourvoice/hyv`.
83
-
84
- For Humanizer's complementary editor skill, install it separately with `npx skills add blader/humanizer`. It is not a HYV dependency: HYV's local AI Editor keeps its own versioned rules and final-output gate.
32
+ for a one-off command, replace `hyv` with `npx @holdyourvoice/hyv`.
85
33
 
86
- ## basic workflow
34
+ ## check and edit a draft
87
35
 
88
- 1. Build a local profile from your samples.
36
+ build a profile from at least two writing samples you have the right to use:
89
37
 
90
38
  ```bash
91
39
  hyv profile profile.json samples/one.md samples/two.md
40
+ hyv analyze draft.md profile.json
92
41
  ```
93
42
 
94
- Add `--avoid=phrase` for wording that must block a draft. Repeat the option for more phrases.
43
+ `analyze` returns separate voice dna, ai editor, and hygiene reports. hygiene is informational here; use `final-check` to gate delivery.
95
44
 
96
- 2. Check a draft against that profile.
45
+ create an editing brief, give it to your editor or a model you choose, then check the candidate:
97
46
 
98
47
  ```bash
99
- hyv analyze draft.md profile.json
48
+ hyv rewrite-prompt draft.md profile.json > rewrite-brief.md
49
+ hyv verify draft.md candidate.md profile.json
50
+ hyv final-check candidate.md
100
51
  ```
101
52
 
102
- The result contains separate VoiceDNA and AI Editor reports. The top-level `passed` value is true only when every required check passes.
53
+ before verification, the editing brief asks for a word economy review: **every word must earn its place.** cut filler, repeated ideas, and needless setup when removing them loses no meaning, evidence, clarity, or voice. preserve necessary uncertainty, attribution, emphasis, and rhythm. this is an editor or model judgment, not an automatic score or a word-count target. edits must stay within the authorized scope.
54
+
55
+ rewrite and verification commands are strict by default: every active ai editor finding must be repaired. a v3 profile can deliberately disable a rule. verification also checks preservation, logic, required facts when supplied, and final-output hygiene. it does not record learning.
103
56
 
104
- 3. Create an editing brief, edit the draft, and verify the candidate.
57
+ `final-check` writes accepted text to stdout. it removes a leading byte-order mark; unresolved hidden characters withhold output and return exit code `2`. run it after the last edit or formatting change. stdin works too:
105
58
 
106
59
  ```bash
107
- hyv rewrite-prompt draft.md profile.json > rewrite-brief.md
108
- hyv verify draft.md candidate.md profile.json
60
+ producer | hyv final-check -
109
61
  ```
110
62
 
111
- Send the brief to a human editor or a model you choose. Delivery stays under your control.
63
+ ## commands
112
64
 
113
- 4. Check the exact text before delivery.
65
+ most commands return json. exit code `0` means completion, `2` means a content or policy gate failed, and `1` means invalid input or a command error. run a command without enough arguments for its usage, or read the [full cli reference](docs/wiki/CLI-Reference.md).
66
+
67
+ | command | purpose |
68
+ | --- | --- |
69
+ | `profile <output> <sample...>` | build a v2 profile; repeat `--avoid=phrase` for blocked phrases. |
70
+ | `profile v3 <output> --id=writer.channel --channel=email <sample...>` | build a revisioned, digest-bound v3 profile. |
71
+ | `profile assess <sample...>` | check sample readiness. |
72
+ | `profile compose --ratio 70:30 <profile...>` | combine v3 measurements and conservative rule policies. |
73
+ | `profile watch ...` | rebuild a local profile as its sample files change. |
74
+ | `team-profile validate\|compose ...` | work with consent-bound team metadata. |
75
+ | `ingest ...` | import owner-authorized gmail or telegram exports into redacted local samples. |
76
+ | `analyze <draft> <profile>` | run the writing checks. |
77
+ | `score <draft> <profile-v3> <heldout...>` | compare against a separate local writing range; abstain when evidence is inadequate. |
78
+ | `strict-check <draft> <profile-v3> <sample...>` | return `strict-ready`, `needs-human-review`, or `blocked` using calibrated voice evidence. |
79
+ | `dispositions <draft> <profile>` | normalize findings into `block`, `review`, and `signal`. |
80
+ | `patterns` | print the active rule catalog. |
81
+ | `batch-analyze <draft...>` | find repeated openings and endings. |
82
+ | `fact-lint <draft\|-> --source=id:path` | check claims against supplied local sources. |
83
+ | `logic-lint <draft\|-> [brief.json]` | check deterministic document logic. |
84
+ | `hygiene <draft> [--fix]` | inspect hidden characters or write a conservative cleaned copy. |
85
+ | `inspect-hidden-text <draft> [policy.json]` | inspect with an optional hidden-text policy. |
86
+ | `apply-hidden-text-policy <draft> <policy.json> <output>` | apply explicitly permitted removals. |
87
+ | `final-check <path\|->` | gate the exact output text. |
88
+ | `delivery-check <path\|-> [policy.json]` | run the separate optional delivery check. |
89
+ | `rewrite-prompt <draft> <profile>` | prepare a constrained editing brief. |
90
+ | `prepare-rewrite ...` / `apply-rewrite ...` | prepare and verify sentence replacements or range edits. |
91
+ | `prepare-judgment ...` / `reduce-judgment ...` | collect and reduce pre-edit or post-candidate judgments. |
92
+ | `prepare-rebuild ...` / `rebuild-writer-request ...` / `apply-rebuild ...` | run a whole-document rebuild with required claims and signed authorization. |
93
+ | `verify <original> <candidate> <profile>` | verify an edited candidate. |
94
+ | `verify-spec ...` | also enforce a `CopySpec` claim contract. |
95
+ | `lifecycle ...` | prepare semantic review, submit verdicts, inspect state, and finalize approval or rejection. |
96
+ | `learning ...` | inspect, add, ratify, supersede, migrate, or clear local learning. |
97
+ | `backtest ...` / `evaluate-local ...` | evaluate supplied candidates locally without generating writing. |
98
+ | `agent list\|validate\|describe\|emit` | inspect or emit portable agent contracts. |
99
+ | `mcp` | start the local server over standard input/output. |
100
+
101
+ `strict-check` requires a v3 profile built from at least five samples, plus five non-duplicate validation samples with a consistent visible format and at least 1,500 words in total.
102
+
103
+ whole-document rebuilding requires an upstream `REBUILD` recommendation, a `CopySpec`, and a signed `hyv.rebuild-authorization` capability. it checks required claims instead of enforcing the ordinary lexical preservation threshold. semantic review and final approval remain separate steps. see the [workflow and architecture](docs/ARCHITECTURE.md) and [recomposition contract](docs/RECOMPOSITION.md).
104
+
105
+ ## use with an agent
106
+
107
+ start the mcp server with `hyv mcp`. compatible hosts can call the same local checks through tools. the `skills/hyv-*` packages describe inputs, outputs, evidence, permissions, and stop conditions for agent workflows.
114
108
 
115
109
  ```bash
116
- hyv final-check candidate.md
117
- producer | hyv final-check -
110
+ hyv agent list
111
+ hyv agent validate
118
112
  ```
119
113
 
120
- `final-check` writes accepted text to stdout. It withholds output and exits with code `2` when unresolved hidden Unicode remains.
114
+ setup guides: [codex](docs/CODEX.md), [claude desktop](docs/CLAUDE-DESKTOP.md), [claude code](docs/CLAUDE-CODE.md), and [portable agents](docs/wiki/Portable-Agents.md).
121
115
 
122
- `delivery-check` is a separate opt-in offline check for placeholders, likely credential patterns, local Markdown links, and citation IDs in a local policy. It never fetches a URL or proves a fact.
116
+ ## local data
123
117
 
124
- ## commands
118
+ hyv has no runtime network requests, telemetry, hosted analysis, or profile sync. samples, drafts, profiles, candidates, and source files stay local unless you choose to send them elsewhere.
125
119
 
126
- | Command | Purpose |
127
- | --- | --- |
128
- | `hyv profile <profile.json> <sample...>` | Build a local profile from two or more samples. |
129
- | `hyv profile v3 <profile.json> --id=writer.channel --channel=email <sample...>` | Build a signed channel-specific Profile v3; optional tone is advisory metadata. |
130
- | `hyv profile compose --ratio 70:30 <profile...>` | Locally compose two or more Profile v3 metrics without creating writing. |
131
- | `hyv score <draft> <profile-v3.json> <heldout...>` | Measure the draft against a separate held-out local writing range; may abstain. |
132
- | `hyv ingest <source> <export> --owner=owner --output=/absolute/dir` | Create redacted, owner-authorized Gmail or Telegram samples and a text-free receipt. |
133
- | `hyv analyze <draft> <profile.json>` | Run VoiceDNA, AI Editor, and hygiene checks. |
134
- | `hyv strict-check <draft> <profile-v3.json> <sample...>` | Run the calibrated local strict-quality gate. It requires a V3 profile built from five or more samples, five non-duplicate validation samples with a consistent visible format and 1,500 words total, and returns `strict-ready`, `needs-human-review`, or `blocked`. |
135
- | `hyv hygiene <draft> [--fix]` | Inspect hidden Unicode or write a conservative cleaned copy. |
136
- | `hyv inspect-hidden-text <draft> [policy.json]` | Inspect hidden text with an optional policy. |
137
- | `hyv apply-hidden-text-policy <draft> <policy.json> <output>` | Apply approved hidden-text removals. |
138
- | `hyv final-check <path\|->` | Gate the exact text before delivery. |
139
- | `hyv delivery-check <path\|-> [policy.json]` | Run optional local delivery-integrity checks. |
140
- | `hyv profile assess <sample...>` | Inspect sample readiness before building a profile. |
141
- | `hyv team-profile validate\|compose ...` | Validate or locally compose consent-bound team profile metadata. |
142
- | `hyv dispositions <draft> <profile>` | Return normalized `block`, `review`, and `signal` findings. |
143
- | `hyv fact-lint <draft\|-> --source=id:path` | Check claims against local source files. |
144
- | `hyv logic-lint <draft\|-> [brief.json]` | Check deterministic document logic. |
145
- | `hyv batch-analyze <draft...>` | Find repeated openings and endings across drafts. |
146
- | `hyv rewrite-prompt <draft> <profile.json>` | Create a strict constrained editing brief. |
147
- | `hyv prepare-rewrite ...` | Create a fingerprint-bound edit task. |
148
- | `hyv apply-rewrite ...` | Apply and verify a response to an edit task. |
149
- | `hyv prepare-judgment ...` | Create a pre-edit or post-candidate judgment task. |
150
- | `hyv reduce-judgment <envelope...>` | Reduce judgments to SHIP, EDIT, REBUILD, CLEAR, or ESCALATE. |
151
- | `hyv prepare-rebuild ...` | Create an authorized whole-document rebuild task. |
152
- | `hyv rebuild-writer-request ...` | Create the writer-only part of a rebuild task. |
153
- | `hyv apply-rebuild ...` | Apply and verify an authorized rebuild response. |
154
- | `hyv verify <original> <candidate> <profile.json>` | Verify a candidate without changing learning state. |
155
- | `hyv verify-spec ...` | Verify a candidate and a CopySpec. |
156
- | `hyv lifecycle ...` | Run semantic review and final approval steps. |
157
- | `hyv learning ...` | Inspect or change local profile learning. |
158
- | `hyv patterns` | Print the active AI Editor rule catalog. |
159
- | `hyv agent list\|validate\|describe\|emit` | Inspect or emit portable agent contracts. |
160
- | `hyv mcp` | Start the local MCP server on standard input/output. |
161
-
162
- `rewrite-prompt`, `prepare-rewrite`, `apply-rewrite`, and `verify` are strict by default: every active AI Editor finding must be repaired, and verification rejects a candidate that leaves one unresolved. A Profile v3 `disabled` policy is the only deliberate exception. `strict-check` adds the separate calibrated voice-evidence decision.
163
-
164
- Most commands return JSON. Exit code `0` means the command completed, `2` means a content or policy gate failed, and `1` means the command or input was invalid.
165
-
166
- Run `hyv <command>` without enough arguments to see its exact usage. Read the [CLI reference](docs/wiki/CLI-Reference.md) for every option.
167
-
168
- ## portable agents and MCP
169
-
170
- The `skills/hyv-*` directories package the CLI workflows as portable agent contracts. Use `hyv agent validate` to check them and `hyv agent emit` to create a host-specific prompt or JSON contract.
171
-
172
- The MCP server exposes the same local engine for compatible hosts. Read [Portable Agents](docs/wiki/Portable-Agents.md), [Claude Desktop setup](docs/CLAUDE-DESKTOP.md), or [Claude Code setup](docs/CLAUDE-CODE.md).
173
-
174
- ## privacy and safety
175
-
176
- Drafts, samples, profiles, candidates, and source files stay on your machine. The package has no accounts, telemetry, hosted analysis, or runtime network requests.
177
-
178
- Learning commands can write text-free events under `~/.hyv/learning/`. A manually added learning instruction is stored as entered. Keep private writing, profiles, and client data out of public repositories.
179
-
180
- VoiceDNA fit, AI-pattern findings, fact consistency, and human approval are separate results. A clean report only states that its configured checks passed. Authorship, factual truth, and publication quality still need separate evidence or review.
181
-
182
- ## performance
183
-
184
- The frozen synthetic runtime benchmark covers natural text, punctuation-heavy text, final checking, cold CLI startup, and fact linting. On its 100,000-character dotted v3 fixture, seven-run median process CPU time fell from 8,823.134 ms to 17.245 ms. That is 99.8045% lower for this stress case, not a whole-application speedup. Read the [runtime benchmark contract and full results](https://github.com/shashank-sn/holdyourvoice/blob/main/benchmarks/runtime/README.md).
185
-
186
- ## development
120
+ learning lives under `~/.hyv/learning/` by default. verified-repair events store findings and hashes rather than draft text. explicit learning instructions store the instruction itself. approval trust is loaded separately from a permission-checked local context. private samples and approval keys do not belong in a public repository.
121
+
122
+ ## version 4 and development
123
+
124
+ version 4 rebuilds the implementation boundaries while retaining the existing cli commands, mcp contracts, profile formats, deep-import paths, rules, and agent packages. existing profiles need no migration. verification artifacts include the package version: prepare fresh version-bound tasks and approvals after upgrading.
125
+
126
+ start with [architecture](docs/ARCHITECTURE.md) to find the owner of a behavior. the compatibility check compares the rebuilt implementation with the frozen v3 source, including serialized outputs and errors.
187
127
 
188
128
  ```bash
189
129
  npm ci
190
130
  npm test
191
131
  npm run check:release
132
+ npm run pack:claude
192
133
  ```
193
134
 
194
- Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. The main design boundaries are in [Architecture](docs/ARCHITECTURE.md) and the full user guides are in the [wiki](https://github.com/shashank-sn/holdyourvoice/wiki).
195
-
196
- See the [roadmap](docs/ROADMAP.md), [rule authoring guide](docs/RULE-AUTHORING.md), and synthetic [benchmark scorecard command](scripts/public-scorecard.mjs). The public fixture scorecard does not measure human preference or model quality.
135
+ read [contributing](CONTRIBUTING.md), [rule authoring](docs/RULE-AUTHORING.md), the [roadmap](docs/ROADMAP.md), and the [wiki](https://github.com/shashank-sn/holdyourvoice/wiki). existing [runtime measurements](benchmarks/runtime/README.md) describe their recorded fixtures and revisions; they are not a new version 4 performance claim. public synthetic scorecards do not measure human preference.
197
136
 
198
137
  ## license
199
138
 
200
- [MIT](LICENSE). Third-party writing and data keep their own rights.
139
+ [mit](LICENSE). third-party writing and data keep their own rights.
@@ -106,6 +106,7 @@ export const rules = [
106
106
  { id: "struct.rhetorical-truth", severity: "yellow", expression: /\b(?:the\s+)?(?:uncomfortable|hard|harsh|brutal|ugly|unsexy|real|honest)\s+(?:truth|reality)\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "state the fact directly, skip the framing" },
107
107
  { id: "struct.truth-is", severity: "yellow", expression: /\bthe\s+truth\s+is\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "just say the truth. the framing is filler." },
108
108
  { id: "struct.lesson-setup", severity: "yellow", expression: /\b(?:here'?s\s+)?what\s+.{3,80}\s+(?:taught|teaches)\s+(?:me|us|you|everyone)\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "just share the lesson" },
109
+ { id: "struct.colon-reveal", severity: "yellow", expression: /^\s*(?!#{1,6}\s)(?:(?:The|the)\s+(?:best\s+part|detail\s+that\s+makes\s+(?:it|this)\s+work|real\s+reason|kicker|twist|secret)|(?:Kicker|kicker|Twist|twist|Secret|secret))\s*:\s*(?=\p{Ll})/u, reason: "A reveal label can manufacture drama before an ordinary point.", suggestion: "State the fact without presenting it as a reveal.", scope: "line" },
109
110
  { id: "struct.negation-cascade", severity: "yellow", expression: /\b(?:no|not)\s+\w[^.!?\n]{0,80}[.!?][ \t]*\n?[ \t]*(?:no|not)\s+\w[^.!?\n]{0,80}[.!?][ \t]*\n?[ \t]*(?:no|not)\s+\w/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "three negations in a row reads like AI performance", scope: "line" },
110
111
  { id: "struct.inline-triplet", severity: "yellow", expression: /\bit'?s\s+\w[^,.!?;\n]{0,30},\s*it'?s\s+\w[^,.!?;\n]{0,30}[,.]?\s*(?:and\s+)?it'?s\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: `the "it's X, it's Y, it's Z" pattern is an AI tell` },
111
112
  { id: "struct.moment-becomes", severity: "red", expression: /\b(?:the\s+)?moment\b.{3,80}\bbecomes?\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: '"the moment X becomes Y" is polished founder cadence \u2014 restructure' },
package/dist/ai-editor.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { rules } from './ai-editor-rules.js';
2
2
  import { sentences } from './text.js';
3
3
  export { rules } from './ai-editor-rules.js';
4
- export const RULESET_VERSION = '3.5.0-local.3';
4
+ export const RULESET_VERSION = '3.5.0-local.4';
5
5
  const sentenceRules = rules.filter((rule) => rule.scope === undefined || rule.scope === 'sentence');
6
6
  const lineRules = rules.filter((rule) => rule.scope === 'line');
7
7
  const documentRules = rules.filter((rule) => rule.scope === 'document');
@@ -104,23 +104,25 @@ export function serializedRules() {
104
104
  scope: rule.scope ?? 'sentence',
105
105
  }));
106
106
  }
107
- function documentFinding(rule, sentence) {
107
+ function ruleFinding(rule, sentence) {
108
108
  return { engine: 'ai_editor', id: rule.id, severity: rule.severity, sentence: sentence.index, excerpt: sentence.text, reason: rule.reason, suggestion: rule.suggestion };
109
109
  }
110
110
  function documentMatches(rule, prose, mapped) {
111
- const at = (index) => mapped[index] ? [documentFinding(rule, mapped[index])] : [];
111
+ const at = (index) => mapped[index] ? [ruleFinding(rule, mapped[index])] : [];
112
112
  if (rule.id === 'ai.repeated-sentence-opening') {
113
- for (let index = 0; index + 2 < mapped.length; index += 1) {
114
- const opening = mapped[index].text.match(/^\s*(\p{L}+)/u)?.[1]?.toLocaleLowerCase();
115
- if (opening && [1, 2].every((offset) => mapped[index + offset].text.match(/^\s*(\p{L}+)/u)?.[1]?.toLocaleLowerCase() === opening))
113
+ const openings = mapped.map((sentence) => sentence.text.match(/^\s*(\p{L}+)/u)?.[1]?.toLocaleLowerCase());
114
+ for (let index = 0; index + 2 < openings.length; index += 1) {
115
+ const opening = openings[index];
116
+ if (opening && openings[index + 1] === opening && openings[index + 2] === opening)
116
117
  return at(index);
117
118
  }
118
119
  return [];
119
120
  }
120
121
  if (rule.id === 'format.bold-density') {
121
- const line = prose.split('\n').findIndex((value) => (value.match(/\*\*[^*\n]+\*\*/gu) ?? []).length >= 2);
122
+ const lines = prose.split('\n');
123
+ const line = lines.findIndex((value) => (value.match(/\*\*[^*\n]+\*\*/gu) ?? []).length >= 2);
122
124
  if (line >= 0)
123
- return at(mapped.findIndex((sentence) => sentence.start >= prose.split('\n').slice(0, line).join('\n').length));
125
+ return at(mapped.findIndex((sentence) => sentence.start >= lines.slice(0, line).join('\n').length));
124
126
  const total = (prose.match(/\*\*[^*\n]+\*\*/gu) ?? []).length;
125
127
  return total >= 4 ? at(0) : [];
126
128
  }
@@ -133,9 +135,11 @@ function documentMatches(rule, prose, mapped) {
133
135
  return [];
134
136
  }
135
137
  if (rule.id === 'ai.clipped-fragment-run') {
136
- for (let index = 0; index + 2 < mapped.length; index += 1)
137
- if ([0, 1, 2].every((offset) => (mapped[index + offset].text.match(/\p{L}+/gu) ?? []).length <= 4))
138
+ const fragments = mapped.map((sentence) => (sentence.text.match(/\p{L}+/gu) ?? []).length <= 4);
139
+ for (let index = 0; index + 2 < fragments.length; index += 1) {
140
+ if (fragments[index] && fragments[index + 1] && fragments[index + 2])
138
141
  return at(index);
142
+ }
139
143
  return [];
140
144
  }
141
145
  if (rule.id === 'ai.jargon-stack') {
@@ -144,12 +148,14 @@ function documentMatches(rule, prose, mapped) {
144
148
  return index >= 0 ? at(index) : [];
145
149
  }
146
150
  if (rule.id === 'ai.sentence-length-cluster') {
147
- for (let index = 0; index + 3 < mapped.length; index += 1)
148
- if ([0, 1, 2, 3].every((offset) => {
149
- const count = (mapped[index + offset].text.match(/\p{L}+/gu) ?? []).length;
150
- return count >= 15 && count <= 20;
151
- }))
151
+ const clustered = mapped.map((sentence) => {
152
+ const count = (sentence.text.match(/\p{L}+/gu) ?? []).length;
153
+ return count >= 15 && count <= 20;
154
+ });
155
+ for (let index = 0; index + 3 < clustered.length; index += 1) {
156
+ if (clustered[index] && clustered[index + 1] && clustered[index + 2] && clustered[index + 3])
152
157
  return at(index);
158
+ }
153
159
  return [];
154
160
  }
155
161
  return [];
@@ -161,15 +167,7 @@ export function analyzeAiEditor(text, profile) {
161
167
  for (const sentence of mapped) {
162
168
  for (const rule of sentenceRules) {
163
169
  if (rule.expression.test(prose.slice(sentence.start, sentence.end))) {
164
- matched.push({
165
- engine: 'ai_editor',
166
- id: rule.id,
167
- severity: rule.severity,
168
- sentence: sentence.index,
169
- excerpt: sentence.text,
170
- reason: rule.reason,
171
- suggestion: rule.suggestion,
172
- });
170
+ matched.push(ruleFinding(rule, sentence));
173
171
  }
174
172
  }
175
173
  }
@@ -184,15 +182,7 @@ export function analyzeAiEditor(text, profile) {
184
182
  ?? mapped.find((candidate) => candidate.start >= lineStart && candidate.start < lineStart + line.length);
185
183
  if (!sentence)
186
184
  continue;
187
- matched.push({
188
- engine: 'ai_editor',
189
- id: rule.id,
190
- severity: rule.severity,
191
- sentence: sentence.index,
192
- excerpt: sentence.text,
193
- reason: rule.reason,
194
- suggestion: rule.suggestion,
195
- });
185
+ matched.push(ruleFinding(rule, sentence));
196
186
  }
197
187
  lineStart += line.length + 1;
198
188
  }
@@ -0,0 +1,33 @@
1
+ import { analyzeAiEditor } from './ai-editor.js';
2
+ import { analyzeEditorial } from './editorial-packs.js';
3
+ import { inspectHygiene } from './hygiene.js';
4
+ import { analyzeVoiceDna } from './voice-dna.js';
5
+ export function analyze(text, profile, brief) {
6
+ const voiceDna = analyzeVoiceDna(text, profile);
7
+ const aiEditor = analyzeAiEditor(text, profile);
8
+ const editorial = brief ? analyzeEditorial(text, brief) : undefined;
9
+ const hygiene = inspectHygiene(text);
10
+ return { version: '2', voiceDna, aiEditor, ...(editorial ? { editorial } : {}), hygiene, passed: voiceDna.passed && aiEditor.passed && (editorial?.passed ?? true) };
11
+ }
12
+ export function isBlockingFinding(finding) {
13
+ return finding.engine === 'ai_editor' ? finding.appliedPolicy === 'blocking' : finding.severity === 'red';
14
+ }
15
+ export function isStrictFinding(finding) {
16
+ return finding.engine === 'ai_editor' || isBlockingFinding(finding);
17
+ }
18
+ export function analysisFindings(result) {
19
+ return [...result.voiceDna.findings, ...result.aiEditor.findings, ...(result.editorial?.findings ?? [])];
20
+ }
21
+ export function strictFindings(result) {
22
+ return analysisFindings(result).filter(isStrictFinding);
23
+ }
24
+ export function deriveEditScope(result, strict = false) {
25
+ const findings = analysisFindings(result);
26
+ const blocking = findings.filter(strict ? isStrictFinding : isBlockingFinding);
27
+ const pendingJudgment = strict ? [] : findings.filter((finding) => finding.appliedPolicy === 'judgment-required');
28
+ return {
29
+ eligibleSentenceIds: [...new Set(blocking.map((finding) => finding.sentence))].sort((left, right) => left - right),
30
+ blocking,
31
+ pendingJudgment,
32
+ };
33
+ }
@@ -0,0 +1,88 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { loadAll, validateAll, validateId, sortedIds, describe, emitJson, emitPrompt } from '../agents/index.js';
3
+ import { json } from './io.js';
4
+ function agentFlags(args) {
5
+ const values = [];
6
+ let host = 'generic';
7
+ let mode;
8
+ let output;
9
+ for (let index = 0; index < args.length; index += 1) {
10
+ const argument = args[index];
11
+ const separator = argument.indexOf('=');
12
+ const name = separator < 0 ? argument : argument.slice(0, separator);
13
+ if (name !== '--host' && name !== '--mode' && name !== '--output') {
14
+ values.push(argument);
15
+ continue;
16
+ }
17
+ const inline = separator >= 0;
18
+ const value = inline ? argument.slice(separator + 1) : args[++index];
19
+ if (name === '--mode') {
20
+ if (value !== 'prompt' && value !== 'json')
21
+ throw new Error('Usage: hyv agent --mode prompt|json requires a mode.');
22
+ mode = value;
23
+ }
24
+ else {
25
+ if (!value || (!inline && value.startsWith('--'))) {
26
+ throw new Error(name === '--host' ? 'Usage: hyv agent --host HOST requires a value.' : 'Usage: hyv agent --output FILE requires a value.');
27
+ }
28
+ if (name === '--host')
29
+ host = value;
30
+ else
31
+ output = value;
32
+ }
33
+ }
34
+ return { values, host, mode, output };
35
+ }
36
+ export function runAgent(args) {
37
+ const [subcommand, ...subargs] = args;
38
+ if (subcommand === 'list') {
39
+ if (subargs.length)
40
+ throw new Error('Usage: hyv agent list');
41
+ const packages = loadAll();
42
+ const ids = sortedIds(packages);
43
+ json(ids.map((id) => {
44
+ const descriptor = packages.get(id).descriptor;
45
+ return { id, role: descriptor.role, workflow_phase: descriptor.workflow_phase, description: descriptor.description };
46
+ }));
47
+ return 0;
48
+ }
49
+ if (subcommand === 'validate') {
50
+ if (subargs.length > 1)
51
+ throw new Error('Usage: hyv agent validate [id]');
52
+ const packages = loadAll();
53
+ const id = subargs[0];
54
+ if (id !== undefined)
55
+ validateId(packages, id);
56
+ validateAll(packages);
57
+ json({ schema_version: '1.0.0', status: 'PASS', agent: id ?? 'all' });
58
+ return 0;
59
+ }
60
+ if (subcommand === 'describe') {
61
+ const { values, host, mode, output } = agentFlags(subargs);
62
+ if (values.length !== 1 || mode !== undefined || output !== undefined)
63
+ throw new Error('Usage: hyv agent describe <id> [--host HOST]');
64
+ const packages = loadAll();
65
+ validateId(packages, values[0]);
66
+ json(describe(packages.get(values[0]), host));
67
+ return 0;
68
+ }
69
+ if (subcommand === 'emit') {
70
+ const { values, host, mode, output } = agentFlags(subargs);
71
+ if (values.length !== 1)
72
+ throw new Error('Usage: hyv agent emit <id> --mode prompt|json [--host HOST] [--output FILE]');
73
+ if (!mode)
74
+ throw new Error('Usage: hyv agent emit <id> --mode prompt|json [--host HOST] [--output FILE]');
75
+ const packages = loadAll();
76
+ validateId(packages, values[0]);
77
+ const pkg = packages.get(values[0]);
78
+ const body = mode === 'prompt' ? emitPrompt(pkg, host) : `${emitJson(pkg, host)}\n`;
79
+ if (output !== undefined) {
80
+ writeFileSync(output, body, { encoding: 'utf8', flag: 'wx' });
81
+ }
82
+ else {
83
+ process.stdout.write(body);
84
+ }
85
+ return 0;
86
+ }
87
+ throw new Error('Usage: hyv agent <list|validate|describe|emit> ...');
88
+ }