@deftai/directive-content 0.59.0 → 0.60.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/.githooks/pre-push +10 -9
- package/Taskfile.yml +48 -58
- package/UPGRADING.md +1 -1
- package/docs/assets/directive-lifecycle-diagram.png +0 -0
- package/docs/directive-lifecycle.md +73 -0
- package/docs/getting-started.md +5 -1
- package/package.json +3 -3
- package/packs/skills/skills-pack-0.1.json +22 -22
- package/scm/github.md +20 -2
- package/tasks/change.yml +16 -31
- package/tasks/ci.yml +8 -0
- package/tasks/commit.yml +12 -19
- package/tasks/core.yml +10 -0
- package/tasks/engine.yml +42 -0
- package/tasks/framework.yml +3 -0
- package/tasks/install.yml +20 -19
- package/tasks/migrate.yml +26 -15
- package/tasks/project.yml +16 -0
- package/tasks/toolchain.yml +15 -5
- package/tasks/vbrief.yml +4 -3
- package/tasks/verify.yml +12 -14
- package/scripts/_agents_md.py +0 -494
- package/scripts/_cache_fetch.py +0 -635
- package/scripts/_cache_quota.py +0 -529
- package/scripts/_cache_refresh.py +0 -163
- package/scripts/_cache_validate.py +0 -209
- package/scripts/_content_root.py +0 -42
- package/scripts/_doctor_state.py +0 -277
- package/scripts/_event_detect.py +0 -305
- package/scripts/_events.py +0 -514
- package/scripts/_lifecycle_hygiene.py +0 -568
- package/scripts/_pathspec.py +0 -91
- package/scripts/_policy_show_cli.py +0 -266
- package/scripts/_precutover.py +0 -92
- package/scripts/_project_context.py +0 -224
- package/scripts/_project_definition_io.py +0 -164
- package/scripts/_relocate_snapshot.py +0 -209
- package/scripts/_relocate_states.py +0 -343
- package/scripts/_resolve_preflight_path.py +0 -152
- package/scripts/_safe_subprocess.py +0 -167
- package/scripts/_session_start_hook.py +0 -205
- package/scripts/_sor_gate_diff.py +0 -365
- package/scripts/_stdio_utf8.py +0 -59
- package/scripts/_triage_bootstrap_gitignore.py +0 -904
- package/scripts/_triage_classify_cli.py +0 -122
- package/scripts/_triage_queue_cli.py +0 -625
- package/scripts/_triage_scope_cli.py +0 -343
- package/scripts/_triage_scope_drift_cli.py +0 -121
- package/scripts/_triage_scope_ignores.py +0 -286
- package/scripts/_triage_scope_milestone.py +0 -432
- package/scripts/_triage_scope_mutations.py +0 -337
- package/scripts/_triage_scope_renderers.py +0 -207
- package/scripts/_triage_smoketest_stages.py +0 -674
- package/scripts/_triage_subscribe_cli.py +0 -140
- package/scripts/_triage_welcome_cli.py +0 -421
- package/scripts/_vbrief_build.py +0 -239
- package/scripts/_vbrief_fidelity.py +0 -479
- package/scripts/_vbrief_legacy.py +0 -589
- package/scripts/_vbrief_reconciliation.py +0 -883
- package/scripts/_vbrief_routing.py +0 -277
- package/scripts/_vbrief_safety.py +0 -778
- package/scripts/_vbrief_sources.py +0 -312
- package/scripts/_vbrief_speckit.py +0 -262
- package/scripts/_vbrief_story_quality.py +0 -353
- package/scripts/_vbrief_validation.py +0 -299
- package/scripts/build_dist.py +0 -412
- package/scripts/cache.py +0 -1078
- package/scripts/cache_scanner.py +0 -745
- package/scripts/candidates_log.py +0 -432
- package/scripts/capacity_backfill.py +0 -680
- package/scripts/capacity_show.py +0 -653
- package/scripts/ci_local.py +0 -689
- package/scripts/code_structure_validate.py +0 -765
- package/scripts/codebase_default_extractor.py +0 -495
- package/scripts/codebase_map.py +0 -304
- package/scripts/codebase_map_fresh.py +0 -104
- package/scripts/codebase_projection_registry.py +0 -94
- package/scripts/codebase_provider.py +0 -582
- package/scripts/doctor.py +0 -2552
- package/scripts/framework_commands.py +0 -505
- package/scripts/gh_rest.py +0 -882
- package/scripts/github_auth_modes.py +0 -437
- package/scripts/github_body.py +0 -292
- package/scripts/ip_risk.py +0 -531
- package/scripts/issue_emit.py +0 -670
- package/scripts/issue_ingest.py +0 -1064
- package/scripts/migrate_preflight.py +0 -418
- package/scripts/migrate_vbrief.py +0 -2677
- package/scripts/monitor_pr.py +0 -401
- package/scripts/pack_migrate_lessons.py +0 -336
- package/scripts/pack_migrate_patterns.py +0 -254
- package/scripts/pack_migrate_rules.py +0 -350
- package/scripts/pack_migrate_skills.py +0 -423
- package/scripts/pack_migrate_strategies.py +0 -311
- package/scripts/pack_migrate_swarm_spec.py +0 -250
- package/scripts/pack_render.py +0 -434
- package/scripts/packs_slice.py +0 -712
- package/scripts/platform_capabilities.py +0 -336
- package/scripts/policy.py +0 -2826
- package/scripts/policy_set.py +0 -324
- package/scripts/pr_check_closing_keywords.py +0 -524
- package/scripts/pr_check_protected_issues.py +0 -267
- package/scripts/pr_merge_readiness.py +0 -1004
- package/scripts/pr_wait_mergeable.py +0 -669
- package/scripts/prd_render.py +0 -159
- package/scripts/preflight_architecture_sor.py +0 -974
- package/scripts/preflight_branch.py +0 -289
- package/scripts/preflight_cache.py +0 -974
- package/scripts/preflight_gh.py +0 -721
- package/scripts/preflight_implementation.py +0 -272
- package/scripts/preflight_story_start.py +0 -838
- package/scripts/preflight_wip_cap.py +0 -149
- package/scripts/probe_session.py +0 -545
- package/scripts/project_render.py +0 -293
- package/scripts/quarantine_ext.py +0 -237
- package/scripts/reconcile_issues.py +0 -1442
- package/scripts/refresh-path.ps1 +0 -107
- package/scripts/release.py +0 -2030
- package/scripts/release_e2e.py +0 -1011
- package/scripts/release_publish.py +0 -486
- package/scripts/release_rollback.py +0 -980
- package/scripts/relocate.py +0 -1034
- package/scripts/resolve_changelog_unreleased.py +0 -667
- package/scripts/resolve_version.py +0 -490
- package/scripts/resume_conditions.py +0 -706
- package/scripts/ritual_sentinel.py +0 -609
- package/scripts/roadmap_render.py +0 -635
- package/scripts/rule_ownership_lint.py +0 -325
- package/scripts/scm.py +0 -591
- package/scripts/scope_audit_log.py +0 -387
- package/scripts/scope_decompose.py +0 -654
- package/scripts/scope_demote.py +0 -509
- package/scripts/scope_lifecycle.py +0 -1126
- package/scripts/scope_undo.py +0 -772
- package/scripts/session_start.py +0 -406
- package/scripts/setup_ghx.py +0 -339
- package/scripts/setup_windows.ps1 +0 -220
- package/scripts/slice_audit.py +0 -585
- package/scripts/slice_record.py +0 -530
- package/scripts/slice_record_existing.py +0 -692
- package/scripts/slug_normalize.py +0 -178
- package/scripts/spec_render.py +0 -477
- package/scripts/spec_validate.py +0 -238
- package/scripts/subagent_monitor.py +0 -658
- package/scripts/swarm_complete_cohort.py +0 -644
- package/scripts/swarm_launch.py +0 -1206
- package/scripts/swarm_readiness.py +0 -554
- package/scripts/swarm_verify_review_clean.py +0 -438
- package/scripts/swarm_worktrees.py +0 -497
- package/scripts/toolchain-check.py +0 -52
- package/scripts/triage_actions.py +0 -871
- package/scripts/triage_bootstrap.py +0 -1153
- package/scripts/triage_bulk.py +0 -630
- package/scripts/triage_classify.py +0 -932
- package/scripts/triage_help.py +0 -1685
- package/scripts/triage_queue.py +0 -1944
- package/scripts/triage_reconcile.py +0 -581
- package/scripts/triage_refresh.py +0 -643
- package/scripts/triage_scope.py +0 -999
- package/scripts/triage_scope_drift.py +0 -575
- package/scripts/triage_smoketest.py +0 -396
- package/scripts/triage_subscribe.py +0 -399
- package/scripts/triage_summary.py +0 -1011
- package/scripts/triage_welcome.py +0 -1178
- package/scripts/ts_check_lane.py +0 -86
- package/scripts/validate-links.py +0 -64
- package/scripts/validate_strategy_output.py +0 -212
- package/scripts/vbrief_activate.py +0 -228
- package/scripts/vbrief_migrate_conformance.py +0 -368
- package/scripts/vbrief_reconcile_graph.py +0 -306
- package/scripts/vbrief_reconcile_labels.py +0 -460
- package/scripts/vbrief_reconcile_umbrellas.py +0 -741
- package/scripts/vbrief_validate.py +0 -1144
- package/scripts/verify-stubs.py +0 -61
- package/scripts/verify_capacity.py +0 -160
- package/scripts/verify_encoding.py +0 -699
- package/scripts/verify_hooks_installed.py +0 -206
- package/scripts/verify_investigation.py +0 -360
- package/scripts/verify_judgment_gates.py +0 -827
- package/scripts/verify_no_task_runtime.py +0 -171
- package/scripts/verify_scm_boundary.py +0 -509
- package/scripts/verify_session_ritual.py +0 -389
- package/scripts/verify_tools.py +0 -426
- package/scripts/verify_vbrief_conformance.py +0 -478
package/scripts/ip_risk.py
DELETED
|
@@ -1,531 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""ip_risk.py -- Permissive heuristic for detecting third-party intellectual
|
|
3
|
-
property (IP) references in interview / research text (#738).
|
|
4
|
-
|
|
5
|
-
The detector is intentionally permissive: it errs on the side of false
|
|
6
|
-
positives so that downstream interview steps can ask the user the
|
|
7
|
-
monetization-intent question rather than silently letting an IP-adjacent
|
|
8
|
-
project sail through PRD / SPECIFICATION generation without legal-risk
|
|
9
|
-
flagging.
|
|
10
|
-
|
|
11
|
-
Usage::
|
|
12
|
-
|
|
13
|
-
from ip_risk import detect_ip_terms, ip_risk_scope_items, plain_risk_summary
|
|
14
|
-
|
|
15
|
-
hits = detect_ip_terms("A Magic: The Gathering deck-builder app")
|
|
16
|
-
if hits:
|
|
17
|
-
items = ip_risk_scope_items(monetization_intent="commercial")
|
|
18
|
-
summary = plain_risk_summary(hits, monetization_intent="commercial")
|
|
19
|
-
|
|
20
|
-
The full guidance (heuristic categories, question script, minimum-protection
|
|
21
|
-
checklist) lives in ``references/ip-risk.md``. Keep the term lists in this
|
|
22
|
-
module synchronised with that document.
|
|
23
|
-
|
|
24
|
-
Exit codes (CLI mode):
|
|
25
|
-
|
|
26
|
-
0 -- no IP terms detected
|
|
27
|
-
1 -- IP terms detected (one term per line on stdout)
|
|
28
|
-
2 -- usage error (no input provided)
|
|
29
|
-
|
|
30
|
-
This module is *advisory*. It does NOT provide legal advice and MUST NOT be
|
|
31
|
-
used as a substitute for lawyer consultation when the project is commercial
|
|
32
|
-
and IP-adjacent.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
from __future__ import annotations
|
|
36
|
-
|
|
37
|
-
import re
|
|
38
|
-
import sys
|
|
39
|
-
from collections.abc import Iterable
|
|
40
|
-
from dataclasses import dataclass
|
|
41
|
-
|
|
42
|
-
# ---------------------------------------------------------------------------
|
|
43
|
-
# Heuristic term lists (#738)
|
|
44
|
-
# ---------------------------------------------------------------------------
|
|
45
|
-
# Curated permissive set keyed by category. All matching is case-insensitive
|
|
46
|
-
# and word-boundary based -- substring matches inside a longer word
|
|
47
|
-
# ("magicwand", "starcraft") do NOT trigger.
|
|
48
|
-
#
|
|
49
|
-
# The lists are intentionally short and well-known. The heuristic is meant to
|
|
50
|
-
# catch the *most common* IP-adjacent project ideas that come up in interview
|
|
51
|
-
# sessions (the #151 playtester case was a Magic: The Gathering deck builder),
|
|
52
|
-
# not to be a comprehensive trademark database.
|
|
53
|
-
|
|
54
|
-
_BRANDED_GAMES_AND_UNIVERSES: tuple[str, ...] = (
|
|
55
|
-
# Tabletop / collectable card games
|
|
56
|
-
"Magic: The Gathering",
|
|
57
|
-
"Magic the Gathering",
|
|
58
|
-
"MTG",
|
|
59
|
-
"Yu-Gi-Oh",
|
|
60
|
-
"Yugioh",
|
|
61
|
-
"Pokemon",
|
|
62
|
-
"Pokémon",
|
|
63
|
-
"Dungeons and Dragons",
|
|
64
|
-
"Dungeons & Dragons",
|
|
65
|
-
"D&D",
|
|
66
|
-
"Warhammer",
|
|
67
|
-
# Video game franchises
|
|
68
|
-
"Mario",
|
|
69
|
-
"Zelda",
|
|
70
|
-
"Final Fantasy",
|
|
71
|
-
"Halo",
|
|
72
|
-
"Call of Duty",
|
|
73
|
-
"Fortnite",
|
|
74
|
-
"Minecraft",
|
|
75
|
-
"Roblox",
|
|
76
|
-
"League of Legends",
|
|
77
|
-
"World of Warcraft",
|
|
78
|
-
"WoW",
|
|
79
|
-
"Overwatch",
|
|
80
|
-
"Counter-Strike",
|
|
81
|
-
"Valorant",
|
|
82
|
-
"Apex Legends",
|
|
83
|
-
# Fictional universes
|
|
84
|
-
"Star Wars",
|
|
85
|
-
"Star Trek",
|
|
86
|
-
"Marvel",
|
|
87
|
-
"DC Comics",
|
|
88
|
-
"Harry Potter",
|
|
89
|
-
"Lord of the Rings",
|
|
90
|
-
"Middle-earth",
|
|
91
|
-
"Game of Thrones",
|
|
92
|
-
"Westeros",
|
|
93
|
-
"Disney",
|
|
94
|
-
"Pixar",
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
_BRANDED_CHARACTERS: tuple[str, ...] = (
|
|
98
|
-
"Mickey Mouse",
|
|
99
|
-
"Spider-Man",
|
|
100
|
-
"Spiderman",
|
|
101
|
-
"Batman",
|
|
102
|
-
"Superman",
|
|
103
|
-
"Wonder Woman",
|
|
104
|
-
"Iron Man",
|
|
105
|
-
"Hulk",
|
|
106
|
-
"Captain America",
|
|
107
|
-
"Pikachu",
|
|
108
|
-
"Sonic the Hedgehog",
|
|
109
|
-
"Luigi",
|
|
110
|
-
"Princess Peach",
|
|
111
|
-
"Kirby",
|
|
112
|
-
"Master Chief",
|
|
113
|
-
"Lara Croft",
|
|
114
|
-
"Indiana Jones",
|
|
115
|
-
"James Bond",
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
_SPORTS_LEAGUES: tuple[str, ...] = (
|
|
119
|
-
"NFL",
|
|
120
|
-
"NBA",
|
|
121
|
-
"MLB",
|
|
122
|
-
"NHL",
|
|
123
|
-
"MLS",
|
|
124
|
-
"FIFA",
|
|
125
|
-
"UEFA",
|
|
126
|
-
"Premier League",
|
|
127
|
-
"La Liga",
|
|
128
|
-
"Bundesliga",
|
|
129
|
-
"Olympics",
|
|
130
|
-
"Olympic Games",
|
|
131
|
-
"Super Bowl",
|
|
132
|
-
"World Cup",
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
_BRANDED_PRODUCTS: tuple[str, ...] = (
|
|
136
|
-
"iPhone",
|
|
137
|
-
"iPad",
|
|
138
|
-
"MacBook",
|
|
139
|
-
"AirPods",
|
|
140
|
-
"PlayStation",
|
|
141
|
-
"Xbox",
|
|
142
|
-
"Nintendo Switch",
|
|
143
|
-
"Coca-Cola",
|
|
144
|
-
"Pepsi",
|
|
145
|
-
"Starbucks",
|
|
146
|
-
"McDonald's",
|
|
147
|
-
"Lego",
|
|
148
|
-
"Barbie",
|
|
149
|
-
"Hot Wheels",
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
_MUSIC_AND_FILM: tuple[str, ...] = (
|
|
153
|
-
"Taylor Swift",
|
|
154
|
-
"Beyonce",
|
|
155
|
-
"Beyoncé",
|
|
156
|
-
# NOTE: "BTS" and "Drake" were removed (Greptile P2 #775) -- both
|
|
157
|
-
# false-positive heavily on common technical / proper-noun uses
|
|
158
|
-
# (BTS = Build-Test-Ship / Behind-The-Scenes / bug-tracking;
|
|
159
|
-
# Drake = the duck, the surname, Drake University, Sir Francis Drake,
|
|
160
|
-
# the Drake equation). Re-add only with a music-specific surrounding
|
|
161
|
-
# context check.
|
|
162
|
-
"Spotify",
|
|
163
|
-
"Netflix",
|
|
164
|
-
"Hulu",
|
|
165
|
-
"HBO",
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
# Generic fictional-universe terms that often signal IP-adjacency even
|
|
169
|
-
# without a specific brand name. Conservative list -- single common nouns
|
|
170
|
-
# like "wizard" are NOT included to avoid pathological false positives.
|
|
171
|
-
_FICTIONAL_UNIVERSE_TERMS: tuple[str, ...] = (
|
|
172
|
-
"Hogwarts",
|
|
173
|
-
"Jedi",
|
|
174
|
-
"Sith",
|
|
175
|
-
"Death Star",
|
|
176
|
-
"Hobbit",
|
|
177
|
-
"Vulcan",
|
|
178
|
-
"Klingon",
|
|
179
|
-
"Mandalorian",
|
|
180
|
-
"Force-sensitive",
|
|
181
|
-
"Muggle",
|
|
182
|
-
"Quidditch",
|
|
183
|
-
"Tatooine",
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
_CATEGORIES: dict[str, tuple[str, ...]] = {
|
|
187
|
-
"branded-game-or-universe": _BRANDED_GAMES_AND_UNIVERSES,
|
|
188
|
-
"branded-character": _BRANDED_CHARACTERS,
|
|
189
|
-
"sports-league": _SPORTS_LEAGUES,
|
|
190
|
-
"branded-product": _BRANDED_PRODUCTS,
|
|
191
|
-
"music-or-film": _MUSIC_AND_FILM,
|
|
192
|
-
"fictional-universe-term": _FICTIONAL_UNIVERSE_TERMS,
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
@dataclass(frozen=True)
|
|
197
|
-
class IPHit:
|
|
198
|
-
"""A single IP-term detection hit."""
|
|
199
|
-
|
|
200
|
-
term: str
|
|
201
|
-
category: str
|
|
202
|
-
|
|
203
|
-
def __str__(self) -> str: # pragma: no cover - trivial
|
|
204
|
-
return f"{self.term} ({self.category})"
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
# Pre-compile a regex per category for fast scanning. Each term is escaped
|
|
208
|
-
# and wrapped in word boundaries so substring matches inside a longer word
|
|
209
|
-
# do not trigger ("magicwand" should NOT match "Magic").
|
|
210
|
-
def _compile_category(terms: Iterable[str]) -> re.Pattern[str]:
|
|
211
|
-
# Use a non-word-char lookaround that also tolerates leading/trailing
|
|
212
|
-
# punctuation common in titles (e.g. "Magic: The Gathering" appearing
|
|
213
|
-
# mid-sentence). Word boundaries (\b) work for ASCII; we accept the
|
|
214
|
-
# ASCII-only behavior for the heuristic.
|
|
215
|
-
escaped = [re.escape(term) for term in terms]
|
|
216
|
-
pattern = r"(?i)(?<!\w)(?:" + "|".join(escaped) + r")(?!\w)"
|
|
217
|
-
return re.compile(pattern)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
_CATEGORY_PATTERNS: dict[str, re.Pattern[str]] = {
|
|
221
|
-
category: _compile_category(terms) for category, terms in _CATEGORIES.items()
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def detect_ip_terms(text: str) -> list[IPHit]:
|
|
226
|
-
"""Scan *text* for known IP terms; return a deduplicated list of hits.
|
|
227
|
-
|
|
228
|
-
The scan is permissive (case-insensitive, word-boundary based) and
|
|
229
|
-
intentionally err on the side of false positives. An empty list means
|
|
230
|
-
"no known IP terms detected" -- it does NOT mean "this project is
|
|
231
|
-
free of IP risk", since the heuristic only knows about the curated
|
|
232
|
-
term lists above.
|
|
233
|
-
|
|
234
|
-
Hits are deduplicated by ``(term, category)`` pair preserving first
|
|
235
|
-
appearance order. The original surface form from *text* is preserved
|
|
236
|
-
(e.g. ``"magic the gathering"`` keeps its lowercase form even though
|
|
237
|
-
the canonical term is ``"Magic the Gathering"``).
|
|
238
|
-
"""
|
|
239
|
-
if not text:
|
|
240
|
-
return []
|
|
241
|
-
seen: set[tuple[str, str]] = set()
|
|
242
|
-
hits: list[IPHit] = []
|
|
243
|
-
for category, pattern in _CATEGORY_PATTERNS.items():
|
|
244
|
-
for match in pattern.finditer(text):
|
|
245
|
-
term = match.group(0)
|
|
246
|
-
key = (term.lower(), category)
|
|
247
|
-
if key in seen:
|
|
248
|
-
continue
|
|
249
|
-
seen.add(key)
|
|
250
|
-
hits.append(IPHit(term=term, category=category))
|
|
251
|
-
return hits
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def is_ip_adjacent(text: str) -> bool:
|
|
255
|
-
"""Return True iff *text* contains at least one detected IP term."""
|
|
256
|
-
return bool(detect_ip_terms(text))
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
# ---------------------------------------------------------------------------
|
|
260
|
-
# Monetization-intent branching + scope-item generation (#738)
|
|
261
|
-
# ---------------------------------------------------------------------------
|
|
262
|
-
|
|
263
|
-
_VALID_INTENTS: frozenset[str] = frozenset({"personal", "commercial", "unknown"})
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def _validate_intent(intent: str) -> str:
|
|
267
|
-
"""Normalise and validate a monetization-intent value.
|
|
268
|
-
|
|
269
|
-
Accepts ``"personal"``, ``"commercial"``, or ``"unknown"`` (case-
|
|
270
|
-
insensitive). Anything else raises ``ValueError`` -- the interview
|
|
271
|
-
flow MUST capture an explicit answer when IP is detected, so an
|
|
272
|
-
unrecognised intent here indicates a programming error in the caller.
|
|
273
|
-
"""
|
|
274
|
-
if not isinstance(intent, str):
|
|
275
|
-
raise ValueError(f"monetization_intent must be a string, got {type(intent)!r}")
|
|
276
|
-
normalized = intent.strip().lower()
|
|
277
|
-
if normalized not in _VALID_INTENTS:
|
|
278
|
-
raise ValueError(
|
|
279
|
-
f"monetization_intent {intent!r} not in {sorted(_VALID_INTENTS)}"
|
|
280
|
-
)
|
|
281
|
-
return normalized
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
def ip_risk_scope_items(monetization_intent: str) -> list[dict[str, str]]:
|
|
285
|
-
"""Return the canonical IP-risk protection scope items for SPECIFICATION
|
|
286
|
-
injection.
|
|
287
|
-
|
|
288
|
-
The minimum-protection checklist (see ``references/ip-risk.md``) lands
|
|
289
|
-
three scope items in the spec:
|
|
290
|
-
|
|
291
|
-
- **Disclaimer stub** -- a "not affiliated with / not endorsed by"
|
|
292
|
-
front-of-app notice the implementation phase fills in once the
|
|
293
|
-
specific IP holder is named.
|
|
294
|
-
- **API-only-asset-access policy** -- never bundle assets (images,
|
|
295
|
-
audio, video, text) from the third-party IP; access only via
|
|
296
|
-
official APIs that grant a license to use them, and gate that
|
|
297
|
-
access behind the user's own credentials.
|
|
298
|
-
- **Hosting policy** -- self-hosted private use only, OR commercial
|
|
299
|
-
hosting only after legal review confirms the licensing terms allow
|
|
300
|
-
it.
|
|
301
|
-
|
|
302
|
-
All three items are emitted regardless of monetization intent because
|
|
303
|
-
even personal IP-adjacent projects can leak into commercial use over
|
|
304
|
-
time. The ``Acceptance`` narrative on each item is tightened to the
|
|
305
|
-
commercial-level checklist (lawyer-confirmed terms, written license,
|
|
306
|
-
etc.) for **any intent other than ``"personal"``** -- the
|
|
307
|
-
wrong-side-of-safe policy means that ``"unknown"`` (interview hasn't
|
|
308
|
-
captured an explicit answer yet) inherits the stricter commercial
|
|
309
|
-
checklist. Only the explicit ``"personal"`` answer relaxes the
|
|
310
|
-
acceptance language.
|
|
311
|
-
|
|
312
|
-
The returned items are plain dicts compatible with
|
|
313
|
-
``vBRIEF v0.6 PlanItem`` shape (``title``, ``status``, ``narrative``).
|
|
314
|
-
Callers append them to ``plan.items`` on the
|
|
315
|
-
``specification.vbrief.json`` draft so they flow naturally into the
|
|
316
|
-
rendered SPECIFICATION.md via the existing ``scripts/spec_render.py``
|
|
317
|
-
pipeline -- no spec_render.py modification is required.
|
|
318
|
-
"""
|
|
319
|
-
intent = _validate_intent(monetization_intent)
|
|
320
|
-
# Wrong-side-of-safe policy (Greptile P1 #775): treat anything other than
|
|
321
|
-
# the explicit `personal` answer as commercial-level. `unknown` (the
|
|
322
|
-
# interview is still asking the question) MUST inherit the stricter
|
|
323
|
-
# checklist so the spec carries lawyer-confirmed acceptance criteria
|
|
324
|
-
# by default. The interview MUST still resolve `unknown` -> `personal` /
|
|
325
|
-
# `commercial` before the confirmation gate; this is just the safe
|
|
326
|
-
# fallback for the scope-item shape if the call lands first.
|
|
327
|
-
commercial = intent != "personal"
|
|
328
|
-
|
|
329
|
-
base_acceptance = (
|
|
330
|
-
"Lawyer-confirmed wording before public release"
|
|
331
|
-
if commercial
|
|
332
|
-
else "Reviewed by the project owner before any public release"
|
|
333
|
-
)
|
|
334
|
-
asset_acceptance = (
|
|
335
|
-
"All third-party assets reach the app via official APIs only with a "
|
|
336
|
-
"license that explicitly permits the planned use; no assets bundled "
|
|
337
|
-
"in the repository or build artifacts; lawyer-confirmed before public "
|
|
338
|
-
"release"
|
|
339
|
-
if commercial
|
|
340
|
-
else "All third-party assets reach the app via official APIs only; "
|
|
341
|
-
"no assets bundled in the repository or build artifacts"
|
|
342
|
-
)
|
|
343
|
-
hosting_acceptance = (
|
|
344
|
-
"Hosting plan reviewed by counsel; written license terms cover the "
|
|
345
|
-
"deployment region and audience; revenue model documented"
|
|
346
|
-
if commercial
|
|
347
|
-
else "Self-hosted private use only; do not deploy publicly until a "
|
|
348
|
-
"monetization decision is made and re-reviewed against this rule"
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
return [
|
|
352
|
-
{
|
|
353
|
-
"title": "IP-risk: disclaimer stub on the app's front surface",
|
|
354
|
-
"status": "pending",
|
|
355
|
-
"narrative": {
|
|
356
|
-
"Description": (
|
|
357
|
-
"Add a 'not affiliated with / not endorsed by' notice on "
|
|
358
|
-
"the app's first user-visible surface (splash screen, "
|
|
359
|
-
"landing page, or CLI banner)."
|
|
360
|
-
),
|
|
361
|
-
"Acceptance": base_acceptance,
|
|
362
|
-
"Traces": "IP-1",
|
|
363
|
-
},
|
|
364
|
-
},
|
|
365
|
-
{
|
|
366
|
-
"title": "IP-risk: API-only third-party asset access policy",
|
|
367
|
-
"status": "pending",
|
|
368
|
-
"narrative": {
|
|
369
|
-
"Description": (
|
|
370
|
-
"Never bundle third-party IP assets (images, audio, "
|
|
371
|
-
"video, text, card data, character likenesses) in the "
|
|
372
|
-
"repository or build artifacts. Access only via official "
|
|
373
|
-
"APIs that grant a license."
|
|
374
|
-
),
|
|
375
|
-
"Acceptance": asset_acceptance,
|
|
376
|
-
"Traces": "IP-2",
|
|
377
|
-
},
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
"title": "IP-risk: hosting policy gated on monetization intent",
|
|
381
|
-
"status": "pending",
|
|
382
|
-
"narrative": {
|
|
383
|
-
"Description": (
|
|
384
|
-
"Document the hosting plan and gate it on the captured "
|
|
385
|
-
"monetization intent. Self-hosted private use is the "
|
|
386
|
-
"default; commercial hosting requires lawyer review."
|
|
387
|
-
),
|
|
388
|
-
"Acceptance": hosting_acceptance,
|
|
389
|
-
"Traces": "IP-3",
|
|
390
|
-
},
|
|
391
|
-
},
|
|
392
|
-
]
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
def plain_risk_summary(
|
|
396
|
-
hits: list[IPHit],
|
|
397
|
-
monetization_intent: str,
|
|
398
|
-
) -> str:
|
|
399
|
-
"""Build a plain-English risk summary suitable for interview output.
|
|
400
|
-
|
|
401
|
-
The summary is intentionally non-alarming and non-legal-advice: it
|
|
402
|
-
states what was detected, what the implication is at a high level,
|
|
403
|
-
and (for commercial intent) it carries a non-optional recommendation
|
|
404
|
-
to consult a lawyer. The summary is meant to be copied verbatim into
|
|
405
|
-
the interview output and into the ``IPRisk`` narrative on the
|
|
406
|
-
specification vBRIEF.
|
|
407
|
-
|
|
408
|
-
Returns an empty string when *hits* is empty (no IP detected); the
|
|
409
|
-
caller MUST NOT inject the summary in that case.
|
|
410
|
-
|
|
411
|
-
.. warning::
|
|
412
|
-
|
|
413
|
-
``monetization_intent="unknown"`` produces a **transitional**
|
|
414
|
-
re-ask prompt -- a status message saying "the interview MUST
|
|
415
|
-
capture an explicit answer". It is NOT a terminal output and
|
|
416
|
-
does NOT carry the lawyer recommendation that
|
|
417
|
-
:func:`ip_risk_scope_items` injects under the wrong-side-of-safe
|
|
418
|
-
policy. Callers MUST loop back to the monetization-intent
|
|
419
|
-
question and re-call this function with ``personal`` or
|
|
420
|
-
``commercial`` before treating the summary as final output --
|
|
421
|
-
otherwise the interview surface (no lawyer rec) and the
|
|
422
|
-
injected spec items (commercial-level acceptance language)
|
|
423
|
-
will mismatch (#775 P2).
|
|
424
|
-
"""
|
|
425
|
-
if not hits:
|
|
426
|
-
return ""
|
|
427
|
-
intent = _validate_intent(monetization_intent)
|
|
428
|
-
|
|
429
|
-
grouped: dict[str, list[str]] = {}
|
|
430
|
-
for hit in hits:
|
|
431
|
-
grouped.setdefault(hit.category, []).append(hit.term)
|
|
432
|
-
|
|
433
|
-
bullets: list[str] = []
|
|
434
|
-
for category, terms in grouped.items():
|
|
435
|
-
unique_terms = sorted(set(terms), key=str.lower)
|
|
436
|
-
bullets.append(f"- {category}: {', '.join(unique_terms)}")
|
|
437
|
-
|
|
438
|
-
header = (
|
|
439
|
-
"Heads up: your project description references third-party "
|
|
440
|
-
"intellectual property (IP). This is a plain-English summary -- "
|
|
441
|
-
"not legal advice."
|
|
442
|
-
)
|
|
443
|
-
detection_block = "Detected IP-adjacent terms:\n" + "\n".join(bullets)
|
|
444
|
-
|
|
445
|
-
if intent == "commercial":
|
|
446
|
-
intent_block = (
|
|
447
|
-
"You said you intend to use this commercially (sell access, "
|
|
448
|
-
"earn revenue, distribute to paying users, or run ads). "
|
|
449
|
-
"Commercial use of someone else's IP without a written license "
|
|
450
|
-
"is the high-risk case. You MUST consult a lawyer before "
|
|
451
|
-
"shipping to paying users -- this is not optional output from "
|
|
452
|
-
"this interview."
|
|
453
|
-
)
|
|
454
|
-
elif intent == "personal":
|
|
455
|
-
intent_block = (
|
|
456
|
-
"You said this is a personal project (no monetization, private "
|
|
457
|
-
"use, learning). Personal use is lower risk but not zero risk: "
|
|
458
|
-
"if your project ever goes public, becomes monetized, or is "
|
|
459
|
-
"shared widely, the risk profile changes and a lawyer review "
|
|
460
|
-
"becomes worthwhile."
|
|
461
|
-
)
|
|
462
|
-
else: # unknown
|
|
463
|
-
intent_block = (
|
|
464
|
-
"You did not choose between personal and commercial use. The "
|
|
465
|
-
"interview MUST capture an explicit answer before generating "
|
|
466
|
-
"the SPECIFICATION -- the legal-risk profile depends on the "
|
|
467
|
-
"answer."
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
next_steps = (
|
|
471
|
-
"Suggested next steps: (1) confirm whether your use is personal "
|
|
472
|
-
"or commercial; (2) keep the disclaimer / API-only-asset / "
|
|
473
|
-
"hosting scope items the SPECIFICATION will include; "
|
|
474
|
-
"(3) for commercial intent, consult a lawyer before public "
|
|
475
|
-
"release."
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
return "\n\n".join([header, detection_block, intent_block, next_steps])
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
# ---------------------------------------------------------------------------
|
|
482
|
-
# CLI entry point (for ad-hoc use; the canonical caller is the interview
|
|
483
|
-
# skill which invokes the helpers directly).
|
|
484
|
-
# ---------------------------------------------------------------------------
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
def main(argv: list[str] | None = None) -> int:
|
|
488
|
-
args = sys.argv[1:] if argv is None else argv
|
|
489
|
-
if not args:
|
|
490
|
-
print(
|
|
491
|
-
"Usage: ip_risk.py <text-to-scan> [--intent personal|commercial|unknown]",
|
|
492
|
-
file=sys.stderr,
|
|
493
|
-
)
|
|
494
|
-
return 2
|
|
495
|
-
|
|
496
|
-
intent = "unknown"
|
|
497
|
-
text_parts: list[str] = []
|
|
498
|
-
i = 0
|
|
499
|
-
while i < len(args):
|
|
500
|
-
arg = args[i]
|
|
501
|
-
if arg == "--intent" and i + 1 < len(args):
|
|
502
|
-
intent = args[i + 1]
|
|
503
|
-
i += 2
|
|
504
|
-
continue
|
|
505
|
-
text_parts.append(arg)
|
|
506
|
-
i += 1
|
|
507
|
-
text = " ".join(text_parts)
|
|
508
|
-
if not text:
|
|
509
|
-
print("Usage: ip_risk.py <text-to-scan>", file=sys.stderr)
|
|
510
|
-
return 2
|
|
511
|
-
|
|
512
|
-
hits = detect_ip_terms(text)
|
|
513
|
-
if not hits:
|
|
514
|
-
print("No IP terms detected.")
|
|
515
|
-
return 0
|
|
516
|
-
|
|
517
|
-
print("Detected IP terms:")
|
|
518
|
-
for hit in hits:
|
|
519
|
-
print(f" - {hit}")
|
|
520
|
-
try:
|
|
521
|
-
summary = plain_risk_summary(hits, intent)
|
|
522
|
-
except ValueError as exc:
|
|
523
|
-
print(f"Invalid --intent value: {exc}", file=sys.stderr)
|
|
524
|
-
return 2
|
|
525
|
-
print()
|
|
526
|
-
print(summary)
|
|
527
|
-
return 1
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if __name__ == "__main__": # pragma: no cover - CLI entry
|
|
531
|
-
sys.exit(main())
|