@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.
- package/Readme.md +93 -154
- package/dist/ai-editor-rules.js +1 -0
- package/dist/ai-editor.js +23 -33
- package/dist/analysis.js +33 -0
- package/dist/cli/agents.js +88 -0
- package/dist/cli/checks.js +226 -0
- package/dist/cli/io.js +141 -0
- package/dist/cli/lifecycle.js +162 -0
- package/dist/cli/profiles.js +246 -0
- package/dist/cli/rewriting.js +111 -0
- package/dist/cli.js +5 -974
- package/dist/copy-spec.js +6 -6
- package/dist/editorial-packs.js +4 -3
- package/dist/fact-linter.js +45 -27
- package/dist/hidden-text.js +6 -6
- package/dist/hold-your-voice.mcpb +0 -0
- package/dist/internal.js +8 -0
- package/dist/judgment-task.js +10 -14
- package/dist/learning.js +77 -71
- package/dist/lifecycle-adapter.js +2 -5
- package/dist/local-eval.js +16 -31
- package/dist/mcp-server.js +272 -0
- package/dist/mcp-tools.js +10 -19
- package/dist/mcp.js +2 -267
- package/dist/pipeline.js +18 -105
- package/dist/profile-compose.js +18 -11
- package/dist/profile-score.js +6 -6
- package/dist/profile.js +17 -19
- package/dist/provenance-status.js +1 -3
- package/dist/rebuild-task.js +18 -52
- package/dist/recomposition.js +18 -13
- package/dist/rewrite-prompt.js +82 -0
- package/dist/rewrite-response.js +27 -0
- package/dist/rewrite-task.js +73 -98
- package/dist/rule-allowances.js +6 -10
- package/dist/semantic-review.js +71 -47
- package/dist/stage1-evaluation.js +29 -14
- package/dist/strict-quality.js +10 -18
- package/dist/version.js +1 -1
- package/dist/voice-dna.js +34 -21
- package/dist/writing-examples.js +2 -7
- package/package.json +6 -3
- package/skills/hyv-prepare-judgment/SKILL.md +4 -0
- package/dist/agents/catalog.test.js +0 -60
- package/dist/agents/emit.test.js +0 -64
- package/dist/agents/load.test.js +0 -149
- package/dist/ai-editor.test.js +0 -265
- package/dist/approval-capability.test.js +0 -52
- package/dist/approval-context.test.js +0 -38
- package/dist/backtest.test.js +0 -20
- package/dist/benchmark.test.js +0 -328
- package/dist/canonical-json.test.js +0 -24
- package/dist/cli.test.js +0 -731
- package/dist/editorial-packs.test.js +0 -94
- package/dist/fact-linter.test.js +0 -85
- package/dist/hidden-text.test.js +0 -26
- package/dist/hygiene.test.js +0 -83
- package/dist/judgment-task.test.js +0 -162
- package/dist/learning.test.js +0 -325
- package/dist/lifecycle-adapter.test.js +0 -56
- package/dist/local-eval.test.js +0 -20
- package/dist/logic-linter-corpus.test.js +0 -22
- package/dist/logic-linter.test.js +0 -39
- package/dist/mcp-tools.test.js +0 -286
- package/dist/mcp.test.js +0 -312
- package/dist/mirror-refs.test.js +0 -63
- package/dist/pipeline.test.js +0 -247
- package/dist/preservation.test.js +0 -22
- package/dist/production-gates.test.js +0 -34
- package/dist/profile-compose.test.js +0 -32
- package/dist/profile-score.test.js +0 -22
- package/dist/profile-watch.test.js +0 -23
- package/dist/profile.test.js +0 -141
- package/dist/provenance-status.test.js +0 -22
- package/dist/rebuild-task.test.js +0 -206
- package/dist/recomposition.test.js +0 -34
- package/dist/release-audit.test.js +0 -292
- package/dist/rewrite-task.test.js +0 -166
- package/dist/rule-allowances.test.js +0 -17
- package/dist/rule-reconciliation.test.js +0 -50
- package/dist/sample-ingest.test.js +0 -52
- package/dist/semantic-review.test.js +0 -101
- package/dist/stage1-dry-run.test.js +0 -39
- package/dist/stage1-evaluation.test.js +0 -184
- package/dist/stage1-human-packet.test.js +0 -102
- package/dist/stage1-schema-contract.test.js +0 -95
- package/dist/stage2-human-packet.test.js +0 -81
- package/dist/strict-quality.test.js +0 -62
- package/dist/text-provenance.feature.test.js +0 -45
- package/dist/text.test.js +0 -16
- package/dist/voice-dna.test.js +0 -121
- package/dist/writing-examples.test.js +0 -35
package/Readme.md
CHANGED
|
@@ -1,200 +1,139 @@
|
|
|
1
|
-
#
|
|
1
|
+
# hold your voice
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@holdyourvoice/hyv)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
9
|
+
## what it does
|
|
75
10
|
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
34
|
+
## check and edit a draft
|
|
87
35
|
|
|
88
|
-
|
|
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
|
-
|
|
43
|
+
`analyze` returns separate voice dna, ai editor, and hygiene reports. hygiene is informational here; use `final-check` to gate delivery.
|
|
95
44
|
|
|
96
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
108
|
-
hyv verify draft.md candidate.md profile.json
|
|
60
|
+
producer | hyv final-check -
|
|
109
61
|
```
|
|
110
62
|
|
|
111
|
-
|
|
63
|
+
## commands
|
|
112
64
|
|
|
113
|
-
|
|
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
|
|
117
|
-
|
|
110
|
+
hyv agent list
|
|
111
|
+
hyv agent validate
|
|
118
112
|
```
|
|
119
113
|
|
|
120
|
-
|
|
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
|
-
|
|
116
|
+
## local data
|
|
123
117
|
|
|
124
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
139
|
+
[mit](LICENSE). third-party writing and data keep their own rights.
|
package/dist/ai-editor-rules.js
CHANGED
|
@@ -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.
|
|
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
|
|
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] ? [
|
|
111
|
+
const at = (index) => mapped[index] ? [ruleFinding(rule, mapped[index])] : [];
|
|
112
112
|
if (rule.id === 'ai.repeated-sentence-opening') {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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 >=
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
}
|
package/dist/analysis.js
ADDED
|
@@ -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
|
+
}
|