@metaobjectsdev/cli 0.21.5 → 0.22.0-rc.1

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.
@@ -0,0 +1,234 @@
1
+ // `meta verify` — the requirement (capability) gate.
2
+ //
3
+ // Requirements are METADATA: `requirement.functional` / `requirement.architectural`
4
+ // are registered metamodel types, declared in `metaobjects/` beside the entities
5
+ // they describe. So this file parses NOTHING. It reads `requirement.*` nodes off
6
+ // the already-loaded model and checks the things the loader cannot.
7
+ //
8
+ // Division of labour, and the reason for it:
9
+ //
10
+ // LOADER (unconditional) the `@status` enum via `allowedValues`, required
11
+ // attrs, child rules, levels being integers.
12
+ // VERIFY (conditional) `@implementedBy` resolution, whose SEVERITY DEPENDS
13
+ // ON `@status`. A loader `references` descriptor
14
+ // always errors on an unresolved target, and an
15
+ // `abandoned` requirement exists precisely to name
16
+ // nodes that are gone — declaring it there would make
17
+ // the entries carrying the mechanism's only controlled
18
+ // evidence fail to load.
19
+ //
20
+ // Two kinds, opposite checks: `functional` fails when NOTHING implements it;
21
+ // `architectural` fails when something VIOLATES it (v1: an empty claim set on a
22
+ // live policy — claim-set arithmetic, deliberately not a predicate DSL).
23
+ import { TYPE_OBJECT, TYPE_REQUIREMENT, OBJECT_SUBTYPE_ENTITY, PACKAGE_SEPARATOR, REQUIREMENT_SUBTYPE_ARCHITECTURAL, REQUIREMENT_LINK_FLOOR_LEVEL, REQUIREMENT_MIN_LEVEL, REQUIREMENT_MAX_LEVEL, REQUIREMENT_LEVEL_MEMBER, REQUIREMENT_STATUSES_REQUIRING_LIVE_NODES, resolveObjectRef, didYouMeanHint, } from "@metaobjectsdev/metadata";
24
+ export const ERR_REQUIREMENT_LINK_ABOVE_FLOOR = "ERR_REQUIREMENT_LINK_ABOVE_FLOOR";
25
+ export const ERR_REQUIREMENT_DANGLING_REF = "ERR_REQUIREMENT_DANGLING_REF";
26
+ export const ERR_REQUIREMENT_BAD_LEVEL = "ERR_REQUIREMENT_BAD_LEVEL";
27
+ export const ERR_REQUIREMENT_LEVEL_NESTING = "ERR_REQUIREMENT_LEVEL_NESTING";
28
+ export const ERR_REQUIREMENT_L4_NOT_OBJECT = "ERR_REQUIREMENT_L4_NOT_OBJECT";
29
+ export const ERR_REQUIREMENT_L5_NOT_MEMBER = "ERR_REQUIREMENT_L5_NOT_MEMBER";
30
+ export const ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS = "ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS";
31
+ export const WARN_REQUIREMENT_OBJECT_UNCLAIMED = "WARN_REQUIREMENT_OBJECT_UNCLAIMED";
32
+ /** Severity of the object-coverage gate. Promotion to `"error"` is a one-line
33
+ * flip here, which activates an already-written test rather than requiring new
34
+ * authoring under release pressure.
35
+ *
36
+ * It stays `"warn"`, and the reason is measured rather than cautious:
37
+ *
38
+ * - On a real 120-file estate carrying a SINGLE requirement, this gate reports
39
+ * 93 unclaimed entities — every entity in the repository. At `"error"` a
40
+ * project adopting requirements incrementally fails its first `verify` after
41
+ * authoring one entry, which teaches people to delete the entry.
42
+ * - The gate is satisfiable without being informative: `claimedObjects` below
43
+ * counts a claim from any requirement at any level and any status, so
44
+ * appending an FQN to an existing list clears it. Green proves an entity is
45
+ * NAMED, never that it is understood.
46
+ * - The experiment meant to settle whether forcing it yields real entries or
47
+ * padding stopped at its ceiling probe: at `"warn"` agents already authored
48
+ * proper L3/L4/L5 entries for what they added, so the arms could not differ.
49
+ * That is not evidence promotion is useless — it is evidence that instrument
50
+ * cannot see it.
51
+ *
52
+ * spec/design-docs/2026-08-11-prereg-duplication-and-levels.md, "Round E result". */
53
+ export const OBJECT_COVERAGE_SEVERITY = "warn";
54
+ /**
55
+ * Split a member reference into its owning object ref and the dotted member path.
56
+ * `::` qualifies the ROOT-level node only, so the object ref ends at the FIRST
57
+ * `.` after the last `::`.
58
+ * `acme::sales::Order.total.display` -> `["acme::sales::Order", ["total","display"]]`
59
+ */
60
+ export function splitMemberRef(ref) {
61
+ const pkgEnd = ref.lastIndexOf(PACKAGE_SEPARATOR);
62
+ const from = pkgEnd === -1 ? 0 : pkgEnd + PACKAGE_SEPARATOR.length;
63
+ const dot = ref.indexOf(".", from);
64
+ if (dot === -1)
65
+ return { owner: ref, path: [] };
66
+ return { owner: ref.slice(0, dot), path: ref.slice(dot + 1).split(".") };
67
+ }
68
+ /** Walk dotted member segments by CHILD NAME from an object node. */
69
+ function resolveMember(obj, path) {
70
+ let cur = obj;
71
+ for (const seg of path) {
72
+ if (cur === undefined)
73
+ return undefined;
74
+ cur = cur.children().find((c) => c.name === seg);
75
+ }
76
+ return cur;
77
+ }
78
+ /** Every `requirement.*` node in the tree, at any nesting depth. Hierarchy IS
79
+ * nesting — an L1 solution contains its L2 segments — so this is a walk, not a
80
+ * scan of a flat list keyed by a `parent` string. */
81
+ export function collectRequirements(root) {
82
+ const out = [];
83
+ const walk = (n) => {
84
+ for (const c of n.children()) {
85
+ if (c.type === TYPE_REQUIREMENT)
86
+ out.push(c);
87
+ walk(c);
88
+ }
89
+ };
90
+ walk(root);
91
+ return out;
92
+ }
93
+ /**
94
+ * Check the requirement tree against the loaded model.
95
+ *
96
+ * What a clean run proves: referential integrity — links sit at or below the
97
+ * link floor, nesting agrees with levels, and references resolve. What it CANNOT
98
+ * prove: that a status is *true*, or that a node actually implements the
99
+ * requirement claiming it. No test can. That truth is the adopter's job.
100
+ */
101
+ export function checkRequirements(root) {
102
+ const out = [];
103
+ const reqs = collectRequirements(root);
104
+ if (reqs.length === 0)
105
+ return out; // opt-in by declaration — no requirements, nothing to say
106
+ const claimedObjects = new Set();
107
+ for (const req of reqs) {
108
+ const architectural = req.subType === REQUIREMENT_SUBTYPE_ARCHITECTURAL;
109
+ const level = req.level();
110
+ const refs = req.implementedBy();
111
+ if (!architectural) {
112
+ if (level === undefined || !Number.isInteger(level)
113
+ || level < REQUIREMENT_MIN_LEVEL || level > REQUIREMENT_MAX_LEVEL) {
114
+ out.push({
115
+ severity: "error", code: ERR_REQUIREMENT_BAD_LEVEL, name: req.name,
116
+ message: `level must be an integer ${REQUIREMENT_MIN_LEVEL}-${REQUIREMENT_MAX_LEVEL} (got ${String(level)}). ` +
117
+ `L1 solution, L2 segment (app/library), L3 service, L4 object, L5 member.`,
118
+ });
119
+ }
120
+ // Nesting IS the hierarchy, so a child must sit strictly below its parent.
121
+ const parent = req.parent;
122
+ if (parent !== undefined && parent.type === TYPE_REQUIREMENT) {
123
+ const pl = parent.level();
124
+ if (pl !== undefined && level !== undefined && level <= pl) {
125
+ out.push({
126
+ severity: "error", code: ERR_REQUIREMENT_LEVEL_NESTING, name: req.name,
127
+ message: `nested under "${parent.name}" (level ${pl}) but declares level ${level}. ` +
128
+ `Nesting is the hierarchy — a child sits strictly below its parent.`,
129
+ });
130
+ }
131
+ }
132
+ }
133
+ // -- the link boundary ----------------------------------------------------
134
+ if (refs.length > 0 && !req.mayReferenceModel()) {
135
+ out.push({
136
+ severity: "error", code: ERR_REQUIREMENT_LINK_ABOVE_FLOOR, name: req.name,
137
+ message: `'implementedBy' is legal at L${REQUIREMENT_LINK_FLOOR_LEVEL} (object) and ` +
138
+ `L${REQUIREMENT_MAX_LEVEL} (member) only. L1-L3 are organisational and never reference ` +
139
+ `the model — move the links to a nested L${REQUIREMENT_LINK_FLOOR_LEVEL} child.`,
140
+ });
141
+ continue;
142
+ }
143
+ for (const ref of refs) {
144
+ const { owner, path } = splitMemberRef(ref);
145
+ // referrerPkg is the requirement's own effective package, so a bare ref
146
+ // binds package-locally under the ADR-0042 contract — the loader's own
147
+ // resolver, never a parallel name scan (#228).
148
+ const referrerPkg = req.package ?? req.fileDefaultPackage ?? "";
149
+ const { node } = resolveObjectRef(root, owner, referrerPkg);
150
+ const isObjectRef = path.length === 0;
151
+ if (node !== undefined && (isObjectRef || resolveMember(node, path) !== undefined)) {
152
+ claimedObjects.add(node.resolutionKey());
153
+ }
154
+ if (!architectural && level === REQUIREMENT_LINK_FLOOR_LEVEL && !isObjectRef) {
155
+ out.push({
156
+ severity: "error", code: ERR_REQUIREMENT_L4_NOT_OBJECT, name: req.name,
157
+ message: `L${REQUIREMENT_LINK_FLOOR_LEVEL} references an object; '${ref}' names a member. ` +
158
+ `Move it to a nested L${REQUIREMENT_LEVEL_MEMBER} child, or reference the object itself.`,
159
+ });
160
+ continue;
161
+ }
162
+ if (!architectural && level === REQUIREMENT_LEVEL_MEMBER && isObjectRef) {
163
+ out.push({
164
+ severity: "error", code: ERR_REQUIREMENT_L5_NOT_MEMBER, name: req.name,
165
+ message: `L${REQUIREMENT_LEVEL_MEMBER} references a member (field, view or identity); ` +
166
+ `'${ref}' names an object. Move it to its L${REQUIREMENT_LINK_FLOOR_LEVEL} parent.`,
167
+ });
168
+ continue;
169
+ }
170
+ const resolved = node !== undefined && (isObjectRef || resolveMember(node, path) !== undefined);
171
+ if (!resolved) {
172
+ // Severity is CONDITIONAL ON STATUS, and the asymmetry inverts as a pair.
173
+ // On abandoned/superseded the nodes are SUPPOSED to be gone — that is the
174
+ // entry doing its job, and the reason this check cannot live in the loader.
175
+ if (req.requiresLiveNodes()) {
176
+ out.push({
177
+ severity: "error", code: ERR_REQUIREMENT_DANGLING_REF, name: req.name,
178
+ message: `'${ref}' does not resolve in the loaded model (status '${String(req.status())}' — ` +
179
+ `the model moved and the requirement is stale).` + didYouMeanHint(root, owner),
180
+ });
181
+ }
182
+ }
183
+ }
184
+ // -- architectural universality, v1: claim-set arithmetic -----------------
185
+ // A live policy claimed by nothing is the audited-base case: declared and
186
+ // applied to nothing. Deliberately NOT a violation-predicate DSL — that
187
+ // would be the registration this design already argued its way to, arriving
188
+ // a second time through the back door.
189
+ const status = req.status();
190
+ const live = status !== undefined
191
+ && REQUIREMENT_STATUSES_REQUIRING_LIVE_NODES.includes(status);
192
+ if (architectural && live && refs.length === 0) {
193
+ out.push({
194
+ severity: "error", code: ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS, name: req.name,
195
+ message: `architectural requirement is '${String(status)}' but nothing implements it. ` +
196
+ `Its check is universality — a claim set of zero means the policy is declared and unapplied.`,
197
+ });
198
+ }
199
+ }
200
+ // -- object coverage: adding an entity forces a requirement -----------------
201
+ // Binary per entity, never a ratio: a "% claimed" number measures what the
202
+ // schema can express, is biased against the hardest rules, and invites
203
+ // optimising the number.
204
+ //
205
+ // SCOPE, stated because it is a decision and not an oversight:
206
+ //
207
+ // ENTITIES ONLY. `object.value` and `object.projection` are exempt. A value is a
208
+ // shape (a DTO, a payload, a message) and a projection is DERIVED from an entity
209
+ // that is itself claimable — requiring both to carry their own capability claim
210
+ // would multiply entries without adding information.
211
+ //
212
+ // OBJECT GRAIN ONLY. Fields, views, validators and identities are never required
213
+ // to be claimed. Member-grain coverage is the "thousands of meaningless links"
214
+ // failure `spec/capability-ledger.md` argues against: plumbing members are covered
215
+ // by ARCHITECTURAL requirements with high fan-out (one uuid-PK rule claims every
216
+ // entity), not by a per-member entry. L5 exists so a claim about a specific member
217
+ // CAN be made when it carries real meaning — never so that every member must.
218
+ //
219
+ // So a green run means "every entity is claimed by something", not "every node is
220
+ // described". The stronger reading would be false.
221
+ for (const ent of root.children()) {
222
+ if (ent.type !== TYPE_OBJECT || ent.subType !== OBJECT_SUBTYPE_ENTITY)
223
+ continue;
224
+ const key = ent.resolutionKey();
225
+ if (!claimedObjects.has(key)) {
226
+ out.push({
227
+ severity: OBJECT_COVERAGE_SEVERITY, code: WARN_REQUIREMENT_OBJECT_UNCLAIMED,
228
+ message: `no requirement claims '${key}'. Add it to an L${REQUIREMENT_LINK_FLOOR_LEVEL} requirement's 'implementedBy'.`,
229
+ });
230
+ }
231
+ }
232
+ return out;
233
+ }
234
+ //# sourceMappingURL=requirement-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requirement-check.js","sourceRoot":"","sources":["../../../src/lib/requirement-check.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { type MetaData } from "@metaobjectsdev/metadata";
2
+ export declare const ERR_REQUIREMENT_TEST_MISSING = "ERR_REQUIREMENT_TEST_MISSING";
3
+ export declare const WARN_REQUIREMENT_TEST_SKIPPED = "WARN_REQUIREMENT_TEST_SKIPPED";
4
+ export interface VerifiedByDiagnostic {
5
+ severity: "error" | "warn";
6
+ code: string;
7
+ name?: string;
8
+ message: string;
9
+ }
10
+ /**
11
+ * Check every `@verifiedBy` name against the project's test corpus.
12
+ *
13
+ * Severity mirrors `@implementedBy`: a broken claim is an ERROR on `live`/`partial`
14
+ * and silent on `abandoned`/`superseded`, because a retired requirement naming a
15
+ * deleted test is the entry doing its job, not drift.
16
+ */
17
+ export declare function checkVerifiedBy(root: MetaData, cwd: string): VerifiedByDiagnostic[];
18
+ //# sourceMappingURL=verified-by-scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verified-by-scan.d.ts","sourceRoot":"","sources":["../../../src/lib/verified-by-scan.ts"],"names":[],"mappings":"AA0BA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAElC,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAC3E,eAAO,MAAM,6BAA6B,kCAAkC,CAAC;AAE7E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AA0FD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAoDnF"}
@@ -0,0 +1,168 @@
1
+ // `meta verify` — the `@verifiedBy` check.
2
+ //
3
+ // A requirement's `@verifiedBy` names the tests that prove the behaviour. verify
4
+ // checks each name EXISTS and is NOT SKIPPED; it never runs them. Running them is
5
+ // the test runner's job, and a requirement gate that shelled out to one would be
6
+ // slow, ecosystem-specific, and wrong in CI.
7
+ //
8
+ // Until this existed, `@verifiedBy` was registered vocabulary that nothing read —
9
+ // the precise pattern ADR-0007 Amendment 2 and the `@role` shrink exist to punish.
10
+ // An attribute the loader accepts and no consumer dispatches on teaches authors
11
+ // that declaring is free and means nothing.
12
+ //
13
+ // PRECISION OVER RECALL, deliberately. The scan matches a name anywhere in the
14
+ // test corpus, as a whole word, in any language. That is the most generous
15
+ // possible reading, so a "missing" verdict means the name appears in NO test file
16
+ // at all — which is a broken claim in any ecosystem. The repo's standing rule for
17
+ // drift checks is to bias toward under-flagging, and a nagging gate gets disabled,
18
+ // which costs more than the misses.
19
+ //
20
+ // FAIL-OPEN ON INABILITY. If the project has no test files this scan can see, it
21
+ // says NOTHING rather than reporting every name missing. Absence of evidence is
22
+ // not evidence of absence, and a monorepo whose tests live outside `--cwd` must
23
+ // not be told its requirements are unverified.
24
+ import { readdirSync, readFileSync, statSync } from "node:fs";
25
+ import { join, relative, sep } from "node:path";
26
+ import { TYPE_REQUIREMENT, } from "@metaobjectsdev/metadata";
27
+ export const ERR_REQUIREMENT_TEST_MISSING = "ERR_REQUIREMENT_TEST_MISSING";
28
+ export const WARN_REQUIREMENT_TEST_SKIPPED = "WARN_REQUIREMENT_TEST_SKIPPED";
29
+ const IGNORE_SEGMENTS = new Set([
30
+ "node_modules", ".git", "dist", "build", "out", ".next", "coverage",
31
+ ".metaobjects", "generated", "target", "bin", "obj", "__pycache__", ".venv", "venv",
32
+ ]);
33
+ /** Test files across the five ecosystems this project ports to. */
34
+ const TEST_FILE = new RegExp([
35
+ "\\.(?:test|spec)\\.[cm]?[jt]sx?$", // bun / jest / vitest / mocha
36
+ "(?:^|[./_-])[Tt]est[^/]*\\.java$", // JUnit — TestFoo.java
37
+ "[A-Za-z0-9]Test(?:s)?\\.java$", // JUnit — FooTest.java / FooTests.java
38
+ "[A-Za-z0-9]Tests?\\.cs$", // xUnit / NUnit
39
+ "^test_[^/]*\\.py$", // pytest
40
+ "[^/]*_test\\.py$", // pytest, trailing convention
41
+ "[A-Za-z0-9]Test(?:s)?\\.kt$", // Kotlin
42
+ ].join("|"));
43
+ /** Markers that a test exists but is disabled, across the same ecosystems. */
44
+ const SKIP_MARKER = new RegExp([
45
+ "\\b(?:it|test|describe)\\.(?:skip|todo)\\b", // jest/vitest/bun
46
+ "\\bx(?:it|test|describe)\\b", // mocha/jasmine
47
+ "@Disabled\\b", // JUnit 5
48
+ "@Ignore\\b", // JUnit 4 / Kotlin
49
+ "@pytest\\.mark\\.skip", // pytest
50
+ "\\[Ignore[\\](]", // MSTest / NUnit
51
+ "\\bSkip\\s*=", // xUnit [Fact(Skip = "...")]
52
+ ].join("|"));
53
+ function walk(dir, root, acc, depth = 0) {
54
+ if (depth > 12)
55
+ return; // pathological trees; the scan is advisory, not exhaustive
56
+ let entries;
57
+ try {
58
+ entries = readdirSync(dir, { withFileTypes: true });
59
+ }
60
+ catch {
61
+ return;
62
+ }
63
+ for (const e of entries) {
64
+ if (e.isDirectory()) {
65
+ if (IGNORE_SEGMENTS.has(e.name) || e.name.startsWith("."))
66
+ continue;
67
+ walk(join(dir, e.name), root, acc, depth + 1);
68
+ continue;
69
+ }
70
+ if (!e.isFile() || !TEST_FILE.test(e.name))
71
+ continue;
72
+ const abs = join(dir, e.name);
73
+ try {
74
+ if (statSync(abs).size > 512 * 1024)
75
+ continue;
76
+ acc.byFile.set(relative(root, abs).split(sep).join("/"), readFileSync(abs, "utf8").split("\n"));
77
+ acc.files++;
78
+ }
79
+ catch {
80
+ /* unreadable file is not a finding */
81
+ }
82
+ }
83
+ }
84
+ /** Every `requirement.*` node in the tree, at any nesting depth. */
85
+ function collect(root) {
86
+ const out = [];
87
+ const rec = (n) => {
88
+ for (const c of n.children()) {
89
+ if (c.type === TYPE_REQUIREMENT)
90
+ out.push(c);
91
+ rec(c);
92
+ }
93
+ };
94
+ rec(root);
95
+ return out;
96
+ }
97
+ /**
98
+ * A whole-word match, so `OrderServiceTest` never satisfies a claim naming `Order`.
99
+ *
100
+ * `_` counts as a SEPARATOR, not a word character: pytest's `def test_OrderServiceTest`
101
+ * plainly is the test a claim naming `OrderServiceTest` means, and refusing it would
102
+ * emit the confident false error this scan is built to avoid. Camel-case boundaries
103
+ * stay strict, which is what actually prevents a short name matching a longer one.
104
+ */
105
+ function wordRx(name) {
106
+ return new RegExp(`(?:^|[^A-Za-z0-9])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9])`);
107
+ }
108
+ /**
109
+ * Check every `@verifiedBy` name against the project's test corpus.
110
+ *
111
+ * Severity mirrors `@implementedBy`: a broken claim is an ERROR on `live`/`partial`
112
+ * and silent on `abandoned`/`superseded`, because a retired requirement naming a
113
+ * deleted test is the entry doing its job, not drift.
114
+ */
115
+ export function checkVerifiedBy(root, cwd) {
116
+ const reqs = collect(root).filter((r) => r.verifiedBy().length > 0);
117
+ if (reqs.length === 0)
118
+ return []; // opt-in by declaration
119
+ const corpus = { files: 0, byFile: new Map() };
120
+ walk(cwd, cwd, corpus);
121
+ if (corpus.files === 0)
122
+ return []; // fail open: nothing to judge against
123
+ const out = [];
124
+ for (const req of reqs) {
125
+ for (const test of req.verifiedBy()) {
126
+ const rx = wordRx(test);
127
+ let foundIn;
128
+ let skippedAt;
129
+ for (const [file, lines] of corpus.byFile) {
130
+ for (let i = 0; i < lines.length; i++) {
131
+ if (!rx.test(lines[i] ?? ""))
132
+ continue;
133
+ foundIn ??= file;
134
+ // a decorator/annotation sits above the declaration it disables
135
+ const window = lines.slice(Math.max(0, i - 3), i + 1).join("\n");
136
+ if (SKIP_MARKER.test(window))
137
+ skippedAt ??= `${file}:${i + 1}`;
138
+ }
139
+ if (foundIn !== undefined && skippedAt !== undefined)
140
+ break;
141
+ }
142
+ if (foundIn === undefined) {
143
+ if (req.requiresLiveNodes()) {
144
+ out.push({
145
+ severity: "error",
146
+ code: ERR_REQUIREMENT_TEST_MISSING,
147
+ name: req.name,
148
+ message: `'verifiedBy' names '${test}', which appears in none of the ` +
149
+ `${corpus.files} test file(s) found under this project. Either the test was ` +
150
+ `renamed or removed, or the claim was never true.`,
151
+ });
152
+ }
153
+ continue;
154
+ }
155
+ if (skippedAt !== undefined) {
156
+ out.push({
157
+ severity: "warn",
158
+ code: WARN_REQUIREMENT_TEST_SKIPPED,
159
+ name: req.name,
160
+ message: `'verifiedBy' names '${test}', but it is disabled at ${skippedAt}. ` +
161
+ `A skipped test proves nothing — the requirement reads as verified and is not.`,
162
+ });
163
+ }
164
+ }
165
+ }
166
+ return out;
167
+ }
168
+ //# sourceMappingURL=verified-by-scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verified-by-scan.js","sourceRoot":"","sources":["../../../src/lib/verified-by-scan.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metaobjectsdev/cli",
3
- "version": "0.21.5",
3
+ "version": "0.22.0-rc.1",
4
4
  "description": "CLI for MetaObjects: scaffold, codegen, migrate, and drift-detection commands.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -49,15 +49,15 @@
