sneakoscope 10.6.2 → 10.7.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.
Files changed (105) hide show
  1. package/README.md +2 -2
  2. package/config/perf-budgets.v1.json +1 -2
  3. package/config/skills-hash-ledger.v1.json +7 -2
  4. package/crates/sks-core/Cargo.lock +1 -1
  5. package/crates/sks-core/Cargo.toml +1 -1
  6. package/dist/bin/sks-dispatch.js +6 -3
  7. package/dist/cli/args.js +3 -1
  8. package/dist/cli/command-manifest-lite.js +48 -114
  9. package/dist/cli/command-registry.js +1 -18
  10. package/dist/cli/commands-fast.js +11 -4
  11. package/dist/cli/help-fast.js +11 -22
  12. package/dist/cli/help.js +3 -3
  13. package/dist/cli/install-helpers.js +1 -1
  14. package/dist/cli/router.js +12 -26
  15. package/dist/commands/codex-app.js +2 -2
  16. package/dist/commands/doctor-helpers.js +1 -1
  17. package/dist/commands/mad-sks.js +2 -2
  18. package/dist/config/skills-manifest.json +13 -8
  19. package/dist/core/agent-bridge/async-command.js +2 -2
  20. package/dist/core/agents/agent-recursion-guard.js +0 -2
  21. package/dist/core/auto-review.js +15 -172
  22. package/dist/core/code-structure.js +0 -1
  23. package/dist/core/codex-control/codex-reliability-shield.js +64 -15
  24. package/dist/core/codex-control/codex-sdk-adapter.js +38 -5
  25. package/dist/core/commands/agent-bridge-command.js +1 -1
  26. package/dist/core/commands/basic-cli.js +60 -233
  27. package/dist/core/commands/command-utils.js +1 -1
  28. package/dist/core/commands/gc-command.js +30 -29
  29. package/dist/core/commands/image-ux-review-command.js +1 -1
  30. package/dist/core/commands/install-package-command.js +1 -1
  31. package/dist/core/commands/intent-normalization/normalizer.js +2 -2
  32. package/dist/core/commands/pipeline-command.js +13 -5
  33. package/dist/core/commands/plan-command.js +2 -3
  34. package/dist/core/commands/research-command.js +0 -3
  35. package/dist/core/commands/review-command.js +1 -1
  36. package/dist/core/commands/seo-command.js +27 -20
  37. package/dist/core/commands/status-command.js +8 -2
  38. package/dist/core/doctor/command-alias-cleanup.js +6 -6
  39. package/dist/core/evaluation.js +1 -1
  40. package/dist/core/feature-fixtures.js +9 -43
  41. package/dist/core/feature-registry.js +9 -27
  42. package/dist/core/git-hygiene/shared-memory-publish.js +1 -1
  43. package/dist/core/hooks-runtime/stop-repeat-guard.js +0 -13
  44. package/dist/core/init/skills.js +5 -13
  45. package/dist/core/init.js +0 -9
  46. package/dist/core/install/installed-package-smoke.js +3 -1
  47. package/dist/core/perf/import-graph-budget.js +0 -1
  48. package/dist/core/perf/release-latency-slo.js +2 -30
  49. package/dist/core/pipeline-internals/pipeline-stage-builder.js +2 -4
  50. package/dist/core/pipeline-internals/runtime-core.js +2 -38
  51. package/dist/core/proof/route-proof-policy.js +2 -4
  52. package/dist/core/proof/runtime-truth-matrix.js +2 -2
  53. package/dist/core/proof-field.js +104 -39
  54. package/dist/core/questions.js +11 -7
  55. package/dist/core/release/gate-affected-globs.js +0 -2
  56. package/dist/core/research/research-reviewer-contract.js +34 -0
  57. package/dist/core/research/research-work-graph.js +1 -1
  58. package/dist/core/research.js +1 -1
  59. package/dist/core/routes/constants.js +0 -1
  60. package/dist/core/routes/diagnostic-allowlist.js +1 -3
  61. package/dist/core/routes/light-routes.js +1 -2
  62. package/dist/core/routes.js +1 -1
  63. package/dist/core/safety/command-contract/contracts.js +5 -1
  64. package/dist/core/search-visibility/marketing-truthfulness.js +10 -7
  65. package/dist/core/search-visibility/marketing.js +194 -149
  66. package/dist/core/skill-forge.js +1 -361
  67. package/dist/core/strategy/adhd-orchestrating-gate.js +3 -3
  68. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +2 -6
  69. package/dist/core/triwiki-wrongness/wrongness-cli.js +9 -27
  70. package/dist/core/version.js +1 -1
  71. package/package.json +1 -2
  72. package/release-gates.v2.json +1 -32
  73. package/dist/cli/recallpulse-command.js +0 -144
  74. package/dist/commands/auto-review.js +0 -5
  75. package/dist/commands/codex-native.js +0 -77
  76. package/dist/commands/recallpulse.js +0 -5
  77. package/dist/core/codex-native/codex-native-interop-policy.js +0 -127
  78. package/dist/core/commands/autoresearch-command.js +0 -4
  79. package/dist/core/commands/code-structure-command.js +0 -52
  80. package/dist/core/commands/goal-command.js +0 -43
  81. package/dist/core/commands/recallpulse-command.js +0 -4
  82. package/dist/core/commands/remote-command.js +0 -119
  83. package/dist/core/commands/skill-dream-command.js +0 -43
  84. package/dist/core/commands/wrongness-command.js +0 -2
  85. package/dist/core/goal-workflow.js +0 -80
  86. package/dist/core/mcp/mcp-tool-name-normalizer.js +0 -28
  87. package/dist/core/recallpulse/policy.js +0 -207
  88. package/dist/core/recallpulse.js +0 -947
  89. package/dist/core/remote/audit-idempotency.js +0 -241
  90. package/dist/core/remote/index.js +0 -10
  91. package/dist/core/remote/machine-registry.js +0 -167
  92. package/dist/core/remote/owner-proof.js +0 -216
  93. package/dist/core/remote/protocol.js +0 -249
  94. package/dist/core/remote/readiness.js +0 -156
  95. package/dist/core/remote/session-index.js +0 -61
  96. package/dist/core/remote/session-snapshot.js +0 -197
  97. package/dist/core/remote/ssh-worker-client.js +0 -377
  98. package/dist/core/remote/types.js +0 -10
  99. package/dist/core/remote/worker.js +0 -360
  100. package/dist/core/search/context-graph-seeds.js +0 -15
  101. package/dist/core/verification/context-graph-affected.js +0 -312
  102. package/dist/core/verification/verification-artifact-lock.js +0 -21
  103. package/dist/core/verification/verification-dag.js +0 -89
  104. package/dist/core/verification/verification-result.js +0 -19
  105. package/dist/core/verification/verification-worker-pool.js +0 -204
