npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@classytic/projects

v0.1.0

Published

Project delivery engine — projects, tasks, timesheets, milestones, client deliverables with approval sign-off, and Stripe-style handover journeys. MongoDB persistence via @classytic/mongokit, arc-compatible events.

Downloads

18

Readme

@classytic/projects

Project delivery engine — not just a task tracker. Projects, tasks, timesheets with an approval → invoice pipeline, milestones with client sign-off and fixed-fee billing, client deliverables with a review loop, and Stripe-onboarding-style handover journeys that end in recorded client acceptance and a support / warranty window.

MongoDB persistence via @classytic/mongokit (claim-based CAS transitions, multi-tenant scoping, transactions), arc-compatible events, host-owned outbox. One createProjects() call and the host gets models, repositories, services, and a wired event bus.

Install

npm install @classytic/projects

Peers: @classytic/mongokit >=3.19, @classytic/repo-core >=0.8, @classytic/primitives >=0.11, mongoose >=9.4, zod >=4.

Boot

import { createProjects, ensureProjectsReady } from '@classytic/projects';

const engine = await createProjects({
  connection: mongoose.connection,
  eventTransport: transport,          // any arc EventTransport (Memory, Redis, Kafka)
  outbox: mongoOutboxStore,           // optional durable delivery (host-composed)
  bridges: {
    invoice: classyticInvoiceAdapter, // optional — enables BillingService
    esign: esignAdapter,              // optional — verifies acceptance signatures
  },
  // tenant: { tenantField: 'organizationId', fieldType: 'objectId' }  ← default
  // modules: { activity: true, templates: true, journeyAutomation: true }
});
await ensureProjectsReady(engine, { skipIndexes: true }); // prod: indexes at deploy

Every repository verb takes a ProjectsContext — { organizationId, actorId, session?, signal? }. Tenant scoping is enforced by mongokit's multiTenantPlugin (fail-closed), never by hand-written filters.

The delivery arc, end to end

const ctx = { organizationId: branchId, actorId: userId };
const R = engine.repositories;

// 1. Provision from a template (project + tasks + milestones + handover journey, atomic)
const tpl = await R.templates.createTemplate({
  name: 'Website build',
  project: { defaultBillableRate: { amount: 15000, currency: 'USD' } },
  milestones: [
    { key: 'launch', name: 'Launch', order: 0,
      billing: { trigger: 'on_sign_off', amount: { amount: 500000, currency: 'USD' } } },
  ],
  tasks: [
    { key: 'wireframes', title: 'Wireframes', milestoneKey: 'launch' },
    { key: 'build', title: 'Build', dependsOnKeys: ['wireframes'] },
  ],
  journey: {
    kind: 'handover', name: 'Site handover',
    stages: [{ key: 'delivery', name: 'Delivery', items: [
      { key: 'launch-ok', name: 'Launch signed off',
        requirement: { type: 'milestone_signed_off', refKey: 'launch' } },
      { key: 'creds', name: 'Credentials handed over',
        requirement: { type: 'credential_handoff' } },
    ]}],
    supportWindow: { durationMs: 90 * 24 * 3600 * 1000, kind: 'warranty' },
  },
}, ctx);
const { project, journey } = await engine.services.provisioning.instantiate(
  String(tpl._id), { code: 'ACME-2026', name: 'Acme site', clientRef: 'account_acme' }, ctx);

// 2. Execute: tasks + timesheets (dependency-gated, approval-gated)
await R.projects.activate(String(project._id), ctx);
const log = await R.timeLogs.log({ projectId, taskId, userId, minutes: 240 }, ctx);
await R.timeLogs.submit(String(log._id), ctx);
await R.timeLogs.approve(String(log._id), ctx, 'manager_1'); // task.actualHours accumulates

// 3. Bill: T&M batch OR fixed-fee milestone
await engine.services.billing.invoiceTimeLogs({ logIds: [String(log._id)] }, ctx);
await engine.services.billing.invoiceMilestone(milestoneId, ctx); // once-only fence

// 4. Deliverables: client review loop (optionally gated by an ApprovalChain)
const d = await R.deliverables.createDeliverable({ projectId, name: 'Style guide' }, ctx);
await R.deliverables.submit(String(d._id), { fileRef: 'media://v1' }, ctx);
await R.deliverables.approve(String(d._id), ctx, 'client_1');
// → journey items with requirement { type: 'deliverable_approved', ref } auto-complete