49
49
  ],
50
50
  "dependencies": {
51
51
  "@libsql/kysely-libsql": "^0.4.0",
52
- "@metaobjectsdev/codegen-ts": "0.21.5",
53
- "@metaobjectsdev/codegen-ts-react": "0.21.5",
54
- "@metaobjectsdev/codegen-ts-tanstack": "0.21.5",
55
- "@metaobjectsdev/docs-site": "0.21.5",
56
- "@metaobjectsdev/metadata": "0.21.5",
57
- "@metaobjectsdev/migrate-ts": "0.21.5",
58
- "@metaobjectsdev/render": "0.21.5",
59
- "@metaobjectsdev/runtime-ts": "0.21.5",
60
- "@metaobjectsdev/sdk": "0.21.5",
52
+ "@metaobjectsdev/codegen-ts": "0.22.0-rc.1",
53
+ "@metaobjectsdev/codegen-ts-react": "0.22.0-rc.1",
54
+ "@metaobjectsdev/codegen-ts-tanstack": "0.22.0-rc.1",
55
+ "@metaobjectsdev/docs-site": "0.22.0-rc.1",
56
+ "@metaobjectsdev/metadata": "0.22.0-rc.1",
57
+ "@metaobjectsdev/migrate-ts": "0.22.0-rc.1",
58
+ "@metaobjectsdev/render": "0.22.0-rc.1",
59
+ "@metaobjectsdev/runtime-ts": "0.22.0-rc.1",
60
+ "@metaobjectsdev/sdk": "0.22.0-rc.1",
61
61
  "@toon-format/toon": "^2.3.0",
62
62
  "jiti": "^2.4.0"
63
63
  },
