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

@aimana/core

v0.1.0

Published

AIMana core: schemas, .aimana/ file layer, templates

Readme

@aimana/core

Единственный способ читать и писать каталог .aimana/ проекта. Схемы, парсер markdown с YAML-frontmatter, файловый слой, дерево проекта, вычисление состояния.

Что внутри

| Модуль | Назначение | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | schema/ | zod-схемы frontmatter: Project, Feature, Task, Stage, Decision, Pipeline, enum-ы статусов | | document/ | parseDocument, serializeDocument (ключи в порядке схемы), splitFrontmatter, секции h2: getSection, setSection | | fs/ | AimanaFs (чтение с валидацией, атомарная запись), AimanaPaths, readTree, ошибки | | state/ | buildGraph (DAG, циклы, битые ссылки), computeState (эффективные статусы, blockedBy, прогресс, nextStage, доступные к старту), таблицы переходов canTransition / assertTransition | | ops/ | Записи в .aimana/ от лица демона: createFeature/createTask, reportProgress/reportArtifact/logDecision, план таски (applyPlan, approvePlan, setTaskStatus) | | templates/ | движок подстановки, разрешение шаблонов, пайплайны, шаблоны архитектур, пресеты стеков, buildPromptContext, renderStagePrompt / renderActionPrompt | | catalogue/ | справочник опций проекта для CLI и веба: языки, менеджеры пакетов, платформы, имена гейтов, политики с подписями и описаниями |

Пример

import { AimanaFs, computeState, readTree } from '@aimana/core';

const afs = new AimanaFs('/path/to/repo');
if (await afs.exists()) {
  const tree = await readTree(afs);
  for (const { feature, tasks } of tree.features) {
    console.log(feature.frontmatter.id, tasks.length);
  }
  for (const error of tree.errors) console.warn(error.file, error.message);
  await afs.updateStageSection('auth', '001-login-form', '02', 'Результат', 'Сделано.');

  const state = computeState(tree);
  for (const task of state.available) console.log('можно стартовать:', task.ref);
  for (const issue of state.issues) console.warn(issue.message);
}

Правила вывода статусов

computeState чистая функция над ProjectTree:

  • Таска done, если сохранено done или все её стейджи done. blocked, если открыты её зависимости из depends_on или зависимости её фичи. in-progress, если хоть один стейдж сдвинулся с todo. Иначе сохранённый статус.
  • Фича done, когда все таски done; blocked при открытых зависимостях; in-progress, если есть таска в работе или заблокированная; archived как есть; status_override: true замораживает сохранённый статус.
  • Доступна к старту: эффективный статус todo или planned, нет блокеров, фича не archived, таска не в цикле.

Переходы статусов, которые демон обязан проверять перед записью: STAGE_TRANSITIONS, TASK_TRANSITIONS, FEATURE_TRANSITIONS.

Гарантии

  • Невалидный файл никогда не попадает на диск: запись валидирует перед rename.
  • Неизвестные ключи frontmatter сохраняются (совместимость с будущими версиями формата).
  • Сериализация детерминирована: порядок ключей по схеме, indent: 2, один \n в конце.
  • readTree не падает на одном битом файле, а возвращает его в errors[].
  • Проблемы графа (цикл, битая ссылка) попадают в state.issues, а не бросаются.

Настройки проекта (ops/project.ts)

updateProjectBody заменяет тело PROJECT.md — правила проекта, которые CLAUDE.md импортирует целиком. Поля updated у проекта в схеме нет, поэтому ничего не проставляется.

updateProjectSettings меняет frontmatter частично: поля, которых нет в патче, не трогаются. Схема — looseObject, у проекта бывают ключи, о которых операция не слышала, и форма с пятью полями не должна стирать шестое. Внутри gates и policies.security мерж идёт по ключам, где null удаляет ключ; пустая команда гейта — тоже удаление, потому что команды без содержимого в формате не бывает. platforms заменяются целиком: иначе платформу не убрать. Результат валидируется схемой до записи — невалидное значение бросает AimanaValidationError и на диск не попадает. Пустой патч — тоже ошибка, а не молчаливая перезапись файла.

