Arhitectură, module și mixin-uri

ai-lib separă decizia privind instrucțiunile aplicabile de scrierea lor pe disc. Catalogul descrie proprietatea și relațiile. Detectarea produce indicii despre repository. Rezolvarea transformă o selecție într-un set efectiv de instrucțiuni. Materializarea produce fișiere, iar reconcilierea compară instalarea curentă cu una nouă propusă.

Straturile implementării

Zonă din sursăResponsabilitate
src/cli.ts și src/commands/Înregistrarea comenzilor Clipanion, opțiuni, ieșire și orchestrare.
src/components/Componente React și Ink pentru explorarea interactivă a catalogului.
src/lib/catalog.tsÎncărcarea manifestelor JSON, importarea detectoarelor și validarea catalogului cu Zod.
src/lib/detect.tsDescoperirea repository-ului și workspace-urilor, contextele detectoarelor și colectarea dovezilor.
src/lib/resolve.tsExtinderea preset-urilor, ordonarea dependențelor, verificarea conflictelor și activarea mixin-urilor.
src/lib/materialize.tsPlanuri de fișiere, marcaje de proveniență, hash-uri SHA-256, inspectare și scriere.
src/lib/reconcile.tsSelecția propusă și diferențele dintre starea curentă și cea dorită.
src/lib/stack.tsPersistența configurației YAML și gestionarea formatului acesteia.

Aceste straturi folosesc structuri de date tipizate, nu ieșirea terminalului ca protocol intern. De exemplu, un plan de reconciliere conține rezolvările curentă și propusă, diferențele modulelor selectate și efective, schimbările mixin-urilor active, diferențele locațiilor locale și un plan complet de materializare.

Un modul deține o responsabilitate

Un modul se află în catalog/modules/<folder>/module.json. Câmpul stabil id este referința folosită de preset-uri și de configurația salvată; nu trebuie să coincidă cu numele directorului. Manifestul descrie numele, versiunea, categoria, dependențele, conflictele, fișierele sursă, destinațiile gestionate și locațiile pentru instrucțiuni locale.

Modulul TypeScript este lang/typescript. Depinde de global/core, deține instrucțiunile comune din .github/instructions/shared/typescript/ și indică .github/instructions/local/typescript/ pentru personalizări locale.

Fișierele sale sunt documente separate despre arhitectura TypeScript, tipuri din namespace-uri asociate funcțiilor, scripturi și compilare, organizarea tsconfig, validare la execuție, tipuri pentru API-urile Node.js, execuție nativă și testare. Separarea fișierelor nu le transformă în module selectabile independent: selecția rămâne la nivelul manifestului.

managedPaths declară proprietatea, iar sourceAssets enumeră fișierele care trebuie instalate. Materializatorul mapează un fișier precum files/instructions/typescript-testing.instructions.md în directorul de instrucțiuni gestionat de modul. overridePaths indică unde aparțin instrucțiunile locale; nu creează singur acele fișiere și nu implementează îmbinarea textului.

Detectarea aparține modulului

Un fișier opțional detect.mjs exportă implicit un detector. Acesta primește căile repository-ului și ale proiectului țintă, metadate opționale despre pachet și funcțiile ajutătoare exists și dependency.

Detectorul TypeScript existent este scurt:

detect.mjs
export default function detect({ dependency }) {
const evidence = dependency('typescript');
return {
applies: evidence.length > 0,
reason: 'The target declares TypeScript in its package manifest.',
evidence,
};
}
js

Funcția dependency verifică dependencies, devDependencies, peerDependencies și optionalDependencies. Un rezultat pozitiv conține atât decizia, cât și dovezi care pot explica selecția. Acest detector nu deduce utilizarea TypeScript doar din prezența unui fișier .ts sau tsconfig.

Descoperirea repository-ului folosește @sabinmarcu/utils-repo pentru a identifica rădăcina și căile workspace-urilor declarate. Fiecare țintă este detectată separat. Căutarea dependențelor folosește manifestul parsat al pachetului țintă, nu graful dependențelor tranzitive instalate.

Detectoarele sunt JavaScript executabil încărcat prin import dinamic, nu expresii declarative izolate într-un sandbox. Un catalog personalizat trebuie, așadar, tratat ca sursă de cod de încredere, nu doar ca documentație de inspectat.

Preset-urile grupează, dependențele specializează

Un preset este o listă denumită de module obișnuite. Preset-ul web curent este:

node-web.json
{
"id": "node-web",
"name": "Node Web Application",
"description": "Composable baseline for TypeScript-driven Node web projects.",
"modules": [
"global/core",
"tooling/yarn",
"lang/typescript",
"guardrails/web-platform",
"guardrails/web-style",
"arch/react",
"arch/web-application"
]
}
json

Un preset nu este o copie fixă a tuturor fișierelor instalate. Rezolvarea extinde în continuare dependențele și activează mixin-urile. Politica pentru rădăcina repository-ului, de exemplu, este detectată contextual, nu enumerată în acest preset de aplicație.

O dependență obișnuită exprimă o relație necondiționată. arch/node-library depinde de arch/node-package-library, care depinde de arch/node-package. O bibliotecă Node.js are întotdeauna nevoie de instrucțiunile comune despre publicare și pachete; nu este o intersecție opțională.

Rezolvarea elimină duplicatele și sortează ID-urile cerute, apoi parcurge dependențele sortate înaintea fiecărui modul dependent. Respinge ID-urile necunoscute, ciclurile de dependențe și conflictele prezente în setul efectiv. De exemplu, arch/node-library declară un conflict cu arch/web-library; selectarea ambelor nu este rezolvată în favoarea celui enumerat ultimul.

Mixin-urile dețin reguli pentru combinații

Un mixin se află în catalog/mixins/<folder>/mixin.json. Câmpul requiresAll numește modulele obișnuite care trebuie să fie toate efective. Mixin-urile nu pot apărea în preset-uri și nu pot fi selectate direct în configurația salvată. Sunt derivate după rezolvarea dependențelor și nu formează un alt lanț de dependențe între mixin-uri.

Mixin-ul TypeScript/ESLint este un exemplu complet:

mixin.json
{
"id": "mixin/typescript-eslint",
"name": "TypeScript ESLint Integration",
"description": "TypeScript peer dependency and validation requirements for the shared ESLint configuration.",
"version": "0.1.0",
"managedPaths": [
".github/instructions/shared/mixins/typescript-eslint/"
],
"sourceAssets": [
"files/instructions/typescript-eslint.instructions.md"
],
"overridePaths": [
".github/instructions/local/mixins/typescript-eslint/"
],
"requiresAll": [
"lang/typescript",
"tooling/eslint"
]
}
json

TypeScript singur nu implică ESLint, iar ESLint singur nu implică TypeScript. Combinația lor necesită o integrare concretă: încărcarea typescript-eslint alături de configurația ESLint comună, păstrarea responsabilității parserului și plugin-ului în acea configurație și inspectarea configurației efective pentru fișiere TypeScript reprezentative.

Instrucțiunile mixin-ului tratează explicit lipsa unei dependențe peer opționale ca eroare de configurare, chiar dacă configurația comună omite suportul TypeScript fără să arunce o eroare. Această regulă nu aparține niciunuia dintre modulele obișnuite în izolare.

Exemplu de flux al conductei de rezolvare și activare a mixin-urilor

Pentru a vedea arhitectura completă în acțiune pe etape, consideră un proiect de bibliotecă Node unde detectarea identifică arch/node-library, lang/typescript și tooling/eslint:

Detalii de execuție pe etape

  1. Etapa 1 (Selecție & detectare): Configurația salvată sau detectorul identifică modulele principale solicitate (arch/node-library, lang/typescript, tooling/eslint).
  2. Etapa 2 (Extinderea dependențelor tranzitive): Rezolvitorul parcurge listele dependsOn. arch/node-library include arch/node-package-library, care include arch/node-package. Se elimină duplicatele și se verifică conflictele.
  3. Etapa 3 (Evaluarea intersecțiilor de mixin-uri): După ce modulele obișnuite sunt complet rezolvate, se evaluează condițiile mixin-urilor (requiresAll). Observă că mixin/typescript-library se activează deoarece arch/node-package-library a fost adăugat în Etapa 2 prin extinderea dependențelor—mixin-urile se evaluează în raport cu setul complet de module efective, nu doar cu selecțiile directe.
  4. Etapa 4 (Materializarea resurselor & punct de intrare): Fiecare modul efectiv și mixin activ contribuie cu fișierele sale pe disc. Hash-urile SHA-256 și metadatele proprietarilor (ownerId și ownerVersion) sunt indexate în .ai/materialized.yml pentru detectarea modificărilor la reconcilieri viitoare, în timp ce .ai/AGENTS.md este asamblat pentru a referi toate instrucțiunile active.

Mixin-ul pentru biblioteci aliniază emiterea TypeScript cu regulile de publicare: src către dist, declarații publice în dist, exporturi de tipuri corespunzătoare și excluderea testelor și story-urilor din rezultatul compilării. Când alt instrument de build deține emiterea, configurația lui deține aceste reguli, în loc să le dubleze în TypeScript.

După introducerea dependențelor relevante într-un repository existent, fluxul operațional este:

ai detect
ai reconcile
ai reconcile --apply
ai status -v
sh

Inspectează planul înainte de aplicare. Argumentul --module mixin/typescript-eslint nu este necesar și nici acceptat. Dacă ESLint părăsește ulterior setul efectiv, acel mixin se dezactivează; instrucțiunile comune pentru biblioteci pot rămâne. Preset-urile existente sau dependențele altui modul pot menține un modul efectiv, deci eliminarea selecției directe nu este neapărat suficientă pentru dezactivare.

Unde aparține o regulă nouă

Folosește un modul pentru o responsabilitate aplicabilă independent și o dependență când o responsabilitate o specializează întotdeauna pe alta. Folosește un mixin când un comportament concret se schimbă doar la intersecția mai multor module obișnuite.

De exemplu, invocarea lint-staged dintr-un hook Husky pre-commit aparține mixin/husky-lint-staged: nici instalarea Husky, nici configurarea lint-staged separat nu implică acea invocare. O recomandare generală de a folosi instrumente pentru calitatea codului nu justifică singură un mixin.

Inspectează toate dependențele tranzitive înainte de a adăuga o regulă de intersecție. Altfel, un mixin poate duplica instrucțiuni deja moștenite necondiționat. Atribuie fiecărui mixin propria destinație gestionată, în loc să folosești ordinea pentru a suprascrie fișierul unui modul.

Proprietatea este explicită, nu decisă de ultima scriere

Încărcarea catalogului validează structura manifestelor, ID-urile referite, existența fișierelor sursă, căile relative, valorile duplicate și ciclurile de dependențe. Respinge declarațiile duplicate ale căilor gestionate normalizate între module și mixin-uri. Materializarea respinge separat doi proprietari activi care produc același fișier țintă.

Nu este o îmbinare generică recursivă de obiecte de configurare. Fiecare fișier instalat are un singur proprietar, o versiune a proprietarului și un hash al conținutului dorit. O personalizare locală este o locație separată pentru instrucțiuni, nu un patch aplicat implicit fișierului comun.

Reconcilierea folosește înregistrările de proprietate pentru a planifica adăugări, actualizări și eliminări. Scrierea este secvențială, nu o tranzacție atomică de filesystem cu revenire automată. CLI-ul construiește din nou planul după aplicare pentru a raporta problemele rămase, dar o scriere eșuată trebuie totuși inspectată înainte de reîncercare.

Construit și întreținut de Sabin Marcu

(2025 -2026)

Cuprins

Experimente