@sdd-method/verify-cli 0.1.1 → 0.2.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/dist/lib/catalogue/baseline-check.js +17 -2
- package/dist/lib/catalogue/canonical-schema.yaml +476 -10
- package/dist/lib/catalogue/coverage-render.js +30 -0
- package/dist/lib/catalogue/engine-validator.js +17 -0
- package/dist/lib/catalogue/graph-projection.js +17 -0
- package/dist/lib/catalogue/impact-run.js +2 -2
- package/dist/lib/catalogue/manifest-loader.js +9 -0
- package/dist/lib/catalogue/platform-scope.js +7 -0
- package/dist/lib/catalogue/rubric-coverage-map.yaml +91 -2
- package/dist/lib/catalogue/scores-coverage.js +38 -0
- package/dist/lib/catalogue/validate.js +33 -9
- package/dist/lib/validate/aggregate.js +31 -15
- package/dist/lib/validate/bundle-coherence.js +7 -2
- package/dist/lib/validate/dispatch.js +25 -0
- package/dist/lib/validate/index.js +32 -10
- package/dist/lib/validate/yaml-parse.js +112 -0
- package/dist/verbs/catalogue-verify.js +1 -1
- package/package.json +1 -1
- package/scripts/validate-app-dependencies.sh +24 -4
- package/scripts/validate-app-docs.sh +18 -4
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// YAML parse gate — every tracked YAML source must parse STRICTLY.
|
|
2
|
+
//
|
|
3
|
+
// The catalogue builders read YAML file by file and, for most sources, log
|
|
4
|
+
// `invalid YAML` and move on: the file becomes wholly invisible to the
|
|
5
|
+
// catalogue and nothing ever fails. Measured: 19 unparseable files across 4
|
|
6
|
+
// SDDs, 16 in one repo, accumulated that way.
|
|
7
|
+
//
|
|
8
|
+
// THE LOADER IS THE GATE. PyYAML (and any last-key-wins loader) accepts a
|
|
9
|
+
// duplicate key and silently keeps the second value, so a parse-only check
|
|
10
|
+
// with it certifies exactly the corruption a line-surgery transformer
|
|
11
|
+
// produces. `yaml` v2 refuses duplicate keys (DUPLICATE_KEY) and the other
|
|
12
|
+
// malformations a lenient parse glosses — e.g. a `: ` inside an unquoted
|
|
13
|
+
// scalar (`path: (none — tel: URL)`), which no pattern rule catches.
|
|
14
|
+
//
|
|
15
|
+
// Scope: the YAML files git TRACKS (`*.yaml`, `*.yml`) — the repo's sources,
|
|
16
|
+
// not build output or dependencies. Outside a git work tree there is no
|
|
17
|
+
// honest scope, so the check reports a SKIP rather than a pass.
|
|
18
|
+
//
|
|
19
|
+
// A TEMPLATE is not YAML until rendered (Helm/Go `{{- … -}}`, Handlebars).
|
|
20
|
+
// A file that fails to parse AND carries a template directive is listed as
|
|
21
|
+
// templated and not failed — UNLESS one of its errors is a duplicate key: a
|
|
22
|
+
// duplicate key is a duplicate however the file is produced, and exempting it
|
|
23
|
+
// would let any `{{ }}` in a value hide exactly the defect this gate exists for.
|
|
24
|
+
import { execFile } from "node:child_process";
|
|
25
|
+
import { readFile } from "node:fs/promises";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { promisify } from "node:util";
|
|
28
|
+
import { parseAllDocuments } from "yaml";
|
|
29
|
+
const execFileAsync = promisify(execFile);
|
|
30
|
+
const TEMPLATE_DIRECTIVE_RE = /\{\{[\s\S]*?\}\}/;
|
|
31
|
+
/** Strict-parse one YAML text; returns the errors (empty = parses). */
|
|
32
|
+
export function strictYamlErrors(text) {
|
|
33
|
+
return parseAllDocuments(text).flatMap((doc) => doc.errors.map((e) => ({
|
|
34
|
+
code: e.code,
|
|
35
|
+
message: e.message.split("\n")[0] ?? e.message,
|
|
36
|
+
})));
|
|
37
|
+
}
|
|
38
|
+
async function trackedYamlFiles(repo) {
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await execFileAsync("git", ["-C", repo, "ls-files", "-z", "--", "*.yaml", "*.yml"], { maxBuffer: 64 * 1024 * 1024 });
|
|
41
|
+
return stdout.split("\0").filter((p) => p !== "");
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function checkYamlParse(targetRepo) {
|
|
48
|
+
const files = await trackedYamlFiles(targetRepo);
|
|
49
|
+
if (files === null) {
|
|
50
|
+
return {
|
|
51
|
+
exitCode: 0,
|
|
52
|
+
stdout: "",
|
|
53
|
+
skipped: "not a git work tree — no tracked-file scope to check",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const failures = [];
|
|
57
|
+
const templated = [];
|
|
58
|
+
let read = 0;
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
let text;
|
|
61
|
+
try {
|
|
62
|
+
text = await readFile(join(targetRepo, file), "utf-8");
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
continue; // tracked but deleted in the work tree: not a parse question
|
|
66
|
+
}
|
|
67
|
+
read += 1;
|
|
68
|
+
const errors = strictYamlErrors(text);
|
|
69
|
+
if (errors.length > 0 &&
|
|
70
|
+
!errors.some((e) => e.code === "DUPLICATE_KEY") &&
|
|
71
|
+
TEMPLATE_DIRECTIVE_RE.test(text)) {
|
|
72
|
+
templated.push(file);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
for (const e of errors) {
|
|
76
|
+
failures.push({ file, code: e.code, message: e.message });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const checked = read - templated.length;
|
|
80
|
+
const templatedNote = templated.length === 0
|
|
81
|
+
? ""
|
|
82
|
+
: `[INFO] yaml-parse: ${templated.length} templated file(s) not checked — they do not parse ` +
|
|
83
|
+
`until rendered and carry a {{ … }} directive: ${templated.join(", ")}\n`;
|
|
84
|
+
if (failures.length === 0 && checked === 0) {
|
|
85
|
+
// Zero files checked is absence of evidence, never a pass.
|
|
86
|
+
return {
|
|
87
|
+
exitCode: 0,
|
|
88
|
+
stdout: templatedNote,
|
|
89
|
+
skipped: "no tracked YAML files to parse — nothing to check",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (failures.length === 0) {
|
|
93
|
+
return {
|
|
94
|
+
exitCode: 0,
|
|
95
|
+
stdout: templatedNote +
|
|
96
|
+
`[PASS] yaml-parse: ${checked} tracked YAML file(s) parse strictly (duplicate keys refused)\n`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const failedFiles = new Set(failures.map((f) => f.file)).size;
|
|
100
|
+
const byCode = new Map();
|
|
101
|
+
for (const f of failures)
|
|
102
|
+
byCode.set(f.code, (byCode.get(f.code) ?? 0) + 1);
|
|
103
|
+
const lines = failures.map((f) => `[FAIL] yaml-parse: ${f.file}: ${f.code}: ${f.message}\n`);
|
|
104
|
+
if (templatedNote !== "")
|
|
105
|
+
lines.unshift(templatedNote);
|
|
106
|
+
lines.push(`[FAIL] yaml-parse: ${failedFiles} of ${checked} tracked YAML file(s) do not parse strictly ` +
|
|
107
|
+
`(${[...byCode].map(([c, n]) => `${c} ×${n}`).join(", ")}). ` +
|
|
108
|
+
`A file that does not parse is invisible to the catalogue; a duplicate key keeps only one ` +
|
|
109
|
+
`of its values under a lenient loader. Fix the file — do not relax the loader.\n`);
|
|
110
|
+
return { exitCode: 1, stdout: lines.join("") };
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=yaml-parse.js.map
|
|
@@ -57,7 +57,7 @@ export function registerCatalogueImpact(catalogue) {
|
|
|
57
57
|
.description("Change-impact closure of a node (M-ADR 0206): each relationship is traversed per its declared impact-flow from the rubric-coverage-map — dependency edges reach their consumers, membership edges propagate part-to-whole only, xref/lineage records are inert — computed by recursive SQL on the embedded engine, with the co-directionality invariant asserted on every run")
|
|
58
58
|
.requiredOption("--of <node-id>", 'Node to compute impact of, in the projection\'s "<catalogue>/<id>" form (e.g. contracts/orders-api)')
|
|
59
59
|
.option("--sdd-path <path>", "Path to the SDD repo root (defaults to cwd)")
|
|
60
|
-
.option("--catalogues-dir <dir>", "Catalogues directory relative to the SDD root (default catalogues)")
|
|
60
|
+
.option("--catalogues-dir <dir>", "Catalogues directory relative to the SDD root (default: catalogues, or catalogues-aggregated at --tier aggregate)")
|
|
61
61
|
.option("--tier <tier>", "Tier, explicit never inferred (member | aggregate; default member)")
|
|
62
62
|
.action(async (opts) => {
|
|
63
63
|
if (opts.tier !== undefined &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdd-method/verify-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Verify an SDD repository without changing it — validate, list, and read-only catalogue and pack checks. Ships only the code those verbs reach; it cannot write to the repository it is pointed at. Authoring and maintenance are in @sdd-method/sdd-cli.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -65,6 +65,9 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
|
65
65
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
66
66
|
log_fail() { echo -e "${RED}[FAIL]${NC} $1"; }
|
|
67
67
|
log_pass() { echo -e "${GREEN}[PASS]${NC} $1"; }
|
|
68
|
+
# R1: an empty population is "measured nothing", not a pass — the [N/A] tag
|
|
69
|
+
# must start the line (no leading colour escape) so callers can detect it.
|
|
70
|
+
log_na() { echo -e "[N/A] $1"; }
|
|
68
71
|
log_section() {
|
|
69
72
|
echo -e "\n${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
|
70
73
|
echo -e "${BLUE} $1${NC}"
|
|
@@ -224,22 +227,38 @@ UNRESOLVED_SERVICE_IDS=0
|
|
|
224
227
|
UNRESOLVED_CONTRACT_PATHS=0
|
|
225
228
|
|
|
226
229
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
230
|
+
# R1: with zero declared dependencies the run measures nothing, so the
|
|
231
|
+
# prerequisite checks report as [INFO] rather than [PASS] — a [PASS] line would
|
|
232
|
+
# make the empty run read as a pass. Counted up front, quietly; the real count
|
|
233
|
+
# (with parse-failure reporting) happens in main.
|
|
234
|
+
PRE_DEP_COUNT=0
|
|
235
|
+
log_prereq() {
|
|
236
|
+
if [[ "$PRE_DEP_COUNT" -gt 0 ]]; then log_pass "$1"; else log_info "$1"; fi
|
|
237
|
+
}
|
|
238
|
+
|
|
227
239
|
check_prerequisites() {
|
|
228
240
|
log_section "Prerequisites"
|
|
229
241
|
|
|
242
|
+
if command -v yq &>/dev/null; then
|
|
243
|
+
local _src
|
|
244
|
+
for _src in "${DEP_SOURCES[@]+"${DEP_SOURCES[@]}"}"; do
|
|
245
|
+
PRE_DEP_COUNT=$((PRE_DEP_COUNT + $(quiet_dep_count "$_src")))
|
|
246
|
+
done
|
|
247
|
+
fi
|
|
248
|
+
|
|
230
249
|
if [[ ${#DEP_SOURCES[@]} -eq 0 ]]; then
|
|
231
250
|
log_fail "no dependency source found: neither $APP_DEPS_FILE nor any reverse-evidence/**/feature-grounding-evidence.yaml"
|
|
232
251
|
exit 1
|
|
233
252
|
fi
|
|
234
253
|
for src in "${DEP_SOURCES[@]}"; do
|
|
235
|
-
|
|
254
|
+
log_prereq "dependency source: ${src#"$APP_ROOT"/}"
|
|
236
255
|
done
|
|
237
256
|
|
|
238
257
|
if ! command -v yq &>/dev/null; then
|
|
239
258
|
log_fail "yq is required but not installed. Install with: brew install yq"
|
|
240
259
|
exit 1
|
|
241
260
|
fi
|
|
242
|
-
|
|
261
|
+
log_prereq "yq available"
|
|
243
262
|
|
|
244
263
|
if [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
|
|
245
264
|
PLATFORM_SERVICES_CSV=""
|
|
@@ -249,7 +268,7 @@ check_prerequisites() {
|
|
|
249
268
|
fi
|
|
250
269
|
|
|
251
270
|
if [[ -f "$PLATFORM_SERVICES_CSV" ]]; then
|
|
252
|
-
|
|
271
|
+
log_prereq "Platform services catalogue found (canonical service_id source)"
|
|
253
272
|
else
|
|
254
273
|
PLATFORM_SERVICES_CSV=""
|
|
255
274
|
fi
|
|
@@ -258,7 +277,7 @@ check_prerequisites() {
|
|
|
258
277
|
log_warn "Platform service-architecture.md not found: $PLATFORM_SERVICE_ARCH"
|
|
259
278
|
PLATFORM_SERVICE_ARCH=""
|
|
260
279
|
else
|
|
261
|
-
|
|
280
|
+
log_prereq "Platform service-architecture.md found"
|
|
262
281
|
fi
|
|
263
282
|
|
|
264
283
|
if [[ -z "$PLATFORM_SERVICES_CSV" && -z "$PLATFORM_SERVICE_ARCH" ]]; then
|
|
@@ -600,6 +619,7 @@ main() {
|
|
|
600
619
|
fi
|
|
601
620
|
log_warn "No platform_dependencies entries found in ${#DEP_SOURCES[@]} source(s)"
|
|
602
621
|
log_info "If this app has no platform deps, this is acceptable."
|
|
622
|
+
log_na "App dependencies: 0 platform_dependencies entries found — nothing to check"
|
|
603
623
|
exit 0
|
|
604
624
|
fi
|
|
605
625
|
|
|
@@ -34,6 +34,9 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
|
34
34
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
35
35
|
log_fail() { echo -e "${RED}[FAIL]${NC} $1"; }
|
|
36
36
|
log_pass() { echo -e "${GREEN}[PASS]${NC} $1"; }
|
|
37
|
+
# R1: an empty population is "measured nothing", not a pass — the [N/A] tag
|
|
38
|
+
# must start the line (no leading colour escape) so callers can detect it.
|
|
39
|
+
log_na() { echo -e "[N/A] $1"; }
|
|
37
40
|
log_section() {
|
|
38
41
|
echo -e "\n${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
|
39
42
|
echo -e "${BLUE} $1${NC}"
|
|
@@ -66,6 +69,14 @@ fi
|
|
|
66
69
|
|
|
67
70
|
FAILURES=0
|
|
68
71
|
|
|
72
|
+
# R1: the feature specs are the population this validator measures. With none,
|
|
73
|
+
# the Group 1 file-presence checks are reported as [INFO] rather than [PASS] so
|
|
74
|
+
# the run reads as "measured nothing", not as a pass.
|
|
75
|
+
FEATURE_SPEC_COUNT="$( (find "$APP_ROOT/features" -type f -name "*.md" 2>/dev/null || true) | wc -l | tr -d ' ')"
|
|
76
|
+
log_found() {
|
|
77
|
+
if [[ "$FEATURE_SPEC_COUNT" -gt 0 ]]; then log_pass "$1"; else log_info "$1"; fi
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
70
81
|
check_required_files() {
|
|
71
82
|
log_section "Group 1: Required App Repo Files"
|
|
@@ -85,7 +96,7 @@ check_required_files() {
|
|
|
85
96
|
log_fail "service-architecture.md not found (checked docs/service-architecture.md and service-architecture.md)"
|
|
86
97
|
checks_failed=$((checks_failed + 1))
|
|
87
98
|
else
|
|
88
|
-
|
|
99
|
+
log_found "service-architecture.md found: ${service_arch#$APP_ROOT/}"
|
|
89
100
|
checks_passed=$((checks_passed + 1))
|
|
90
101
|
fi
|
|
91
102
|
|
|
@@ -103,13 +114,13 @@ check_required_files() {
|
|
|
103
114
|
log_warn "feature-tree.md not found (checked docs/platform/, docs/, and repo root) — create when features are classified"
|
|
104
115
|
# warn only; not a hard fail at exploration stage
|
|
105
116
|
else
|
|
106
|
-
|
|
117
|
+
log_found "feature-tree.md found: ${feature_tree#$APP_ROOT/}"
|
|
107
118
|
checks_passed=$((checks_passed + 1))
|
|
108
119
|
fi
|
|
109
120
|
|
|
110
121
|
# 1c. app-platform-dependencies.yaml
|
|
111
122
|
if [[ -f "$APP_ROOT/app-platform-dependencies.yaml" ]]; then
|
|
112
|
-
|
|
123
|
+
log_found "app-platform-dependencies.yaml found"
|
|
113
124
|
checks_passed=$((checks_passed + 1))
|
|
114
125
|
else
|
|
115
126
|
log_fail "app-platform-dependencies.yaml not found in repo root"
|
|
@@ -173,7 +184,10 @@ main() {
|
|
|
173
184
|
|
|
174
185
|
log_section "Summary"
|
|
175
186
|
|
|
176
|
-
if [[ "$FAILURES" -eq 0 ]]; then
|
|
187
|
+
if [[ "$FAILURES" -eq 0 && "$FEATURE_SPEC_COUNT" -eq 0 ]]; then
|
|
188
|
+
log_na "App documentation: required files present, but 0 feature specs found under features/ — nothing to check"
|
|
189
|
+
exit 0
|
|
190
|
+
elif [[ "$FAILURES" -eq 0 ]]; then
|
|
177
191
|
log_pass "All app documentation checks passed."
|
|
178
192
|
exit 0
|
|
179
193
|
else
|