@@ -523,13 +523,19 @@ async function ensureEsmPackageType(cwd: string, result: InitResult): Promise<vo
523
523
 
524
524
  /**
525
525
  * ADR-0034 scaffold-and-own hands the project real source files under
526
- * `codegen/generators/`, and those files import `@metaobjectsdev/codegen-ts`,
527
- * `@metaobjectsdev/metadata` and `ts-poet`. Installing `@metaobjectsdev/cli` alone
528
- * does not put any of them where the project can resolve them, so the scaffold
529
- * arrived un-typecheckable: ten TS2307s on files `meta init` had just written.
526
+ * `codegen/generators/`, and those files import `@metaobjectsdev/codegen-ts` and
527
+ * `@metaobjectsdev/metadata`. Installing `@metaobjectsdev/cli` alone does not put
528
+ * either of them where the project can resolve them, so the scaffold arrived
529
+ * un-typecheckable: ten TS2307s on files `meta init` had just written.
530
530
  *
531
531
  * Declaring them is the honest fix — they are dependencies of code that now lives in
532
- * the adopter's repo. Only ever ADDS a missing key: an existing pin is the user's.
532
+ * the adopter's repo. Deliberately NOT declared: `ts-poet`. The scaffolded templates
533
+ * import the ts-poet combinators via @metaobjectsdev/codegen-ts (re-exported from its
534
+ * own ts-poet instance) precisely so that the Code objects they compose share ONE
535
+ * ts-poet copy with the engine's render* primitives — a project-local ts-poet is the
536
+ * second physical copy that split the class identity under a globally-installed /
537
+ * linked CLI (duplicate imports in generated files, TS2300 on first tsc; see the
538
+ * gen-split-tree gate). Only ever ADDS a missing key: an existing pin is the user's.
533
539
  * Returns what it added so the caller can tell them to install.
534
540
  */