@@ -3,51 +3,38 @@ import { exists, readJson, readText, sha256, writeJsonAtomic } from '../fsx.js';
3
3
  import { detectProject, discoverSiteInventory } from './discovery.js';
4
4
  import { createSearchVisibilityMission, resolveSearchVisibilityMission, routeForMode } from './mission.js';
5
5
  import { runSuperSearch } from '../super-search/index.js';
6
- import { evaluateMarketingTruthfulness } from './marketing-truthfulness.js';
7
- const INTERNAL_SOURCE_CANDIDATES = [
8
- 'package.json',
9
- 'README.md',
10
- 'CHANGELOG.md',
11
- 'src/core/routes.ts',
12
- 'src/core/routes/dollar-manifest-lite.ts',
13
- 'src/core/agents/agent-orchestrator.ts',
14
- 'src/cli/command-manifest-lite.ts',
15
- 'config/perf-budgets.v1.json',
16
- '.sneakoscope/reports/perf-budget.json',
17
- '.sneakoscope/reports/super-search-local-http-smoke.json',
18
- '.sneakoscope/reports/installed-package-smoke.json',
19
- ];
20
- const EXTERNAL_SOURCE_URLS = [
21
- 'https://github.com/mandarange/Sneakoscope-Codex',
22
- 'https://www.npmjs.com/package/sneakoscope',
23
- ];
24
- const COMPETITOR_SOURCE_URLS = [
25
- 'https://github.com/Yeachan-Heo/oh-my-codex',
26
- 'https://github.com/code-yeongyu/lazycodex',
27
- 'https://www.npmjs.com/package/oh-my-codex',
28
- 'https://www.npmjs.com/package/lazycodex-ai',
29
- ];
6
+ import { classifyClaim, evaluateMarketingTruthfulness } from './marketing-truthfulness.js';
7
+ const INTERNAL_SOURCE_FILES = ['package.json', 'CHANGELOG.md'];
8
+ const NON_POSITIONING_SECTION = /^(?:install(?:ation|ing)?|usage|getting started|quick ?start|setup|requirements|prerequisites|configuration|development|contributing|contributors|license|changelog|faq|support|credits|acknowledg(?:e)?ments|table of contents|contents|testing|tests?|build(?:ing)?|scripts|roadmap|security|code of conduct|authors?|sponsors?|related|see also|links|api|reference|examples?|demo|screenshots?)\b/i;
9
+ const MAX_MESSAGE_PILLARS = 4;
10
+ const MAX_KEYWORDS = 20;
30
11
  export async function runMarketingResearch(mode, missionRef, options) {
31
12
  const mission = await resolveOrCreateMarketingMission(mode, missionRef, options, 'marketing research');
32
13
  const ctx = context(mode, mission.root, options);
33
14
  const detected = await detectProject(ctx);
34
15
  const inventory = await discoverSiteInventory(ctx, detected);
35
16
  await writeJsonAtomic(path.join(mission.artifactDir, 'site-inventory.json'), inventory);
36
- const internalSources = await collectInternalSources(mission.root);
17
+ const internalSources = await collectInternalSources(mission.root, inventory);
18
+ const projectUrls = await projectSourceUrls(mission.root, inventory, options);
19
+ const competitorUrls = unique(options.competitorUrls || []);
37
20
  const externalSources = options.offline
38
21
  ? []
39
- : await collectSuperSearchSources(mission, EXTERNAL_SOURCE_URLS, 'external', options.maxMarketingSources);
40
- const competitorSources = !options.offline && options.includeCompetitors
41
- ? await collectSuperSearchSources(mission, COMPETITOR_SOURCE_URLS, 'competitor', options.maxMarketingSources)
22
+ : await collectSuperSearchSources(mission, projectUrls, 'external', options.maxMarketingSources);
23
+ const competitorSources = !options.offline && options.includeCompetitors && competitorUrls.length
24
+ ? await collectSuperSearchSources(mission, competitorUrls, 'competitor', options.maxMarketingSources)
42
25
  : [];
43
- const claims = buildMarketingClaims(inventory, internalSources, externalSources);
26
+ const claims = buildMarketingClaims(inventory, internalSources);
44
27
  const truth = evaluateMarketingTruthfulness({ claims });
45
28
  const blockedClaims = claims.filter((claim) => !claim.publishable || claim.blockers.length || truth.unsupported_claims.includes(claim.id));
46
- const blockers = [...truth.blockers];
29
+ const blockers = [];
47
30
  if (!internalSources.length)
48
31
  blockers.push('marketing_internal_sources_missing');
49
- if (!options.offline && !externalSources.some((source) => source.verified))
32
+ if (!options.offline && !projectUrls.length)
33
+ blockers.push('marketing_project_urls_missing');
34
+ else if (!options.offline && !externalSources.some((source) => source.verified))
50
35
  blockers.push('marketing_external_sources_unverified');
36
+ if (options.includeCompetitors && !competitorUrls.length)
37
+ blockers.push('marketing_competitor_urls_required');
51
38
  const research = {
52
39
  schema: 'sks.search-visibility.marketing-research.v1',
53
40
  ok: blockers.length === 0,
@@ -63,6 +50,8 @@ export async function runMarketingResearch(mode, missionRef, options) {
63
50
  await writeJsonAtomic(path.join(mission.artifactDir, 'marketing-source-ledger.json'), {
64
51
  schema: 'sks.search-visibility.marketing-source-ledger.v1',
65
52
  mission_id: mission.id,
53
+ project_urls: projectUrls,
54
+ competitor_urls: competitorUrls,
66
55
  sources: [...internalSources, ...externalSources, ...competitorSources],
67
56
  });
68
57
  await writeJsonAtomic(path.join(mission.artifactDir, 'marketing-claim-ledger.json'), {
@@ -87,6 +76,7 @@ export async function runMarketingResearch(mode, missionRef, options) {
87
76
  route: routeForMode(mode),
88
77
  status: research.ok ? 'verified_partial' : 'blocked',
89
78
  artifacts_dir: `.sneakoscope/missions/${mission.id}/search-visibility`,
79
+ project_urls: projectUrls,
90
80
  internal_sources: internalSources.length,
91
81
  external_sources: externalSources.length,
92
82
  competitor_sources: competitorSources.length,
@@ -107,7 +97,11 @@ export async function runMarketingStrategy(mode, missionRef, options) {
107
97
  const inventory = await readJson(path.join(mission.artifactDir, 'site-inventory.json'), null);
108
98
  if (!inventory)
109
99
  return blockedStrategy(mission, ['site_inventory_required_for_strategy']);
110
- const strategy = buildMarketingStrategy(mission.id, inventory, research);
100
+ const readmeSource = research.internal_sources.find((source) => Boolean(source.path) && source.path === inventory.readme.path);
101
+ const readmeText = readmeSource?.path ? await readText(path.join(mission.root, readmeSource.path), '') : '';
102
+ if (readmeSource?.sha256 && sha256(readmeText) !== readmeSource.sha256)
103
+ return blockedStrategy(mission, ['marketing_readme_changed_since_research']);
104
+ const strategy = buildMarketingStrategy(mission.id, inventory, research, readmeText);
111
105
  const truth = evaluateMarketingTruthfulness({ strategy });
112
106
  const strategy_quality = scoreMarketingStrategy(strategy, truth);
113
107
  const blockers = [...strategy.blockers, ...truth.blockers, ...strategy_quality.blockers];
@@ -137,101 +131,109 @@ export async function runMarketingStrategy(mode, missionRef, options) {
137
131
  blockers,
138
132
  };
139
133
  }
140
- function buildMarketingStrategy(missionId, inventory, research) {
141
- const sources = [...research.internal_sources, ...research.external_sources];
142
- const sourceIds = sources.filter((source) => source.verified).map((source) => source.id);
143
- const packageSource = sourceIds.find((id) => id.includes('package-json')) || sourceIds[0] || '';
144
- const readmeSource = sourceIds.find((id) => id.includes('readme')) || packageSource;
145
- const perfSource = sourceIds.find((id) => id.includes('perf-budget') || id.includes('perf'));
146
- const parallelSource = sourceIds.find((id) => id.includes('agent-orchestrator') || id.includes('naruto'));
147
- const superSearchSource = sourceIds.find((id) => id.includes('super-search'));
148
- const oneLiner = `${inventory.package.name || 'Sneakoscope'} is a proof-first Codex trust layer for bounded agent workflows, search visibility, and evidence-backed release gates.`;
149
- const strategySources = unique([packageSource, readmeSource, superSearchSource, parallelSource, perfSource].filter(Boolean));
150
- const competitorContrast = buildCompetitorContrast(research, strategySources);
151
- const keywords = unique([
152
- 'sneakoscope',
153
- 'codex',
154
- 'sks',
155
- 'codex-cli',
156
- 'codex-app',
157
- 'agent-orchestration',
158
- 'proof-gates',
159
- ...(superSearchSource ? ['super-search'] : []),
160
- 'seo',
161
- 'generative-engine-optimization',
162
- 'bounded-memory',
163
- 'rollback',
164
- 'release-integrity',
165
- ]).slice(0, 20);
166
- const messagePillars = [
167
- { title: 'Proof-first route gates', claim: 'SKS records route artifacts, blockers, and release gates so Codex workflows can separate verified work from unverified claims.', source_ids: [readmeSource || packageSource].filter(Boolean) },
168
- ...(superSearchSource ? [{ title: 'Source-backed search visibility', claim: 'Super-Search and SEO/GEO artifacts keep source ledgers and claim ledgers attached to search visibility work.', source_ids: [superSearchSource] }] : []),
169
- ...(parallelSource ? [{ title: 'Bounded parallel implementation', claim: 'The Naruto agent orchestrator runs bounded multi-agent coding workflows.', source_ids: [parallelSource] }] : []),
170
- ...(perfSource ? [{ title: 'Measured fast paths', claim: 'Performance budget reports define p95 budgets for selected SKS fast-path commands.', source_ids: [perfSource] }] : []),
134
+ function buildMarketingStrategy(missionId, inventory, research, readmeText) {
135
+ const verifiedId = (source) => (source?.verified ? source.id : null);
136
+ const packageSource = verifiedId(research.internal_sources.find((source) => Boolean(source.path) && source.path === inventory.package.path));
137
+ const readmeSource = verifiedId(research.internal_sources.find((source) => Boolean(source.path) && source.path === inventory.readme.path));
138
+ const projectName = inventory.package.name || inventory.readme.h1 || 'This project';
139
+ const excluded = [];
140
+ const passes = (id, text, claimType, sourceIds) => {
141
+ const gate = evaluateMarketingTruthfulness({
142
+ claims: [{ id, text, claim_type: claimType, source_ids: sourceIds, publishable: true, blockers: sourceIds.length ? [] : ['source_ids_required'] }],
143
+ });
144
+ if (!gate.ok)
145
+ excluded.push(...gate.blockers);
146
+ return gate.ok;
147
+ };
148
+ const candidates = [
149
+ { id: 'positioning-package-description', text: (inventory.package.description || '').trim(), source: packageSource },
150
+ { id: 'positioning-readme-lead', text: readmeLead(readmeText), source: readmeSource },
171
151
  ];
152
+ const positioning = candidates.find((candidate) => Boolean(candidate.text && candidate.source)
153
+ && passes(candidate.id, candidate.text, 'positioning', [candidate.source])) || null;
154
+ const positioningSource = positioning?.source || null;
155
+ const oneLiner = positioning?.text || '';
156
+ const messagePillars = readmeSections(readmeText)
157
+ .filter((section) => !NON_POSITIONING_SECTION.test(section.title))
158
+ .map((section) => ({ title: section.title, claim: section.lead, source_ids: readmeSource ? [readmeSource] : [] }))
159
+ .filter((pillar, index) => Boolean(pillar.claim)
160
+ && passes(`pillar-${index + 1}`, `${pillar.title}: ${pillar.claim}`, classifyClaim(`${pillar.title}: ${pillar.claim}`), pillar.source_ids))
161
+ .slice(0, MAX_MESSAGE_PILLARS);
162
+ const existingKeywords = unique(inventory.package.keywords.map((keyword) => keyword.trim()));
163
+ const identityKeywords = unique([bareName(inventory.package.name), ...frameworkKeywords(inventory)])
164
+ .filter((keyword) => !existingKeywords.includes(keyword));
165
+ const keywords = [...existingKeywords, ...identityKeywords].slice(0, MAX_KEYWORDS);
166
+ const keywordClusters = packageSource
167
+ ? [
168
+ { name: 'package keywords', keywords: existingKeywords, source_ids: [packageSource] },
169
+ { name: 'project identity', keywords: identityKeywords, source_ids: [packageSource] },
170
+ ].filter((cluster) => cluster.keywords.length)
171
+ : [];
172
+ const competitorContrast = research.competitor_sources
173
+ .filter((source) => source.verified && oneLiner)
174
+ .map((source) => ({
175
+ competitor: source.title,
176
+ their_claim: source.summary,
177
+ project_contrast: `${projectName}: ${oneLiner}`,
178
+ source_ids: unique([source.id, positioningSource || '']),
179
+ safe_to_publish: true,
180
+ }))
181
+ .filter((contrast, index) => passes(`competitor-contrast-${index + 1}`, `${contrast.competitor}: ${contrast.their_claim} ${contrast.project_contrast}`, 'competitor', contrast.source_ids));
182
+ const readmePlan = positioningSource ? [{
183
+ operation: 'readme-positioning-block-update',
184
+ text: [
185
+ '<!-- BEGIN SKS MARKETING POSITIONING -->',
186
+ oneLiner,
187
+ ...(messagePillars.length ? ['', ...messagePillars.map((pillar) => `- **${pillar.title}:** ${pillar.claim}`)] : []),
188
+ '<!-- END SKS MARKETING POSITIONING -->',
189
+ '',
190
+ ].join('\n'),
191
+ source_ids: unique([positioningSource, ...messagePillars.flatMap((pillar) => pillar.source_ids)]),
192
+ }] : [];
193
+ const packagePlan = [];
194
+ if (packageSource && positioningSource && !inventory.package.description) {
195
+ packagePlan.push({ operation: 'package-description-update', description: oneLiner, source_ids: [positioningSource] });
196
+ }
197
+ if (packageSource && identityKeywords.length && passes('package-keywords', keywords.join(', '), 'positioning', [packageSource])) {
198
+ packagePlan.push({ operation: 'package-keywords-update', keywords, source_ids: [packageSource] });
199
+ }
172
200
  const strategy = {
173
201
  schema: 'sks.search-visibility.marketing-strategy.v1',
174
202
  ok: true,
175
203
  mission_id: missionId,
176
204
  positioning: {
177
205
  one_liner: oneLiner,
178
- source_ids: [packageSource || readmeSource].filter(Boolean),
206
+ source_ids: positioningSource ? [positioningSource] : [],
179
207
  },
180
208
  message_pillars: messagePillars,
181
- keyword_clusters: [
182
- { name: 'codex trust layer', keywords: keywords.slice(0, 7), source_ids: [packageSource || readmeSource].filter(Boolean) },
183
- { name: 'agent release integrity', keywords: keywords.slice(7, 14), source_ids: [readmeSource || packageSource].filter(Boolean) },
184
- { name: 'search visibility', keywords: keywords.slice(7, 11), source_ids: [superSearchSource || packageSource].filter(Boolean) },
185
- ],
209
+ keyword_clusters: keywordClusters,
186
210
  strategy_quality: {
187
211
  score: 0,
188
212
  source_backed_claims: 0,
189
213
  unsupported_claims: 0,
190
214
  competitor_contrast_count: competitorContrast.length,
191
- keyword_cluster_count: 3,
215
+ keyword_cluster_count: keywordClusters.length,
192
216
  blockers: [],
193
217
  },
194
218
  competitor_contrast: competitorContrast,
195
- readme_plan: [{
196
- operation: 'readme-positioning-block-update',
197
- text: [
198
- '<!-- BEGIN SKS MARKETING POSITIONING -->',
199
- '## Search Visibility Positioning',
200
- '',
201
- oneLiner,
202
- '',
203
- '- Source-ledger claims are kept in SKS marketing research and strategy artifacts.',
204
- '- SEO/GEO mutation plans update only package metadata and this managed README block in this release.',
205
- '- External visibility outcomes require separate measured evidence.',
206
- '<!-- END SKS MARKETING POSITIONING -->',
207
- '',
208
- ].join('\n'),
209
- source_ids: strategySources,
210
- }],
211
- package_plan: [
212
- {
213
- operation: 'package-description-update',
214
- description: 'Proof-first Codex trust layer for bounded agent workflows, search visibility evidence, and release integrity gates.',
215
- source_ids: strategySources,
216
- },
217
- {
218
- operation: 'package-keywords-update',
219
- keywords,
220
- source_ids: strategySources,
221
- },
222
- ],
219
+ readme_plan: readmePlan,
220
+ package_plan: packagePlan,
223
221
  docs_plan: [
224
- { title: 'Document marketing claim sources and prohibited guarantees in release notes.', source_ids: [readmeSource || packageSource].filter(Boolean), auto_apply: false },
222
+ { title: 'Document marketing claim sources and prohibited guarantees in release notes.', source_ids: [positioningSource || packageSource || readmeSource].filter(Boolean), auto_apply: false },
225
223
  ],
226
224
  do_not_claim: [
227
225
  'guaranteed ranking',
228
226
  'guaranteed traffic',
229
227
  'guaranteed AI citation',
230
- 'best or fastest Codex tool',
228
+ 'best or fastest in its category without a measurement',
231
229
  'competitor disparagement',
232
230
  ],
231
+ excluded_claims: unique(excluded),
233
232
  blockers: [],
234
233
  };
234
+ if (!positioningSource) {
235
+ strategy.blockers.push(candidates.some((candidate) => candidate.text && candidate.source) ? 'positioning_claim_unsupported' : 'positioning_source_missing');
236
+ }
235
237
  for (const pillar of strategy.message_pillars) {
236
238
  if (!pillar.source_ids.length)
237
239
  strategy.blockers.push(`pillar_source_missing:${pillar.title}`);
@@ -240,53 +242,34 @@ function buildMarketingStrategy(missionId, inventory, research) {
240
242
  if (!plan.source_ids.length)
241
243
  strategy.blockers.push(`package_plan_source_missing:${plan.operation}`);
242
244
  }
243
- if (strategy.positioning.source_ids.length === 0)
244
- strategy.blockers.push('positioning_source_missing');
245
245
  return strategy;
246
246
  }
247
- function buildCompetitorContrast(research, strategySources) {
248
- return research.competitor_sources
249
- .filter((source) => source.verified)
250
- .map((source) => ({
251
- competitor: source.title,
252
- their_claim: source.summary,
253
- sks_contrast: 'SKS positions itself around proof-first release integrity and source-backed search visibility, while workflow-first Codex layers emphasize orchestration convenience.',
254
- source_ids: unique([source.id, ...strategySources]),
255
- safe_to_publish: true,
256
- }));
257
- }
258
247
  function scoreMarketingStrategy(strategy, truth) {
259
248
  const blockers = [];
260
249
  let score = 0;
261
250
  const sourceBackedClaims = countSourceBackedStrategyClaims(strategy);
262
251
  if (strategy.positioning.source_ids.length)
263
- score += 20;
252
+ score += 25;
264
253
  else
265
254
  blockers.push('strategy_quality_source_backed_positioning_missing');
266
- if (strategy.message_pillars.length >= 3 && strategy.message_pillars.every((pillar) => pillar.source_ids.length))
267
- score += 20;
255
+ if (strategy.message_pillars.length >= 2 && strategy.message_pillars.every((pillar) => pillar.source_ids.length))
256
+ score += 25;
268
257
  else
269
258
  blockers.push('strategy_quality_message_pillars_below_threshold');
270
- if (strategy.keyword_clusters.length >= 2 && strategy.keyword_clusters.every((cluster) => cluster.source_ids.length))
259
+ if (strategy.keyword_clusters.length >= 1 && strategy.keyword_clusters.every((cluster) => cluster.source_ids.length))
271
260
  score += 15;
272
261
  else
273
- blockers.push('strategy_quality_keyword_clusters_below_threshold');
262
+ blockers.push('strategy_quality_keyword_clusters_missing');
274
263
  if (strategy.competitor_contrast.every((contrast) => contrast.safe_to_publish && contrast.source_ids.length))
275
- score += 15;
264
+ score += 10;
276
265
  else
277
266
  blockers.push('strategy_quality_competitor_contrast_unsafe');
278
267
  if (truth.forbidden_phrases.length === 0)
279
268
  score += 15;
280
269
  else
281
270
  blockers.push('strategy_quality_forbidden_phrases_present');
282
- if (hasSupportedSpecialClaim(strategy, /p95|latency|performance|fast/i, /perf|budget|report/i)
283
- && hasSupportedSpecialClaim(strategy, /parallel|worker|naruto/i, /parallel|naruto|agent|report|routes/i)
284
- && hasSupportedSpecialClaim(strategy, /super-search|source-backed|source backed/i, /super-search|source|report/i)) {
285
- score += 15;
286
- }
287
- else {
288
- blockers.push('strategy_quality_special_claim_sources_missing');
289
- }
271
+ if (truth.unsupported_claims.length === 0)
272
+ score += 10;
290
273
  if (score < 80)
291
274
  blockers.push('strategy_quality_score_below_80');
292
275
  if (truth.unsupported_claims.length)
@@ -313,16 +296,84 @@ function countSourceBackedStrategyClaims(strategy) {
313
296
  ...strategy.competitor_contrast,
314
297
  ].filter((item) => item.source_ids.length > 0).length;
315
298
  }
316
- function hasSupportedSpecialClaim(strategy, textPattern, sourcePattern) {
317
- const candidates = [
318
- ...strategy.message_pillars.map((pillar) => ({ text: pillar.claim, source_ids: pillar.source_ids })),
319
- ...strategy.readme_plan.map((plan) => ({ text: plan.text, source_ids: plan.source_ids })),
320
- ...strategy.package_plan.map((plan) => ({
321
- text: plan.operation === 'package-description-update' ? plan.description : plan.keywords.join(', '),
322
- source_ids: plan.source_ids,
323
- })),
324
- ];
325
- return candidates.some((candidate) => textPattern.test(candidate.text) && candidate.source_ids.some((id) => sourcePattern.test(id)));
299
+ function readmeLead(text) {
300
+ for (const paragraph of paragraphs(stripFencedCode(text))) {
301
+ if (/^(?:#|<|!\[|\[!\[|>|[-*+] |\d+\. |\||<!--)/.test(paragraph))
302
+ continue;
303
+ const plain = plainMarkdown(paragraph);
304
+ if (plain.length >= 20)
305
+ return firstSentences(plain);
306
+ }
307
+ return '';
308
+ }
309
+ function readmeSections(text) {
310
+ const out = [];
311
+ const body = stripFencedCode(text);
312
+ const matches = [...body.matchAll(/^##[ \t]+(.+?)[ \t]*#*[ \t]*$/gm)];
313
+ for (const [index, match] of matches.entries()) {
314
+ const start = (match.index || 0) + match[0].length;
315
+ const end = index + 1 < matches.length ? matches[index + 1].index || body.length : body.length;
316
+ const section = body.slice(start, end).split(/^#{1,6}[ \t]/m)[0] || '';
317
+ const lead = paragraphs(section)
318
+ .filter((paragraph) => !/^(?:<|!\[|\[!\[|\||<!--)/.test(paragraph))
319
+ .map((paragraph) => plainMarkdown(paragraph.replace(/^(?:[-*+]|\d+\.)\s+/, '')))
320
+ .find((paragraph) => paragraph.length >= 12) || '';
321
+ out.push({ title: plainMarkdown(match[1] || ''), lead: lead ? firstSentences(lead, 1) : '' });
322
+ }
323
+ return out.filter((section) => section.title);
324
+ }
325
+ function stripFencedCode(text) {
326
+ return String(text || '').replace(/^(```|~~~)[^\n]*\n[\s\S]*?^\1[ \t]*$/gm, '');
327
+ }
328
+ function paragraphs(text) {
329
+ return String(text || '').split(/\r?\n[ \t]*\r?\n/).map((paragraph) => paragraph.trim()).filter(Boolean);
330
+ }
331
+ function plainMarkdown(text) {
332
+ return String(text || '')
333
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
334
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
335
+ .replace(/`([^`]+)`/g, '$1')
336
+ .replace(/(\*\*|__|\*|_)(.+?)\1/g, '$2')
337
+ .replace(/<[^>]+>/g, '')
338
+ .replace(/\s+/g, ' ')
339
+ .trim();
340
+ }
341
+ function firstSentences(text, count = 2) {
342
+ const sentences = text.match(/[^.!?]+[.!?]+(?=\s|$)/g);
343
+ const picked = sentences ? sentences.slice(0, count).join(' ').trim() : text;
344
+ return picked.length > 280 ? `${picked.slice(0, 277).trimEnd()}...` : picked;
345
+ }
346
+ function bareName(name) {
347
+ return String(name || '').replace(/^@[^/]+\//, '').trim().toLowerCase();
348
+ }
349
+ function frameworkKeywords(inventory) {
350
+ const names = { next: 'nextjs', react: 'react', astro: 'astro', nuxt: 'nuxt', svelte: 'svelte' };
351
+ return Object.keys(inventory.package.framework_versions || {}).map((dependency) => names[dependency] || '').filter(Boolean);
352
+ }
353
+ async function projectSourceUrls(root, inventory, options) {
354
+ const pkg = await readJson(path.join(root, 'package.json'), null);
355
+ const published = Boolean(inventory.package.name) && pkg?.private !== true;
356
+ return unique([
357
+ options.url || '',
358
+ inventory.package.homepage || '',
359
+ httpsRepositoryUrl(inventory.package.repository) || '',
360
+ published ? `https://www.npmjs.com/package/${inventory.package.name}` : '',
361
+ ].filter((url) => /^https?:\/\//i.test(url)));
362
+ }
363
+ function httpsRepositoryUrl(value) {
364
+ const raw = String(value || '').trim();
365
+ if (!raw)
366
+ return null;
367
+ const shorthand = raw.match(/^(?:(github|gitlab|bitbucket):)?([\w.-]+\/[\w.-]+)$/i);
368
+ if (shorthand) {
369
+ const hosts = { github: 'github.com', gitlab: 'gitlab.com', bitbucket: 'bitbucket.org' };
370
+ return `https://${hosts[(shorthand[1] || 'github').toLowerCase()]}/${shorthand[2].replace(/\.git$/i, '')}`;
371
+ }
372
+ const ssh = raw.match(/^git@([^:]+):(.+?)(?:\.git)?$/i);
373
+ if (ssh)
374
+ return `https://${ssh[1]}/${ssh[2]}`;
375
+ const url = raw.replace(/^git\+/i, '').replace(/^git:\/\//i, 'https://').replace(/\.git$/i, '');
376
+ return /^https?:\/\//i.test(url) ? url : null;
326
377
  }
327
378
  async function resolveOrCreateMarketingMission(mode, missionRef, options, prompt) {
328
379
  const explicit = missionRef && missionRef !== 'latest' ? await resolveSearchVisibilityMission(options.root, missionRef, mode) : null;
@@ -335,9 +386,9 @@ async function resolveOrCreateMarketingMission(mode, missionRef, options, prompt
335
386
  }
336
387
  return createSearchVisibilityMission(mode, prompt, options);
337
388
  }
338
- async function collectInternalSources(root) {
389
+ async function collectInternalSources(root, inventory) {
339
390
  const out = [];
340
- for (const rel of INTERNAL_SOURCE_CANDIDATES) {
391
+ for (const rel of unique([inventory.package.path || '', inventory.readme.path || '', ...INTERNAL_SOURCE_FILES])) {
341
392
  const full = path.join(root, rel);
342
393
  if (!(await exists(full)))
343
394
  continue;
@@ -387,22 +438,16 @@ async function collectSuperSearchSources(mission, urls, kind, maxSources) {
387
438
  }
388
439
  return out;
389
440
  }
390
- function buildMarketingClaims(inventory, internalSources, externalSources) {
391
- const packageSource = internalSources.find((source) => source.path === 'package.json');
392
- const readmeSource = internalSources.find((source) => source.path === 'README.md');
393
- const perfSource = internalSources.find((source) => source.path?.includes('perf-budget'));
394
- const superSearchSource = internalSources.find((source) => source.path?.includes('super-search-local-http-smoke')) || externalSources.find((source) => source.verified);
441
+ function buildMarketingClaims(inventory, internalSources) {
442
+ const packageSource = internalSources.find((source) => Boolean(source.path) && source.path === inventory.package.path);
443
+ const readmeSource = internalSources.find((source) => Boolean(source.path) && source.path === inventory.readme.path);
395
444
  const claims = [];
396
445
  if (inventory.package.name)
397
446
  claims.push(publishableClaim('pkg-name', `${inventory.package.name} is the package name published in package metadata.`, 'identity', [packageSource?.id]));
398
447
  if (inventory.package.description)
399
448
  claims.push(publishableClaim('pkg-description', inventory.package.description, 'capability', [packageSource?.id]));
400
449
  if (readmeSource)
401
- claims.push(publishableClaim('readme-positioning', `README describes ${inventory.readme.h1 || inventory.package.name || 'the project'} and its SKS command surfaces.`, 'positioning', [readmeSource.id]));
402
- if (perfSource)
403
- claims.push(publishableClaim('perf-budget-source', 'Performance budget artifacts define command p95 budgets for release gates.', 'performance', [perfSource.id]));
404
- if (superSearchSource)
405
- claims.push(publishableClaim('super-search-source-backed', 'Super-Search artifacts provide source ledgers for search visibility evidence.', 'super_search', [superSearchSource.id]));
450
+ claims.push(publishableClaim('readme-positioning', `README describes ${inventory.readme.h1 || inventory.package.name || 'the project'}.`, 'positioning', [readmeSource.id]));
406
451
  return claims;
407
452
  }
408
453
  function publishableClaim(id, text, claimType, sourceIds) {