@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.
Files changed (87) hide show
  1. package/Readme.md +91 -96
  2. package/dist/ai-editor-rules.js +1 -0
  3. package/dist/ai-editor.js +19 -13
  4. package/dist/analysis.js +33 -0
  5. package/dist/cli/agents.js +15 -35
  6. package/dist/cli/io.js +41 -37
  7. package/dist/copy-spec.js +6 -6
  8. package/dist/editorial-packs.js +4 -3
  9. package/dist/fact-linter.js +87 -37
  10. package/dist/hidden-text.js +6 -6
  11. package/dist/hold-your-voice.mcpb +0 -0
  12. package/dist/internal.js +8 -0
  13. package/dist/judgment-task.js +10 -14
  14. package/dist/learning.js +66 -67
  15. package/dist/lifecycle-adapter.js +2 -5
  16. package/dist/local-eval.js +16 -31
  17. package/dist/mcp-server.js +272 -0
  18. package/dist/mcp-tools.js +2 -2
  19. package/dist/mcp.js +2 -261
  20. package/dist/pipeline.js +9 -85
  21. package/dist/profile-compose.js +18 -11
  22. package/dist/profile-score.js +6 -6
  23. package/dist/profile.js +17 -19
  24. package/dist/provenance-status.js +1 -3
  25. package/dist/rebuild-task.js +14 -25
  26. package/dist/recomposition.js +18 -13
  27. package/dist/rewrite-prompt.js +125 -0
  28. package/dist/rewrite-task.js +117 -70
  29. package/dist/rule-allowances.js +6 -10
  30. package/dist/semantic-review.js +71 -47
  31. package/dist/stage1-evaluation.js +29 -14
  32. package/dist/strict-quality.js +10 -18
  33. package/dist/version.js +1 -1
  34. package/dist/voice-dna.js +34 -21
  35. package/dist/writing-examples.js +2 -7
  36. package/package.json +6 -3
  37. package/skills/hyv-prepare-judgment/SKILL.md +4 -0
  38. package/dist/agents/catalog.test.js +0 -60
  39. package/dist/agents/emit.test.js +0 -64
  40. package/dist/agents/load.test.js +0 -149
  41. package/dist/ai-editor.test.js +0 -265
  42. package/dist/approval-capability.test.js +0 -52
  43. package/dist/approval-context.test.js +0 -38
  44. package/dist/backtest.test.js +0 -20
  45. package/dist/benchmark.test.js +0 -328
  46. package/dist/canonical-json.test.js +0 -24
  47. package/dist/cli/context.test.js +0 -55
  48. package/dist/cli.test.js +0 -731
  49. package/dist/editorial-packs.test.js +0 -94
  50. package/dist/fact-linter.test.js +0 -85
  51. package/dist/hidden-text.test.js +0 -26
  52. package/dist/hygiene.test.js +0 -83
  53. package/dist/judgment-task.test.js +0 -162
  54. package/dist/learning.test.js +0 -325
  55. package/dist/lifecycle-adapter.test.js +0 -56
  56. package/dist/local-eval.test.js +0 -20
  57. package/dist/logic-linter-corpus.test.js +0 -22
  58. package/dist/logic-linter.test.js +0 -39
  59. package/dist/mcp-tools.test.js +0 -286
  60. package/dist/mcp.test.js +0 -312
  61. package/dist/mirror-refs.test.js +0 -63
  62. package/dist/pipeline.test.js +0 -247
  63. package/dist/preservation.test.js +0 -22
  64. package/dist/production-gates.test.js +0 -34
  65. package/dist/profile-compose.test.js +0 -32
  66. package/dist/profile-score.test.js +0 -22
  67. package/dist/profile-watch.test.js +0 -23
  68. package/dist/profile.test.js +0 -141
  69. package/dist/provenance-status.test.js +0 -22
  70. package/dist/rebuild-task.test.js +0 -206
  71. package/dist/recomposition.test.js +0 -34
  72. package/dist/release-audit.test.js +0 -292
  73. package/dist/rewrite-task.test.js +0 -166
  74. package/dist/rule-allowances.test.js +0 -17
  75. package/dist/rule-reconciliation.test.js +0 -50
  76. package/dist/sample-ingest.test.js +0 -52
  77. package/dist/semantic-review.test.js +0 -101
  78. package/dist/stage1-dry-run.test.js +0 -39
  79. package/dist/stage1-evaluation.test.js +0 -184
  80. package/dist/stage1-human-packet.test.js +0 -102
  81. package/dist/stage1-schema-contract.test.js +0 -95
  82. package/dist/stage2-human-packet.test.js +0 -81
  83. package/dist/strict-quality.test.js +0 -62
  84. package/dist/text-provenance.feature.test.js +0 -45
  85. package/dist/text.test.js +0 -16
  86. package/dist/voice-dna.test.js +0 -121
  87. package/dist/writing-examples.test.js +0 -35
