@tacuchi/agent-workflow-cli 20.26.0 → 21.1.0
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/README.md +2 -1
- package/dist/adapters/node-file-system.js +3 -0
- package/dist/adapters/node-file-system.js.map +1 -1
- package/dist/application/capability/compose.js +161 -0
- package/dist/application/capability/compose.js.map +1 -0
- package/dist/application/capability/descriptor-loader.js +109 -0
- package/dist/application/capability/descriptor-loader.js.map +1 -0
- package/dist/application/capability/design-handler.js +299 -0
- package/dist/application/capability/design-handler.js.map +1 -0
- package/dist/application/capability/dispatcher.js +312 -0
- package/dist/application/capability/dispatcher.js.map +1 -0
- package/dist/application/capability/durable-effect.js +220 -0
- package/dist/application/capability/durable-effect.js.map +1 -0
- package/dist/application/capability/installed-inventory.js +168 -0
- package/dist/application/capability/installed-inventory.js.map +1 -0
- package/dist/application/capability/readiness.js +204 -0
- package/dist/application/capability/readiness.js.map +1 -0
- package/dist/application/capability/resolution.js +264 -0
- package/dist/application/capability/resolution.js.map +1 -0
- package/dist/application/capability/wrapper.js +187 -0
- package/dist/application/capability/wrapper.js.map +1 -0
- package/dist/application/context/manifest.js +11 -5
- package/dist/application/context/manifest.js.map +1 -1
- package/dist/application/context/measure.js +5 -2
- package/dist/application/context/measure.js.map +1 -1
- package/dist/application/context/plan-service.js +6 -6
- package/dist/application/context/plan-service.js.map +1 -1
- package/dist/application/design/design-bundle-service.js +148 -0
- package/dist/application/design/design-bundle-service.js.map +1 -0
- package/dist/application/design/design-gate-service.js +246 -0
- package/dist/application/design/design-gate-service.js.map +1 -0
- package/dist/application/design/design-graph-service.js +85 -0
- package/dist/application/design/design-graph-service.js.map +1 -0
- package/dist/application/design/design-index-service.js +132 -0
- package/dist/application/design/design-index-service.js.map +1 -0
- package/dist/application/design/design-proposal-service.js +69 -0
- package/dist/application/design/design-proposal-service.js.map +1 -0
- package/dist/application/design/design-publish-service.js +756 -0
- package/dist/application/design/design-publish-service.js.map +1 -0
- package/dist/application/design/design-record-service.js +100 -0
- package/dist/application/design/design-record-service.js.map +1 -0
- package/dist/application/design/design-resolver-service.js +165 -0
- package/dist/application/design/design-resolver-service.js.map +1 -0
- package/dist/application/flow/advance.js +533 -0
- package/dist/application/flow/advance.js.map +1 -0
- package/dist/application/flow/flow-service.js +65 -0
- package/dist/application/flow/flow-service.js.map +1 -0
- package/dist/application/flow/run-projection.js +80 -0
- package/dist/application/flow/run-projection.js.map +1 -0
- package/dist/application/flow/run-state-service.js +107 -0
- package/dist/application/flow/run-state-service.js.map +1 -0
- package/dist/application/flow/submit.js +423 -0
- package/dist/application/flow/submit.js.map +1 -0
- package/dist/application/resume-service.js +46 -14
- package/dist/application/resume-service.js.map +1 -1
- package/dist/application/self/install-skill.js +11 -0
- package/dist/application/self/install-skill.js.map +1 -1
- package/dist/application/self/uninstall.js +21 -0
- package/dist/application/self/uninstall.js.map +1 -1
- package/dist/application/semantic-operation/protocol.js +18 -12
- package/dist/application/semantic-operation/protocol.js.map +1 -1
- package/dist/application/semantic-operation/publish.js +56 -9
- package/dist/application/semantic-operation/publish.js.map +1 -1
- package/dist/application/skills-resolver-service.js +27 -6
- package/dist/application/skills-resolver-service.js.map +1 -1
- package/dist/application/status-service.js +5 -1
- package/dist/application/status-service.js.map +1 -1
- package/dist/application/workline-index-service.js +6 -0
- package/dist/application/workline-index-service.js.map +1 -1
- package/dist/application/workspace-init-service.js +22 -2
- package/dist/application/workspace-init-service.js.map +1 -1
- package/dist/cli/commands/capability.js +182 -0
- package/dist/cli/commands/capability.js.map +1 -0
- package/dist/cli/commands/designs.js +110 -0
- package/dist/cli/commands/designs.js.map +1 -0
- package/dist/cli/commands/flow.js +71 -0
- package/dist/cli/commands/flow.js.map +1 -0
- package/dist/cli/commands/index.js +6 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/resume.js +8 -0
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/commands/skills.js +55 -2
- package/dist/cli/commands/skills.js.map +1 -1
- package/dist/cli/commands/status.js +50 -0
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/help-groups.js +9 -0
- package/dist/cli/help-groups.js.map +1 -1
- package/dist/cli/parser.js +7 -0
- package/dist/cli/parser.js.map +1 -1
- package/dist/cli/tui/data/workflow-content.js +5 -0
- package/dist/cli/tui/data/workflow-content.js.map +1 -1
- package/dist/cli/tui/tabs/workflow-tab.js +5 -1
- package/dist/cli/tui/tabs/workflow-tab.js.map +1 -1
- package/dist/domain/capability/descriptor.js +553 -0
- package/dist/domain/capability/descriptor.js.map +1 -0
- package/dist/domain/capability/effects.js +90 -0
- package/dist/domain/capability/effects.js.map +1 -0
- package/dist/domain/capability/protocol.js +494 -0
- package/dist/domain/capability/protocol.js.map +1 -0
- package/dist/domain/contract-reader.js +84 -0
- package/dist/domain/contract-reader.js.map +1 -0
- package/dist/domain/design/adapter.js +161 -0
- package/dist/domain/design/adapter.js.map +1 -0
- package/dist/domain/design/artifact-body.js +223 -0
- package/dist/domain/design/artifact-body.js.map +1 -0
- package/dist/domain/design/artifact.js +750 -0
- package/dist/domain/design/artifact.js.map +1 -0
- package/dist/domain/design/baseline.js +359 -0
- package/dist/domain/design/baseline.js.map +1 -0
- package/dist/domain/design/capability.js +310 -0
- package/dist/domain/design/capability.js.map +1 -0
- package/dist/domain/design/closure.js +161 -0
- package/dist/domain/design/closure.js.map +1 -0
- package/dist/domain/design/direct.js +108 -0
- package/dist/domain/design/direct.js.map +1 -0
- package/dist/domain/design/external-send.js +65 -0
- package/dist/domain/design/external-send.js.map +1 -0
- package/dist/domain/design/governance.js +298 -0
- package/dist/domain/design/governance.js.map +1 -0
- package/dist/domain/design/identity.js +145 -0
- package/dist/domain/design/identity.js.map +1 -0
- package/dist/domain/design/manifest.js +552 -0
- package/dist/domain/design/manifest.js.map +1 -0
- package/dist/domain/design/maturity.js +305 -0
- package/dist/domain/design/maturity.js.map +1 -0
- package/dist/domain/design/naming.js +117 -0
- package/dist/domain/design/naming.js.map +1 -0
- package/dist/domain/design/offline.js +92 -0
- package/dist/domain/design/offline.js.map +1 -0
- package/dist/domain/design/profiles.js +212 -0
- package/dist/domain/design/profiles.js.map +1 -0
- package/dist/domain/design/projections.js +165 -0
- package/dist/domain/design/projections.js.map +1 -0
- package/dist/domain/design/proposal.js +234 -0
- package/dist/domain/design/proposal.js.map +1 -0
- package/dist/domain/design/reference.js +234 -0
- package/dist/domain/design/reference.js.map +1 -0
- package/dist/domain/design/render-bundle.js +0 -0
- package/dist/domain/design/render-bundle.js.map +1 -0
- package/dist/domain/design/rendition.js +472 -0
- package/dist/domain/design/rendition.js.map +1 -0
- package/dist/domain/design/retired.js +61 -0
- package/dist/domain/design/retired.js.map +1 -0
- package/dist/domain/design/revision.js +219 -0
- package/dist/domain/design/revision.js.map +1 -0
- package/dist/domain/design/secrets.js +86 -0
- package/dist/domain/design/secrets.js.map +1 -0
- package/dist/domain/design/sources.js +180 -0
- package/dist/domain/design/sources.js.map +1 -0
- package/dist/domain/design/validation.js +19 -0
- package/dist/domain/design/validation.js.map +1 -0
- package/dist/domain/design/visual-evidence.js +155 -0
- package/dist/domain/design/visual-evidence.js.map +1 -0
- package/dist/domain/design/yaml-subset.js +390 -0
- package/dist/domain/design/yaml-subset.js.map +1 -0
- package/dist/domain/flow/answer.js +459 -0
- package/dist/domain/flow/answer.js.map +1 -0
- package/dist/domain/flow/authority.js +2276 -0
- package/dist/domain/flow/authority.js.map +1 -0
- package/dist/domain/flow/authorization.js +62 -0
- package/dist/domain/flow/authorization.js.map +1 -0
- package/dist/domain/flow/directive.js +421 -0
- package/dist/domain/flow/directive.js.map +1 -0
- package/dist/domain/flow/rules.js +140 -0
- package/dist/domain/flow/rules.js.map +1 -0
- package/dist/domain/flow/run-state.js +317 -0
- package/dist/domain/flow/run-state.js.map +1 -0
- package/dist/domain/harnesses.js +21 -0
- package/dist/domain/harnesses.js.map +1 -1
- package/dist/domain/safe-path.js +23 -0
- package/dist/domain/safe-path.js.map +1 -0
- package/dist/domain/skills.js +26 -3
- package/dist/domain/skills.js.map +1 -1
- package/package.json +1 -1
- package/skills/w/SKILL.md +15 -13
- package/skills/w/artifacts/README.md +3 -4
- package/skills/w/artifacts/artifacts-core/CHECKPOINT.md +2 -2
- package/skills/w/commands/plan-exec.md +11 -5
- package/skills/w/commands/plan-new.md +6 -2
- package/skills/w/commands/plan-refine.md +6 -3
- package/skills/w/commands/quick.md +2 -2
- package/skills/w/commands/spec-new.md +2 -2
- package/skills/w/commands/spec-refine.md +1 -0
- package/skills/w/context/MANIFEST.json +57 -9
- package/skills/w/harness/HARNESS.md +20 -0
- package/skills/w/loops/CHASSIS.md +11 -20
- package/skills/w/loops/CODE-POLICIES.md +18 -9
- package/skills/w/loops/README.md +1 -1
- package/skills/w/loops/plan-exec-loop/LOOP.md +126 -62
- package/skills/w/loops/plan-new-loop/LOOP.md +71 -73
- package/skills/w/loops/plan-refine-loop/LOOP.md +20 -10
- package/skills/w/loops/quick-loop/LOOP.md +18 -26
- package/skills/w/loops/spec-refine-loop/LOOP.md +28 -30
- package/skills/w/modules/COMPACTION.md +2 -6
- package/skills/w/modules/DB-SCRIPTS-ONLY.md +2 -0
- package/skills/w/modules/DESIGN-REFERENCES.md +113 -0
- package/skills/w/modules/IDEATION-GATE.md +3 -3
- package/skills/w/modules/PERSIST-ROUTING.md +1 -0
- package/skills/w/modules/PLAN-EXECUTION-BATCHES.md +65 -0
- package/skills/w/modules/PLAN-INPUT.md +5 -3
- package/skills/w/modules/PLAN-REFINE-SPLIT.md +3 -1
- package/skills/w/modules/PLAN-SPLIT-GATE.md +7 -5
- package/skills/w/modules/PROMPT-CONTINUITY.md +3 -1
- package/skills/w/modules/SPEC-CHANGE-SHAPE.md +4 -4
- package/skills/w/roles/README.md +9 -5
- package/skills/w/roles/design/CONTRACT.md +80 -0
- package/skills/w/roles/design/ROLE.md +224 -0
- package/skills/w/schemas/capability-descriptor.schema.json +260 -0
- package/skills/w/schemas/design/design-baseline.v1.schema.json +71 -0
- package/skills/w/schemas/design/design-manifest.v1.schema.json +322 -0
- package/skills/w/schemas/design/design-render-bundle.v1.schema.json +191 -0
- package/skills/w/schemas/design/design-rendition.v1.schema.json +232 -0
- package/skills/w/schemas/design/design-review.v1.schema.json +72 -0
- package/skills/w/schemas/design/design-revocation.v1.schema.json +54 -0
- package/skills/w/schemas/design/ui-flow.v1.schema.json +211 -0
- package/skills/w/schemas/design/ui-screen.v1.schema.json +242 -0
- package/skills/w/artifacts/artifacts-design/SPEC.md +0 -42
- package/skills/w/modules/PLAN-DESIGN-SPECS.md +0 -12
- package/skills/w/modules/PLAN-REFINE-DESIGN-SPECS.md +0 -7
- package/skills/w/roles/ui-spec/ROLE.md +0 -147
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
import { PLACEHOLDER, bodyCriteria, bodyDigests, bodyReferences, checkHeadings, conditionalKeys, parseBody, } from "./artifact-body.js";
|
|
2
|
+
import { CANONICAL_SCHEMAS } from "./capability.js";
|
|
3
|
+
import { ARTIFACT_PREFIX, anchorIsTheProblem, isDigest, isPackageId, isRevision, parseArtifactId, parseArtifactRef, } from "./identity.js";
|
|
4
|
+
import { Reader, eachRecord, isNonEmptyString, isRecord, } from "./validation.js";
|
|
5
|
+
import { parseYamlSubset } from "./yaml-subset.js";
|
|
6
|
+
/**
|
|
7
|
+
* Flow and Screen Specifications v1 — the two documents that carry the design
|
|
8
|
+
* semantics of a package.
|
|
9
|
+
*
|
|
10
|
+
* The contract's whole point: **identity, graph, states, dependencies and
|
|
11
|
+
* traceability resolve from the frontmatter alone**. Nothing normative may
|
|
12
|
+
* require reading a sentence. The body still has a contract (fixed headings,
|
|
13
|
+
* real text), but it is checked AGAINST the frontmatter, never trusted instead
|
|
14
|
+
* of it — and where the body cites an artifact, it must cite one the
|
|
15
|
+
* frontmatter already declares.
|
|
16
|
+
*/
|
|
17
|
+
export const FLOW_SCHEMA_ID = CANONICAL_SCHEMAS.flow;
|
|
18
|
+
export const SCREEN_SCHEMA_ID = CANONICAL_SCHEMAS.screen;
|
|
19
|
+
export const SCHEMA_ID = {
|
|
20
|
+
flow: FLOW_SCHEMA_ID,
|
|
21
|
+
screen: SCREEN_SCHEMA_ID,
|
|
22
|
+
};
|
|
23
|
+
export const CRITERION_CLASSIFICATIONS = [
|
|
24
|
+
"visual",
|
|
25
|
+
"interaction",
|
|
26
|
+
"not_visual",
|
|
27
|
+
];
|
|
28
|
+
const COMMON_KEYS = [
|
|
29
|
+
"schema",
|
|
30
|
+
"id",
|
|
31
|
+
"revision",
|
|
32
|
+
"maturity",
|
|
33
|
+
"supersedes",
|
|
34
|
+
"purpose",
|
|
35
|
+
"platform",
|
|
36
|
+
"trace",
|
|
37
|
+
"unknowns",
|
|
38
|
+
"not_applicable",
|
|
39
|
+
"external",
|
|
40
|
+
];
|
|
41
|
+
export const FLOW_ALLOWED_KEYS = {
|
|
42
|
+
"": [...COMMON_KEYS, "actors", "entry", "nodes", "edges", "dependencies"],
|
|
43
|
+
"edges[]": ["from", "trigger", "action", "condition", "to"],
|
|
44
|
+
"trace[]": ["criterion", "source"],
|
|
45
|
+
"unknowns[]": ["question", "blocking"],
|
|
46
|
+
"external[]": ["provider", "revision", "digest"],
|
|
47
|
+
};
|
|
48
|
+
export const SCREEN_ALLOWED_KEYS = {
|
|
49
|
+
"": [...COMMON_KEYS, "title", "default_state", "states", "flow_refs", "dependencies"],
|
|
50
|
+
"states[]": ["anchor", "purpose"],
|
|
51
|
+
"trace[]": ["criterion", "source", "classification", "states", "renditions", "reason"],
|
|
52
|
+
"unknowns[]": ["question", "blocking"],
|
|
53
|
+
"external[]": ["provider", "revision", "digest"],
|
|
54
|
+
dependencies: ["rules", "tokens", "assets"],
|
|
55
|
+
};
|
|
56
|
+
export const ALLOWED_KEYS = {
|
|
57
|
+
flow: FLOW_ALLOWED_KEYS,
|
|
58
|
+
screen: SCREEN_ALLOWED_KEYS,
|
|
59
|
+
};
|
|
60
|
+
const ANCHOR_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
61
|
+
/**
|
|
62
|
+
* A free-text frontmatter field, checked against the SAME placeholder list the
|
|
63
|
+
* body uses. AC-SEM-08 forbids `N/A` in identity, purpose, platform, base state,
|
|
64
|
+
* references and traceability — and the only sanctioned way to say "does not
|
|
65
|
+
* apply" is `not_applicable.<section>`, which covers sections, never fields.
|
|
66
|
+
*/
|
|
67
|
+
function checkText(r, artifact, field, value, action) {
|
|
68
|
+
if (!isNonEmptyString(value)) {
|
|
69
|
+
r.invalid(artifact, `'${field}' es obligatorio y no admite vacío`, action);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (PLACEHOLDER.test(value.trim())) {
|
|
73
|
+
r.fail("DESIGN_FIELD_PLACEHOLDER", artifact, `'${field}' dice solo '${value.trim()}'`, "un campo del frontmatter no se declara no aplicable: escribí su valor");
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
const DELIMITER = "---";
|
|
79
|
+
/** Split `---`-fenced frontmatter from the Markdown body. */
|
|
80
|
+
export function splitDesignDocument(text) {
|
|
81
|
+
const lines = text.split(/\r?\n/);
|
|
82
|
+
if ((lines[0] ?? "").trim() !== DELIMITER)
|
|
83
|
+
return null;
|
|
84
|
+
for (let i = 1; i < lines.length; i++) {
|
|
85
|
+
if ((lines[i] ?? "").trim() !== DELIMITER)
|
|
86
|
+
continue;
|
|
87
|
+
return {
|
|
88
|
+
frontmatter: lines.slice(1, i).join("\n"),
|
|
89
|
+
body: lines.slice(i + 1).join("\n"),
|
|
90
|
+
bodyLine: i + 2,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
export function validateDesignArtifact(text, kind, artifact) {
|
|
96
|
+
const r = new Reader(ALLOWED_KEYS[kind]);
|
|
97
|
+
const split = splitDesignDocument(text);
|
|
98
|
+
if (split === null) {
|
|
99
|
+
r.fail("DESIGN_FRONTMATTER_MISSING", artifact, "el documento no abre y cierra un frontmatter '---'", `empezá el archivo con '---', el frontmatter ${SCHEMA_ID[kind]} y otro '---'`);
|
|
100
|
+
return { ok: false, value: null, failures: r.failures, touched: r.touched };
|
|
101
|
+
}
|
|
102
|
+
const parsed = parseYamlSubset(split.frontmatter);
|
|
103
|
+
if (!parsed.ok) {
|
|
104
|
+
r.fail("DESIGN_FRONTMATTER_UNREADABLE", artifact, `el frontmatter no es legible (línea ${parsed.line + 1}): ${parsed.why}`, "el contrato admite un subconjunto de YAML: mappings, secuencias, flow style y escalares");
|
|
105
|
+
return { ok: false, value: null, failures: r.failures, touched: r.touched };
|
|
106
|
+
}
|
|
107
|
+
const front = parsed.value;
|
|
108
|
+
// The version gate runs first and alone: reading fields off a frontmatter
|
|
109
|
+
// whose shape we do not know would report a pile of derived nonsense.
|
|
110
|
+
const schema = r.read(front, "schema");
|
|
111
|
+
if (schema !== SCHEMA_ID[kind]) {
|
|
112
|
+
r.fail("DESIGN_SCHEMA_UNKNOWN", artifact, `versión de formato no soportada: ${JSON.stringify(schema)}`, `este Workline entiende ${SCHEMA_ID[kind]}`);
|
|
113
|
+
return { ok: false, value: null, failures: r.failures, touched: r.touched };
|
|
114
|
+
}
|
|
115
|
+
r.closed(front, "", artifact);
|
|
116
|
+
const common = readCommon(r, front, kind, artifact);
|
|
117
|
+
const specific = kind === "flow" ? readFlow(r, front, artifact) : readScreen(r, front, artifact);
|
|
118
|
+
const body = parseBody(split.body, split.bodyLine);
|
|
119
|
+
checkHeadings(r, body, kind, artifact);
|
|
120
|
+
checkNotApplicable(r, common.not_applicable, kind, artifact);
|
|
121
|
+
// The classification matrix names states, so it can only be checked once the
|
|
122
|
+
// states have been read. Coherence only — whether the matrix is COMPLETE is
|
|
123
|
+
// the `handoff` gate's question, and an `outline` is allowed to leave it open.
|
|
124
|
+
if (kind === "screen") {
|
|
125
|
+
checkTraceCoherence(r, artifact, common.trace, specific.states);
|
|
126
|
+
}
|
|
127
|
+
checkBodyAgainstFrontmatter(r, body, kind, artifact, common, specific);
|
|
128
|
+
if (r.failures.length > 0) {
|
|
129
|
+
return { ok: false, value: null, failures: r.failures, touched: r.touched };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
value: { kind, ...common, ...specific },
|
|
134
|
+
failures: [],
|
|
135
|
+
touched: r.touched,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function readCommon(r, front, kind, artifact) {
|
|
139
|
+
const rawId = r.read(front, "id");
|
|
140
|
+
const parsedId = parseArtifactId(rawId);
|
|
141
|
+
const expectedPrefix = ARTIFACT_PREFIX[kind];
|
|
142
|
+
if (parsedId === null || !parsedId.artifact.startsWith(`${expectedPrefix}-`)) {
|
|
143
|
+
r.invalid(artifact, `'id' debe ser DES-NNN/${expectedPrefix}-NNN y llegó ${JSON.stringify(rawId)}`, `escribí la identidad completa, por ejemplo DES-001/${expectedPrefix}-001`);
|
|
144
|
+
}
|
|
145
|
+
const revision = r.read(front, "revision");
|
|
146
|
+
if (!isRevision(revision)) {
|
|
147
|
+
r.invalid(artifact, "'revision' debe ser un entero >= 1", "las revisiones son lógicas y empiezan en 1");
|
|
148
|
+
}
|
|
149
|
+
const maturity = r.read(front, "maturity");
|
|
150
|
+
if (maturity !== "outline" && maturity !== "handoff") {
|
|
151
|
+
r.invalid(artifact, `'maturity' debe ser 'outline' o 'handoff' y llegó ${JSON.stringify(maturity)}`, "declará la madurez de esta revisión");
|
|
152
|
+
}
|
|
153
|
+
const supersedes = r.read(front, "supersedes");
|
|
154
|
+
checkSupersedes(r, supersedes, artifact, rawId, revision);
|
|
155
|
+
const purpose = r.read(front, "purpose");
|
|
156
|
+
checkText(r, artifact, "purpose", purpose, "escribí para qué existe este artefacto");
|
|
157
|
+
const platform = r.read(front, "platform");
|
|
158
|
+
checkText(r, artifact, "platform", platform, "declará la plataforma, por ejemplo web");
|
|
159
|
+
return {
|
|
160
|
+
schema: SCHEMA_ID[kind],
|
|
161
|
+
id: typeof rawId === "string" ? rawId : "",
|
|
162
|
+
revision: isRevision(revision) ? revision : 0,
|
|
163
|
+
maturity: maturity === "handoff" ? "handoff" : "outline",
|
|
164
|
+
supersedes: typeof supersedes === "string" ? supersedes : null,
|
|
165
|
+
purpose: typeof purpose === "string" ? purpose : "",
|
|
166
|
+
platform: typeof platform === "string" ? platform : "",
|
|
167
|
+
trace: readTrace(r, front, artifact, kind),
|
|
168
|
+
unknowns: readUnknowns(r, front, artifact),
|
|
169
|
+
not_applicable: readNotApplicable(r, front, artifact),
|
|
170
|
+
external: readExternal(r, front, artifact),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The design systems this revision borrows from (AC-PKG-09).
|
|
175
|
+
*
|
|
176
|
+
* Pinning is the whole point: a provider without a revision and a digest is a
|
|
177
|
+
* promise that can change underneath the artifact, which is the situation
|
|
178
|
+
* `outline` exists to describe.
|
|
179
|
+
*/
|
|
180
|
+
function readExternal(r, front, artifact) {
|
|
181
|
+
const raw = r.read(front, "external");
|
|
182
|
+
if (raw === undefined) {
|
|
183
|
+
r.invalid(artifact, "'external' es obligatorio", "usá [] si no dependés de otro design system");
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const entry of eachRecord(r, front, "external", artifact)) {
|
|
188
|
+
const provider = r.read(entry, "external[].provider");
|
|
189
|
+
const revision = r.read(entry, "external[].revision");
|
|
190
|
+
const digest = r.read(entry, "external[].digest");
|
|
191
|
+
if (!isPackageId(provider) || !isRevision(revision) || !isDigest(digest)) {
|
|
192
|
+
r.invalid(artifact, "cada 'external' fija provider DES-NNN, revision entera y digest sha256", "sin proveedor, revisión y digest la dependencia no está fijada");
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
out.push({ provider: provider, revision, digest: digest });
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* A revision may only supersede an EARLIER revision of ITSELF. Pointing at
|
|
201
|
+
* another artifact would make `supersedes` a second, weaker way of saying
|
|
202
|
+
* "replaces", and currentness is derived from it.
|
|
203
|
+
*/
|
|
204
|
+
function checkSupersedes(r, supersedes, artifact, rawId, revision) {
|
|
205
|
+
if (supersedes === undefined) {
|
|
206
|
+
r.invalid(artifact, "falta 'supersedes': el contrato la declara obligatoria", "poné 'supersedes: null' si esta revisión no reemplaza a ninguna");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (supersedes === null)
|
|
210
|
+
return;
|
|
211
|
+
if (anchorIsTheProblem(supersedes)) {
|
|
212
|
+
r.invalid(artifact, `'supersedes' lleva un anchor de estado y supersede una REVISIÓN entera`, "quitá el anchor: se supersede la revisión, no uno de sus estados");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const ref = parseArtifactRef(supersedes);
|
|
216
|
+
if (ref === null) {
|
|
217
|
+
r.invalid(artifact, `'supersedes' debe ser null o DES-NNN/XXX-NNN@rN y llegó ${JSON.stringify(supersedes)}`, "referenciá la revisión anterior completa, o null");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (ref.state !== undefined) {
|
|
221
|
+
r.invalid(artifact, `'supersedes' lleva un anchor de estado (#${ref.state}) y supersede una REVISIÓN entera`, "quitá el anchor: se supersede la revisión, no uno de sus estados");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const own = parseArtifactId(rawId);
|
|
225
|
+
if (own !== null && (ref.package !== own.package || ref.artifact !== own.artifact)) {
|
|
226
|
+
r.fail("DESIGN_RELATION_BROKEN", artifact, `'supersedes' apunta a ${ref.package}/${ref.artifact} y este artefacto es ${rawId}`, "una revisión solo supersede a una revisión anterior de sí misma");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (isRevision(revision) && ref.revision >= revision) {
|
|
230
|
+
r.fail("DESIGN_RELATION_BROKEN", artifact, `'supersedes' apunta a r${ref.revision} y esta revisión es r${revision}`, "solo se supersede una revisión anterior");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function readTrace(r, front, artifact, kind) {
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const entry of eachRecord(r, front, "trace", artifact, " de entradas de trazabilidad")) {
|
|
236
|
+
const criterion = r.read(entry, "trace[].criterion");
|
|
237
|
+
const source = r.read(entry, "trace[].source");
|
|
238
|
+
if (!checkText(r, artifact, "trace[].criterion", criterion, "citá el acceptance criterion, por ejemplo S013/AC-SEM-11")) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (source !== null &&
|
|
242
|
+
source !== undefined &&
|
|
243
|
+
!checkText(r, artifact, `trace[${criterion}].source`, source, "poné el path del documento que define el criterio, o null")) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const base = { criterion, source: typeof source === "string" ? source : null };
|
|
247
|
+
// Only a screen declares the evidence half, so only a screen's validator may
|
|
248
|
+
// READ those properties: the drift guard proves a validator reads nothing the
|
|
249
|
+
// published schema does not declare, and `ui-flow/v1` does not declare them.
|
|
250
|
+
out.push(kind === "flow" ? base : { ...base, ...readEvidence(r, entry, artifact, criterion) });
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
/** The evidence half of a screen's trace entry. Shape only — completeness is the gate's. */
|
|
255
|
+
function readEvidence(r, entry, artifact, criterion) {
|
|
256
|
+
const empty = { classification: null, states: [], renditions: [], reason: null };
|
|
257
|
+
const raw = r.read(entry, "trace[].classification");
|
|
258
|
+
const states = r.read(entry, "trace[].states");
|
|
259
|
+
const renditions = r.read(entry, "trace[].renditions");
|
|
260
|
+
const reason = r.read(entry, "trace[].reason");
|
|
261
|
+
const classification = raw === undefined || raw === null
|
|
262
|
+
? null
|
|
263
|
+
: CRITERION_CLASSIFICATIONS.includes(raw)
|
|
264
|
+
? raw
|
|
265
|
+
: undefined;
|
|
266
|
+
if (classification === undefined) {
|
|
267
|
+
r.invalid(artifact, `trace['${criterion}']: 'classification' debe ser ${CRITERION_CLASSIFICATIONS.join(", ")} o null y llegó ${JSON.stringify(raw)}`, "clasificá cómo se demuestra el criterio, o dejalo en null hasta hacerlo");
|
|
268
|
+
return empty;
|
|
269
|
+
}
|
|
270
|
+
const anchors = readAnchorList(r, artifact, `trace['${criterion}'].states`, states);
|
|
271
|
+
const shown = readRenditionList(r, artifact, `trace['${criterion}'].renditions`, renditions);
|
|
272
|
+
const why = reason === null || reason === undefined
|
|
273
|
+
? null
|
|
274
|
+
: checkText(r, artifact, `trace['${criterion}'].reason`, reason, "escribí por qué el criterio no tiene nada que mirar, o poné null")
|
|
275
|
+
? reason
|
|
276
|
+
: null;
|
|
277
|
+
const contradiction = contradictionIn(classification, anchors.length + shown.length, why);
|
|
278
|
+
if (contradiction !== null) {
|
|
279
|
+
r.invalid(artifact, `trace['${criterion}']: ${contradiction.what}`, contradiction.action);
|
|
280
|
+
return empty;
|
|
281
|
+
}
|
|
282
|
+
return { classification, states: anchors, renditions: shown, reason: why };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The three ways the evidence half can contradict its own classification.
|
|
286
|
+
*
|
|
287
|
+
* A contradiction is malformed whatever the maturity: `not_visual` MEANS there is
|
|
288
|
+
* nothing to look at, so enumerating a picture of it is not an incomplete claim to
|
|
289
|
+
* be finished later — it is two claims that cannot both hold. Incompleteness is a
|
|
290
|
+
* different verdict and belongs to the `handoff` gate.
|
|
291
|
+
*/
|
|
292
|
+
function contradictionIn(classification, enumerated, reason) {
|
|
293
|
+
if (classification === null) {
|
|
294
|
+
if (enumerated === 0 && reason === null)
|
|
295
|
+
return null;
|
|
296
|
+
return {
|
|
297
|
+
what: "declara evidencia sin clasificar el criterio",
|
|
298
|
+
action: "poné 'classification' en visual, interaction o not_visual",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (classification === "not_visual") {
|
|
302
|
+
if (enumerated === 0)
|
|
303
|
+
return null;
|
|
304
|
+
return {
|
|
305
|
+
what: "está clasificado 'not_visual' y enumera estados o renditions",
|
|
306
|
+
action: "si hay algo que mirar, clasificalo 'visual' o 'interaction'",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (reason === null)
|
|
310
|
+
return null;
|
|
311
|
+
return {
|
|
312
|
+
what: `'reason' explica por qué NO hay nada que mirar y este criterio es '${classification}'`,
|
|
313
|
+
action: "quitá 'reason', o reclasificá el criterio como 'not_visual'",
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function readAnchorList(r, artifact, path, raw) {
|
|
317
|
+
if (raw === undefined)
|
|
318
|
+
return [];
|
|
319
|
+
if (!Array.isArray(raw)) {
|
|
320
|
+
r.invalid(artifact, `'${path}' debe ser un array de anchors`, `escribí '${path}': []`);
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
const out = [];
|
|
324
|
+
for (const value of raw) {
|
|
325
|
+
if (typeof value !== "string" || !ANCHOR_RE.test(value)) {
|
|
326
|
+
r.invalid(artifact, `'${path}' trae un anchor inválido: ${JSON.stringify(value)}`, "nombrá el anchor de un estado declarado, sin '#'");
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (out.includes(value)) {
|
|
330
|
+
r.fail("DESIGN_ID_DUPLICATE", artifact, `'${path}' repite '${value}'`, "dejá una sola aparición");
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
out.push(value);
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
function readRenditionList(r, artifact, path, raw) {
|
|
338
|
+
if (raw === undefined)
|
|
339
|
+
return [];
|
|
340
|
+
if (!Array.isArray(raw)) {
|
|
341
|
+
r.invalid(artifact, `'${path}' debe ser un array de referencias`, `escribí '${path}': []`);
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
const out = [];
|
|
345
|
+
for (const value of raw) {
|
|
346
|
+
const ref = parseArtifactRef(value);
|
|
347
|
+
if (ref === null || !ref.artifact.startsWith("VIS-")) {
|
|
348
|
+
r.invalid(artifact, `'${path}' solo admite renditions DES-NNN/VIS-NNN@rN y llegó ${JSON.stringify(value)}`, "referenciá la rendition exacta que muestra el criterio");
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (out.includes(value)) {
|
|
352
|
+
r.fail("DESIGN_ID_DUPLICATE", artifact, `'${path}' repite ${value}`, "dejá una sola aparición");
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
out.push(value);
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* The matrix names states of THIS screen. A criterion demonstrated on an anchor
|
|
361
|
+
* the revision does not declare points at nothing, and a gate that accepted it
|
|
362
|
+
* would report coverage of a state nobody can open.
|
|
363
|
+
*/
|
|
364
|
+
function checkTraceCoherence(r, artifact, trace, states) {
|
|
365
|
+
if (states.length === 0)
|
|
366
|
+
return; // ya reportado por readScreen
|
|
367
|
+
const anchors = new Set(states.map((s) => s.anchor));
|
|
368
|
+
for (const entry of trace) {
|
|
369
|
+
for (const anchor of entry.states ?? []) {
|
|
370
|
+
if (anchors.has(anchor))
|
|
371
|
+
continue;
|
|
372
|
+
r.fail("DESIGN_RELATION_BROKEN", artifact, `trace['${entry.criterion}'] se demuestra en '${anchor}' y no está entre los states declarados (${[...anchors].join(", ")})`, "declaralo en 'states' o nombrá uno existente");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function readUnknowns(r, front, artifact) {
|
|
377
|
+
const out = [];
|
|
378
|
+
for (const entry of eachRecord(r, front, "unknowns", artifact, " (vacío si no quedan incógnitas)")) {
|
|
379
|
+
const question = r.read(entry, "unknowns[].question");
|
|
380
|
+
const blocking = r.read(entry, "unknowns[].blocking");
|
|
381
|
+
if (!isNonEmptyString(question)) {
|
|
382
|
+
r.invalid(artifact, "cada entrada de 'unknowns' necesita 'question' no vacía", "escribí la incógnita como pregunta");
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (typeof blocking !== "boolean") {
|
|
386
|
+
r.invalid(artifact, `unknowns['${question}']: 'blocking' debe ser true o false`, "declará si resolverla puede cambiar comportamiento o aceptación");
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
out.push({ question, blocking });
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
function readNotApplicable(r, front, artifact) {
|
|
394
|
+
const node = r.read(front, "not_applicable");
|
|
395
|
+
if (node === undefined) {
|
|
396
|
+
r.invalid(artifact, "falta 'not_applicable': el contrato la declara obligatoria", "escribí 'not_applicable: {}' si todo aplica");
|
|
397
|
+
return {};
|
|
398
|
+
}
|
|
399
|
+
if (node === null)
|
|
400
|
+
return {};
|
|
401
|
+
if (!isRecord(node)) {
|
|
402
|
+
r.invalid(artifact, "'not_applicable' debe ser un mapping de sección a razón", "escribí 'not_applicable: {}' si todo aplica");
|
|
403
|
+
return {};
|
|
404
|
+
}
|
|
405
|
+
const out = {};
|
|
406
|
+
for (const [key, reason] of Object.entries(node)) {
|
|
407
|
+
out[key] = typeof reason === "string" ? reason : "";
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Non-applicability is a claim with a price: it names a CONDITIONAL section and
|
|
413
|
+
* it states why. An essential section cannot be waived at all, and a reason left
|
|
414
|
+
* empty is the same refusal to answer that "N/A" was.
|
|
415
|
+
*/
|
|
416
|
+
function checkNotApplicable(r, declared, kind, artifact) {
|
|
417
|
+
const allowed = conditionalKeys(kind);
|
|
418
|
+
for (const [key, reason] of Object.entries(declared)) {
|
|
419
|
+
if (!allowed.includes(key)) {
|
|
420
|
+
r.fail("DESIGN_NOT_APPLICABLE_FORBIDDEN", artifact, `'not_applicable.${key}' no es una sección condicional de ${kind} v1`, `solo estas admiten no-aplicabilidad: ${allowed.join(", ")}`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (reason.trim().length === 0 || PLACEHOLDER.test(reason.trim())) {
|
|
424
|
+
r.fail("DESIGN_NOT_APPLICABLE_NO_REASON", artifact, `'not_applicable.${key}' no explica por qué no aplica`, "escribí la razón; ni vacía ni 'N/A' satisfacen el contrato");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function readFlow(r, front, artifact) {
|
|
429
|
+
const actors = readStringList(r, front, "actors", artifact, "un actor");
|
|
430
|
+
const nodes = readRefList(r, front, "nodes", artifact, "screen", true);
|
|
431
|
+
const dependencies = readRefList(r, front, "dependencies", artifact, null);
|
|
432
|
+
const entry = r.read(front, "entry");
|
|
433
|
+
const entryRef = parseArtifactRef(entry);
|
|
434
|
+
if (entryRef === null) {
|
|
435
|
+
r.invalid(artifact, `'entry' debe ser el estado de pantalla por el que arranca el flow y llegó ${JSON.stringify(entry)}`, "escribí DES-NNN/SCR-NNN@rN#estado");
|
|
436
|
+
}
|
|
437
|
+
else if (entryRef.state === undefined) {
|
|
438
|
+
r.invalid(artifact, "'entry' debe apuntar a un ESTADO de pantalla", "agregá el anchor, por ejemplo @r2#default");
|
|
439
|
+
}
|
|
440
|
+
else if (typeof entry === "string" && nodes.length > 0 && !nodes.includes(entry)) {
|
|
441
|
+
r.fail("DESIGN_RELATION_BROKEN", artifact, `'entry' (${entry}) no está en 'nodes'`, "el punto de entrada es uno de los nodos del grafo: agregalo a 'nodes'");
|
|
442
|
+
}
|
|
443
|
+
const edges = readEdges(r, front, artifact, nodes);
|
|
444
|
+
// With no nodes, every containment check above is vacuous — an edge could point
|
|
445
|
+
// anywhere and nobody would say so. One diagnostic for the root cause beats a
|
|
446
|
+
// pile of derived ones, and beats silence.
|
|
447
|
+
if (nodes.length === 0 && (edges.length > 0 || typeof entry === "string")) {
|
|
448
|
+
r.fail("DESIGN_RELATION_BROKEN", artifact, "'nodes' está vacío y el flow declara entrada o transiciones", "enumerá en 'nodes' los estados de pantalla que el recorrido visita");
|
|
449
|
+
}
|
|
450
|
+
return { actors, entry: typeof entry === "string" ? entry : "", nodes, edges, dependencies };
|
|
451
|
+
}
|
|
452
|
+
function readEdges(r, front, artifact, nodes) {
|
|
453
|
+
const out = [];
|
|
454
|
+
for (const entry of eachRecord(r, front, "edges", artifact, " de transiciones")) {
|
|
455
|
+
const edge = readEdge(r, entry, artifact, nodes);
|
|
456
|
+
if (edge !== null)
|
|
457
|
+
out.push(edge);
|
|
458
|
+
}
|
|
459
|
+
return out;
|
|
460
|
+
}
|
|
461
|
+
function readEdge(r, entry, artifact, nodes) {
|
|
462
|
+
const from = r.read(entry, "edges[].from");
|
|
463
|
+
const trigger = r.read(entry, "edges[].trigger");
|
|
464
|
+
const action = r.read(entry, "edges[].action");
|
|
465
|
+
const condition = r.read(entry, "edges[].condition");
|
|
466
|
+
const to = r.read(entry, "edges[].to");
|
|
467
|
+
if (!isNonEmptyString(trigger)) {
|
|
468
|
+
r.invalid(artifact, "cada arista necesita 'trigger' no vacío", "declará qué dispara la transición");
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
let ok = true;
|
|
472
|
+
for (const [side, value] of [
|
|
473
|
+
["from", from],
|
|
474
|
+
["to", to],
|
|
475
|
+
]) {
|
|
476
|
+
if (!checkEndpoint(r, artifact, trigger, side, value, nodes))
|
|
477
|
+
ok = false;
|
|
478
|
+
}
|
|
479
|
+
for (const [label, value] of [
|
|
480
|
+
["action", action],
|
|
481
|
+
["condition", condition],
|
|
482
|
+
]) {
|
|
483
|
+
if (value === null || value === undefined || isNonEmptyString(value))
|
|
484
|
+
continue;
|
|
485
|
+
r.invalid(artifact, `edges['${trigger}']: '${label}' debe ser texto o null`, `escribí ${label} o poné null`);
|
|
486
|
+
ok = false;
|
|
487
|
+
}
|
|
488
|
+
if (!ok)
|
|
489
|
+
return null;
|
|
490
|
+
return {
|
|
491
|
+
from: from,
|
|
492
|
+
trigger,
|
|
493
|
+
action: typeof action === "string" ? action : null,
|
|
494
|
+
condition: typeof condition === "string" ? condition : null,
|
|
495
|
+
to: to,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* The graph has to close on itself: an endpoint nobody declared as a node is a
|
|
500
|
+
* transition into a screen the flow never says it visits.
|
|
501
|
+
*/
|
|
502
|
+
function checkEndpoint(r, artifact, trigger, side, value, nodes) {
|
|
503
|
+
const ref = parseArtifactRef(value);
|
|
504
|
+
if (ref === null) {
|
|
505
|
+
r.invalid(artifact, `edges['${trigger}']: '${side}' debe ser DES-NNN/SCR-NNN@rN#estado y llegó ${JSON.stringify(value)}`, "referenciá el estado de pantalla completo");
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
// Checked BEFORE membership: without it the author is told to add the
|
|
509
|
+
// unanchored reference to `nodes`, which is the wrong fix for the real problem.
|
|
510
|
+
if (ref.state === undefined) {
|
|
511
|
+
r.invalid(artifact, `edges['${trigger}']: '${side}' (${String(value)}) apunta a una pantalla y una arista conecta ESTADOS`, "agregá el anchor del estado, por ejemplo @r2#default");
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
if (nodes.length > 0 && !nodes.includes(value)) {
|
|
515
|
+
r.fail("DESIGN_RELATION_BROKEN", artifact, `edges['${trigger}']: '${side}' (${String(value)}) no está en 'nodes'`, "agregá ese estado a 'nodes' o corregí la arista");
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
function readScreen(r, front, artifact) {
|
|
521
|
+
const title = r.read(front, "title");
|
|
522
|
+
checkText(r, artifact, "title", title, "poné el título profesional de la pantalla");
|
|
523
|
+
const states = readStates(r, front, artifact);
|
|
524
|
+
const anchors = states.map((s) => s.anchor);
|
|
525
|
+
const defaultState = r.read(front, "default_state");
|
|
526
|
+
if (!isNonEmptyString(defaultState)) {
|
|
527
|
+
r.invalid(artifact, "'default_state' es obligatorio", "nombrá el anchor del estado base");
|
|
528
|
+
}
|
|
529
|
+
else if (states.length > 0 && !anchors.includes(defaultState)) {
|
|
530
|
+
r.fail("DESIGN_RELATION_BROKEN", artifact, `'default_state' es '${defaultState}' y no está entre los states declarados (${anchors.join(", ")})`, "declaralo en 'states' o apuntá a uno existente");
|
|
531
|
+
}
|
|
532
|
+
if (states.length === 0) {
|
|
533
|
+
r.invalid(artifact, "'states' está vacío: una screen sin estados no tiene nada direccionable", "declará al menos el estado base con su anchor y su propósito");
|
|
534
|
+
}
|
|
535
|
+
const flowRefs = readRefList(r, front, "flow_refs", artifact, "flow");
|
|
536
|
+
return {
|
|
537
|
+
title: typeof title === "string" ? title : "",
|
|
538
|
+
default_state: typeof defaultState === "string" ? defaultState : "",
|
|
539
|
+
states,
|
|
540
|
+
flow_refs: flowRefs,
|
|
541
|
+
dependencies: readScreenDependencies(r, front, artifact),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Screen states. The anchor is what every flow, task and rendition addresses
|
|
546
|
+
* (`DES-001/SCR-002@r2#empty`), so it has to be unique WITHIN THIS REVISION —
|
|
547
|
+
* two states answering to one anchor make every reference to it ambiguous.
|
|
548
|
+
*/
|
|
549
|
+
function readStates(r, front, artifact) {
|
|
550
|
+
const out = [];
|
|
551
|
+
const seen = new Set();
|
|
552
|
+
for (const entry of eachRecord(r, front, "states", artifact, " de estados")) {
|
|
553
|
+
const anchor = r.read(entry, "states[].anchor");
|
|
554
|
+
const purpose = r.read(entry, "states[].purpose");
|
|
555
|
+
if (typeof anchor !== "string" || !ANCHOR_RE.test(anchor)) {
|
|
556
|
+
r.invalid(artifact, `anchor de estado inválido: ${JSON.stringify(anchor)}`, "usá letras, números, guion y guion bajo, empezando por alfanumérico (default, empty, permission-denied)");
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (seen.has(anchor)) {
|
|
560
|
+
r.fail("DESIGN_ID_DUPLICATE", artifact, `el anchor de estado '${anchor}' está declarado dos veces`, "un anchor identifica un solo estado dentro de la revisión: renombrá el repetido");
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
seen.add(anchor);
|
|
564
|
+
if (!checkText(r, artifact, `states['${anchor}'].purpose`, purpose, "escribí qué muestra ese estado")) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
out.push({ anchor, purpose });
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
function readScreenDependencies(r, front, artifact) {
|
|
572
|
+
const node = r.read(front, "dependencies");
|
|
573
|
+
const empty = { rules: [], tokens: [], assets: [] };
|
|
574
|
+
if (!isRecord(node)) {
|
|
575
|
+
r.invalid(artifact, "'dependencies' debe ser un objeto con 'rules', 'tokens' y 'assets'", "escribí las tres claves, con array vacío donde no haya dependencias");
|
|
576
|
+
return empty;
|
|
577
|
+
}
|
|
578
|
+
r.closed(node, "dependencies", artifact);
|
|
579
|
+
return {
|
|
580
|
+
rules: readRefList(r, node, "dependencies.rules", artifact, "rule"),
|
|
581
|
+
tokens: readRefList(r, node, "dependencies.tokens", artifact, "token"),
|
|
582
|
+
assets: readDigestList(r, node, "dependencies.assets", artifact),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
/** A list of exact artifact references, optionally constrained to one kind. */
|
|
586
|
+
function readRefList(r, node, path, artifact, kind, requireAnchor = false) {
|
|
587
|
+
const raw = r.read(node, path);
|
|
588
|
+
if (!Array.isArray(raw)) {
|
|
589
|
+
r.invalid(artifact, `'${path}' debe ser un array de referencias`, `escribí '${path}': []`);
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
const out = [];
|
|
593
|
+
for (const value of raw) {
|
|
594
|
+
if (!checkRef(r, artifact, path, value, kind, requireAnchor))
|
|
595
|
+
continue;
|
|
596
|
+
if (out.includes(value)) {
|
|
597
|
+
r.fail("DESIGN_ID_DUPLICATE", artifact, `'${path}' repite ${String(value)}`, "dejá una sola aparición");
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
out.push(value);
|
|
601
|
+
}
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
604
|
+
/** One reference: well-formed, of the expected kind, and anchored or not as declared. */
|
|
605
|
+
function checkRef(r, artifact, path, value, kind, requireAnchor) {
|
|
606
|
+
if (anchorIsTheProblem(value)) {
|
|
607
|
+
r.invalid(artifact, `'${path}' referencia revisiones enteras y ${String(value)} lleva un anchor de estado`, "quitá el '#estado' de la referencia: solo una screen tiene estados");
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
const ref = parseArtifactRef(value);
|
|
611
|
+
if (ref === null) {
|
|
612
|
+
r.invalid(artifact, `'${path}' solo admite referencias DES-NNN/XXX-NNN@rN y llegó ${JSON.stringify(value)}`, "referenciá el artefacto completo, con package y revisión");
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
if (requireAnchor !== (ref.state !== undefined)) {
|
|
616
|
+
r.invalid(artifact, requireAnchor
|
|
617
|
+
? `'${path}' referencia ESTADOS de pantalla y ${String(value)} no lleva anchor`
|
|
618
|
+
: `'${path}' referencia revisiones enteras y ${String(value)} lleva un anchor de estado`, requireAnchor
|
|
619
|
+
? "agregá el anchor, por ejemplo @r2#default"
|
|
620
|
+
: "quitá el '#estado' de la referencia");
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
const prefix = kind === null ? null : ARTIFACT_PREFIX[kind];
|
|
624
|
+
if (prefix !== null && !ref.artifact.startsWith(`${prefix}-`)) {
|
|
625
|
+
r.invalid(artifact, `'${path}' espera artefactos ${prefix}- y llegó ${String(value)}`, `referenciá un ${kind}`);
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
function readDigestList(r, node, path, artifact) {
|
|
631
|
+
const raw = r.read(node, path);
|
|
632
|
+
if (!Array.isArray(raw)) {
|
|
633
|
+
r.invalid(artifact, `'${path}' debe ser un array de digests`, `escribí '${path}': []`);
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
const out = [];
|
|
637
|
+
for (const value of raw) {
|
|
638
|
+
if (!isDigest(value)) {
|
|
639
|
+
r.invalid(artifact, `'${path}' referencia assets por digest y llegó ${JSON.stringify(value)}`, "usá 'sha256:' + 64 hex: los assets son content-addressed");
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (!out.includes(value))
|
|
643
|
+
out.push(value);
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
function readStringList(r, node, path, artifact, what) {
|
|
648
|
+
const raw = r.read(node, path);
|
|
649
|
+
if (!Array.isArray(raw)) {
|
|
650
|
+
r.invalid(artifact, `'${path}' debe ser un array`, `escribí '${path}': []`);
|
|
651
|
+
return [];
|
|
652
|
+
}
|
|
653
|
+
const out = [];
|
|
654
|
+
for (const value of raw) {
|
|
655
|
+
if (!checkText(r, artifact, `${path}[]`, value, `revisá '${path}': ${what}`))
|
|
656
|
+
continue;
|
|
657
|
+
if (!out.includes(value))
|
|
658
|
+
out.push(value);
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Cross-validation. Only the canonical reference form is extracted from the
|
|
664
|
+
* body, so this never depends on interpreting prose: a mention the frontmatter
|
|
665
|
+
* does not declare is either a typo or a relation nobody made machine-readable,
|
|
666
|
+
* and both are errors.
|
|
667
|
+
*/
|
|
668
|
+
function checkBodyAgainstFrontmatter(r, body, kind, artifact, common, specific) {
|
|
669
|
+
checkBodyRefs(r, body, kind, artifact, common, specific);
|
|
670
|
+
checkBodyCriteria(r, body, artifact, common);
|
|
671
|
+
if ("states" in specific)
|
|
672
|
+
checkBodyAssets(r, body, artifact, specific.dependencies.assets);
|
|
673
|
+
}
|
|
674
|
+
function checkBodyRefs(r, body, kind, artifact, common, specific) {
|
|
675
|
+
const declared = declaredReferences(common, specific);
|
|
676
|
+
const own = parseArtifactId(common.id);
|
|
677
|
+
const ownAnchors = "states" in specific ? specific.states.map((s) => s.anchor) : [];
|
|
678
|
+
for (const ref of bodyReferences(body)) {
|
|
679
|
+
const parsedRef = parseArtifactRef(ref);
|
|
680
|
+
if (parsedRef === null)
|
|
681
|
+
continue;
|
|
682
|
+
if (own !== null && parsedRef.package === own.package && parsedRef.artifact === own.artifact) {
|
|
683
|
+
// A reference to THIS artifact is legitimate without being declared — but
|
|
684
|
+
// if it names a state, the state has to exist in this revision.
|
|
685
|
+
checkOwnAnchor(r, artifact, kind, parsedRef.state, ownAnchors);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (declared.has(ref))
|
|
689
|
+
continue;
|
|
690
|
+
r.fail("DESIGN_BODY_REFERENCE_UNKNOWN", artifact, `el cuerpo cita ${ref} y el frontmatter no lo declara`, kind === "flow"
|
|
691
|
+
? "agregá esa referencia a 'nodes' o a 'dependencies', o quitala del texto"
|
|
692
|
+
: "agregá esa referencia a 'flow_refs' o a 'dependencies', o quitala del texto");
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* The spec asks for FOUR things to be contrasted, not one: references, states,
|
|
697
|
+
* criteria and dependencies. A criterion narrated in the body and absent from
|
|
698
|
+
* `trace` is traceability that exists only in prose — the exact thing the
|
|
699
|
+
* frontmatter exists to prevent.
|
|
700
|
+
*/
|
|
701
|
+
function checkBodyCriteria(r, body, artifact, common) {
|
|
702
|
+
const traced = new Set(common.trace.map((t) => t.criterion));
|
|
703
|
+
for (const criterion of bodyCriteria(body)) {
|
|
704
|
+
if (traced.has(criterion))
|
|
705
|
+
continue;
|
|
706
|
+
r.fail("DESIGN_BODY_CRITERION_UNKNOWN", artifact, `el cuerpo cita el criterio ${criterion} y 'trace' no lo declara`, "agregalo a 'trace' con su documento fuente, o quitalo del texto");
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function checkBodyAssets(r, body, artifact, declared) {
|
|
710
|
+
const assets = new Set(declared);
|
|
711
|
+
for (const digest of bodyDigests(body)) {
|
|
712
|
+
if (assets.has(digest))
|
|
713
|
+
continue;
|
|
714
|
+
r.fail("DESIGN_BODY_REFERENCE_UNKNOWN", artifact, `el cuerpo cita el asset ${digest} y 'dependencies.assets' no lo declara`, "agregá ese digest a 'dependencies.assets' o quitalo del texto");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function checkOwnAnchor(r, artifact, kind, state, ownAnchors) {
|
|
718
|
+
if (kind !== "screen" || state === undefined || ownAnchors.includes(state))
|
|
719
|
+
return;
|
|
720
|
+
r.fail("DESIGN_RELATION_BROKEN", artifact, `el cuerpo cita el estado '#${state}' y 'states' no lo declara`, `declaralo en 'states' o citá uno de: ${ownAnchors.join(", ") || "(ninguno)"}`);
|
|
721
|
+
}
|
|
722
|
+
/** Every reference the frontmatter makes machine-readable. */
|
|
723
|
+
function declaredReferences(common, specific) {
|
|
724
|
+
const declared = new Set();
|
|
725
|
+
if (common.supersedes !== null)
|
|
726
|
+
declared.add(common.supersedes);
|
|
727
|
+
// The evidence a screen's matrix enumerates IS declared in the frontmatter, so
|
|
728
|
+
// prose that names it is citing something the document already committed to.
|
|
729
|
+
// Without this, the section that explains the evidence could not mention it.
|
|
730
|
+
for (const entry of common.trace) {
|
|
731
|
+
for (const ref of entry.renditions ?? [])
|
|
732
|
+
declared.add(ref);
|
|
733
|
+
}
|
|
734
|
+
if ("nodes" in specific) {
|
|
735
|
+
for (const ref of [...specific.nodes, ...specific.dependencies])
|
|
736
|
+
declared.add(ref);
|
|
737
|
+
if (specific.entry.length > 0)
|
|
738
|
+
declared.add(specific.entry);
|
|
739
|
+
return declared;
|
|
740
|
+
}
|
|
741
|
+
for (const ref of [
|
|
742
|
+
...specific.flow_refs,
|
|
743
|
+
...specific.dependencies.rules,
|
|
744
|
+
...specific.dependencies.tokens,
|
|
745
|
+
]) {
|
|
746
|
+
declared.add(ref);
|
|
747
|
+
}
|
|
748
|
+
return declared;
|
|
749
|
+
}
|
|
750
|
+
//# sourceMappingURL=artifact.js.map
|