@papi-ai/shared
Advanced tools
+52
-2
@@ -32,3 +32,3 @@ /** Valid task statuses on the cycle board. */ | ||
| /** The value-add capabilities that are switchable from the dashboard (Tier 1). */ | ||
| type CapabilityKey = 'prReviewer' | 'securityScan' | 'changelog' | 'verifyHealthCheck' | 'gestaltPreBuild' | 'batchBuildRollup' | 'modelRecommendation' | 'discoveredIssues' | 'publishDirective' | 'releaseGate' | 'deployHook' | 'acceptanceGate'; | ||
| type CapabilityKey = 'prReviewer' | 'securityScan' | 'changelog' | 'verifyHealthCheck' | 'gestaltPreBuild' | 'batchBuildRollup' | 'modelRecommendation' | 'discoveredIssues' | 'publishDirective' | 'releaseGate' | 'deployHook' | 'acceptanceGate' | 'autoBranch' | 'autoCommit' | 'autoPush' | 'papiMetaFraming'; | ||
| /** A curated capability descriptor rendered by the dashboard toggle surface. */ | ||
@@ -120,3 +120,53 @@ interface CapabilityDescriptor { | ||
| declare function isSensitiveChangelogLine(line: string): boolean; | ||
| /** Why a doc deletion was refused. */ | ||
| type DocDeletionBlockReason = 'not_found' | 'not_authorized' | 'supersession_dependency' | 'reference_dependency'; | ||
| /** Facts the deletion guard decides over. All dependency lookups are resolved by | ||
| * the caller (adapter or dashboard) and passed in as booleans so this stays pure. */ | ||
| interface DocDeletionInput { | ||
| /** False when no row matched id + project scope. */ | ||
| docExists: boolean; | ||
| /** The acting user's id (config.userId / bearer identity). Null → never authorized. */ | ||
| requesterUserId: string | null | undefined; | ||
| /** doc_registry.owner_user_id — the doc's creator lock. */ | ||
| creatorUserId: string | null | undefined; | ||
| /** projects.user_id — the project owner. */ | ||
| projectOwnerUserId: string | null | undefined; | ||
| /** True when another doc_registry row has superseded_by = this doc's id. */ | ||
| hasSupersessionDependent: boolean; | ||
| /** True when a cycle_task.doc_ref targets this doc, or an action links a task. */ | ||
| hasReferenceDependent: boolean; | ||
| } | ||
| type DocDeletionDecision = { | ||
| allowed: true; | ||
| } | { | ||
| allowed: false; | ||
| reason: DocDeletionBlockReason; | ||
| }; | ||
| /** | ||
| * Evaluate whether a doc may be hard-deleted. Authorization is checked BEFORE | ||
| * dependencies so an unauthorized caller never learns a doc's dependency state. | ||
| * Fail-closed: a null/blank requester is never the creator or owner. | ||
| */ | ||
| declare function evaluateDocDeletion(input: DocDeletionInput): DocDeletionDecision; | ||
| /** Human-readable explanation for a block reason — reused across MCP + dashboard. */ | ||
| declare function docDeletionBlockMessage(reason: DocDeletionBlockReason): string; | ||
| /** | ||
| * The soft Team upsell shown to a non-Team caller who invites someone. Advisory | ||
| * ONLY — it accompanies a successful invite, it never blocks one (AD-73). | ||
| */ | ||
| declare function contributorTeamUpsell(tier: string): string; | ||
| /** The contributor-invite decision. `allowed` is always true — see PERMISSIVE note above. */ | ||
| type ContributorGateDecision = { | ||
| allowed: true; | ||
| upsell?: string; | ||
| }; | ||
| /** | ||
| * Evaluate the contributor-invite gate. PERMISSIVE (AD-73): the invite is ALWAYS | ||
| * allowed — this function never returns a block. Non-Team hosted tiers (free/pro) | ||
| * receive a soft Team `upsell`; owner/local (null/undefined tier) and team callers | ||
| * receive none. Tier is resolved SERVER-SIDE by the caller and passed in — this | ||
| * pure function never reads client input. | ||
| */ | ||
| declare function evaluateContributorGate(tier: string | null | undefined): ContributorGateDecision; | ||
| export { CAPABILITY_KEYS, CAPABILITY_REGISTRY, type CapabilityDescriptor, type CapabilityKey, type CapabilityMap, type CapabilityStep, type EffortSize, RETIRED_DECISION_OUTCOMES, type ReviewStage, type ReviewVerdict, SENSITIVE_CHANGELOG_PATTERNS, TASK_STATUSES, type TaskComplexity, type TaskPriority, type TaskStatus, type TaskType, VALID_TRANSITIONS, isCapabilityEnabled, isCapabilityKey, isLiveDecision, isSensitiveChangelogLine, isValidStatus, isValidTransition, validateTransition }; | ||
| export { CAPABILITY_KEYS, CAPABILITY_REGISTRY, type CapabilityDescriptor, type CapabilityKey, type CapabilityMap, type CapabilityStep, type ContributorGateDecision, type DocDeletionBlockReason, type DocDeletionDecision, type DocDeletionInput, type EffortSize, RETIRED_DECISION_OUTCOMES, type ReviewStage, type ReviewVerdict, SENSITIVE_CHANGELOG_PATTERNS, TASK_STATUSES, type TaskComplexity, type TaskPriority, type TaskStatus, type TaskType, VALID_TRANSITIONS, contributorTeamUpsell, docDeletionBlockMessage, evaluateContributorGate, evaluateDocDeletion, isCapabilityEnabled, isCapabilityKey, isLiveDecision, isSensitiveChangelogLine, isValidStatus, isValidTransition, validateTransition }; |
+49
-1
@@ -57,3 +57,12 @@ // src/index.ts | ||
| // chooses to hold builds to their own acceptance criteria. | ||
| { key: "acceptanceGate", label: "Acceptance-criteria gate", description: "Requires confirming the handoff acceptance criteria before a build completes.", step: "build", defaultEnabled: false } | ||
| { key: "acceptanceGate", label: "Acceptance-criteria gate", description: "Requires confirming the handoff acceptance criteria before a build completes.", step: "build", defaultEnabled: false }, | ||
| // task-2325 (C344): configurable workflow guardrails — let a user opt out of the | ||
| // git/branch/commit/PR ceremony and PAPI-meta framing so PAPI adapts to their | ||
| // workflow instead of imposing one. Every toggle DEFAULTS ON (no defaultEnabled) | ||
| // so current behaviour is byte-identical until the user flips it off. The no-git | ||
| // fallback that these ride on top of is a separate task (task-2353). | ||
| { key: "autoBranch", label: "Auto branch", description: "Creates a feature branch per task/cycle at build start.", step: "build" }, | ||
| { key: "autoCommit", label: "Auto commit", description: "Commits your work automatically when a build completes.", step: "build" }, | ||
| { key: "autoPush", label: "Auto push & PR", description: "Pushes the branch and opens a pull request on build complete.", step: "build" }, | ||
| { key: "papiMetaFraming", label: "PAPI self-referential framing", description: "Includes PAPI-meta build-discipline framing in handoff output.", step: "build" } | ||
| ]; | ||
@@ -114,2 +123,37 @@ var CAPABILITY_KEYS = CAPABILITY_REGISTRY.map((c) => c.key); | ||
| } | ||
| function normId(v) { | ||
| return (v ?? "").trim().toLowerCase(); | ||
| } | ||
| function evaluateDocDeletion(input) { | ||
| if (!input.docExists) return { allowed: false, reason: "not_found" }; | ||
| const requester = normId(input.requesterUserId); | ||
| const isCreator = requester.length > 0 && requester === normId(input.creatorUserId); | ||
| const isOwner = requester.length > 0 && requester === normId(input.projectOwnerUserId); | ||
| if (!isCreator && !isOwner) return { allowed: false, reason: "not_authorized" }; | ||
| if (input.hasSupersessionDependent) return { allowed: false, reason: "supersession_dependency" }; | ||
| if (input.hasReferenceDependent) return { allowed: false, reason: "reference_dependency" }; | ||
| return { allowed: true }; | ||
| } | ||
| function docDeletionBlockMessage(reason) { | ||
| switch (reason) { | ||
| case "not_found": | ||
| return "Doc not found in this project."; | ||
| case "not_authorized": | ||
| return "Only the doc creator or the project owner may delete this doc."; | ||
| case "supersession_dependency": | ||
| return "Another doc supersedes this one \u2014 deleting it would orphan the supersession chain. Re-point or remove the dependent first."; | ||
| case "reference_dependency": | ||
| return "A task references this doc (doc_ref or a linked action) \u2014 resolve the reference before deleting."; | ||
| } | ||
| } | ||
| var CONTRIBUTOR_PRICING_URL = "https://getpapi.ai/pricing"; | ||
| function contributorTeamUpsell(tier) { | ||
| return `**Heads up \u2014 shared projects are a Team feature.** | ||
| You're on the ${tier} plan and the invite still went through. Team adds roles and the Quality Gate, and read-only viewer seats are free, so you only pay for who is actually building: ${CONTRIBUTOR_PRICING_URL}`; | ||
| } | ||
| function evaluateContributorGate(tier) { | ||
| if (tier == null || tier === "team") return { allowed: true }; | ||
| return { allowed: true, upsell: contributorTeamUpsell(tier) }; | ||
| } | ||
| export { | ||
@@ -122,2 +166,6 @@ CAPABILITY_KEYS, | ||
| VALID_TRANSITIONS, | ||
| contributorTeamUpsell, | ||
| docDeletionBlockMessage, | ||
| evaluateContributorGate, | ||
| evaluateDocDeletion, | ||
| isCapabilityEnabled, | ||
@@ -124,0 +172,0 @@ isCapabilityKey, |
+1
-1
| { | ||
| "name": "@papi-ai/shared", | ||
| "version": "0.1.6", | ||
| "version": "0.1.7", | ||
| "description": "Shared types and business rules for PAPI — used by both MCP server and dashboard", | ||
@@ -5,0 +5,0 @@ "license": "Elastic-2.0", |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
24922
29.71%346
40.08%1
Infinity%