535
541
  function addScaffoldDevDependencies(pkg: Record<string, unknown>): string[] {
@@ -537,7 +543,6 @@ function addScaffoldDevDependencies(pkg: Record<string, unknown>): string[] {
537
543
  const wanted: Record<string, string> = {
538
544
  "@metaobjectsdev/codegen-ts": `^${version}`,
539
545
  "@metaobjectsdev/metadata": `^${version}`,
540
- "ts-poet": "^6.10.0",
541
546
  };
542
547
  const dev = (pkg.devDependencies ?? {}) as Record<string, string>;
543
548
  const deps = (pkg.dependencies ?? {}) as Record<string, string>;
@@ -16,6 +16,8 @@ import { FileProvider } from "../lib/file-provider.js";
16
16
  import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
17
17
  import { loadMetaobjectsConfig } from "../lib/load-metaobjects-config.js";
18
18
  import { computeCodegenDrift } from "../lib/codegen-drift.js";
19
+ import { checkRequirements } from "../lib/requirement-check.js";
20
+ import { checkVerifiedBy } from "../lib/verified-by-scan.js";
19
21
  import { resolveD1Config } from "../lib/config.js";
20
22
  import {
21
23
  buildWranglerExecuteArgs,
@@ -158,6 +160,10 @@ export async function verifyCommand(
158
160
  const templateExit = runTemplates ? runTemplateVerify() : 0;
159
161
  const schemaExit = await runSchemaVerify();
160
162
  const codegenExit = runCodegen ? await runCodegenVerify() : 0;
163
+ // Requirements have no subverb: `requirement.*` nodes are metadata, so they
164
+ // are checked on every `meta verify`. Opt-in by DECLARATION — a model with no
165
+ // requirement nodes is silent, not in drift.
166
+ const requirementExit = runRequirementVerify();
161
167
 
162
168
  // Advisory verify-as-teacher pass: surface hand-rolled work the metadata could
163
169
  // model. Warnings ONLY — never changes the exit code (bias to under-flagging).
@@ -165,7 +171,28 @@ export async function verifyCommand(
165
171
  // noisy project (both opt-outs work on `meta verify` and `meta gen`).
166
172
  if (!flags.noAntipatterns && process.env.META_NO_ANTIPATTERNS !== "1") runAntiPatternAdvisory();
167
173
 
168
- return Math.max(templateExit, schemaExit, codegenExit);
174
+ return Math.max(templateExit, schemaExit, codegenExit, requirementExit);
175
+
176
+ // -- requirements (#290) ---------------------------------------------------
177
+ function runRequirementVerify(): number {
178
+ // `@verifiedBy` resolution needs the project on disk, so it is a separate
179
+ // scan; its diagnostics carry the same severities and share this reporter.
180
+ const diags = [...checkRequirements(root), ...checkVerifiedBy(root, cwd)];
181
+ if (diags.length === 0) return 0;
182
+ const errors = diags.filter((d) => d.severity === "error");
183
+ const warns = diags.filter((d) => d.severity === "warn");
184
+ const CAP = 20;
185
+ for (const d of errors) log.error(` ${d.code}${d.name !== undefined ? ` [${d.name}]` : ""}: ${d.message}`);
186
+ for (const d of warns.slice(0, CAP)) {
187
+ log.warn(` ${d.code}${d.name !== undefined ? ` [${d.name}]` : ""}: ${d.message}`);
188
+ }
189
+ if (warns.length > CAP) log.warn(` …and ${warns.length - CAP} more.`);
190
+ if (errors.length > 0) {
191
+ log.error(`meta verify — requirements: ${errors.length} error(s).`);
192
+ return 1;
193
+ }
194
+ return 0;
195
+ }
169
196
 
170
197
  // -- verify-as-teacher (advisory) ------------------------------------------
171
198
  function runAntiPatternAdvisory(): void {
@@ -1,7 +1,8 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import type { Dirent } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import {
4
- detectStack, makeStack,
5
+ detectStack, detectConcerns, makeStack,
5
6
  type ServerLang, type ClientFramework, type Stack, type ProjectProbe,
6
7
  SERVER_LANGS, CLIENT_FRAMEWORKS,
7
8
  } from "@metaobjectsdev/sdk/agent-context";
@@ -20,22 +21,64 @@ function depNames(cwd: string): Set<string> {
20
21
  return out;
21
22
  }
22
23
 
24
+ const METADATA_DIR = "metaobjects";
25
+ const METADATA_FILE_PATTERN = /\.(json|ya?ml)$/i;
26
+ // Cheap substring probe, not a metamodel load: matches both canonical JSON's
27
+ // quoted `"requirement.functional"` key and sigil-free YAML's bare
28
+ // `requirement.functional:` authoring form.
29
+ const REQUIREMENT_NODE_MARKER = "requirement.";
30
+
31
+ /** Recursively scans `metaobjects/` for any `.json`/`.yaml`/`.yml` file containing a
32
+ * `requirement.*` node marker. Defensive throughout: a missing/unreadable directory
33
+ * or file is treated as "not found", never thrown — this is a cheap heuristic, not
34
+ * a metamodel load. */
35
+ function hasRequirementNodes(cwd: string): boolean {
36
+ const root = join(cwd, METADATA_DIR);
37
+ if (!existsSync(root)) return false;
38
+ const pending: string[] = [root];
39
+ while (pending.length > 0) {
40
+ const dir = pending.pop()!;
41
+ let entries: Dirent[];
42
+ try {
43
+ entries = readdirSync(dir, { withFileTypes: true });
44
+ } catch {
45
+ continue; // unreadable directory — skip it, keep scanning siblings
46
+ }
47
+ for (const entry of entries) {
48
+ const full = join(dir, entry.name);
49
+ if (entry.isDirectory()) {
50
+ pending.push(full);
51
+ } else if (METADATA_FILE_PATTERN.test(entry.name)) {
52
+ try {
53
+ if (readFileSync(full, "utf8").includes(REQUIREMENT_NODE_MARKER)) return true;
54
+ } catch { /* unreadable file — treat as no match */ }
55
+ }
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+
23
61
  function probe(cwd: string): ProjectProbe {
24
62
  const deps = depNames(cwd);
25
63
  const names = existsSync(cwd) ? readdirSync(cwd) : [];
26
64
  return {
27
65
  hasDep: (name) => deps.has(name),
28
66
  hasFileMatching: (re) => names.some((n) => re.test(n)),
67
+ hasRequirementNodes: () => hasRequirementNodes(cwd),
29
68
  };
30
69
  }
31
70
 
32
- /** Resolve the stack: explicit --server/--client overrides take precedence; otherwise detect. */
71
+ /** Resolve the stack: explicit --server/--client overrides take precedence; otherwise detect.
72
+ * Concern tokens (e.g. requirements) are always OBSERVED from project state, independent of
73
+ * any --server/--client override — a concern is not a stack axis. */
33
74
  export function resolveStack(cwd: string, overrides: { servers: string[]; clients: string[] }): Stack {
34
75
  const validServers = SERVER_LANGS as readonly string[];
35
76
  const validClients = CLIENT_FRAMEWORKS as readonly string[];
36
77
  const oServers = overrides.servers.filter((s): s is ServerLang => validServers.includes(s));
37
78
  const oClients = overrides.clients.filter((c): c is ClientFramework => validClients.includes(c));
38
- if (oServers.length > 0 || oClients.length > 0) return makeStack(oServers, oClients);
39
- const detected = detectStack(probe(cwd));
40
- return makeStack(detected.servers, detected.clients);
79
+ const p = probe(cwd);
80
+ const concerns = detectConcerns(p);
81
+ if (oServers.length > 0 || oClients.length > 0) return makeStack(oServers, oClients, concerns);
82
+ const detected = detectStack(p);
83
+ return makeStack(detected.servers, detected.clients, concerns);
41
84
  }