package/Readme.md CHANGED
@@ -1,144 +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.
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
- It runs two independent checks:
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
- - 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.
9
+ ## what it does
11
10
 
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
- inspect the draft, let your editor make the changes, then verify the candidate and check the exact output. the [architecture guide](docs/ARCHITECTURE.md#writing-workflow) shows the full flow, including judgments, authorized rebuilds, and approval gates.
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
- For a one-off command, replace `hyv` with `npx @holdyourvoice/hyv`.
32
+ for a one-off command, replace `hyv` with `npx @holdyourvoice/hyv`.
27
33
 
28
- 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.
34
+ ## check and edit a draft
29
35
 
30
- ## basic workflow
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
- 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.
39
44
 
40
- 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:
41
46
 
42
47
  ```bash
43
- 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
44
51
  ```
45
52
 
46
- 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.
47
54
 
48
- 3. Create an editing brief, edit the draft, and verify the candidate.
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 rewrite-prompt draft.md profile.json > rewrite-brief.md
52
- hyv verify draft.md candidate.md profile.json
60
+ producer | hyv final-check -
53
61
  ```
54
62
 
55
- Send the brief to a human editor or a model you choose. Delivery stays under your control.
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
- 4. Check the exact text before delivery.
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 final-check candidate.md
61
- producer | hyv final-check -
110
+ hyv agent list
111
+ hyv agent validate
62
112
  ```
63
113
 
64
- `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).
65
115
 
66
- `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
67
117
 
68
- ## 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.
69
119
 
70
- | Command | Purpose |
71
- | --- | --- |
72
- | `hyv profile <profile.json> <sample...>` | Build a local profile from two or more samples. |
73
- | `hyv profile v3 <profile.json> --id=writer.channel --channel=email <sample...>` | Build a signed channel-specific Profile v3; optional tone is advisory metadata. |
74
- | `hyv profile compose --ratio 70:30 <profile...>` | Locally compose two or more Profile v3 metrics without creating writing. |
75
- | `hyv score <draft> <profile-v3.json> <heldout...>` | Measure the draft against a separate held-out local writing range; may abstain. |
76
- | `hyv ingest <source> <export> --owner=owner --output=/absolute/dir` | Create redacted, owner-authorized Gmail or Telegram samples and a text-free receipt. |
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
- 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).
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
- [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');
@@ -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
- 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 [];
@@ -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
+ }
@@ -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
- if (argument === '--host') {
12
- const value = args[index + 1];
13
- if (!value || value.startsWith('--'))
14
- throw new Error('Usage: hyv agent --host HOST requires a value.');
15
- host = value;
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
- else if (argument.startsWith('--mode=')) {
32
- const value = argument.slice('--mode='.length);
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
- values.push(argument);
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
- export function readJson(path) {
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, constants.O_RDONLY);
38
- return parseBoundedJson(readBoundedDescriptor(descriptor));
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
- if (descriptor !== undefined)
42
- closeSync(descriptor);
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
- if (args[index] === '--capability-stdin') {
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 (args[index] === '--capability-file') {
56
- const path = args[index + 1];
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
- values.push(args[index]);
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
- let raw;
70
- if (source.kind === 'stdin')
71
- raw = readBoundedDescriptor(0);
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
- if (atoms.some((atom) => atomMatches(sentence.normalizedText, atom))) {
44
- for (const atom of atoms)
45
- if (atomMatches(sentence.normalizedText, atom))
46
- presentAtoms.add(atom);
47
- (sentenceClaims[sentence.index] ??= []).push(claim.id);
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) {
@@ -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(source, text) {
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
- return brief.requiredFacts.every((fact) => brief.factSources?.some((source) => hasAffirmedSourceText(source.text, fact.text) || (fact.atoms?.length && fact.atoms.every((atom) => hasAffirmedSourceText(source.text, atom)))));
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))