@clossys/writer 0.3.2
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/CHANGELOG.md +196 -0
- package/LICENSE +21 -0
- package/README.md +593 -0
- package/dist/addressability.d.ts +245 -0
- package/dist/addressability.d.ts.map +1 -0
- package/dist/addressability.js +567 -0
- package/dist/addressability.js.map +1 -0
- package/dist/checker.d.ts +159 -0
- package/dist/checker.d.ts.map +1 -0
- package/dist/checker.js +177 -0
- package/dist/checker.js.map +1 -0
- package/dist/cli.d.ts +149 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1049 -0
- package/dist/cli.js.map +1 -0
- package/dist/copy-gate.d.ts +107 -0
- package/dist/copy-gate.d.ts.map +1 -0
- package/dist/copy-gate.js +132 -0
- package/dist/copy-gate.js.map +1 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +64 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/locale-coverage.d.ts +227 -0
- package/dist/locale-coverage.d.ts.map +1 -0
- package/dist/locale-coverage.js +414 -0
- package/dist/locale-coverage.js.map +1 -0
- package/dist/passage.d.ts +294 -0
- package/dist/passage.d.ts.map +1 -0
- package/dist/passage.js +463 -0
- package/dist/passage.js.map +1 -0
- package/dist/path-exclusions.d.ts +114 -0
- package/dist/path-exclusions.d.ts.map +1 -0
- package/dist/path-exclusions.js +213 -0
- package/dist/path-exclusions.js.map +1 -0
- package/dist/registry.d.ts +61 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +64 -0
- package/dist/registry.js.map +1 -0
- package/dist/resolve.d.ts +33 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +133 -0
- package/dist/resolve.js.map +1 -0
- package/dist/scan.d.ts +457 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +1698 -0
- package/dist/scan.js.map +1 -0
- package/dist/schema.d.ts +81 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +482 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +268 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +40 -0
- package/dist/types.js.map +1 -0
- package/dist/voice/checker.d.ts +186 -0
- package/dist/voice/checker.d.ts.map +1 -0
- package/dist/voice/checker.js +483 -0
- package/dist/voice/checker.js.map +1 -0
- package/dist/voice/derivation-coverage.d.ts +168 -0
- package/dist/voice/derivation-coverage.d.ts.map +1 -0
- package/dist/voice/derivation-coverage.js +181 -0
- package/dist/voice/derivation-coverage.js.map +1 -0
- package/dist/voice/fields.d.ts +107 -0
- package/dist/voice/fields.d.ts.map +1 -0
- package/dist/voice/fields.js +144 -0
- package/dist/voice/fields.js.map +1 -0
- package/dist/voice/index.d.ts +19 -0
- package/dist/voice/index.d.ts.map +1 -0
- package/dist/voice/index.js +24 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/internal/parse-template.d.ts +27 -0
- package/dist/voice/internal/parse-template.d.ts.map +1 -0
- package/dist/voice/internal/parse-template.js +111 -0
- package/dist/voice/internal/parse-template.js.map +1 -0
- package/dist/voice/internal/pattern-safety.d.ts +111 -0
- package/dist/voice/internal/pattern-safety.d.ts.map +1 -0
- package/dist/voice/internal/pattern-safety.js +278 -0
- package/dist/voice/internal/pattern-safety.js.map +1 -0
- package/dist/voice/schema.d.ts +65 -0
- package/dist/voice/schema.d.ts.map +1 -0
- package/dist/voice/schema.js +535 -0
- package/dist/voice/schema.js.map +1 -0
- package/dist/voice/types.d.ts +282 -0
- package/dist/voice/types.d.ts.map +1 -0
- package/dist/voice/types.js +37 -0
- package/dist/voice/types.js.map +1 -0
- package/package.json +72 -0
- package/src/addressability.ts +664 -0
- package/src/checker.ts +293 -0
- package/src/cli.ts +1166 -0
- package/src/copy-gate.ts +197 -0
- package/src/fingerprint.ts +66 -0
- package/src/index.ts +163 -0
- package/src/locale-coverage.ts +512 -0
- package/src/passage.ts +644 -0
- package/src/path-exclusions.ts +265 -0
- package/src/registry.ts +103 -0
- package/src/resolve.ts +172 -0
- package/src/scan.ts +1959 -0
- package/src/schema.ts +528 -0
- package/src/types.ts +301 -0
- package/src/voice/checker.ts +590 -0
- package/src/voice/derivation-coverage.ts +217 -0
- package/src/voice/fields.ts +160 -0
- package/src/voice/index.ts +55 -0
- package/src/voice/internal/parse-template.ts +114 -0
- package/src/voice/internal/pattern-safety.ts +336 -0
- package/src/voice/schema.ts +585 -0
- package/src/voice/types.ts +329 -0
- package/templates/voice-record.template.jsonc +135 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `checkVoiceDerivationCoverage` — the voice half of `@clossys/
|
|
3
|
+
* strategy`'s `checkBrandCoverage`, which this file mirrors closely on
|
|
4
|
+
* purpose. Read `packages/strategy/src/brand-derivation.ts`'s header
|
|
5
|
+
* comment in full before touching this one; the design below is that
|
|
6
|
+
* file's, ported to the other side of the seam it documents.
|
|
7
|
+
*
|
|
8
|
+
* THE GAP THIS CLOSES
|
|
9
|
+
* ---------------------------------------------------------------------------
|
|
10
|
+
* A `BrandDerivation` (`strategy`) names two kinds of obligation by PLAIN
|
|
11
|
+
* STRING: `tokenSlots` and `voiceRules` — see that file's "THE NAME-ONLY
|
|
12
|
+
* SEAM, AND WHY". `checkBrandCoverage` verifies the `tokenSlots` half
|
|
13
|
+
* against a caller-supplied `brandableSlots: string[]`, in both directions.
|
|
14
|
+
* It does NOT, and by its own package's constraints CANNOT, verify the
|
|
15
|
+
* `voiceRules` half — `strategy` ships zero runtime dependencies and does
|
|
16
|
+
* not import `@clossys/writer/voice`, so it has no way to know which
|
|
17
|
+
* voice rule ids a real `VoiceRecord` actually declares. `strategy`'s own
|
|
18
|
+
* README says this plainly: "whether a `voiceRules` id resolves to a real
|
|
19
|
+
* `@clossys/writer/voice` glossary entry... [is] a later,
|
|
20
|
+
* cross-package gate's job — one with visibility into... `voice` that this
|
|
21
|
+
* package deliberately does not have." A brand attribute can oblige a voice
|
|
22
|
+
* rule that no `VoiceRecord` anywhere declares, and nothing catches it: the
|
|
23
|
+
* token half of the gate passes cleanly, because it only ever compares
|
|
24
|
+
* against a token slot list, and the voice half is never compared against
|
|
25
|
+
* anything at all. This file is that later gate — built here, in `copy`,
|
|
26
|
+
* because this is the one package that actually holds a `VoiceRecord`.
|
|
27
|
+
*
|
|
28
|
+
* THE NAME-ONLY SEAM, AND WHY
|
|
29
|
+
* ---------------------------------------------------------------------------
|
|
30
|
+
* This function takes two PLAIN STRING LISTS, `obligations` and
|
|
31
|
+
* `brandDerivedRuleIds` — the exact same discipline `BrandDerivation.
|
|
32
|
+
* voiceRules` documents for itself, carried through to this side of the
|
|
33
|
+
* seam, and the exact same shape `checkBrandCoverage` takes
|
|
34
|
+
* `brandableSlots` in. This file does not import `@clossys/
|
|
35
|
+
* strategy` or know anything about `BrandDerivation`, and it does not read
|
|
36
|
+
* a `VoiceRecord` either. Earlier revisions of this function DID take a
|
|
37
|
+
* `VoiceRecord` and scanned it for every glossary term, claim id, and
|
|
38
|
+
* pattern id, treating that whole scan as the set direction 2 had to cover.
|
|
39
|
+
* That was wrong, and it was wrong for the identical reason `strategy`
|
|
40
|
+
* cannot look up `tokens`' `brandable` flag itself: EVERY field in a
|
|
41
|
+
* `VoiceRecord` is consumer-authored, but authored is not the same claim as
|
|
42
|
+
* brand-derived. `@example/ui` ships 154 tokens and marks a curated
|
|
43
|
+
* 42 of them `brandable: true` — a deliberate subset a `BrandDerivation`'s
|
|
44
|
+
* `tokenSlots` is checked against, never the full 154. `voice` has no
|
|
45
|
+
* `brandable`-equivalent flag on a glossary entry or a claim, so scanning
|
|
46
|
+
* the whole record and calling that "the brand-derived set" silently
|
|
47
|
+
* substituted "everything a consumer happened to author" for "everything a
|
|
48
|
+
* brand attribute actually obliges" — a consumer with 200 glossary terms
|
|
49
|
+
* and one real brand obligation would need 200 `BrandDerivation.voiceRules`
|
|
50
|
+
* entries just to pass direction 2, a false-positive machine that gets
|
|
51
|
+
* disabled the first time it fires on a real record. Nothing inside a
|
|
52
|
+
* `VoiceRecord`, and nothing inside this package, can tell a brand-derived
|
|
53
|
+
* glossary term from an editorially-authored one — that judgment lives only
|
|
54
|
+
* where a `BrandDerivation` and a `VoiceRecord` are both in scope, i.e. the
|
|
55
|
+
* caller.
|
|
56
|
+
*
|
|
57
|
+
* THE CHECKER'S SEAM — READ THIS BEFORE CALLING `checkVoiceDerivationCoverage`
|
|
58
|
+
* ---------------------------------------------------------------------------
|
|
59
|
+
* Because this package cannot know which of a `VoiceRecord`'s ids a brand
|
|
60
|
+
* attribute actually derived, `checkVoiceDerivationCoverage` cannot look
|
|
61
|
+
* that set up itself. It takes it as an argument (`brandDerivedRuleIds:
|
|
62
|
+
* readonly string[]`) instead. This is deliberate, not a missing feature:
|
|
63
|
+
* the CALLER — a consumer repo that depends on both `strategy` and `copy`,
|
|
64
|
+
* or a later cross-package gate built with visibility into both — is the
|
|
65
|
+
* one place that seam can be closed for real, e.g. by flattening every
|
|
66
|
+
* `BrandDerivation.voiceRules` entry (across every derivation) into the
|
|
67
|
+
* flat `brandDerivedRuleIds` list this function takes, the same way a
|
|
68
|
+
* caller closes `checkBrandCoverage`'s seam by passing
|
|
69
|
+
* `Object.values(TOKENS).filter(t => t.brandable).map(t => t.property)`
|
|
70
|
+
* from `@example/ui/tokens`. `checkVoiceDerivationCoverage` only
|
|
71
|
+
* ever sees the two plain lists it's handed; it has no way to tell a real,
|
|
72
|
+
* current, brand-derived rule-id list from a stale or empty one, which is
|
|
73
|
+
* exactly why the degenerate cases below (an empty list either way) are
|
|
74
|
+
* treated as failures rather than silent passes — see "Fails closed",
|
|
75
|
+
* below.
|
|
76
|
+
*
|
|
77
|
+
* WHAT COUNTS AS A "VOICE RULE ID"
|
|
78
|
+
* ---------------------------------------------------------------------------
|
|
79
|
+
* This function no longer inspects a `VoiceRecord` at all, so it does not
|
|
80
|
+
* decide what counts as a rule id — the caller does, by what it puts in
|
|
81
|
+
* `brandDerivedRuleIds`. In practice that id space is still the one
|
|
82
|
+
* `strategy`'s own README names for `voiceRules`: "a real
|
|
83
|
+
* `@clossys/writer/voice` glossary entry" — i.e. a glossary term's
|
|
84
|
+
* `term` or a claim's `id` (`types.ts`'s `GlossaryEntry`/`Claim`). Earlier
|
|
85
|
+
* revisions of this file additionally counted `patterns` ids as voice rule
|
|
86
|
+
* ids by auto-deriving the whole set from a record; that widened direction
|
|
87
|
+
* 2 beyond what `strategy`'s documented `voiceRules` contract actually
|
|
88
|
+
* names, purely as a side effect of this package doing the scanning itself.
|
|
89
|
+
* With no record to scan and a caller-supplied list instead, this package
|
|
90
|
+
* no longer makes that decision — the caller decides what belongs in
|
|
91
|
+
* `brandDerivedRuleIds`, exactly as the caller decides what belongs in
|
|
92
|
+
* `checkBrandCoverage`'s `brandableSlots`.
|
|
93
|
+
*
|
|
94
|
+
* THE CHECK ITSELF — BOTH DIRECTIONS, LIKE `checkBrandCoverage`
|
|
95
|
+
* ---------------------------------------------------------------------------
|
|
96
|
+
* 1. Every id in `obligations` is named by `brandDerivedRuleIds` — an
|
|
97
|
+
* obligation naming an id nothing derived is a stale or typo'd
|
|
98
|
+
* reference (`obligationsMissingFromRecord`).
|
|
99
|
+
* 2. Every id in `brandDerivedRuleIds` is reached by at least one
|
|
100
|
+
* obligation — a brand-derived rule id with no obligation behind it
|
|
101
|
+
* is undocumented brand intent, exactly `checkBrandCoverage`'s
|
|
102
|
+
* `slotsMissingDerivation` mirrored onto the other list
|
|
103
|
+
* (`recordRulesNotObliged`).
|
|
104
|
+
*
|
|
105
|
+
* A ONE-DIRECTIONAL CHECKER PASSES CASE 2 AND HIDES IT — this file exists
|
|
106
|
+
* specifically because a checker that only implements direction 1 (every
|
|
107
|
+
* obligation resolves) looks complete, passes every obligation-side test,
|
|
108
|
+
* and still never notices a whole brand-derived rule id nobody obliged. See
|
|
109
|
+
* `derivation-coverage.test.ts` for the fixture built to prove this file
|
|
110
|
+
* does not make that mistake.
|
|
111
|
+
*
|
|
112
|
+
* FAILS CLOSED, IDENTICAL PRECEDENT TO `checkBrandCoverage`
|
|
113
|
+
* ---------------------------------------------------------------------------
|
|
114
|
+
* A checker given nothing to check must never report the same shape as a
|
|
115
|
+
* checker that checked everything and found it clean. Both degenerate
|
|
116
|
+
* inputs are an explicit failure, never a vacuous pass:
|
|
117
|
+
*
|
|
118
|
+
* - `obligations.length === 0` — nothing was obliged at all. `ok: false`,
|
|
119
|
+
* `reason: "no-obligations-provided"`.
|
|
120
|
+
* - `brandDerivedRuleIds.length === 0` — nothing to check obligations
|
|
121
|
+
* against. Maybe the caller forgot to pass the real list, maybe a
|
|
122
|
+
* consumer genuinely derived zero voice rules from its brand; either
|
|
123
|
+
* way this function cannot tell the difference, so it cannot report
|
|
124
|
+
* success. `ok: false`, `reason: "no-brand-derived-rules-provided"`.
|
|
125
|
+
*
|
|
126
|
+
* `obligationsChecked` and `rulesChecked` are always present, in every
|
|
127
|
+
* branch, for the identical reason `checkBrandCoverage`'s own header states
|
|
128
|
+
* for `slotsChecked`/`derivationsChecked`: so "zero things were checked"
|
|
129
|
+
* and "everything checked out fine" can never be told apart by a caller who
|
|
130
|
+
* only glances at `ok`.
|
|
131
|
+
*
|
|
132
|
+
* Pure — no I/O, never throws.
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* Why a `VoiceDerivationCoverageResult` is not `ok`, when it isn't.
|
|
136
|
+
* `"coverage-gap"` is a REAL finding — some obligation names an id nothing
|
|
137
|
+
* derived, or some brand-derived rule id is reached by no obligation —
|
|
138
|
+
* over a genuine, non-empty comparison. `"no-obligations-provided"` and
|
|
139
|
+
* `"no-brand-derived-rules-provided"` are the OTHER kind of failure:
|
|
140
|
+
* nothing meaningful was compared at all. See this file's top-of-file doc
|
|
141
|
+
* comment, "Fails closed".
|
|
142
|
+
*/
|
|
143
|
+
export type VoiceDerivationCoverageFailureReason = "no-obligations-provided" | "no-brand-derived-rules-provided" | "coverage-gap";
|
|
144
|
+
export interface VoiceDerivationCoverageResult {
|
|
145
|
+
ok: boolean;
|
|
146
|
+
/** `obligations.length`, always present — see this file's header comment, "Fails closed". */
|
|
147
|
+
obligationsChecked: number;
|
|
148
|
+
/** `brandDerivedRuleIds.length`, always present, for the same reason. */
|
|
149
|
+
rulesChecked: number;
|
|
150
|
+
/** Direction 1: every obligation naming an id `brandDerivedRuleIds` does not declare. */
|
|
151
|
+
obligationsMissingFromRecord: string[];
|
|
152
|
+
/** Direction 2: every id in `brandDerivedRuleIds` that no obligation names. */
|
|
153
|
+
recordRulesNotObliged: string[];
|
|
154
|
+
/** Present exactly when `ok` is `false`. See `VoiceDerivationCoverageFailureReason`. */
|
|
155
|
+
reason?: VoiceDerivationCoverageFailureReason;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Checks whether `obligations` fully accounts for `brandDerivedRuleIds`, in
|
|
159
|
+
* both directions, and fails closed on either empty input. Pure — no I/O,
|
|
160
|
+
* never throws. See this file's top-of-file doc comment for the seam
|
|
161
|
+
* (`brandDerivedRuleIds` is caller-supplied, since this package has no way
|
|
162
|
+
* to tell a brand-derived voice rule id from any other consumer-authored
|
|
163
|
+
* one) and for why the two degenerate inputs are failures rather than
|
|
164
|
+
* vacuous passes. See `strategy`'s `checkBrandCoverage` (which this
|
|
165
|
+
* mirrors) for the precedent.
|
|
166
|
+
*/
|
|
167
|
+
export declare function checkVoiceDerivationCoverage(obligations: readonly string[], brandDerivedRuleIds: readonly string[]): VoiceDerivationCoverageResult;
|
|
168
|
+
//# sourceMappingURL=derivation-coverage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derivation-coverage.d.ts","sourceRoot":"","sources":["../../src/voice/derivation-coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoIG;AAEH;;;;;;;;GAQG;AACH,MAAM,MAAM,oCAAoC,GAC5C,yBAAyB,GACzB,iCAAiC,GACjC,cAAc,CAAC;AAEnB,MAAM,WAAW,6BAA6B;IAC5C,EAAE,EAAE,OAAO,CAAC;IACZ,6FAA6F;IAC7F,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,4BAA4B,EAAE,MAAM,EAAE,CAAC;IACvC,+EAA+E;IAC/E,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,wFAAwF;IACxF,MAAM,CAAC,EAAE,oCAAoC,CAAC;CAC/C;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,mBAAmB,EAAE,SAAS,MAAM,EAAE,GACrC,6BAA6B,CAyC/B"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `checkVoiceDerivationCoverage` — the voice half of `@clossys/
|
|
3
|
+
* strategy`'s `checkBrandCoverage`, which this file mirrors closely on
|
|
4
|
+
* purpose. Read `packages/strategy/src/brand-derivation.ts`'s header
|
|
5
|
+
* comment in full before touching this one; the design below is that
|
|
6
|
+
* file's, ported to the other side of the seam it documents.
|
|
7
|
+
*
|
|
8
|
+
* THE GAP THIS CLOSES
|
|
9
|
+
* ---------------------------------------------------------------------------
|
|
10
|
+
* A `BrandDerivation` (`strategy`) names two kinds of obligation by PLAIN
|
|
11
|
+
* STRING: `tokenSlots` and `voiceRules` — see that file's "THE NAME-ONLY
|
|
12
|
+
* SEAM, AND WHY". `checkBrandCoverage` verifies the `tokenSlots` half
|
|
13
|
+
* against a caller-supplied `brandableSlots: string[]`, in both directions.
|
|
14
|
+
* It does NOT, and by its own package's constraints CANNOT, verify the
|
|
15
|
+
* `voiceRules` half — `strategy` ships zero runtime dependencies and does
|
|
16
|
+
* not import `@clossys/writer/voice`, so it has no way to know which
|
|
17
|
+
* voice rule ids a real `VoiceRecord` actually declares. `strategy`'s own
|
|
18
|
+
* README says this plainly: "whether a `voiceRules` id resolves to a real
|
|
19
|
+
* `@clossys/writer/voice` glossary entry... [is] a later,
|
|
20
|
+
* cross-package gate's job — one with visibility into... `voice` that this
|
|
21
|
+
* package deliberately does not have." A brand attribute can oblige a voice
|
|
22
|
+
* rule that no `VoiceRecord` anywhere declares, and nothing catches it: the
|
|
23
|
+
* token half of the gate passes cleanly, because it only ever compares
|
|
24
|
+
* against a token slot list, and the voice half is never compared against
|
|
25
|
+
* anything at all. This file is that later gate — built here, in `copy`,
|
|
26
|
+
* because this is the one package that actually holds a `VoiceRecord`.
|
|
27
|
+
*
|
|
28
|
+
* THE NAME-ONLY SEAM, AND WHY
|
|
29
|
+
* ---------------------------------------------------------------------------
|
|
30
|
+
* This function takes two PLAIN STRING LISTS, `obligations` and
|
|
31
|
+
* `brandDerivedRuleIds` — the exact same discipline `BrandDerivation.
|
|
32
|
+
* voiceRules` documents for itself, carried through to this side of the
|
|
33
|
+
* seam, and the exact same shape `checkBrandCoverage` takes
|
|
34
|
+
* `brandableSlots` in. This file does not import `@clossys/
|
|
35
|
+
* strategy` or know anything about `BrandDerivation`, and it does not read
|
|
36
|
+
* a `VoiceRecord` either. Earlier revisions of this function DID take a
|
|
37
|
+
* `VoiceRecord` and scanned it for every glossary term, claim id, and
|
|
38
|
+
* pattern id, treating that whole scan as the set direction 2 had to cover.
|
|
39
|
+
* That was wrong, and it was wrong for the identical reason `strategy`
|
|
40
|
+
* cannot look up `tokens`' `brandable` flag itself: EVERY field in a
|
|
41
|
+
* `VoiceRecord` is consumer-authored, but authored is not the same claim as
|
|
42
|
+
* brand-derived. `@example/ui` ships 154 tokens and marks a curated
|
|
43
|
+
* 42 of them `brandable: true` — a deliberate subset a `BrandDerivation`'s
|
|
44
|
+
* `tokenSlots` is checked against, never the full 154. `voice` has no
|
|
45
|
+
* `brandable`-equivalent flag on a glossary entry or a claim, so scanning
|
|
46
|
+
* the whole record and calling that "the brand-derived set" silently
|
|
47
|
+
* substituted "everything a consumer happened to author" for "everything a
|
|
48
|
+
* brand attribute actually obliges" — a consumer with 200 glossary terms
|
|
49
|
+
* and one real brand obligation would need 200 `BrandDerivation.voiceRules`
|
|
50
|
+
* entries just to pass direction 2, a false-positive machine that gets
|
|
51
|
+
* disabled the first time it fires on a real record. Nothing inside a
|
|
52
|
+
* `VoiceRecord`, and nothing inside this package, can tell a brand-derived
|
|
53
|
+
* glossary term from an editorially-authored one — that judgment lives only
|
|
54
|
+
* where a `BrandDerivation` and a `VoiceRecord` are both in scope, i.e. the
|
|
55
|
+
* caller.
|
|
56
|
+
*
|
|
57
|
+
* THE CHECKER'S SEAM — READ THIS BEFORE CALLING `checkVoiceDerivationCoverage`
|
|
58
|
+
* ---------------------------------------------------------------------------
|
|
59
|
+
* Because this package cannot know which of a `VoiceRecord`'s ids a brand
|
|
60
|
+
* attribute actually derived, `checkVoiceDerivationCoverage` cannot look
|
|
61
|
+
* that set up itself. It takes it as an argument (`brandDerivedRuleIds:
|
|
62
|
+
* readonly string[]`) instead. This is deliberate, not a missing feature:
|
|
63
|
+
* the CALLER — a consumer repo that depends on both `strategy` and `copy`,
|
|
64
|
+
* or a later cross-package gate built with visibility into both — is the
|
|
65
|
+
* one place that seam can be closed for real, e.g. by flattening every
|
|
66
|
+
* `BrandDerivation.voiceRules` entry (across every derivation) into the
|
|
67
|
+
* flat `brandDerivedRuleIds` list this function takes, the same way a
|
|
68
|
+
* caller closes `checkBrandCoverage`'s seam by passing
|
|
69
|
+
* `Object.values(TOKENS).filter(t => t.brandable).map(t => t.property)`
|
|
70
|
+
* from `@example/ui/tokens`. `checkVoiceDerivationCoverage` only
|
|
71
|
+
* ever sees the two plain lists it's handed; it has no way to tell a real,
|
|
72
|
+
* current, brand-derived rule-id list from a stale or empty one, which is
|
|
73
|
+
* exactly why the degenerate cases below (an empty list either way) are
|
|
74
|
+
* treated as failures rather than silent passes — see "Fails closed",
|
|
75
|
+
* below.
|
|
76
|
+
*
|
|
77
|
+
* WHAT COUNTS AS A "VOICE RULE ID"
|
|
78
|
+
* ---------------------------------------------------------------------------
|
|
79
|
+
* This function no longer inspects a `VoiceRecord` at all, so it does not
|
|
80
|
+
* decide what counts as a rule id — the caller does, by what it puts in
|
|
81
|
+
* `brandDerivedRuleIds`. In practice that id space is still the one
|
|
82
|
+
* `strategy`'s own README names for `voiceRules`: "a real
|
|
83
|
+
* `@clossys/writer/voice` glossary entry" — i.e. a glossary term's
|
|
84
|
+
* `term` or a claim's `id` (`types.ts`'s `GlossaryEntry`/`Claim`). Earlier
|
|
85
|
+
* revisions of this file additionally counted `patterns` ids as voice rule
|
|
86
|
+
* ids by auto-deriving the whole set from a record; that widened direction
|
|
87
|
+
* 2 beyond what `strategy`'s documented `voiceRules` contract actually
|
|
88
|
+
* names, purely as a side effect of this package doing the scanning itself.
|
|
89
|
+
* With no record to scan and a caller-supplied list instead, this package
|
|
90
|
+
* no longer makes that decision — the caller decides what belongs in
|
|
91
|
+
* `brandDerivedRuleIds`, exactly as the caller decides what belongs in
|
|
92
|
+
* `checkBrandCoverage`'s `brandableSlots`.
|
|
93
|
+
*
|
|
94
|
+
* THE CHECK ITSELF — BOTH DIRECTIONS, LIKE `checkBrandCoverage`
|
|
95
|
+
* ---------------------------------------------------------------------------
|
|
96
|
+
* 1. Every id in `obligations` is named by `brandDerivedRuleIds` — an
|
|
97
|
+
* obligation naming an id nothing derived is a stale or typo'd
|
|
98
|
+
* reference (`obligationsMissingFromRecord`).
|
|
99
|
+
* 2. Every id in `brandDerivedRuleIds` is reached by at least one
|
|
100
|
+
* obligation — a brand-derived rule id with no obligation behind it
|
|
101
|
+
* is undocumented brand intent, exactly `checkBrandCoverage`'s
|
|
102
|
+
* `slotsMissingDerivation` mirrored onto the other list
|
|
103
|
+
* (`recordRulesNotObliged`).
|
|
104
|
+
*
|
|
105
|
+
* A ONE-DIRECTIONAL CHECKER PASSES CASE 2 AND HIDES IT — this file exists
|
|
106
|
+
* specifically because a checker that only implements direction 1 (every
|
|
107
|
+
* obligation resolves) looks complete, passes every obligation-side test,
|
|
108
|
+
* and still never notices a whole brand-derived rule id nobody obliged. See
|
|
109
|
+
* `derivation-coverage.test.ts` for the fixture built to prove this file
|
|
110
|
+
* does not make that mistake.
|
|
111
|
+
*
|
|
112
|
+
* FAILS CLOSED, IDENTICAL PRECEDENT TO `checkBrandCoverage`
|
|
113
|
+
* ---------------------------------------------------------------------------
|
|
114
|
+
* A checker given nothing to check must never report the same shape as a
|
|
115
|
+
* checker that checked everything and found it clean. Both degenerate
|
|
116
|
+
* inputs are an explicit failure, never a vacuous pass:
|
|
117
|
+
*
|
|
118
|
+
* - `obligations.length === 0` — nothing was obliged at all. `ok: false`,
|
|
119
|
+
* `reason: "no-obligations-provided"`.
|
|
120
|
+
* - `brandDerivedRuleIds.length === 0` — nothing to check obligations
|
|
121
|
+
* against. Maybe the caller forgot to pass the real list, maybe a
|
|
122
|
+
* consumer genuinely derived zero voice rules from its brand; either
|
|
123
|
+
* way this function cannot tell the difference, so it cannot report
|
|
124
|
+
* success. `ok: false`, `reason: "no-brand-derived-rules-provided"`.
|
|
125
|
+
*
|
|
126
|
+
* `obligationsChecked` and `rulesChecked` are always present, in every
|
|
127
|
+
* branch, for the identical reason `checkBrandCoverage`'s own header states
|
|
128
|
+
* for `slotsChecked`/`derivationsChecked`: so "zero things were checked"
|
|
129
|
+
* and "everything checked out fine" can never be told apart by a caller who
|
|
130
|
+
* only glances at `ok`.
|
|
131
|
+
*
|
|
132
|
+
* Pure — no I/O, never throws.
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* Checks whether `obligations` fully accounts for `brandDerivedRuleIds`, in
|
|
136
|
+
* both directions, and fails closed on either empty input. Pure — no I/O,
|
|
137
|
+
* never throws. See this file's top-of-file doc comment for the seam
|
|
138
|
+
* (`brandDerivedRuleIds` is caller-supplied, since this package has no way
|
|
139
|
+
* to tell a brand-derived voice rule id from any other consumer-authored
|
|
140
|
+
* one) and for why the two degenerate inputs are failures rather than
|
|
141
|
+
* vacuous passes. See `strategy`'s `checkBrandCoverage` (which this
|
|
142
|
+
* mirrors) for the precedent.
|
|
143
|
+
*/
|
|
144
|
+
export function checkVoiceDerivationCoverage(obligations, brandDerivedRuleIds) {
|
|
145
|
+
const obligationsChecked = obligations.length;
|
|
146
|
+
const rulesChecked = brandDerivedRuleIds.length;
|
|
147
|
+
const ruleIdSet = new Set(brandDerivedRuleIds);
|
|
148
|
+
const obligedSet = new Set(obligations);
|
|
149
|
+
const obligationsMissingFromRecord = obligations.filter((id) => !ruleIdSet.has(id));
|
|
150
|
+
const recordRulesNotObliged = brandDerivedRuleIds.filter((id) => !obligedSet.has(id));
|
|
151
|
+
if (obligationsChecked === 0) {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
obligationsChecked,
|
|
155
|
+
rulesChecked,
|
|
156
|
+
obligationsMissingFromRecord,
|
|
157
|
+
recordRulesNotObliged,
|
|
158
|
+
reason: "no-obligations-provided",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (rulesChecked === 0) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
obligationsChecked,
|
|
165
|
+
rulesChecked,
|
|
166
|
+
obligationsMissingFromRecord,
|
|
167
|
+
recordRulesNotObliged,
|
|
168
|
+
reason: "no-brand-derived-rules-provided",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const ok = obligationsMissingFromRecord.length === 0 && recordRulesNotObliged.length === 0;
|
|
172
|
+
return {
|
|
173
|
+
ok,
|
|
174
|
+
obligationsChecked,
|
|
175
|
+
rulesChecked,
|
|
176
|
+
obligationsMissingFromRecord,
|
|
177
|
+
recordRulesNotObliged,
|
|
178
|
+
reason: ok ? undefined : "coverage-gap",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=derivation-coverage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derivation-coverage.js","sourceRoot":"","sources":["../../src/voice/derivation-coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoIG;AA8BH;;;;;;;;;GASG;AACH,MAAM,UAAU,4BAA4B,CAC1C,WAA8B,EAC9B,mBAAsC;IAEtC,MAAM,kBAAkB,GAAG,WAAW,CAAC,MAAM,CAAC;IAC9C,MAAM,YAAY,GAAG,mBAAmB,CAAC,MAAM,CAAC;IAEhD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;IAExC,MAAM,4BAA4B,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IACpF,MAAM,qBAAqB,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAEtF,IAAI,kBAAkB,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,kBAAkB;YAClB,YAAY;YACZ,4BAA4B;YAC5B,qBAAqB;YACrB,MAAM,EAAE,yBAAyB;SAClC,CAAC;IACJ,CAAC;IAED,IAAI,YAAY,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,kBAAkB;YAClB,YAAY;YACZ,4BAA4B;YAC5B,qBAAqB;YACrB,MAAM,EAAE,iCAAiC;SAC1C,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,GAAG,4BAA4B,CAAC,MAAM,KAAK,CAAC,IAAI,qBAAqB,CAAC,MAAM,KAAK,CAAC,CAAC;IAC3F,OAAO;QACL,EAAE;QACF,kBAAkB;QAClB,YAAY;QACZ,4BAA4B;QAC5B,qBAAqB;QACrB,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc;KACxC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bindable/fixed split for `VoiceRecord` — the `TokenDefinition.brandable`
|
|
3
|
+
* analog for this package. See `@example/ui/tokens`' `src/tokens.ts` for
|
|
4
|
+
* the model this file ports.
|
|
5
|
+
*
|
|
6
|
+
* WHERE THE LINE ACTUALLY FALLS (a finding, not an assumption)
|
|
7
|
+
* ---------------------------------------------------------------------------
|
|
8
|
+
* In `tokens`, the split runs WITHIN one flat namespace: `styles/tokens.css`
|
|
9
|
+
* declares 154 real CSS custom properties, and only 42 of them are
|
|
10
|
+
* `brandable: true` — the other 112 (spacing, the type ramp, z-index,
|
|
11
|
+
* duration, easing, breakpoints, the neutral ramp, ...) are declared with a
|
|
12
|
+
* real value and STAY that value for every brand, forever. Both the
|
|
13
|
+
* brandable and the non-brandable tokens live in the same file, the same
|
|
14
|
+
* `TOKENS` record, distinguished only by the boolean.
|
|
15
|
+
*
|
|
16
|
+
* `VoiceRecord` does not have that shape, and pretending it does would be
|
|
17
|
+
* dishonest. Every field a `VoiceRecord` instance actually carries — `id`,
|
|
18
|
+
* both rules' `description`s, `forbiddenPronouns`, `forbiddenMarkers`,
|
|
19
|
+
* `formality`, `tone`, `glossary`, `claims`, `patterns` — is content one specific
|
|
20
|
+
* consumer authors: there is no field here that ships a working default
|
|
21
|
+
* value and stays that value for every voice the way `--spacing-lg: 16px`
|
|
22
|
+
* does for every brand. `VoiceRecord` IS the consumer-owned binding layer in
|
|
23
|
+
* its entirety, the same way `brand.css` is: nothing in `brand-template.css`
|
|
24
|
+
* is "shipped, leave it alone" either — every slot IN that file is
|
|
25
|
+
* `bindable`, by construction, because only the bindable slots were ever
|
|
26
|
+
* put in the template in the first place. The non-brandable 112 in `tokens`
|
|
27
|
+
* live in `tokens.css`, a different file `brand-template.css` never touches;
|
|
28
|
+
* `VoiceRecord`'s analog of that other file is `types.ts` and `schema.ts` —
|
|
29
|
+
* TypeScript interfaces and string-literal unions, not runtime record
|
|
30
|
+
* fields at all.
|
|
31
|
+
*
|
|
32
|
+
* So the split for voice runs BETWEEN two files, not within one:
|
|
33
|
+
*
|
|
34
|
+
* FIXED (rule KINDS — never appear below, never appear in the template,
|
|
35
|
+
* shipped once in `types.ts`, identical for every consumer forever):
|
|
36
|
+
* - the shape of `VoiceRules` itself — that a voice has exactly a
|
|
37
|
+
* `person` rule, a `tense` rule, a `formality` axis, and a `tone`
|
|
38
|
+
* axis, no more, no fewer;
|
|
39
|
+
* - `FormalityLevel`'s three possible values (`"casual"`, `"neutral"`,
|
|
40
|
+
* `"formal"`) and `GlossaryStatus`'s two (`"forbidden"`,
|
|
41
|
+
* `"preferred"`) — the ENUM, not which member a given entry picks;
|
|
42
|
+
* - the field set of `PersonRule`, `TenseRule`, `GlossaryEntry`, and
|
|
43
|
+
* `Claim` — that a glossary entry has a `term`/`status`/`reason`/
|
|
44
|
+
* `alternative`/`caseSensitive` and nothing else, for instance.
|
|
45
|
+
*
|
|
46
|
+
* BINDABLE (rule VALUES — every entry below, every one `true`, all
|
|
47
|
+
* consumer-owned, all present in `voice-record.template.jsonc`):
|
|
48
|
+
* - which pronouns are forbidden, which tense markers, which specific
|
|
49
|
+
* terms are forbidden or preferred and why, which claims are
|
|
50
|
+
* registered and whether each needs a `factRef` — the actual data
|
|
51
|
+
* that fills the fixed shape above, one consumer's own.
|
|
52
|
+
*
|
|
53
|
+
* This file catalogs only the second half — the runtime fields of a
|
|
54
|
+
* `VoiceRecord` instance — because those are the only things a template can
|
|
55
|
+
* meaningfully hold a fill-in-the-blank for. It is `Readonly<Record<...>>`,
|
|
56
|
+
* every entry `bindable: true`, deliberately: `src/field-coverage.test.ts`
|
|
57
|
+
* still runs a two-way check against it (and a third, currently-vacuous but
|
|
58
|
+
* future-guarding check that no `bindable: false` entry is ever live in the
|
|
59
|
+
* template), the same shape as `tokens`' `brand-coverage.test.ts`, so that
|
|
60
|
+
* if a future field is ever added here as `bindable: false`, the existing
|
|
61
|
+
* tests immediately start meaning something rather than needing to be
|
|
62
|
+
* written from scratch.
|
|
63
|
+
*
|
|
64
|
+
* A LEAF, NOT A TREE — WHY ARRAYS STOP HERE
|
|
65
|
+
* ---------------------------------------------------------------------------
|
|
66
|
+
* `glossary` and `claims` are each catalogued as ONE path, not expanded
|
|
67
|
+
* into their per-entry fields (`glossary.term`, `glossary.reason`, ...).
|
|
68
|
+
* Both are variable-length collections of consumer-authored records — the
|
|
69
|
+
* PER-ENTRY shape (`GlossaryEntry`, `Claim`) is the FIXED half described
|
|
70
|
+
* above, already enumerated once in `types.ts`; re-stating it here as
|
|
71
|
+
* catalog entries would just be a second, driftable copy of the same
|
|
72
|
+
* interface. What's bindable about `glossary` and `claims` is that the
|
|
73
|
+
* ARRAY exists and a consumer populates it — exactly what one path each,
|
|
74
|
+
* marked `bindable: true`, records.
|
|
75
|
+
*/
|
|
76
|
+
/** One runtime field path of a `VoiceRecord` instance, and its bindable/fixed classification. */
|
|
77
|
+
export interface VoiceFieldDefinition {
|
|
78
|
+
/** Dot-path from the record root, e.g. `"rules.person.forbiddenPronouns"`. Array-valued fields stop here — see this file's header comment, "A leaf, not a tree". */
|
|
79
|
+
readonly path: string;
|
|
80
|
+
/**
|
|
81
|
+
* `true` for every entry in this catalog: a rule VALUE, consumer-owned,
|
|
82
|
+
* expected to appear in `voice-record.template.jsonc`. See this file's
|
|
83
|
+
* header comment for why no `VoiceRecord` field is ever `false` here, and
|
|
84
|
+
* where the fixed half of this package's contract actually lives instead.
|
|
85
|
+
*/
|
|
86
|
+
readonly bindable: boolean;
|
|
87
|
+
/** Human-readable note on what this slot holds. */
|
|
88
|
+
readonly description: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Every runtime field path a `VoiceRecord` instance carries, keyed by that
|
|
92
|
+
* same path. `src/field-coverage.test.ts` asserts, in both directions, that
|
|
93
|
+
* this set and `templates/voice-record.template.jsonc`'s own set of paths
|
|
94
|
+
* are identical.
|
|
95
|
+
*/
|
|
96
|
+
export declare const VOICE_FIELDS: Readonly<Record<string, VoiceFieldDefinition>>;
|
|
97
|
+
/**
|
|
98
|
+
* The literal placeholder value `voice-record.template.jsonc` uses for every
|
|
99
|
+
* bindable string slot. Loud and specific on purpose — see
|
|
100
|
+
* `checker.ts`'s `findPlaceholderPaths` for why an unmissable, near-zero-
|
|
101
|
+
* collision sentinel is exactly what makes the unbound signal trustworthy:
|
|
102
|
+
* a real voice's copy of the template that forgot to fill in even one slot
|
|
103
|
+
* still trips it, and nothing a real consumer would plausibly type by
|
|
104
|
+
* coincidence ever does.
|
|
105
|
+
*/
|
|
106
|
+
export declare const TEMPLATE_PLACEHOLDER = "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER";
|
|
107
|
+
//# sourceMappingURL=fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../../src/voice/fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AAEH,iGAAiG;AACjG,MAAM,WAAW,oBAAoB;IACnC,oKAAoK;IACpK,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,mDAAmD;IACnD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAmDvE,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,2CAA2C,CAAC"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bindable/fixed split for `VoiceRecord` — the `TokenDefinition.brandable`
|
|
3
|
+
* analog for this package. See `@example/ui/tokens`' `src/tokens.ts` for
|
|
4
|
+
* the model this file ports.
|
|
5
|
+
*
|
|
6
|
+
* WHERE THE LINE ACTUALLY FALLS (a finding, not an assumption)
|
|
7
|
+
* ---------------------------------------------------------------------------
|
|
8
|
+
* In `tokens`, the split runs WITHIN one flat namespace: `styles/tokens.css`
|
|
9
|
+
* declares 154 real CSS custom properties, and only 42 of them are
|
|
10
|
+
* `brandable: true` — the other 112 (spacing, the type ramp, z-index,
|
|
11
|
+
* duration, easing, breakpoints, the neutral ramp, ...) are declared with a
|
|
12
|
+
* real value and STAY that value for every brand, forever. Both the
|
|
13
|
+
* brandable and the non-brandable tokens live in the same file, the same
|
|
14
|
+
* `TOKENS` record, distinguished only by the boolean.
|
|
15
|
+
*
|
|
16
|
+
* `VoiceRecord` does not have that shape, and pretending it does would be
|
|
17
|
+
* dishonest. Every field a `VoiceRecord` instance actually carries — `id`,
|
|
18
|
+
* both rules' `description`s, `forbiddenPronouns`, `forbiddenMarkers`,
|
|
19
|
+
* `formality`, `tone`, `glossary`, `claims`, `patterns` — is content one specific
|
|
20
|
+
* consumer authors: there is no field here that ships a working default
|
|
21
|
+
* value and stays that value for every voice the way `--spacing-lg: 16px`
|
|
22
|
+
* does for every brand. `VoiceRecord` IS the consumer-owned binding layer in
|
|
23
|
+
* its entirety, the same way `brand.css` is: nothing in `brand-template.css`
|
|
24
|
+
* is "shipped, leave it alone" either — every slot IN that file is
|
|
25
|
+
* `bindable`, by construction, because only the bindable slots were ever
|
|
26
|
+
* put in the template in the first place. The non-brandable 112 in `tokens`
|
|
27
|
+
* live in `tokens.css`, a different file `brand-template.css` never touches;
|
|
28
|
+
* `VoiceRecord`'s analog of that other file is `types.ts` and `schema.ts` —
|
|
29
|
+
* TypeScript interfaces and string-literal unions, not runtime record
|
|
30
|
+
* fields at all.
|
|
31
|
+
*
|
|
32
|
+
* So the split for voice runs BETWEEN two files, not within one:
|
|
33
|
+
*
|
|
34
|
+
* FIXED (rule KINDS — never appear below, never appear in the template,
|
|
35
|
+
* shipped once in `types.ts`, identical for every consumer forever):
|
|
36
|
+
* - the shape of `VoiceRules` itself — that a voice has exactly a
|
|
37
|
+
* `person` rule, a `tense` rule, a `formality` axis, and a `tone`
|
|
38
|
+
* axis, no more, no fewer;
|
|
39
|
+
* - `FormalityLevel`'s three possible values (`"casual"`, `"neutral"`,
|
|
40
|
+
* `"formal"`) and `GlossaryStatus`'s two (`"forbidden"`,
|
|
41
|
+
* `"preferred"`) — the ENUM, not which member a given entry picks;
|
|
42
|
+
* - the field set of `PersonRule`, `TenseRule`, `GlossaryEntry`, and
|
|
43
|
+
* `Claim` — that a glossary entry has a `term`/`status`/`reason`/
|
|
44
|
+
* `alternative`/`caseSensitive` and nothing else, for instance.
|
|
45
|
+
*
|
|
46
|
+
* BINDABLE (rule VALUES — every entry below, every one `true`, all
|
|
47
|
+
* consumer-owned, all present in `voice-record.template.jsonc`):
|
|
48
|
+
* - which pronouns are forbidden, which tense markers, which specific
|
|
49
|
+
* terms are forbidden or preferred and why, which claims are
|
|
50
|
+
* registered and whether each needs a `factRef` — the actual data
|
|
51
|
+
* that fills the fixed shape above, one consumer's own.
|
|
52
|
+
*
|
|
53
|
+
* This file catalogs only the second half — the runtime fields of a
|
|
54
|
+
* `VoiceRecord` instance — because those are the only things a template can
|
|
55
|
+
* meaningfully hold a fill-in-the-blank for. It is `Readonly<Record<...>>`,
|
|
56
|
+
* every entry `bindable: true`, deliberately: `src/field-coverage.test.ts`
|
|
57
|
+
* still runs a two-way check against it (and a third, currently-vacuous but
|
|
58
|
+
* future-guarding check that no `bindable: false` entry is ever live in the
|
|
59
|
+
* template), the same shape as `tokens`' `brand-coverage.test.ts`, so that
|
|
60
|
+
* if a future field is ever added here as `bindable: false`, the existing
|
|
61
|
+
* tests immediately start meaning something rather than needing to be
|
|
62
|
+
* written from scratch.
|
|
63
|
+
*
|
|
64
|
+
* A LEAF, NOT A TREE — WHY ARRAYS STOP HERE
|
|
65
|
+
* ---------------------------------------------------------------------------
|
|
66
|
+
* `glossary` and `claims` are each catalogued as ONE path, not expanded
|
|
67
|
+
* into their per-entry fields (`glossary.term`, `glossary.reason`, ...).
|
|
68
|
+
* Both are variable-length collections of consumer-authored records — the
|
|
69
|
+
* PER-ENTRY shape (`GlossaryEntry`, `Claim`) is the FIXED half described
|
|
70
|
+
* above, already enumerated once in `types.ts`; re-stating it here as
|
|
71
|
+
* catalog entries would just be a second, driftable copy of the same
|
|
72
|
+
* interface. What's bindable about `glossary` and `claims` is that the
|
|
73
|
+
* ARRAY exists and a consumer populates it — exactly what one path each,
|
|
74
|
+
* marked `bindable: true`, records.
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* Every runtime field path a `VoiceRecord` instance carries, keyed by that
|
|
78
|
+
* same path. `src/field-coverage.test.ts` asserts, in both directions, that
|
|
79
|
+
* this set and `templates/voice-record.template.jsonc`'s own set of paths
|
|
80
|
+
* are identical.
|
|
81
|
+
*/
|
|
82
|
+
export const VOICE_FIELDS = {
|
|
83
|
+
id: {
|
|
84
|
+
path: "id",
|
|
85
|
+
bindable: true,
|
|
86
|
+
description: "This voice's own identifier, e.g. the product or repo it belongs to.",
|
|
87
|
+
},
|
|
88
|
+
"rules.person.description": {
|
|
89
|
+
path: "rules.person.description",
|
|
90
|
+
bindable: true,
|
|
91
|
+
description: "Human-readable statement of the person rule, e.g. \"second-person, you-voice\".",
|
|
92
|
+
},
|
|
93
|
+
"rules.person.forbiddenPronouns": {
|
|
94
|
+
path: "rules.person.forbiddenPronouns",
|
|
95
|
+
bindable: true,
|
|
96
|
+
description: "The word list `checkCopy` actually scans for — pronouns that contradict the person rule.",
|
|
97
|
+
},
|
|
98
|
+
"rules.tense.description": {
|
|
99
|
+
path: "rules.tense.description",
|
|
100
|
+
bindable: true,
|
|
101
|
+
description: "Human-readable statement of the tense rule, e.g. \"present tense, no future promises\".",
|
|
102
|
+
},
|
|
103
|
+
"rules.tense.forbiddenMarkers": {
|
|
104
|
+
path: "rules.tense.forbiddenMarkers",
|
|
105
|
+
bindable: true,
|
|
106
|
+
description: "The word list `checkCopy` actually scans for — tense markers that contradict the tense rule.",
|
|
107
|
+
},
|
|
108
|
+
"rules.formality": {
|
|
109
|
+
path: "rules.formality",
|
|
110
|
+
bindable: true,
|
|
111
|
+
description: "Which `FormalityLevel` this voice picks. Descriptive only — never read by `checkCopy`.",
|
|
112
|
+
},
|
|
113
|
+
"rules.tone": {
|
|
114
|
+
path: "rules.tone",
|
|
115
|
+
bindable: true,
|
|
116
|
+
description: "Free-text tone descriptors, e.g. [\"warm\", \"direct\", \"no jargon\"]. Descriptive only.",
|
|
117
|
+
},
|
|
118
|
+
glossary: {
|
|
119
|
+
path: "glossary",
|
|
120
|
+
bindable: true,
|
|
121
|
+
description: "This voice's own forbidden/preferred term list. Per-entry shape (`GlossaryEntry`) is fixed; the entries themselves are this consumer's own.",
|
|
122
|
+
},
|
|
123
|
+
claims: {
|
|
124
|
+
path: "claims",
|
|
125
|
+
bindable: true,
|
|
126
|
+
description: "This voice's own claims register. Per-entry shape (`Claim`) is fixed; the entries themselves are this consumer's own.",
|
|
127
|
+
},
|
|
128
|
+
patterns: {
|
|
129
|
+
path: "patterns",
|
|
130
|
+
bindable: true,
|
|
131
|
+
description: "This voice's own regex pattern rules (alternation, optional apostrophes, hard character bans). Per-entry shape (`PatternRule`) is fixed; the entries themselves are this consumer's own. Optional — a voice with no pattern rules simply omits this array (`patterns` is the one field in this catalog that is also optional at the `VoiceRecord` TYPE level, not merely defaulted at validation time — see `types.ts`'s `VoiceRecord` doc comment for why).",
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* The literal placeholder value `voice-record.template.jsonc` uses for every
|
|
136
|
+
* bindable string slot. Loud and specific on purpose — see
|
|
137
|
+
* `checker.ts`'s `findPlaceholderPaths` for why an unmissable, near-zero-
|
|
138
|
+
* collision sentinel is exactly what makes the unbound signal trustworthy:
|
|
139
|
+
* a real voice's copy of the template that forgot to fill in even one slot
|
|
140
|
+
* still trips it, and nothing a real consumer would plausibly type by
|
|
141
|
+
* coincidence ever does.
|
|
142
|
+
*/
|
|
143
|
+
export const TEMPLATE_PLACEHOLDER = "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER";
|
|
144
|
+
//# sourceMappingURL=fields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fields.js","sourceRoot":"","sources":["../../src/voice/fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AAiBH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAmD;IAC1E,EAAE,EAAE;QACF,IAAI,EAAE,IAAI;QACV,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,sEAAsE;KACpF;IACD,0BAA0B,EAAE;QAC1B,IAAI,EAAE,0BAA0B;QAChC,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,iFAAiF;KAC/F;IACD,gCAAgC,EAAE;QAChC,IAAI,EAAE,gCAAgC;QACtC,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,0FAA0F;KACxG;IACD,yBAAyB,EAAE;QACzB,IAAI,EAAE,yBAAyB;QAC/B,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,yFAAyF;KACvG;IACD,8BAA8B,EAAE;QAC9B,IAAI,EAAE,8BAA8B;QACpC,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,8FAA8F;KAC5G;IACD,iBAAiB,EAAE;QACjB,IAAI,EAAE,iBAAiB;QACvB,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,wFAAwF;KACtG;IACD,YAAY,EAAE;QACZ,IAAI,EAAE,YAAY;QAClB,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,2FAA2F;KACzG;IACD,QAAQ,EAAE;QACR,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,6IAA6I;KAC3J;IACD,MAAM,EAAE;QACN,IAAI,EAAE,QAAQ;QACd,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,uHAAuH;KACrI;IACD,QAAQ,EAAE;QACR,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,8bAA8b;KAC5c;CACF,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,wCAAwC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clossys/writer/voice — public entry point. See README.md for the full
|
|
3
|
+
* picture: the machinery-vs-values split this package mirrors from
|
|
4
|
+
* @example/ui/tokens, exactly what `checkCopy` does and does not
|
|
5
|
+
* attempt, and the `factRef` seam into a separate `strategy` package this
|
|
6
|
+
* package deliberately does not import from.
|
|
7
|
+
*/
|
|
8
|
+
export { FORMALITY_LEVELS, GLOSSARY_STATUSES, VOICE_SEVERITIES } from "./types.js";
|
|
9
|
+
export type { PersonRule, TenseRule, FormalityLevel, VoiceRules, GlossaryStatus, GlossaryEntry, Claim, VoiceChannel, VoicePattern, PatternRule, VoiceSeverity, VoiceRecord, VoiceFinding, } from "./types.js";
|
|
10
|
+
export { VOICE_FIELDS, TEMPLATE_PLACEHOLDER } from "./fields.js";
|
|
11
|
+
export type { VoiceFieldDefinition } from "./fields.js";
|
|
12
|
+
export { validateVoiceRecordShape, parseVoiceRecord } from "./schema.js";
|
|
13
|
+
export { checkCopy, auditClaimsRegister, isCiBlockingSeverity } from "./checker.js";
|
|
14
|
+
export type { VoiceCheckDimension, VoiceDimensionSkip, VoiceCheckWaiver, WaivedVoiceFinding, VoiceCheckOptions, VoiceCheckReport, } from "./checker.js";
|
|
15
|
+
export { checkVoiceDerivationCoverage } from "./derivation-coverage.js";
|
|
16
|
+
export type { VoiceDerivationCoverageFailureReason, VoiceDerivationCoverageResult } from "./derivation-coverage.js";
|
|
17
|
+
export { checkPatternSafety } from "./internal/pattern-safety.js";
|
|
18
|
+
export type { PatternSafetyIssue, PatternSafetyResult } from "./internal/pattern-safety.js";
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/voice/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnF,YAAY,EACV,UAAU,EACV,SAAS,EACT,cAAc,EACd,UAAU,EACV,cAAc,EACd,aAAa,EACb,KAAK,EACL,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,aAAa,EACb,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEzE,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpF,YAAY,EACV,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,cAAc,CAAC;AAMtB,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AACxE,YAAY,EAAE,oCAAoC,EAAE,6BAA6B,EAAE,MAAM,0BAA0B,CAAC;AAQpH,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC"}
|