@holdyourvoice/hyv 3.6.1 → 4.0.1
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 +91 -96
- package/dist/ai-editor-rules.js +1 -0
- package/dist/ai-editor.js +19 -13
- package/dist/analysis.js +33 -0
- package/dist/cli/agents.js +15 -35
- package/dist/cli/io.js +41 -37
- package/dist/copy-spec.js +6 -6
- package/dist/editorial-packs.js +4 -3
- package/dist/fact-linter.js +87 -37
- 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 +66 -67
- 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 +2 -2
- package/dist/mcp.js +2 -261
- package/dist/pipeline.js +9 -85
- 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 +14 -25
- package/dist/recomposition.js +18 -13
- package/dist/rewrite-prompt.js +125 -0
- package/dist/rewrite-task.js +117 -70
- 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/context.test.js +0 -55
- 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,144 +1,139 @@
|
|
|
1
|
-
#
|
|
1
|
+
# hold your voice
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@holdyourvoice/hyv)
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
6
6
|
|
|
7
|
-
|
|
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.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- AI Editor finds deterministic patterns that often make writing generic or formulaic.
|
|
9
|
+
## what it does
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
17
25
|
|
|
18
26
|
## install
|
|
19
27
|
|
|
20
|
-
You need Node.js 20 or newer and at least two writing samples you have the right to use.
|
|
21
|
-
|
|
22
28
|
```bash
|
|
23
29
|
npm install --global @holdyourvoice/hyv
|
|
24
30
|
```
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
for a one-off command, replace `hyv` with `npx @holdyourvoice/hyv`.
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
## check and edit a draft
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
1. Build a local profile from your samples.
|
|
36
|
+
build a profile from at least two writing samples you have the right to use:
|
|
33
37
|
|
|
34
38
|
```bash
|
|
35
39
|
hyv profile profile.json samples/one.md samples/two.md
|
|
40
|
+
hyv analyze draft.md profile.json
|
|
36
41
|
```
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
`analyze` returns separate voice dna, ai editor, and hygiene reports. hygiene is informational here; use `final-check` to gate delivery.
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
create an editing brief, give it to your editor or a model you choose, then check the candidate:
|
|
41
46
|
|
|
42
47
|
```bash
|
|
43
|
-
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
|
|
44
51
|
```
|
|
45
52
|
|
|
46
|
-
|
|
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.
|
|
47
54
|
|
|
48
|
-
|
|
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.
|
|
56
|
+
|
|
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:
|
|
49
58
|
|
|
50
59
|
```bash
|
|
51
|
-
hyv
|
|
52
|
-
hyv verify draft.md candidate.md profile.json
|
|
60
|
+
producer | hyv final-check -
|
|
53
61
|
```
|
|
54
62
|
|
|
55
|
-
|
|
63
|
+
## commands
|
|
64
|
+
|
|
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).
|
|
56
66
|
|
|
57
|
-
|
|
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.
|
|
58
108
|
|
|
59
109
|
```bash
|
|
60
|
-
hyv
|
|
61
|
-
|
|
110
|
+
hyv agent list
|
|
111
|
+
hyv agent validate
|
|
62
112
|
```
|
|
63
113
|
|
|
64
|
-
|
|
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).
|
|
65
115
|
|
|
66
|
-
|
|
116
|
+
## local data
|
|
67
117
|
|
|
68
|
-
|
|
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.
|
|
69
119
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| `hyv analyze <draft> <profile.json>` | Run VoiceDNA, AI Editor, and hygiene checks. |
|
|
78
|
-
| `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`. |
|
|
79
|
-
| `hyv hygiene <draft> [--fix]` | Inspect hidden Unicode or write a conservative cleaned copy. |
|
|
80
|
-
| `hyv inspect-hidden-text <draft> [policy.json]` | Inspect hidden text with an optional policy. |
|
|
81
|
-
| `hyv apply-hidden-text-policy <draft> <policy.json> <output>` | Apply approved hidden-text removals. |
|
|
82
|
-
| `hyv final-check <path\|->` | Gate the exact text before delivery. |
|
|
83
|
-
| `hyv delivery-check <path\|-> [policy.json]` | Run optional local delivery-integrity checks. |
|
|
84
|
-
| `hyv profile assess <sample...>` | Inspect sample readiness before building a profile. |
|
|
85
|
-
| `hyv team-profile validate\|compose ...` | Validate or locally compose consent-bound team profile metadata. |
|
|
86
|
-
| `hyv dispositions <draft> <profile>` | Return normalized `block`, `review`, and `signal` findings. |
|
|
87
|
-
| `hyv fact-lint <draft\|-> --source=id:path` | Check claims against local source files. |
|
|
88
|
-
| `hyv logic-lint <draft\|-> [brief.json]` | Check deterministic document logic. |
|
|
89
|
-
| `hyv batch-analyze <draft...>` | Find repeated openings and endings across drafts. |
|
|
90
|
-
| `hyv rewrite-prompt <draft> <profile.json>` | Create a strict constrained editing brief. |
|
|
91
|
-
| `hyv prepare-rewrite ...` | Create a fingerprint-bound edit task. |
|
|
92
|
-
| `hyv apply-rewrite ...` | Apply and verify a response to an edit task. |
|
|
93
|
-
| `hyv prepare-judgment ...` | Create a pre-edit or post-candidate judgment task. |
|
|
94
|
-
| `hyv reduce-judgment <envelope...>` | Reduce judgments to SHIP, EDIT, REBUILD, CLEAR, or ESCALATE. |
|
|
95
|
-
| `hyv prepare-rebuild ...` | Create an authorized whole-document rebuild task. |
|
|
96
|
-
| `hyv rebuild-writer-request ...` | Create the writer-only part of a rebuild task. |
|
|
97
|
-
| `hyv apply-rebuild ...` | Apply and verify an authorized rebuild response. |
|
|
98
|
-
| `hyv verify <original> <candidate> <profile.json>` | Verify a candidate without changing learning state. |
|
|
99
|
-
| `hyv verify-spec ...` | Verify a candidate and a CopySpec. |
|
|
100
|
-
| `hyv lifecycle ...` | Run semantic review and final approval steps. |
|
|
101
|
-
| `hyv learning ...` | Inspect or change local profile learning. |
|
|
102
|
-
| `hyv patterns` | Print the active AI Editor rule catalog. |
|
|
103
|
-
| `hyv agent list\|validate\|describe\|emit` | Inspect or emit portable agent contracts. |
|
|
104
|
-
| `hyv mcp` | Start the local MCP server on standard input/output. |
|
|
105
|
-
|
|
106
|
-
`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.
|
|
107
|
-
|
|
108
|
-
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.
|
|
109
|
-
|
|
110
|
-
Run `hyv <command>` without enough arguments to see its exact usage. Read the [CLI reference](docs/wiki/CLI-Reference.md) for every option.
|
|
111
|
-
|
|
112
|
-
## portable agents and MCP
|
|
113
|
-
|
|
114
|
-
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.
|
|
115
|
-
|
|
116
|
-
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).
|
|
117
|
-
|
|
118
|
-
## privacy and safety
|
|
119
|
-
|
|
120
|
-
Drafts, samples, profiles, candidates, and source files stay on your machine. The package has no accounts, telemetry, hosted analysis, or runtime network requests.
|
|
121
|
-
|
|
122
|
-
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.
|
|
123
|
-
|
|
124
|
-
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.
|
|
125
|
-
|
|
126
|
-
## performance
|
|
127
|
-
|
|
128
|
-
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).
|
|
129
|
-
|
|
130
|
-
## 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.
|
|
131
127
|
|
|
132
128
|
```bash
|
|
133
129
|
npm ci
|
|
134
130
|
npm test
|
|
135
131
|
npm run check:release
|
|
132
|
+
npm run pack:claude
|
|
136
133
|
```
|
|
137
134
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
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.
|
|
141
136
|
|
|
142
137
|
## license
|
|
143
138
|
|
|
144
|
-
[
|
|
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');
|
|
@@ -110,17 +110,19 @@ function ruleFinding(rule, sentence) {
|
|
|
110
110
|
function documentMatches(rule, prose, mapped) {
|
|
111
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 [];
|
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
|
+
}
|
package/dist/cli/agents.js
CHANGED
|
@@ -8,47 +8,27 @@ function agentFlags(args) {
|
|
|
8
8
|
let output;
|
|
9
9
|
for (let index = 0; index < args.length; index += 1) {
|
|
10
10
|
const argument = args[index];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
index += 1;
|
|
17
|
-
}
|
|
18
|
-
else if (argument.startsWith('--host=')) {
|
|
19
|
-
const value = argument.slice('--host='.length);
|
|
20
|
-
if (!value)
|
|
21
|
-
throw new Error('Usage: hyv agent --host HOST requires a value.');
|
|
22
|
-
host = value;
|
|
23
|
-
}
|
|
24
|
-
else if (argument === '--mode') {
|
|
25
|
-
const value = args[index + 1];
|
|
26
|
-
if (value !== 'prompt' && value !== 'json')
|
|
27
|
-
throw new Error('Usage: hyv agent --mode prompt|json requires a mode.');
|
|
28
|
-
mode = value;
|
|
29
|
-
index += 1;
|
|
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;
|
|
30
16
|
}
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
const inline = separator >= 0;
|
|
18
|
+
const value = inline ? argument.slice(separator + 1) : args[++index];
|
|
19
|
+
if (name === '--mode') {
|
|
33
20
|
if (value !== 'prompt' && value !== 'json')
|
|
34
21
|
throw new Error('Usage: hyv agent --mode prompt|json requires a mode.');
|
|
35
22
|
mode = value;
|
|
36
23
|
}
|
|
37
|
-
else if (argument === '--output') {
|
|
38
|
-
const value = args[index + 1];
|
|
39
|
-
if (!value || value.startsWith('--'))
|
|
40
|
-
throw new Error('Usage: hyv agent --output FILE requires a value.');
|
|
41
|
-
output = value;
|
|
42
|
-
index += 1;
|
|
43
|
-
}
|
|
44
|
-
else if (argument.startsWith('--output=')) {
|
|
45
|
-
const value = argument.slice('--output='.length);
|
|
46
|
-
if (!value)
|
|
47
|
-
throw new Error('Usage: hyv agent --output FILE requires a value.');
|
|
48
|
-
output = value;
|
|
49
|
-
}
|
|
50
24
|
else {
|
|
51
|
-
|
|
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;
|
|
52
32
|
}
|
|
53
33
|
}
|
|
54
34
|
return { values, host, mode, output };
|
package/dist/cli/io.js
CHANGED
|
@@ -29,66 +29,70 @@ function readBoundedDescriptor(descriptor) {
|
|
|
29
29
|
throw new Error('JSON input exceeds the byte limit.');
|
|
30
30
|
return Buffer.concat(chunks, size).toString('utf8');
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
if (path === '-')
|
|
34
|
-
return parseBoundedJson(readBoundedDescriptor(0));
|
|
32
|
+
function withDescriptor(path, flags, read, openError) {
|
|
35
33
|
let descriptor;
|
|
36
34
|
try {
|
|
37
|
-
descriptor = openSync(path,
|
|
38
|
-
|
|
35
|
+
descriptor = openSync(path, flags);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw openError ? new Error(openError) : error;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return read(descriptor);
|
|
39
42
|
}
|
|
40
43
|
finally {
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
closeSync(descriptor);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function readJson(path) {
|
|
48
|
+
return path === '-'
|
|
49
|
+
? parseBoundedJson(readBoundedDescriptor(0))
|
|
50
|
+
: withDescriptor(path, constants.O_RDONLY, (descriptor) => parseBoundedJson(readBoundedDescriptor(descriptor)));
|
|
51
|
+
}
|
|
52
|
+
function readCapabilityDescriptor(descriptor) {
|
|
53
|
+
try {
|
|
54
|
+
const before = fstatSync(descriptor);
|
|
55
|
+
if (!before.isFile() || before.uid !== process.geteuid?.() || (before.mode & 0o077) !== 0 || before.nlink !== 1 || before.size > MAX_JSON_BYTES) {
|
|
56
|
+
throw new Error('Capability file is unavailable or unsafe.');
|
|
57
|
+
}
|
|
58
|
+
const raw = readBoundedDescriptor(descriptor);
|
|
59
|
+
const after = fstatSync(descriptor);
|
|
60
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
|
|
61
|
+
throw new Error('Capability file is unavailable or unsafe.');
|
|
62
|
+
}
|
|
63
|
+
return raw;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error('Capability file is unavailable or unsafe.');
|
|
43
67
|
}
|
|
44
68
|
}
|
|
45
69
|
export function capabilityArguments(args) {
|
|
46
70
|
const values = [];
|
|
47
71
|
let source;
|
|
48
72
|
for (let index = 0; index < args.length; index += 1) {
|
|
49
|
-
|
|
73
|
+
const argument = args[index];
|
|
74
|
+
if (argument === '--capability-stdin') {
|
|
50
75
|
if (source)
|
|
51
76
|
throw new Error('Choose one capability source.');
|
|
52
77
|
source = { kind: 'stdin' };
|
|
53
|
-
continue;
|
|
54
78
|
}
|
|
55
|
-
if (
|
|
56
|
-
const path = args[index
|
|
79
|
+
else if (argument === '--capability-file') {
|
|
80
|
+
const path = args[++index];
|
|
57
81
|
if (source || !path || path.startsWith('--capability-'))
|
|
58
82
|
throw new Error('Choose one capability source.');
|
|
59
83
|
source = { kind: 'file', path };
|
|
60
|
-
index += 1;
|
|
61
|
-
continue;
|
|
62
84
|
}
|
|
63
|
-
|
|
85
|
+
else {
|
|
86
|
+
values.push(argument);
|
|
87
|
+
}
|
|
64
88
|
}
|
|
65
89
|
if (!source)
|
|
66
90
|
return { values };
|
|
67
91
|
if (source.kind === 'stdin' && values.includes('-'))
|
|
68
92
|
throw new Error('Capability stdin cannot be combined with another stdin input.');
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
else {
|
|
73
|
-
let descriptor;
|
|
74
|
-
try {
|
|
75
|
-
descriptor = openSync(source.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
76
|
-
const before = fstatSync(descriptor);
|
|
77
|
-
if (!before.isFile() || before.uid !== process.geteuid?.() || (before.mode & 0o077) !== 0 || before.nlink !== 1 || before.size > MAX_JSON_BYTES)
|
|
78
|
-
throw new Error('Capability file is unavailable or unsafe.');
|
|
79
|
-
raw = readBoundedDescriptor(descriptor);
|
|
80
|
-
const after = fstatSync(descriptor);
|
|
81
|
-
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs)
|
|
82
|
-
throw new Error('Capability file is unavailable or unsafe.');
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
throw new Error('Capability file is unavailable or unsafe.');
|
|
86
|
-
}
|
|
87
|
-
finally {
|
|
88
|
-
if (descriptor !== undefined)
|
|
89
|
-
closeSync(descriptor);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
93
|
+
const raw = source.kind === 'stdin'
|
|
94
|
+
? readBoundedDescriptor(0)
|
|
95
|
+
: withDescriptor(source.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0), readCapabilityDescriptor, 'Capability file is unavailable or unsafe.');
|
|
92
96
|
if (Buffer.byteLength(raw, 'utf8') > MAX_JSON_BYTES)
|
|
93
97
|
throw new Error('JSON input exceeds the byte limit.');
|
|
94
98
|
return { values, capability: parseCanonicalJson(Buffer.from(raw, 'utf8')) };
|
package/dist/copy-spec.js
CHANGED
|
@@ -40,12 +40,12 @@ export function verifyClaims(candidate, spec) {
|
|
|
40
40
|
const atoms = claim.atoms.map(normalized);
|
|
41
41
|
const presentAtoms = new Set();
|
|
42
42
|
for (const sentence of draftSentences) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
const matches = atoms.filter((atom) => atomMatches(sentence.normalizedText, atom));
|
|
44
|
+
if (!matches.length)
|
|
45
|
+
continue;
|
|
46
|
+
for (const atom of matches)
|
|
47
|
+
presentAtoms.add(atom);
|
|
48
|
+
(sentenceClaims[sentence.index] ??= []).push(claim.id);
|
|
49
49
|
}
|
|
50
50
|
const missingAtoms = atoms.filter((atom) => !presentAtoms.has(atom));
|
|
51
51
|
if (!claim.mutable && missingAtoms.length) {
|
package/dist/editorial-packs.js
CHANGED
|
@@ -23,9 +23,8 @@ function isDenied(text) {
|
|
|
23
23
|
function isFollowUpDenial(text) {
|
|
24
24
|
return /^(?:that|this) (?:statement|claim|fact|assertion|point) (?:is|was) (?:not|false|untrue|incorrect)\b/i.test(text.trim());
|
|
25
25
|
}
|
|
26
|
-
function hasAffirmedSourceText(
|
|
26
|
+
function hasAffirmedSourceText(sourceSentences, text) {
|
|
27
27
|
const expected = text.toLowerCase();
|
|
28
|
-
const sourceSentences = sentences(source);
|
|
29
28
|
return sourceSentences.some((sentence, index) => sentence.text.toLowerCase().includes(expected) && !isDenied(sentence.text) && !isFollowUpDenial(sourceSentences[index + 1]?.text ?? ''));
|
|
30
29
|
}
|
|
31
30
|
function requiredFactsAreSourced(brief) {
|
|
@@ -33,7 +32,9 @@ function requiredFactsAreSourced(brief) {
|
|
|
33
32
|
return true;
|
|
34
33
|
if (!brief.factSources?.length)
|
|
35
34
|
return false;
|
|
36
|
-
|
|
35
|
+
const sources = brief.factSources.map((source) => sentences(source.text));
|
|
36
|
+
return brief.requiredFacts.every((fact) => sources.some((source) => hasAffirmedSourceText(source, fact.text)
|
|
37
|
+
|| (fact.atoms?.length && fact.atoms.every((atom) => hasAffirmedSourceText(source, atom)))));
|
|
37
38
|
}
|
|
38
39
|
function isArgumentMap(value) {
|
|
39
40
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|