// 5. Handover: journey → acceptance → delivered project + support window (one transaction)
await R.journeys.activate(String(journey._id), ctx);
await R.journeys.requestAcceptance(String(journey._id), ctx); // gated on required items
const result = await engine.services.handover.accept(
  String(journey._id), { acceptedBy: 'client_ceo', signatureRef: 'env_123' }, ctx);
// result.project.status === 'delivered', supportWindow running

State machines

Project:      draft → active ↔ on_hold;  active → delivered → closed;  * → cancelled
Task:         todo → in_progress ↔ blocked → in_review → done          (+ cancelled)
TimeLog:      draft → submitted → approved → invoiced   (rejected → draft resubmit loop)
Milestone:    planned → in_progress → completed → signed_off           (+ reopen, cancelled)
Deliverable:  draft → submitted → in_review → approved | changes_requested → submitted
Journey:      draft → active → in_acceptance → accepted | active (rework) | declined → active

All transitions are defineStateMachine tables (compile-time) paired with repo.claim() CAS (runtime) — a race loser throws the same typed InvalidTransitionError (status: 422) as an illegal move.

Handover journeys (the differentiator)

A journey is a staged checklist with typed requirements:

| Requirement type | Auto-completed when | |---|---| | deliverable_approved | the referenced deliverable is approved | | milestone_completed / milestone_signed_off | the referenced milestone reaches that state | | task_completed | the referenced task is done | | manual, document, credential_handoff, training, time_logs_settled, payment_settled | a human calls completeItem() (with optional evidenceRef) |

journeyProgress(journey) gives the Stripe-style progress bar (per-stage ratios, acceptanceReady, blocking items). requestAcceptance() is gated on every required item; HandoverService.accept() verifies the signature (esign bridge), flips the project to delivered, and opens the support window — one transaction, events flushed post-commit.

Post-handover support is deliberately light: a supportWindow on the project plus isWithinSupportWindow() — ticketing, SLA policies, and warranty claims live in @classytic/support (bridge via events).

Activity timeline (client portal feed)

With modules.activity (default on), every domain event materializes into an append-only project_activities collection with visibility: 'internal' | 'client' — milestones, deliverable submissions, journey progress, and delivery are client-visible out of the box. activities.comment() adds human posts to the same feed.

Billing bridge

interface InvoiceBridge {
  postLineItem?(req: InvoiceLineItemRequest, ctx): Promise<{ invoiceLineRef: string }>;
}

BillingService.invoiceTimeLogs batches approved billable logs (same project / user / currency) into one line; invoiceMilestone posts a fixed-fee line once (AlreadyInvoicedError on retry). Both send a deterministic idempotencyKey so the host bridge can dedupe. Honest race note: the bridge post is external — if the process dies between post and flip, logs stay approved while the line exists; the idempotency key is the recovery handle.

Events

~50 events under projects:* (rule 12), full Zod catalog exported as projectsEventDefinitions from the root and /events subpath — arc hosts register them for publish-time validation + OpenAPI:

import { projectsEventDefinitions } from '@classytic/projects/events';
for (const def of projectsEventDefinitions) registry.register(def);

Unit-of-work discipline (P8.1): events inside engine.withTransaction() queue under PENDING_EVENTS and flush after commit; outbox rows commit with the transaction; a host-owned session with neither queue nor outbox throws UnmanagedSessionError.

Arc integration

export default defineResource({
  name: 'project',
  prefix: '/projects',
  adapter: createAdapter(engine.models.Project, engine.repositories.projects),
  actions: {
    activate: { handler: (id, _d, req) => engine.repositories.projects.activate(id, req.scope) },
    deliver:  { handler: (id, d, req) => engine.repositories.projects.deliver(id, req.scope, d) },
    // ... hold / close / cancel — all state transitions (rule 30)
  },
});

CRUD, pagination, and QueryParser filters come free from the adapter; domain verbs map 1:1 to arc actions.

Zod schemas

@classytic/projects/schemas exports create/update schemas per entity (projectCreateSchema, journeyCreateSchema, …) — the same schemas the repositories validate with, reusable for arc route bodies.

Pure calculators

billableTotal, budgetBurn, taskProgress, velocity, journeyProgress, isWithinSupportWindow — safe anywhere, no DB.

Tests

npm test                 # unit + integration (mongodb-memory-server replica set)

Integration suite covers the full template → provision → execute → bill → handover → support-window scenario, tenant isolation (probe test), and the P8.1 ghost-event contract.

License

MIT