@liftleadershipgroup/lifty 0.1.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # LIFTY installer
2
+
3
+ Installs the public `lifty-onboarding` skill for one supported agent at a time.
4
+
5
+ ```sh
6
+ npx --yes @liftleadershipgroup/lifty@next init
7
+ ```
8
+
9
+ Project scope is the default. Select an agent explicitly with `--agent cursor`,
10
+ `--agent claude-code`, or `--agent codex`; use `--global` only when you intend to
11
+ install into that agent's user-level skill directory.
12
+
13
+ The package embeds and verifies its onboarding bundle. Installation itself
14
+ performs no network, authentication, protected-tool, telemetry, or data-send
15
+ operation. Run `lifty status`, `lifty update`, `lifty rollback`, or
16
+ `lifty uninstall` with the same agent and scope flags to manage the
17
+ installation.
18
+
19
+ ## Account commands
20
+
21
+ Once the onboarding skill has written `<project>/.lifty/onboarding-draft.json`
22
+ with `status: "ready_for_auth"`, five agent-independent commands take over. They
23
+ accept no `--agent`, `--global`, `--dry-run`, or `--force` flag; `lifty push`
24
+ accepts `--project-dir PATH`.
25
+
26
+ ```sh
27
+ lifty login # browser handoff, stores ~/.lifty/credentials.json (0600)
28
+ lifty whoami # prints the signed-in email and Supabase Auth host
29
+ lifty push # provisions the workspace from the local onboarding draft
30
+ lifty connect hubspot # opens hosted OAuth and waits for portal connection
31
+ lifty logout # revokes the session best-effort and deletes the credentials
32
+ ```
33
+
34
+ `lifty login` starts a listener on `127.0.0.1` with an OS-assigned port, opens
35
+ `{LIFTY_APP_URL}/cli/auth?state=<nonce>&port=<port>`, and waits up to ten
36
+ minutes for the hosted page to POST `{state, access_token, refresh_token,
37
+ expires_at}` back to `/callback`. Callbacks whose `state` does not match the
38
+ nonce are rejected with 403 and the CLI keeps waiting; exactly one valid
39
+ callback is accepted. Set `LIFTY_NO_BROWSER=1`, or run where no browser can be
40
+ spawned, and the URL is printed for manual use instead. Tokens travel only in
41
+ the loopback POST body. They never appear in a URL, log line, or terminal output.
42
+
43
+ `lifty push` refuses to send anything unless the draft is a real file (never a
44
+ symlink), parses as a JSON object, and records `status: "ready_for_auth"`. It
45
+ calls `POST {LIFTY_API_URL}/v1/workspaces` with the account's JWT, prints the
46
+ returned workspace name, state, and draft digest, and leaves the draft file
47
+ byte-for-byte untouched. `lifty status` reads
48
+ `GET {LIFTY_API_URL}/v1/workspace`. Neither business request sends a Supabase
49
+ API key; Supabase remains the auth provider only. Status never fails when the
50
+ network or configuration is unavailable.
51
+
52
+ `lifty connect hubspot` requires a provisioned workspace. It asks the hosted
53
+ LIFTY API for a ten-minute, one-use connection URL, opens the DigitalOcean
54
+ handoff in the browser, and polls secret-free connection status. The CLI
55
+ accepts only an HTTPS URL on the configured API origin and prints only the
56
+ connected HubSpot portal ID. HubSpot access and refresh tokens are exchanged,
57
+ verified, encrypted, and rotated server-side; no founder environment variable
58
+ or provider credential is required.
59
+
60
+ Credentials live at `~/.lifty/credentials.json`, written atomically through a
61
+ temporary file and rename, mode 0600 inside a 0700 directory. Access tokens are
62
+ refreshed automatically when they are within sixty seconds of expiry; an
63
+ unusable refresh token produces `Your session expired. Run lifty login again.`
64
+ and nothing else.
65
+
66
+ ## Endpoint configuration
67
+
68
+ No endpoint setup is required for the hosted LIFTY environment. The package
69
+ embeds the DigitalOcean app/API ingress, Supabase project URL, and Supabase publishable key,
70
+ so a registry install can proceed directly to `lifty login`.
71
+
72
+ `LIFTY_APP_URL`, `LIFTY_API_URL`, `LIFTY_SUPABASE_URL`, and
73
+ `LIFTY_SUPABASE_PUBLISHABLE_KEY` remain explicit overrides for local development
74
+ and staged cutovers. The app URL hosts the browser handoff, the API URL handles
75
+ workspace business operations, and the Supabase values are used only for
76
+ account profile, token refresh, and logout. All three URLs must be HTTPS, except
77
+ on loopback hosts. `LIFTY_SUPABASE_ANON_KEY` remains a compatibility alias.
78
+
79
+ Each managed target has a private ownership receipt at
80
+ `<agent-config-root>/.lifty-onboarding-owner.json`. It contains only installer
81
+ identity, version, provenance, and one-way path digests. It never stores filesystem paths,
82
+ onboarding data, or credentials. The receipt prevents another profile from
83
+ force-claiming the same physical target and is removed when managed ownership
84
+ ends.
85
+
86
+ Lifecycle mutations coordinate through private, checksummed lease files in
87
+ `<agent-config-root>/.lifty-onboarding.lock`. A lease records only a random
88
+ token, process ID, user ID, and creation time. Locks whose owner process is
89
+ demonstrably dead are recovered automatically; live locks remain busy. Corrupt
90
+ or unverifiable lock metadata fails closed. If that happens, inspect the path
91
+ reported by the error and remove it manually only after confirming that no
92
+ LIFTY process is active.
@@ -0,0 +1,43 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "packageVersion": "0.1.0-next.0",
4
+ "bundleVersion": "0.1.0-next.0",
5
+ "treeDigest": "ac1693c8f0f979fbea64b2581f251dea768249cd4475c27739fcc1d9360cfe48",
6
+ "rootMode": 493,
7
+ "directories": [
8
+ {
9
+ "path": "references",
10
+ "mode": 493
11
+ },
12
+ {
13
+ "path": "scripts",
14
+ "mode": 493
15
+ }
16
+ ],
17
+ "files": [
18
+ {
19
+ "path": "SKILL.md",
20
+ "mode": 420,
21
+ "size": 6412,
22
+ "sha256": "8bc20af7ab5c5166dfd33f9dabd5335154d9d316c184048e59a37b1b478252cc"
23
+ },
24
+ {
25
+ "path": "references/interview-contract.md",
26
+ "mode": 420,
27
+ "size": 4915,
28
+ "sha256": "5ea57a996a3a7f1c559cd074cc1a45b7ff0909df19647b66e493516cb9004091"
29
+ },
30
+ {
31
+ "path": "references/onboarding-draft.schema.json",
32
+ "mode": 420,
33
+ "size": 8022,
34
+ "sha256": "e6e584a4d8c9189d6032d9ab94ed2139eb3d18ff5a8cf8274e90b3a1650ac9a9"
35
+ },
36
+ {
37
+ "path": "scripts/write-onboarding-draft.mjs",
38
+ "mode": 493,
39
+ "size": 19114,
40
+ "sha256": "368535d1f798af1246d7dd15593be91aee7fe77f402429eaa83a932c8d6d638c"
41
+ }
42
+ ]
43
+ }
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: lifty-onboarding
3
+ description: Use when a founder has installed LIFTY and wants to define one primary outbound motion, authenticate, provision the hosted workspace, and optionally connect HubSpot.
4
+ ---
5
+
6
+ # LIFTY founder onboarding
7
+
8
+ Build one founder-confirmed targeting draft, save it privately in the active
9
+ project, complete the hosted browser handoff, and provision one LIFTY workspace.
10
+
11
+ ## Boundary and completion
12
+
13
+ This flow may research public company information, interview the founder, run
14
+ the installed LIFTY CLI's hosted login, and submit the validated local draft to
15
+ the LIFTY API, and—when the founder asks for it—launch the CLI's reviewed
16
+ HubSpot OAuth handoff. It never asks for or prints tokens, manually installs
17
+ provider apps, launches lead research, activates outreach, or sends anything.
18
+ Configure one primary motion; record every secondary motion as parked.
19
+
20
+ Completion means exactly:
21
+
22
+ - `<active-project>/.lifty/onboarding-draft.json` exists with status
23
+ `ready_for_auth` and remains byte-for-byte local after submission;
24
+ - the local writer has validated the draft and privacy controls;
25
+ - hosted browser authentication has returned successfully to the CLI; and
26
+ - `lifty push` has returned workspace state `ready_for_connections`.
27
+
28
+ When HubSpot connection was part of the founder's request, completion also
29
+ requires `lifty connect hubspot` to report the secret-free portal ID as
30
+ connected. OAuth credentials remain backend-only.
31
+
32
+ Read [the interview contract](references/interview-contract.md) in full before
33
+ starting. Use [the draft schema](references/onboarding-draft.schema.json) as the
34
+ only output shape.
35
+
36
+ ## Workflow
37
+
38
+ 1. Identify the active project and the founder's company/domain. If either is
39
+ ambiguous, ask one clarifying question.
40
+ 2. Before interview questions, research public sources: the company website
41
+ and relevant subpages, public founder/company profiles, case studies,
42
+ reviews, hiring pages, and reputable news. Degrade gracefully when a source
43
+ is unavailable.
44
+ 3. Present a short research playback that opens with what could not be learned.
45
+ Label every researched value `inferred`. An inference cannot enter the
46
+ confirmed configuration until the founder explicitly confirms or corrects
47
+ it.
48
+ 4. Interview one question at a time. Wait for the answer before asking the
49
+ next question. Reject adjective-only sizing; require a numeric floor and
50
+ the founder's unit. When two operating states are bundled, separate them
51
+ and ask which is the priority. Capture the founder's CTA.
52
+ 5. When an answer changes, say what changed. The newest statement replaces the
53
+ older value; never average or merge contradictions.
54
+ 6. Run negative playback throughout: name what is excluded, weak, unknown, or
55
+ still inferred and ask the founder to correct it.
56
+ 7. Before writing, verify all eight hard gates in the interview contract,
57
+ workspace-specific Decision Maker/Influencer tells, one-rule boundary
58
+ cases, CTA, one primary motion, and parked secondary motions. Continue the
59
+ one-question loop if anything is missing.
60
+ 8. Build the JSON defined by the schema. Pass it to the bundled writer over
61
+ process stdin; do not embed draft content in command text, shell history,
62
+ logs, or telemetry:
63
+
64
+ ```text
65
+ node <skill-root>/scripts/write-onboarding-draft.mjs \
66
+ --project-dir <active-project> --input -
67
+ ```
68
+
69
+ 9. After a successful write, tell the founder that the draft is private and
70
+ local and that LIFTY will now open the hosted authentication page. Resolve
71
+ the persisted CLI as `<active-project>/.lifty/bin/lifty.mjs`; refuse to use
72
+ it if it is missing, not a real file, or resolves outside the active
73
+ project's `.lifty` directory.
74
+ 10. Run the browser handoff and wait for its result:
75
+
76
+ ```text
77
+ node <active-project>/.lifty/bin/lifty.mjs login
78
+ ```
79
+
80
+ The founder may need to sign up or sign in and approve the local CLI. Never
81
+ ask them to paste a token or callback payload. If they deny, close the page,
82
+ or the CLI times out, preserve the draft and tell them to rerun login.
83
+ 11. Only after login succeeds, submit the draft through the hosted REST API:
84
+
85
+ ```text
86
+ node <active-project>/.lifty/bin/lifty.mjs push \
87
+ --project-dir <active-project>
88
+ ```
89
+
90
+ Report only the CLI's secret-free workspace name, state, and draft digest.
91
+ Confirm that the returned state is `ready_for_connections`; otherwise stop
92
+ and report the CLI error without attempting provider setup or lead work.
93
+ 12. Tell the founder that the workspace is provisioned, the local draft was
94
+ left untouched, and outreach remains off. If HubSpot connection was part
95
+ of their request, run the persisted CLI's hosted OAuth flow:
96
+
97
+ ```text
98
+ node <active-project>/.lifty/bin/lifty.mjs connect hubspot
99
+ ```
100
+
101
+ The CLI owns the short-lived connection URL, opens it in the browser, and
102
+ waits for secret-free status. The founder approves the reviewed LIFTY app
103
+ in HubSpot; never ask for a client ID, client secret, authorization code,
104
+ access token, or refresh token. Report only the portal ID and connected
105
+ status returned by the CLI. If HubSpot was not requested, describe this as
106
+ the next explicit command and do not run it.
107
+ 13. Stop after provisioning or the requested HubSpot connection. Do not launch
108
+ lead work or outreach.
109
+
110
+ ## Hard stops
111
+
112
+ - Do not accept "mid-market", "enterprise", or another adjective as size.
113
+ - Do not promote public research because it seems credible. Founder
114
+ confirmation is the boundary.
115
+ - Do not invent an exclusion or tooling policy. Ask for the founder's explicit
116
+ judgment; an empty exclusion set is incomplete.
117
+ - Do not configure two motions. Park the secondary motion in the draft.
118
+ - Do not hand-write the destination file. The bundled writer owns validation,
119
+ symlink refusal, atomic replacement, mode `0600`, and `.lifty/.gitignore`.
120
+ - Do not ask for, paste, echo, log, or summarize access tokens, refresh tokens,
121
+ callback payloads, Supabase keys, or provider credentials.
122
+ - Do not run `push` before the writer succeeds and hosted login returns
123
+ successfully.
124
+ - Do not manually install provider apps or invent provider authorization URLs.
125
+ The only allowed provider handoff is the persisted CLI's `connect hubspot`
126
+ command after a workspace exists and HubSpot was requested.
127
+ - Do not launch lead work, enable outreach, or send anything.
@@ -0,0 +1,104 @@
1
+ # Founder interview contract
2
+
3
+ Use this sequence conversationally. Ask exactly one question, wait for the
4
+ answer, then choose the next question or follow-up. Confirmed research may skip
5
+ the corresponding question; founder-only judgments never skip.
6
+
7
+ ## Public research playback
8
+
9
+ Research first. Draft possible answers for the description, named customer or
10
+ look-alike examples, industries, geography, structural signals, and persona
11
+ titles. Each stays `inferred` until explicit founder confirmation.
12
+ When confirmed research enters the configuration, `field` is its dotted output
13
+ path and `value` exactly matches the value stored there. Corrected research
14
+ records the corrected value, never the stale inference.
15
+
16
+ Open playback with the negatives: what was unavailable, ambiguous, excluded,
17
+ or weak. Ask the founder to correct one item at a time. Do not infer numeric
18
+ bounds, exclusions, Decision Maker/Influencer roles or tells, tooling judgment,
19
+ boundary decisions, voice bans, sender intent, or CTA.
20
+
21
+ ## Interview sequence
22
+
23
+ ### 1. What the company sells
24
+
25
+ 1. Confirm a standalone description a smart outsider can understand. Reject
26
+ unexplained category jargon.
27
+ 2. Get three named example companies that represent the intended ICP. They may
28
+ be best customers or founder-confirmed look-alikes.
29
+ 3. Ask what buyers use instead today.
30
+
31
+ If the founder answers with an abstraction, recover in this order: verticals;
32
+ why those; pain; how the company solves it; three named examples. If the
33
+ founder does not know, derive a hypothesis from their best examples and ask
34
+ them to confirm or correct it. Keep it inferred until they do.
35
+
36
+ ### 2. The company that buys
37
+
38
+ 1. Ask what was operationally true about the best-fit company that made it
39
+ need the offer. The final structural signal must be publicly observable.
40
+ 2. If the answer bundles different states, split them—for example,
41
+ founder-solo versus has-reps—and ask which closes faster or is the primary
42
+ fit. Record the priority; do not silently combine them.
43
+ 3. Ask industries in and adjacent industries out. Empty exclusions are not a
44
+ completed judgment.
45
+ 4. Force size to numbers: minimum, unit, and—when the founder knows it—a
46
+ ceiling. Probe one notch below the minimum: customer or mistake?
47
+ 5. Ask for a look-alike company that appears right but must be excluded and
48
+ the hard, observable tell. Capture at least one disqualifier.
49
+ 6. Ask whether existing or competitor tooling is a positive signal, a
50
+ disqualifier, or mixed. Record the founder's rationale; never create a
51
+ policy for them.
52
+
53
+ ### 3. The person who buys
54
+
55
+ 1. Ask who champions a good deal and who makes the decision.
56
+ 2. For each persona, capture whether that person is a `decision_maker` or
57
+ `influencer` for this specific motion and the organizational tell that
58
+ proves it. Titles alone do not decide the role.
59
+ 3. Capture at least three first-contact titles for one persona. Ask which
60
+ similar titles waste time and who actually replies at the target size.
61
+
62
+ ### 4. Signals and boundary cases
63
+
64
+ Ask what public evidence makes an account timely and what separates the best
65
+ ten from fifty plausible accounts. Translate private judgments into observable
66
+ proxies and confirm the translation.
67
+
68
+ Create two or three cases from the founder's hard rules. Each case changes
69
+ exactly one hard rule while holding all others constant. Ask for `A`, `B`, or
70
+ `pass` and one-line reasoning. Record any refinement as a new statement; the
71
+ newest statement replaces the old one.
72
+
73
+ ### 5. Voice and motion
74
+
75
+ 1. Ask what outreach the founder deletes instantly. Capture at least one
76
+ forbidden move and the positive voice adjectives they want protected.
77
+ 2. Ask whose voice the writing represents and how that sender asks for a
78
+ meeting. Capture an explicit CTA.
79
+ 3. Confirm one primary motion. Name each secondary motion, mark it `parked`,
80
+ and do not interview or configure it further.
81
+
82
+ ## Negative close
83
+
84
+ Play back the complete configuration in founder language. Start with what is
85
+ out, weak, unknown, parked, or still inferred. Invite corrections one at a
86
+ time. If a correction contradicts an earlier answer, announce the replacement
87
+ and retain the sequence in `founder_statement_history`; never average.
88
+
89
+ ## Eight hard gates
90
+
91
+ Keep interviewing until every item exists:
92
+
93
+ 1. Standalone company description without unexplained jargon.
94
+ 2. Three named example companies matching the ICP.
95
+ 3. Numeric size floor plus the founder's unit.
96
+ 4. At least one hard disqualifier.
97
+ 5. At least one publicly observable structural buy signal.
98
+ 6. At least one persona with three or more titles.
99
+ 7. Explicit founder judgment of tooling as signal, disqualifier, or mixed.
100
+ 8. At least one tone forbidden move.
101
+
102
+ The JSON writer also requires industries in/out, persona roles and tells,
103
+ voice adjectives, sender intent, CTA, negative playback, and one-rule boundary
104
+ cases before it will produce `ready_for_auth`.
@@ -0,0 +1,266 @@
1
+ {
2
+ "title": "LIFTY pre-authentication onboarding draft",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "required": [
6
+ "schema_version",
7
+ "status",
8
+ "company",
9
+ "primary_motion",
10
+ "parked_secondary_motions",
11
+ "icp",
12
+ "personas",
13
+ "voice",
14
+ "boundary_cases",
15
+ "negative_playback",
16
+ "research_findings",
17
+ "founder_statement_history"
18
+ ],
19
+ "properties": {
20
+ "schema_version": {"const": "1.0"},
21
+ "status": {"const": "ready_for_auth"},
22
+ "company": {
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "required": ["name", "description", "example_companies"],
26
+ "properties": {
27
+ "name": {"type": "string", "minLength": 1},
28
+ "description": {"type": "string", "minLength": 20},
29
+ "example_companies": {
30
+ "type": "array",
31
+ "minItems": 3,
32
+ "uniqueItems": true,
33
+ "items": {"type": "string", "minLength": 1}
34
+ }
35
+ }
36
+ },
37
+ "primary_motion": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "required": ["name", "outcome"],
41
+ "properties": {
42
+ "name": {"type": "string", "minLength": 1},
43
+ "outcome": {"type": "string", "minLength": 1}
44
+ }
45
+ },
46
+ "parked_secondary_motions": {
47
+ "type": "array",
48
+ "items": {
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "required": ["name", "status"],
52
+ "properties": {
53
+ "name": {"type": "string", "minLength": 1},
54
+ "status": {"const": "parked"}
55
+ }
56
+ }
57
+ },
58
+ "icp": {
59
+ "type": "object",
60
+ "additionalProperties": false,
61
+ "required": [
62
+ "industries_in",
63
+ "industries_out",
64
+ "size",
65
+ "hard_disqualifiers",
66
+ "structural_buy_signals",
67
+ "tooling_judgment",
68
+ "operating_state_split"
69
+ ],
70
+ "properties": {
71
+ "industries_in": {
72
+ "type": "array",
73
+ "minItems": 1,
74
+ "items": {"type": "string", "minLength": 1}
75
+ },
76
+ "industries_out": {
77
+ "type": "array",
78
+ "minItems": 1,
79
+ "items": {"type": "string", "minLength": 1}
80
+ },
81
+ "size": {
82
+ "type": "object",
83
+ "additionalProperties": false,
84
+ "required": ["floor", "ceiling", "unit"],
85
+ "properties": {
86
+ "floor": {"type": "number", "exclusiveMinimum": 0},
87
+ "ceiling": {"type": ["number", "null"], "exclusiveMinimum": 0},
88
+ "unit": {"type": "string", "minLength": 1}
89
+ }
90
+ },
91
+ "hard_disqualifiers": {
92
+ "type": "array",
93
+ "minItems": 1,
94
+ "items": {"type": "string", "minLength": 1}
95
+ },
96
+ "structural_buy_signals": {
97
+ "type": "array",
98
+ "minItems": 1,
99
+ "items": {
100
+ "type": "object",
101
+ "additionalProperties": false,
102
+ "required": ["signal", "observable_via"],
103
+ "properties": {
104
+ "signal": {"type": "string", "minLength": 1},
105
+ "observable_via": {"type": "string", "minLength": 1}
106
+ }
107
+ }
108
+ },
109
+ "tooling_judgment": {
110
+ "type": "object",
111
+ "additionalProperties": false,
112
+ "required": ["classification", "rationale"],
113
+ "properties": {
114
+ "classification": {
115
+ "enum": ["signal", "disqualifier", "mixed"]
116
+ },
117
+ "rationale": {"type": "string", "minLength": 1}
118
+ }
119
+ },
120
+ "operating_state_split": {
121
+ "type": ["object", "null"],
122
+ "additionalProperties": false,
123
+ "required": ["states", "priority"],
124
+ "properties": {
125
+ "states": {
126
+ "type": "array",
127
+ "minItems": 2,
128
+ "items": {
129
+ "type": "object",
130
+ "additionalProperties": false,
131
+ "required": ["name", "fit"],
132
+ "properties": {
133
+ "name": {"type": "string", "minLength": 1},
134
+ "fit": {"type": "string", "minLength": 1}
135
+ }
136
+ }
137
+ },
138
+ "priority": {"type": "string", "minLength": 1}
139
+ }
140
+ }
141
+ }
142
+ },
143
+ "personas": {
144
+ "type": "array",
145
+ "minItems": 1,
146
+ "items": {
147
+ "type": "object",
148
+ "additionalProperties": false,
149
+ "required": ["name", "role", "titles", "tell"],
150
+ "properties": {
151
+ "name": {"type": "string", "minLength": 1},
152
+ "role": {"enum": ["decision_maker", "influencer"]},
153
+ "titles": {
154
+ "type": "array",
155
+ "minItems": 3,
156
+ "uniqueItems": true,
157
+ "items": {"type": "string", "minLength": 1}
158
+ },
159
+ "tell": {"type": "string", "minLength": 1}
160
+ }
161
+ }
162
+ },
163
+ "voice": {
164
+ "type": "object",
165
+ "additionalProperties": false,
166
+ "required": ["adjectives", "forbidden_moves", "sender_intent", "cta"],
167
+ "properties": {
168
+ "adjectives": {
169
+ "type": "array",
170
+ "minItems": 1,
171
+ "items": {"type": "string", "minLength": 1}
172
+ },
173
+ "forbidden_moves": {
174
+ "type": "array",
175
+ "minItems": 1,
176
+ "items": {"type": "string", "minLength": 1}
177
+ },
178
+ "sender_intent": {"type": "string", "minLength": 1},
179
+ "cta": {"type": "string", "minLength": 1}
180
+ }
181
+ },
182
+ "boundary_cases": {
183
+ "type": "array",
184
+ "minItems": 2,
185
+ "items": {
186
+ "type": "object",
187
+ "additionalProperties": false,
188
+ "required": [
189
+ "rule_under_test",
190
+ "all_other_hard_rules_held",
191
+ "scenario",
192
+ "decision",
193
+ "reason"
194
+ ],
195
+ "properties": {
196
+ "rule_under_test": {"type": "string", "minLength": 1},
197
+ "all_other_hard_rules_held": {"const": true},
198
+ "scenario": {"type": "string", "minLength": 1},
199
+ "decision": {"enum": ["A", "B", "pass"]},
200
+ "reason": {"type": "string", "minLength": 1}
201
+ }
202
+ }
203
+ },
204
+ "negative_playback": {
205
+ "type": "array",
206
+ "minItems": 1,
207
+ "items": {
208
+ "type": "object",
209
+ "additionalProperties": false,
210
+ "required": ["omission_or_weak_signal", "founder_response", "result"],
211
+ "properties": {
212
+ "omission_or_weak_signal": {"type": "string", "minLength": 1},
213
+ "founder_response": {"type": "string", "minLength": 1},
214
+ "result": {"type": "string", "minLength": 1}
215
+ }
216
+ }
217
+ },
218
+ "research_findings": {
219
+ "type": "array",
220
+ "items": {
221
+ "type": "object",
222
+ "additionalProperties": false,
223
+ "required": [
224
+ "field",
225
+ "value",
226
+ "source",
227
+ "state",
228
+ "used_in_configuration",
229
+ "founder_confirmation"
230
+ ],
231
+ "properties": {
232
+ "field": {
233
+ "type": "string",
234
+ "minLength": 1,
235
+ "description": "Dotted path to the real configured output field when used_in_configuration is true."
236
+ },
237
+ "value": {
238
+ "description": "Must deep-equal the configured field resolved from field when used_in_configuration is true."
239
+ },
240
+ "source": {"type": "string", "minLength": 1},
241
+ "state": {
242
+ "enum": ["inferred", "founder_confirmed", "founder_corrected"]
243
+ },
244
+ "used_in_configuration": {
245
+ "type": "boolean",
246
+ "description": "True only after founder confirmation or correction and exact placement at field."
247
+ },
248
+ "founder_confirmation": {"type": ["string", "null"]}
249
+ }
250
+ }
251
+ },
252
+ "founder_statement_history": {
253
+ "type": "array",
254
+ "items": {
255
+ "type": "object",
256
+ "additionalProperties": false,
257
+ "required": ["sequence", "field", "value"],
258
+ "properties": {
259
+ "sequence": {"type": "integer", "minimum": 1},
260
+ "field": {"type": "string", "minLength": 1},
261
+ "value": {}
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }