@specatlas/core 0.1.4 → 0.1.5
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.
- package/dist/index.d.ts +29 -1
- package/dist/index.js +84 -13
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1655,4 +1655,32 @@ interface DoctorReport {
|
|
|
1655
1655
|
declare function runDoctor(root: string): Promise<DoctorReport>;
|
|
1656
1656
|
declare function specHashOf(filePath: string): Promise<string | undefined>;
|
|
1657
1657
|
|
|
1658
|
-
|
|
1658
|
+
interface CiCheck {
|
|
1659
|
+
name: string;
|
|
1660
|
+
errors: number;
|
|
1661
|
+
warnings: number;
|
|
1662
|
+
}
|
|
1663
|
+
interface CiExtraCheck {
|
|
1664
|
+
name: string;
|
|
1665
|
+
diagnostics: Diagnostic[];
|
|
1666
|
+
}
|
|
1667
|
+
interface CiResult {
|
|
1668
|
+
checks: CiCheck[];
|
|
1669
|
+
diagnostics: Diagnostic[];
|
|
1670
|
+
errors: number;
|
|
1671
|
+
warnings: number;
|
|
1672
|
+
failed: boolean;
|
|
1673
|
+
}
|
|
1674
|
+
interface CiOptions {
|
|
1675
|
+
root: string;
|
|
1676
|
+
strict?: boolean;
|
|
1677
|
+
extra?: CiExtraCheck[];
|
|
1678
|
+
}
|
|
1679
|
+
declare function livingRequirementsMap(specs: Array<{
|
|
1680
|
+
spec: {
|
|
1681
|
+
requirements: Requirement[];
|
|
1682
|
+
};
|
|
1683
|
+
}>): Map<string, Requirement>;
|
|
1684
|
+
declare function runCiGate(opts: CiOptions): Promise<CiResult>;
|
|
1685
|
+
|
|
1686
|
+
export { type AdoptDomain, type AdoptOptions, type AdoptResult, type AgingBucket, type AnalyzeOptions, type AnalyzePackSummary, type AnalyzeResult, type Approval, type ApprovalStatus, type ApprovalsFile, type ApprovalsIndex, type ApproveFromGithubOptions, type ApproveFromGithubResult, type ArchiveOptions, type ArchiveResult, type AtlasConfig, type AttentionItem, BLOCK_HEAD_RE, BUILTIN_PACKS, type BlockWavePlan, CONFIG_FILE, CORE_VERSION, type CaptureResult, type Change, type ChangeMeta, type ChangeMetrics, type ChangeState, type CiCheck, type CiExtraCheck, type CiOptions, type CiResult, type Delta, type DeltaOp, type DeriveInput, type DerivedState, type DetectionResult, type Diagnostic, type DoctorReport, type Domain, type Evidence, type EvidenceMethod, type EvidenceResult, type ExecOptions, type ExecResult, type FoldOutcome, type FrontmatterResult, type GhRunner, type GitHubRepo, type InitOptions, type InitResult, type IssueBodyInput, type Lane, type Language, type LinkedIssue, type LintOptions, type LoadedConfig, type MockupCheckResult, type MockupManifest, type MockupPlan, type MockupPlanScreen, type MockupScreen, type NewChangeOptions, type NewChangeResult, type NextAction, type Override, type Pack, type PackCheck, type PackCheckResult, type PackCheckType, type PackEvaluation, type PresentOptions, type PresentResult, type ProfileMatch, REQ_HEAD_RE, REQ_ID_RE, RULE_ID_RE, RULE_RE, RUN_EVENT_TYPES, type RecordEvidenceOptions, type RecordEvidenceResult, type Rename, type Requirement, type RiskLevel, type Rule, type RunEvent, type RunEventType, type RunRecord, type RunState, type RunStatus, SCENARIO_HEAD_RE, SCENARIO_ID_RE, SDD_DIR, SLUG_RE, type Scenario, type Severity, type SignApprovalOptions, type SignApprovalResult, type SpecFile, type SpecRef, type StackProfile, type SyncGithubOptions, type SyncGithubResult, TASK_ID_RE, TASK_LINE_RE, type Task, type TaskBlock, type TasksFile, type TemplateSet, type TraceEdge, type TraceFinding, type TraceGraph, type TraceInput, type TraceNode, type TraceResult, type VerifyFile, type WalkEntry, type WavePlan, type Workspace, type WorkspaceMetrics, adoptWorkspace, appendRunEvent, approvalsSchema, approveFromGithub, archiveChange, artifactHash, atlasConfigSchema, buildTraceGraph, canonicalizeMarkdown, captureMockups, changeMarker, changeMetaSchema, changeMetaYaml, checkMockups, checkTrace, collectMetrics, commentIssue, compareTaskIds, computeInputsHash, configToYaml, copyFile, countBySeverity, createChange, createIssue, createRun, defaultConfig, deriveState, detectProfiles, detectRepo, detectionToYaml, diag, editIssue, emitFrontmatter, ensureDir, ensureSddDirs, esc, evaluatePacks, evidenceSummary, exists, findLinkedIssue, findWorkspaceRoot, firstToken, foldDelta, generatePresentation, generateRunId, getNumber, getString, ghAuthStatus, ghAvailable, hasErrors, hasShellMetacharacters, indexMarkdown, initWorkspace, inline, isCommandAllowed, isDirectory, issueBody, issueLabels, laneOrDefault, lintDelta, lintMockupHtml, lintMockupManifest, lintRequirement, lintSpec, lintTasks, lintVerify, listChangeSlugs, listDir, listDirs, listRuns, livingRequirementsMap, loadActiveProfile, loadApprovals, loadChange, loadConfig, loadDetectedBest, loadProfileFile, loadProfilesFromDir, loadProjectPacks, loadSpecs, loadWorkspace, matchGlob, mockupManifestSchema, mockupsDir, packFindings, parseApprovals, parseChangeMeta, parseConfig, parseDelta, parseFrontmatter, parseGitHubRemote, parseIssueUrl, parseRequirementBlocks, parseSpecFile, parseTasksFile, parseVerifyFile, patchChangeMeta, planBlock, planMockups, planWaves, profileSchema, readMockupManifest, readRun, readText, readTextIfExists, recordEvidence, regenerateIndex, renderMarkdown, renderRequirement, renderTemplate, resolvePacks, runAnalyze, runCiGate, runDoctor, runProcess, sha256, shortHash, signApproval, specHashOf, splitCommand, stateLabel, syncGithubIssue, templatesFor, toPosix, tokensFile, updateMockupScreenshots, updateRunStatus, verifyApproval, walkFiles, writeConfig, writeMockupManifest, writeMockupPlan, writeText };
|
package/dist/index.js
CHANGED
|
@@ -645,14 +645,14 @@ function wordBoundary(text, term) {
|
|
|
645
645
|
const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
|
|
646
646
|
return re.test(text);
|
|
647
647
|
}
|
|
648
|
-
function lintText(text, code, terms, label,
|
|
648
|
+
function lintText(text, code, terms, label, path19, line) {
|
|
649
649
|
const out = [];
|
|
650
650
|
const lower = text.toLowerCase();
|
|
651
651
|
for (const term of terms) {
|
|
652
652
|
if (wordBoundary(lower, term)) {
|
|
653
653
|
out.push(
|
|
654
654
|
diag(code, "error", `${label}: "${term}"`, {
|
|
655
|
-
path:
|
|
655
|
+
path: path19,
|
|
656
656
|
line,
|
|
657
657
|
suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
|
|
658
658
|
})
|
|
@@ -661,7 +661,7 @@ function lintText(text, code, terms, label, path18, line) {
|
|
|
661
661
|
}
|
|
662
662
|
return out;
|
|
663
663
|
}
|
|
664
|
-
function lintRequirement(req,
|
|
664
|
+
function lintRequirement(req, path19, opts = {}) {
|
|
665
665
|
const out = [];
|
|
666
666
|
const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
|
|
667
667
|
const tech = opts.language === "en" ? TECH_EN : TECH_ES;
|
|
@@ -672,32 +672,32 @@ function lintRequirement(req, path18, opts = {}) {
|
|
|
672
672
|
...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
|
|
673
673
|
];
|
|
674
674
|
for (const part of parts) {
|
|
675
|
-
out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n",
|
|
675
|
+
out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path19, part.line));
|
|
676
676
|
if (opts.businessOnly !== false) {
|
|
677
|
-
out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio",
|
|
677
|
+
out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path19, part.line));
|
|
678
678
|
}
|
|
679
679
|
}
|
|
680
680
|
if (req.scenarios.length === 0) {
|
|
681
|
-
out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path:
|
|
681
|
+
out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path19, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
|
|
682
682
|
}
|
|
683
683
|
return out;
|
|
684
684
|
}
|
|
685
|
-
function lintDelta(delta, livingRequirements,
|
|
685
|
+
function lintDelta(delta, livingRequirements, path19, opts = {}) {
|
|
686
686
|
const out = [...delta.diagnostics];
|
|
687
687
|
for (const req of [...delta.added, ...delta.modified]) {
|
|
688
|
-
out.push(...lintRequirement(req,
|
|
688
|
+
out.push(...lintRequirement(req, path19, opts));
|
|
689
689
|
}
|
|
690
690
|
for (const req of delta.modified) {
|
|
691
691
|
const living = livingRequirements.get(req.id);
|
|
692
692
|
if (!living) {
|
|
693
|
-
out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path:
|
|
693
|
+
out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path19, line: req.line }));
|
|
694
694
|
continue;
|
|
695
695
|
}
|
|
696
696
|
for (const existing of living.scenarios) {
|
|
697
697
|
if (!req.scenarios.some((s) => s.id === existing.id)) {
|
|
698
698
|
out.push(
|
|
699
699
|
diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
|
|
700
|
-
path:
|
|
700
|
+
path: path19,
|
|
701
701
|
line: req.line,
|
|
702
702
|
suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
|
|
703
703
|
})
|
|
@@ -708,15 +708,15 @@ function lintDelta(delta, livingRequirements, path18, opts = {}) {
|
|
|
708
708
|
for (const req of delta.removed) {
|
|
709
709
|
const living = livingRequirements.get(req.id);
|
|
710
710
|
if (!living) {
|
|
711
|
-
out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path:
|
|
711
|
+
out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path19, line: req.line }));
|
|
712
712
|
}
|
|
713
713
|
}
|
|
714
714
|
for (const rename of delta.renamed) {
|
|
715
715
|
if (rename.from.id !== rename.to.id) {
|
|
716
|
-
out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename.from.id} \u2192 ${rename.to.id}); los ids son inmutables`, { path:
|
|
716
|
+
out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename.from.id} \u2192 ${rename.to.id}); los ids son inmutables`, { path: path19, line: rename.line }));
|
|
717
717
|
}
|
|
718
718
|
if (!livingRequirements.has(rename.from.id)) {
|
|
719
|
-
out.push(diag("TRACE-007", "error", `RENAMED ${rename.from.id} no existe en la spec viva`, { path:
|
|
719
|
+
out.push(diag("TRACE-007", "error", `RENAMED ${rename.from.id} no existe en la spec viva`, { path: path19, line: rename.line }));
|
|
720
720
|
}
|
|
721
721
|
}
|
|
722
722
|
return out;
|
|
@@ -3864,6 +3864,75 @@ async function specHashOf(filePath) {
|
|
|
3864
3864
|
if (content === void 0) return void 0;
|
|
3865
3865
|
return artifactHash(content);
|
|
3866
3866
|
}
|
|
3867
|
+
|
|
3868
|
+
// src/gate.ts
|
|
3869
|
+
import path18 from "path";
|
|
3870
|
+
var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
3871
|
+
function count(name, diagnostics) {
|
|
3872
|
+
return {
|
|
3873
|
+
name,
|
|
3874
|
+
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
3875
|
+
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
3876
|
+
};
|
|
3877
|
+
}
|
|
3878
|
+
function livingRequirementsMap(specs) {
|
|
3879
|
+
const map = /* @__PURE__ */ new Map();
|
|
3880
|
+
for (const spec of specs) {
|
|
3881
|
+
for (const requirement of spec.spec.requirements) map.set(requirement.id, requirement);
|
|
3882
|
+
}
|
|
3883
|
+
return map;
|
|
3884
|
+
}
|
|
3885
|
+
async function runCiGate(opts) {
|
|
3886
|
+
const root = path18.resolve(opts.root);
|
|
3887
|
+
const { workspace, config } = await loadWorkspace(root);
|
|
3888
|
+
const diagnostics = [];
|
|
3889
|
+
const checks = [];
|
|
3890
|
+
const specFindings = workspace.specs.flatMap((spec) => lintSpec(spec.spec, { language: config.spec.language, businessOnly: config.spec.business_only }));
|
|
3891
|
+
diagnostics.push(...specFindings);
|
|
3892
|
+
checks.push(count("specs vivas", specFindings));
|
|
3893
|
+
const living = livingRequirementsMap(workspace.specs);
|
|
3894
|
+
let changesErrors = 0;
|
|
3895
|
+
let changesWarnings = 0;
|
|
3896
|
+
for (const change of workspace.changes) {
|
|
3897
|
+
const lintFindings = change.delta ? lintDelta(change.delta, living, path18.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
|
|
3898
|
+
const trace = checkTrace({
|
|
3899
|
+
specs: workspace.specs,
|
|
3900
|
+
change,
|
|
3901
|
+
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
3902
|
+
});
|
|
3903
|
+
const changeDiags = [...lintFindings, ...trace.findings];
|
|
3904
|
+
if (change.tasks && change.tasks.counts.total > 0) {
|
|
3905
|
+
const plan = planWaves(change.tasks, { maxParallel: config.waves.max_parallel });
|
|
3906
|
+
changeDiags.push(...plan.blocks.flatMap((block) => block.diagnostics));
|
|
3907
|
+
}
|
|
3908
|
+
if (UI_DOMAINS2.has(change.meta?.domain ?? "")) {
|
|
3909
|
+
const mockups = await checkMockups(root, change.slug, change);
|
|
3910
|
+
changeDiags.push(...mockups.findings);
|
|
3911
|
+
}
|
|
3912
|
+
if (config.packs.length > 0) {
|
|
3913
|
+
const resolved = await resolvePacks(workspace.sddDir, config);
|
|
3914
|
+
const evaluations = evaluatePacks(resolved.packs, change, config);
|
|
3915
|
+
changeDiags.push(...packFindings(evaluations, change));
|
|
3916
|
+
}
|
|
3917
|
+
diagnostics.push(...changeDiags);
|
|
3918
|
+
const entry = count(change.slug, changeDiags);
|
|
3919
|
+
changesErrors += entry.errors;
|
|
3920
|
+
changesWarnings += entry.warnings;
|
|
3921
|
+
}
|
|
3922
|
+
checks.push({ name: "cambios (lint + trace + waves + mockups)", errors: changesErrors, warnings: changesWarnings });
|
|
3923
|
+
const doctor = await runDoctor(root);
|
|
3924
|
+
const doctorErrors = doctor.findings.filter((d) => d.severity === "error");
|
|
3925
|
+
diagnostics.push(...doctorErrors);
|
|
3926
|
+
checks.push({ name: "doctor", errors: doctorErrors.length, warnings: 0 });
|
|
3927
|
+
for (const extra of opts.extra ?? []) {
|
|
3928
|
+
diagnostics.push(...extra.diagnostics);
|
|
3929
|
+
checks.push(count(extra.name, extra.diagnostics));
|
|
3930
|
+
}
|
|
3931
|
+
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
3932
|
+
const warnings = diagnostics.filter((d) => d.severity === "warning").length;
|
|
3933
|
+
const failed = errors > 0 || (opts.strict ?? false) && warnings > 0;
|
|
3934
|
+
return { checks, diagnostics, errors, warnings, failed };
|
|
3935
|
+
}
|
|
3867
3936
|
export {
|
|
3868
3937
|
BLOCK_HEAD_RE,
|
|
3869
3938
|
BUILTIN_PACKS,
|
|
@@ -3951,6 +4020,7 @@ export {
|
|
|
3951
4020
|
listDir,
|
|
3952
4021
|
listDirs,
|
|
3953
4022
|
listRuns,
|
|
4023
|
+
livingRequirementsMap,
|
|
3954
4024
|
loadActiveProfile,
|
|
3955
4025
|
loadApprovals,
|
|
3956
4026
|
loadChange,
|
|
@@ -3994,6 +4064,7 @@ export {
|
|
|
3994
4064
|
renderTemplate,
|
|
3995
4065
|
resolvePacks,
|
|
3996
4066
|
runAnalyze,
|
|
4067
|
+
runCiGate,
|
|
3997
4068
|
runDoctor,
|
|
3998
4069
|
runProcess,
|
|
3999
4070
|
sha256,
|