@specatlas/core 0.1.4 → 0.1.6
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 +30 -1
- package/dist/index.js +88 -14
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -203,6 +203,7 @@ interface Change {
|
|
|
203
203
|
fix?: VerifyFile;
|
|
204
204
|
planPath?: string;
|
|
205
205
|
reviewPath?: string;
|
|
206
|
+
presentationPath?: string;
|
|
206
207
|
mockupManifestPath?: string;
|
|
207
208
|
diagnostics: Diagnostic[];
|
|
208
209
|
}
|
|
@@ -1655,4 +1656,32 @@ interface DoctorReport {
|
|
|
1655
1656
|
declare function runDoctor(root: string): Promise<DoctorReport>;
|
|
1656
1657
|
declare function specHashOf(filePath: string): Promise<string | undefined>;
|
|
1657
1658
|
|
|
1658
|
-
|
|
1659
|
+
interface CiCheck {
|
|
1660
|
+
name: string;
|
|
1661
|
+
errors: number;
|
|
1662
|
+
warnings: number;
|
|
1663
|
+
}
|
|
1664
|
+
interface CiExtraCheck {
|
|
1665
|
+
name: string;
|
|
1666
|
+
diagnostics: Diagnostic[];
|
|
1667
|
+
}
|
|
1668
|
+
interface CiResult {
|
|
1669
|
+
checks: CiCheck[];
|
|
1670
|
+
diagnostics: Diagnostic[];
|
|
1671
|
+
errors: number;
|
|
1672
|
+
warnings: number;
|
|
1673
|
+
failed: boolean;
|
|
1674
|
+
}
|
|
1675
|
+
interface CiOptions {
|
|
1676
|
+
root: string;
|
|
1677
|
+
strict?: boolean;
|
|
1678
|
+
extra?: CiExtraCheck[];
|
|
1679
|
+
}
|
|
1680
|
+
declare function livingRequirementsMap(specs: Array<{
|
|
1681
|
+
spec: {
|
|
1682
|
+
requirements: Requirement[];
|
|
1683
|
+
};
|
|
1684
|
+
}>): Map<string, Requirement>;
|
|
1685
|
+
declare function runCiGate(opts: CiOptions): Promise<CiResult>;
|
|
1686
|
+
|
|
1687
|
+
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;
|
|
@@ -1042,10 +1042,11 @@ function deriveState(input) {
|
|
|
1042
1042
|
}
|
|
1043
1043
|
if (approval.status === "missing" || approval.status === "stale") {
|
|
1044
1044
|
blockedBy.push(approval.status === "stale" ? "la firma de la spec qued\xF3 obsoleta (el archivo cambi\xF3)" : "la spec no est\xE1 aprobada");
|
|
1045
|
+
const presented = change.presentationPath !== void 0;
|
|
1045
1046
|
return {
|
|
1046
1047
|
state: "awaiting_approval",
|
|
1047
1048
|
blockedBy,
|
|
1048
|
-
nextAction: next(`satlas present ${change.slug}`, "Presentar la propuesta y firmar la aprobaci\xF3n (satlas approve)"),
|
|
1049
|
+
nextAction: presented ? next(`satlas approve ${change.slug} --by "<nombre>"`, "Firmar la aprobaci\xF3n (la presentaci\xF3n ya est\xE1 generada)") : next(`satlas present ${change.slug}`, "Presentar la propuesta y firmar la aprobaci\xF3n (satlas approve)"),
|
|
1049
1050
|
progress
|
|
1050
1051
|
};
|
|
1051
1052
|
}
|
|
@@ -1280,6 +1281,8 @@ async function loadChange(root, slug, relDir) {
|
|
|
1280
1281
|
if (await exists(planFile)) change.planPath = planFile;
|
|
1281
1282
|
const reviewFile = path4.join(dir, "review.md");
|
|
1282
1283
|
if (await exists(reviewFile)) change.reviewPath = reviewFile;
|
|
1284
|
+
const presentationFile = path4.join(dir, "presentation", "index.html");
|
|
1285
|
+
if (await exists(presentationFile)) change.presentationPath = presentationFile;
|
|
1283
1286
|
const tasksFile = path4.join(dir, "tasks.md");
|
|
1284
1287
|
const tasksRaw = await readTextIfExists(tasksFile);
|
|
1285
1288
|
if (tasksRaw !== void 0) {
|
|
@@ -3864,6 +3867,75 @@ async function specHashOf(filePath) {
|
|
|
3864
3867
|
if (content === void 0) return void 0;
|
|
3865
3868
|
return artifactHash(content);
|
|
3866
3869
|
}
|
|
3870
|
+
|
|
3871
|
+
// src/gate.ts
|
|
3872
|
+
import path18 from "path";
|
|
3873
|
+
var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
3874
|
+
function count(name, diagnostics) {
|
|
3875
|
+
return {
|
|
3876
|
+
name,
|
|
3877
|
+
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
3878
|
+
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
3879
|
+
};
|
|
3880
|
+
}
|
|
3881
|
+
function livingRequirementsMap(specs) {
|
|
3882
|
+
const map = /* @__PURE__ */ new Map();
|
|
3883
|
+
for (const spec of specs) {
|
|
3884
|
+
for (const requirement of spec.spec.requirements) map.set(requirement.id, requirement);
|
|
3885
|
+
}
|
|
3886
|
+
return map;
|
|
3887
|
+
}
|
|
3888
|
+
async function runCiGate(opts) {
|
|
3889
|
+
const root = path18.resolve(opts.root);
|
|
3890
|
+
const { workspace, config } = await loadWorkspace(root);
|
|
3891
|
+
const diagnostics = [];
|
|
3892
|
+
const checks = [];
|
|
3893
|
+
const specFindings = workspace.specs.flatMap((spec) => lintSpec(spec.spec, { language: config.spec.language, businessOnly: config.spec.business_only }));
|
|
3894
|
+
diagnostics.push(...specFindings);
|
|
3895
|
+
checks.push(count("specs vivas", specFindings));
|
|
3896
|
+
const living = livingRequirementsMap(workspace.specs);
|
|
3897
|
+
let changesErrors = 0;
|
|
3898
|
+
let changesWarnings = 0;
|
|
3899
|
+
for (const change of workspace.changes) {
|
|
3900
|
+
const lintFindings = change.delta ? lintDelta(change.delta, living, path18.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
|
|
3901
|
+
const trace = checkTrace({
|
|
3902
|
+
specs: workspace.specs,
|
|
3903
|
+
change,
|
|
3904
|
+
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
3905
|
+
});
|
|
3906
|
+
const changeDiags = [...lintFindings, ...trace.findings];
|
|
3907
|
+
if (change.tasks && change.tasks.counts.total > 0) {
|
|
3908
|
+
const plan = planWaves(change.tasks, { maxParallel: config.waves.max_parallel });
|
|
3909
|
+
changeDiags.push(...plan.blocks.flatMap((block) => block.diagnostics));
|
|
3910
|
+
}
|
|
3911
|
+
if (UI_DOMAINS2.has(change.meta?.domain ?? "")) {
|
|
3912
|
+
const mockups = await checkMockups(root, change.slug, change);
|
|
3913
|
+
changeDiags.push(...mockups.findings);
|
|
3914
|
+
}
|
|
3915
|
+
if (config.packs.length > 0) {
|
|
3916
|
+
const resolved = await resolvePacks(workspace.sddDir, config);
|
|
3917
|
+
const evaluations = evaluatePacks(resolved.packs, change, config);
|
|
3918
|
+
changeDiags.push(...packFindings(evaluations, change));
|
|
3919
|
+
}
|
|
3920
|
+
diagnostics.push(...changeDiags);
|
|
3921
|
+
const entry = count(change.slug, changeDiags);
|
|
3922
|
+
changesErrors += entry.errors;
|
|
3923
|
+
changesWarnings += entry.warnings;
|
|
3924
|
+
}
|
|
3925
|
+
checks.push({ name: "cambios (lint + trace + waves + mockups)", errors: changesErrors, warnings: changesWarnings });
|
|
3926
|
+
const doctor = await runDoctor(root);
|
|
3927
|
+
const doctorErrors = doctor.findings.filter((d) => d.severity === "error");
|
|
3928
|
+
diagnostics.push(...doctorErrors);
|
|
3929
|
+
checks.push({ name: "doctor", errors: doctorErrors.length, warnings: 0 });
|
|
3930
|
+
for (const extra of opts.extra ?? []) {
|
|
3931
|
+
diagnostics.push(...extra.diagnostics);
|
|
3932
|
+
checks.push(count(extra.name, extra.diagnostics));
|
|
3933
|
+
}
|
|
3934
|
+
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
3935
|
+
const warnings = diagnostics.filter((d) => d.severity === "warning").length;
|
|
3936
|
+
const failed = errors > 0 || (opts.strict ?? false) && warnings > 0;
|
|
3937
|
+
return { checks, diagnostics, errors, warnings, failed };
|
|
3938
|
+
}
|
|
3867
3939
|
export {
|
|
3868
3940
|
BLOCK_HEAD_RE,
|
|
3869
3941
|
BUILTIN_PACKS,
|
|
@@ -3951,6 +4023,7 @@ export {
|
|
|
3951
4023
|
listDir,
|
|
3952
4024
|
listDirs,
|
|
3953
4025
|
listRuns,
|
|
4026
|
+
livingRequirementsMap,
|
|
3954
4027
|
loadActiveProfile,
|
|
3955
4028
|
loadApprovals,
|
|
3956
4029
|
loadChange,
|
|
@@ -3994,6 +4067,7 @@ export {
|
|
|
3994
4067
|
renderTemplate,
|
|
3995
4068
|
resolvePacks,
|
|
3996
4069
|
runAnalyze,
|
|
4070
|
+
runCiGate,
|
|
3997
4071
|
runDoctor,
|
|
3998
4072
|
runProcess,
|
|
3999
4073
|
sha256,
|