Справочник опций (catalogue/project-options.ts) — списки языков, менеджеров пакетов, платформ, имён гейтов и политик с русскими подписями. Всё это подсказки, а не ограничения: схема принимает любой язык, любую платформу и любое имя гейта. Живёт в core, потому что нужен и @aimana/cli (опрос aimana init), и @aimana/web (форма настроек), а третья копия была бы третьей правдой.

План таски (ops/plan.ts)

Модель отдаёт план как данные — TaskPlanSchema:

{ stages: [{ kind, title, goal, size?, artifacts, gates, criteria }], risks, out_of_scope }

parsePlanJson(text) достаёт то же самое из ответа, когда инструмент aimana_submit_plan не был вызван: последний ```json-блок, затем любой блок, затем фрагмент от первой { до последней }.

Дальше данные превращаются в markdown и markdown становится источником правды:

  • applyPlan пишет stages во frontmatter TASK.md и разворачивает план в секции ## План (блок ### Стейдж NN: … на каждый стейдж), ## Риски, ## Не входит. Статус таски → plan-review.
  • Человек правит эти секции руками до подтверждения — хоть в вебе, хоть в редакторе.
  • approvePlan перечитывает ## План парсером (parsePlanSection), а не то, что записала модель: дописанный руками стейдж попадает во frontmatter и получает свой STAGE-xx.md. Статус и run_id стейджей, переживших перепланирование (тот же id, то же название), сохраняются.

renderPlanSection/parsePlanSection обратимы, это проверяется round-trip тестом: иначе правка руками теряла бы данные. Созданные файлы стейджей получают заготовку ТЗ (цель, ожидаемые артефакты, критерии приёмки чекбоксами) — само ТЗ генерирует T-032.

Фикстуры

test/fixtures/sample и test/fixtures/broken генерируются скриптом test/make-fixtures.mjs через реальный сериализатор. Перегенерировать после изменения формата:

pnpm --filter @aimana/core build && node packages/core/test/make-fixtures.mjs

Шаблоны промптов

Промпт для запуска Claude собирается из шаблона и контекста, собранного по .aimana/.

Где лежат шаблоны

Каждый файл ищется по цепочке, побеждает первый найденный (мержа содержимого нет):

  1. <repo>/.aimana/templates/ — переопределение для конкретного проекта, лежит в git;
  2. ~/.aimana/templates/ — личные шаблоны пользователя;
  3. packages/core/templates/ — встроенные, едут в npm-пакет.
templates/
  pipelines/{default,hotfix,research}.yaml   каркасы стейджей таски
  stages/<kind>.md                           промпт стейджа: spec, plan, implement, test,
                                             review, security, docs, custom
  actions/<action>.md                        plan-task, spec-full, spec-stage, summarize-result
  partials/{context,stage,system-tail}.md    общие куски, подключаются через {{> name}}

Чтобы переопределить один промпт, достаточно положить свой файл рядом по тому же пути: .aimana/templates/stages/implement.md. Партиалы переопределяются так же по отдельности, поэтому можно поменять только «системный хвост», не трогая остальное.

Движок

Своё подмножество mustache (~230 строк, без зависимостей). eta из плана не подошёл: нужен синтаксис {{...}} и строгий режим — незаполненный плейсхолдер должен ронять рендер, а не молча давать дыру в промпте. Ни eta, ни mustache так не умеют.

| Конструкция | Что делает | | --------------------- | -------------------------------------------------------------------------------------------------------------------- | | {{path.to.value}} | подстановка; отсутствующее имя — ошибка, объект — ошибка, массив строк склеивается через \n | | {{#name}}…{{/name}} | секция: массив — повтор на каждый элемент, объект или скаляр — кладётся на стек контекстов, пусто или ложь — пропуск | | {{^name}}…{{/name}} | обратная секция: рендерится, когда значения нет или оно пустое | | {{.}} | текущий элемент стека (элемент массива, значение скалярной секции) | | {{> partial}} | вставка partials/<partial>.md; отступ строки переносится на все её строки | | {{! комментарий }} | выкидывается |

Строка, в которой кроме тега секции, закрытия, комментария или partial-а ничего нет, исчезает целиком: служебные теги не оставляют пустых строк.

Основные плейсхолдеры

buildPromptContext собирает контекст из готового ProjectTree (чистая функция, диск не читает).

| Плейсхолдер | Что внутри | | ---------------------- | -------------------------------------------------------------------------------- | | {{project.rules}} | тело PROJECT.md без ведущего # ... | | {{policies}} | политики безопасности и тестирования готовым markdown-списком | | {{stage.spec}} | секция ## ТЗ текущего стейджа, пусто если ТЗ ещё нет | | {{artifacts}} | ожидаемые артефакты стейджа списком (stage.artifacts — тот же список массивом) | | {{previous.results}} | секции ## Результат предыдущих стейджей, блоками с заголовками | | {{decisionsText}} | ADR, чьи теги пересекаются с тегами фичи | | {{gateLog}} | лог упавшего гейта, если запуск повторный |

Рядом с готовыми блоками лежат структурированные данные (project.gates, task.stages, stage.artifacts, decisions), чтобы шаблон мог собрать список по-своему.

Пример

import {
  AimanaFs,
  buildPromptContext,
  createTemplateResolver,
  loadPipeline,
  readTree,
  renderStagePrompt,
} from '@aimana/core';

const afs = new AimanaFs('/path/to/repo');
const tree = await readTree(afs);
const resolver = createTemplateResolver({ root: '/path/to/repo' });

const pipeline = await loadPipeline(resolver, 'default');
const context = buildPromptContext({
  tree,
  featureId: 'auth',
  taskId: '001-login-form',
  stageId: '02',
  pipeline,
});

const prompt = await renderStagePrompt(resolver, 'implement', context);

renderActionPrompt(resolver, 'plan-task', context) рендерит шаблон действия. Обе функции строгие: опечатка в имени плейсхолдера даёт TemplateRenderError с именем шаблона и строкой.

Пайплайны валидируются PipelineSchema: битый YAML или несовпадение id с именем файла дают AimanaValidationError, неизвестный id — TemplateNotFoundError.

Файлы packages/core/templates/**/*.md исключены из prettier: он считает {{#section}} абзацем и вставляет пустые строки внутрь списков, ломая рендер.

Свои шаблоны

Архитектуры, пресеты стеков и политики ищутся по одной цепочке, побеждает первый уровень, объявивший id:

  1. <repo>/.aimana/templates/ — шаблоны этого репозитория, лежат в git;
  2. ~/.aimana/templates/ — личные шаблоны пользователя, общие для всех его проектов;
  3. встроенные в пакет.
~/.aimana/templates/            или  <repo>/.aimana/templates/
  architectures/<id>/template.yaml   плюс ARCHITECTURE.md, rules.md, structure.yaml, pipeline.yaml
  stacks/<id>.yaml
  policies/<id>.yaml

id внутри файла обязан совпадать с именем файла или каталога, иначе load* и list* разошлись бы в ответах на один и тот же вопрос.

Сломанный шаблон не исчезает. listArchitectures, listStacks и listPolicies возвращают { items, errors }: файл, который не проходит схему, попадает в errors вместе с абсолютным путём и полями, на которых он развалился, а остальные шаблоны остаются в items. Выбрать его нельзя — применять нечего; loadArchitecture, loadStack и loadPolicy, которых просят по имени, по-прежнему бросают AimanaValidationError.

const { items, errors } = await listStacks(createTemplateResolver({ root }));
// errors: [{ id, source, file, message, issues: [{ path, message }] }]

Обход каталога не уходит по симлинкам за его пределы: сам каталог шаблонов симлинком быть может (~/.aimana/templates в дотфайлах — обычное дело), а отдельная запись внутри него, ведущая наружу, не читается и попадает в errors.

Шаблоны архитектур

Каталог templates/architectures/<id>/ — каркас архитектуры, который разворачивается в проект. Ищется по той же цепочке (проект → пользователь → встроенные), причём пофайлово: чтобы поменять только правила встроенного шаблона, достаточно положить свой .aimana/templates/architectures/hexagonal/rules.md.

architectures/<id>/
  template.yaml     id, title, description, tags — манифест, он же объявляет шаблон
  ARCHITECTURE.md   каркас с заголовками и вопросами, уезжает в .aimana/ARCHITECTURE.md
  rules.md          правила, которые дописываются в PROJECT.md
  structure.yaml    (необязательно) рекомендуемое дерево папок: entries[{path, note}]
  pipeline.yaml     (необязательно) пайплайн, который шаблон предлагает вместо дефолтного

Встроенных восемь: modular-monolith, clean-architecture, hexagonal, feature-sliced, microservices, mobile-mvvm, cli-tool, library.

import {
  AimanaFs,
  applyArchitecture,
  createTemplateResolver,
  listArchitectures,
  previewArchitecture,
} from '@aimana/core';

const resolver = createTemplateResolver({ root });
await listArchitectures(resolver);
// { items: [{ id, title, description?, tags, source }], errors: [] }

const preview = await previewArchitecture(new AimanaFs(root), resolver, 'clean-architecture');
// preview.architecture / preview.rules: { action, current, next, diff } — ничего не записано
await applyArchitecture(new AimanaFs(root), resolver, 'clean-architecture', {
  overwriteArchitecture: preview.overwritesArchitecture,
});

Правила шаблона живут в теле PROJECT.md между маркерами:

<!-- aimana:template rules start -->
...правила шаблона...
<!-- aimana:template rules end -->

Граница нужна, чтобы смена шаблона убирала ровно правила прежнего и не трогала дописанное руками. Одинокий маркер без пары считается отсутствующей секцией: гадать, где кончался наполовину стёртый блок, значит съесть чужой текст.

ARCHITECTURE.md, написанный человеком, без спроса не перезаписывается: applyArchitecture бросает AimanaConflictError, пока не передан overwriteArchitecture. Дифф строк считает diffLines — свой LCS на тридцать строк, ради двух markdown-файлов зависимость не заводится.

Пресеты стеков

Файл templates/stacks/<id>.yaml — пресет стека: чем проект написан, какими командами проверяется и по каким признакам его узнать. Ищется по той же цепочке (проект → пользователь → встроенные), побеждает ближайший к проекту.

id: next
title: Next.js
description: 'React на сервере и в браузере: маршруты по файлам…'
language: typescript
framework: nextjs
package_manager: npm
platforms: [web]
gates:
  lint: 'npm run lint'
  typecheck: 'npx tsc --noEmit'
  build: 'npm run build'
rules:
  - 'Компонент серверный по умолчанию…'
detect:
  files: [next.config.ts]
  dependencies: [next]
  contains: [{ file: pom.xml, text: spring-boot-starter }]
  priority: 20

Встроенных восемнадцать: next, remix, vite-react, nest, fastify, express, django, fastapi, spring-boot, quarkus, go-std, rust-axum, swiftui, kotlin-compose, flutter, react-native, electron, tauri.

Гейт есть только тогда, когда у стека есть настоящая команда. У Next нет тестового раннера из коробки — гейта test в пресете нет; у Express нет ни линтера, ни типов — остаётся один npm test. Выдуманная команда падает на первом запуске и учит человека не верить гейтам, поэтому её лучше не писать вовсе. Имена гейтов не ограничены четвёркой: у Django есть migrations, у Rust и Flutter — format:check.

Автодетект

import { createTemplateResolver, detectStackPreset } from '@aimana/core';

const matches = await detectStackPreset(root, createTemplateResolver({ root }));
// [{ preset, score, signals: [{ kind: 'file', text: 'next.config.ts' }, …] }] — лучший первым

Признак срабатывает, если файл есть (* матчит внутри одного сегмента, поэтому *.xcodeproj работает), если имя есть среди зависимостей package.json (точное совпадение, иначе react объявил бы React Native реактом) или если в файле встречается строка (без учёта регистра — в requirements.txt пишут Django). Ранжирование — по числу сработавших признаков, при равенстве по detect.priority: приложение на Electron с Vite даёт два признака обоим пресетам, и специфичный должен побеждать. Пустой ответ — это ответ, а не ошибка: большинство репозиториев написаны на стеке, под который пресета нет. Битый пресет в .aimana/templates/stacks/ при этом всё равно бросает AimanaValidationError.

Применение

import { applyStackPreset, previewStackPreset } from '@aimana/core';

const preview = await previewStackPreset(new AimanaFs(root), resolver, 'django');
// preview.settings: [{ field: 'stack.language', current: 'typescript', next: 'python' }, …]
// preview.rules: { action, current, next, diff } — на диск ничего не записано
await applyStackPreset(new AimanaFs(root), resolver, 'django');

Пресет пишет stack.* (включая stack.preset), гейты своих имён и правила — всё через updateProjectSettings и updateProjectBody, то есть документ остаётся валидным по схеме. Гейт, о котором пресет ничего не говорит, остаётся как был. Платформы добавляются, а не заменяются: пресет знает про свой стек, но не знает, что у проекта есть ещё и CLI.

Правила пресета живут в своей паре маркеров:

<!-- aimana:stack rules start -->
...правила пресета...
<!-- aimana:stack rules end -->

Отдельно от aimana:template rules шаблона архитектуры — иначе выбор стека стирал бы правила архитектуры и наоборот.

Политики

Файл templates/policies/<id>.yaml — то, во что разворачивается чекбокс PROJECT.md: policies: текст правила, дополнительные гейты и дополнительные стейджи. Цепочка та же (проект → пользователь → встроенные), побеждает ближайший к проекту. Идентификатор — snake_case: это ключ из policies.security или policies.testing, а не kebab-case-id шаблона.

id: dependency_audit
title: Аудит зависимостей
kind: security
description: 'Зависимости проверяются на известные уязвимости.'
rule: 'Новая зависимость приходит вместе с проверкой на известные уязвимости…'
gates:
  - name: audit
    package_managers:
      pnpm: 'pnpm audit --audit-level=high'
      cargo: 'cargo audit'
    languages:
      go: 'go run golang.org/x/vuln/cmd/govulncheck@latest ./...'
stages: []

Встроенных семь: input_validation, no_secrets_in_code, dependency_audit, auth_review, unit_required, integration_required, e2e_required — ровно чекбоксы справочника catalogue/project-options.ts.

Команда гейта зависит от стека, поэтому её нет одной на всех: pnpm audit ничего не значит для проекта на cargo. Сначала спрашивается пакетный менеджер, потом язык, и если ни один не знает команды — policyGates честно возвращает пустой список, а гейт не появляется вовсе. По той же причине у no_secrets_in_code гейта нет ни для кого: сканеры секретов не входят ни в один пакетный менеджер, а выдуманная команда падает на первом запуске.

Применение

import { applyPolicies, previewPolicies } from '@aimana/core';

const preview = await previewPolicies(new AimanaFs(root), resolver); // как есть сейчас
await applyPolicies(new AimanaFs(root), resolver, { dependency_audit: true });

Включение пишет флаг, гейты политики и её правила — всё через updateProjectSettings и updateProjectBody. Выключение убирает ровно своё: флаг становится false (ключ остаётся в файле — это же чекбокс), правило уходит из секции, а гейт снимается только пока его команда всё ещё одна из тех, что политика могла написать сама. Команду, переписанную руками, выключение не трогает.

Правила политик живут в третьей паре маркеров:

<!-- aimana:policy rules start -->
...правила включённых политик, их стейджи и гейты...
<!-- aimana:policy rules end -->

Отдельно от aimana:template rules и aimana:stack rules: три владельца пишут в один файл, и общая граница означала бы, что каждый выбор стирает два других. Когда не включено ничего, секция убирается целиком вместе с маркерами — пустой заголовок хуже отсутствующего.

Дополнительные стейджи

policyStages собирает стейджи включённых политик, withPolicyStages подмешивает их в пайплайн, по которому планируется таска (стейдж с тем же видом и названием второй раз не добавляется). Сейчас планировщик демона этим ещё не пользуется: до плана дополнительные стейджи доезжают текстом — секция «Дополнительные стейджи» в правилах PROJECT.md, а правила проекта лежат в каждом промпте. Чтобы стейдж появился и в каркасе пайплайна, PlanService должен позвать withPolicyStages — это одна строка, но она в packages/daemon/src/plans.