@openwop/openwop-conformance 1.152.0 → 1.153.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/CHANGELOG.md +9 -0
- package/dist/lib/requirement-ids.js +100 -0
- package/dist/lib/scenario-disposition.js +22 -0
- package/package.json +3 -1
- package/requirement-aliases.json +4 -0
- package/requirements.json +23554 -0
- package/schemas/CORPUS-STAMP.json +2 -2
- package/src/lib/requirement-ids.test.ts +83 -0
- package/src/lib/requirement-ids.ts +111 -0
- package/src/lib/scenario-disposition.ts +22 -0
- package/src/setup.ts +84 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [1.153.0] — 2026-09-02 — per-`it` requirement rows and the requirement registry (RFC 0148 §A / G3; v2 charter Phase 1)
|
|
4
|
+
|
|
5
|
+
No new scenario file. Three packed changes, all additive to bundle v2:
|
|
6
|
+
|
|
7
|
+
- **Every test records its own ledger row.** `setup.ts` now records one RFC 0148 §A disposition per `it()` under `openwop.it.<file-stem>.<title-slug>` (`src/lib/requirement-ids.ts`), next to the retained file-level row the floors key on. A test that passes with ≥1 assertion is `executed-pass`; a failure is `executed-fail` with the first error message; a pass with zero assertions takes the gate reason recorded during that test (`softSkip` / `seamAbsent` / `behaviorGate`) or resolves to `blocked`. This is the durable fix for certification gap G8: a file that asserted a positive control and then soft-skipped the requirement no longer certifies the requirement, because the requirement's own row says `skipped`/`inapplicable`/`blocked`.
|
|
8
|
+
- **`--certify` emits the per-`it` rows** in `results.requirements[]`, attributed to their scenario file. Existing verifiers accept them (the row shape is unchanged; ids are new). Expect bundles to grow from ~470 rows to ~2,400.
|
|
9
|
+
- **`conformance/requirements.json`** (packed) is the generated registry: one record per test with its id, file, line, title, and the `driver.describe` / `req()` citations found in its body. `scripts/generate-requirement-registry.mjs --check` runs in `openwop:check`; a reworded title needs a row in `requirement-aliases.json` or the check fails, so a bundle that cited the old id still resolves. Measured at generation: 1,950 tests in 468 files, 1,927 stable ids, 23 interpolated titles (those rows exist at run time keyed by the rendered title and map to the registry by file+line).
|
|
10
|
+
- New `req(id, section, requirement)` helper: a scenario may attach a hand-authored registry id to the current test; it doubles as the assertion message.
|
|
11
|
+
|
|
3
12
|
## [1.152.0] — 2026-09-02 — v2 charter Phase 0: `--certify` defaults to bundle v2; the RFC 0050 reference suite leaves the scenario file
|
|
4
13
|
|
|
5
14
|
No new scenario file. Two packed changes:
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-`it` requirement ids (RFC 0148 §A, gap G3; v2 charter Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* The ledger has recorded ONE row per scenario FILE (`openwop.floor.<file>` /
|
|
5
|
+
* `openwop.scenario.<file>`). That granularity is what produces certification
|
|
6
|
+
* gap G8: a file that asserted a positive control and then soft-skipped the
|
|
7
|
+
* requirement still resolves to `executed-pass` for the whole file. The durable
|
|
8
|
+
* fix named in `scenario-disposition.ts` is per-`it` recording — this module
|
|
9
|
+
* supplies the ids for it.
|
|
10
|
+
*
|
|
11
|
+
* Why the id derives from the TEST TITLE and not from the `driver.describe`
|
|
12
|
+
* text: 190 of 1,756 citations interpolate the requirement text at run time
|
|
13
|
+
* (`docs/REQUIREMENT-REGISTRY-FEASIBILITY.md`), so no text-derived id can be
|
|
14
|
+
* stable for them. Test titles are literals in every scenario file, stable
|
|
15
|
+
* across hosts, and already the unit vitest reports on. The registry
|
|
16
|
+
* (`conformance/requirements.json`, generated) maps each id back to the
|
|
17
|
+
* citations found inside that test's body, so a bundle reader can still ask
|
|
18
|
+
* "which spec section did this row witness".
|
|
19
|
+
*
|
|
20
|
+
* Grammar: `openwop.it.<file-stem>.<title-slug>` where the stem is the scenario
|
|
21
|
+
* file name without `.test.ts` and the slug is the title lower-cased, every run
|
|
22
|
+
* of non-alphanumerics folded to one `-`, trimmed, and capped at 80 characters.
|
|
23
|
+
* A second test in the same file whose title slugs identically gets `~2`,
|
|
24
|
+
* `~3`, … (vitest allows duplicate titles; the ledger does not allow duplicate
|
|
25
|
+
* ids). A scenario MAY override the derived id for the current test with
|
|
26
|
+
* `req()` when it wants a hand-authored, registry-listed id.
|
|
27
|
+
*/
|
|
28
|
+
export const IT_ID_PREFIX = 'openwop.it.';
|
|
29
|
+
export const IT_ID_GRAMMAR = /^openwop\.it\.[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*(?:~[2-9][0-9]*)?$/;
|
|
30
|
+
const MAX_SLUG = 80;
|
|
31
|
+
/** Title → slug: lower-case, non-alphanumeric runs → `-`, trimmed, capped. */
|
|
32
|
+
export function slugTitle(title) {
|
|
33
|
+
const s = title
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
36
|
+
.replace(/^-+|-+$/g, '');
|
|
37
|
+
const capped = s.length > MAX_SLUG ? s.slice(0, MAX_SLUG).replace(/-+$/g, '') : s;
|
|
38
|
+
return capped.length > 0 ? capped : 'untitled';
|
|
39
|
+
}
|
|
40
|
+
/** `auth-subject-link.test.ts` → `auth-subject-link`. */
|
|
41
|
+
export function fileStem(scenarioFile) {
|
|
42
|
+
return scenarioFile.replace(/\.test\.ts$/, '');
|
|
43
|
+
}
|
|
44
|
+
/** Derived id for one test, before collision suffixing. */
|
|
45
|
+
export function itRequirementId(scenarioFile, title) {
|
|
46
|
+
return `${IT_ID_PREFIX}${fileStem(scenarioFile)}.${slugTitle(title)}`;
|
|
47
|
+
}
|
|
48
|
+
/** The scenario file a per-`it` id belongs to, or null when the id is not one. */
|
|
49
|
+
export function scenarioFileOfItId(requirementId) {
|
|
50
|
+
if (!requirementId.startsWith(IT_ID_PREFIX))
|
|
51
|
+
return null;
|
|
52
|
+
const rest = requirementId.slice(IT_ID_PREFIX.length);
|
|
53
|
+
const dot = rest.indexOf('.');
|
|
54
|
+
if (dot <= 0)
|
|
55
|
+
return null;
|
|
56
|
+
return `${rest.slice(0, dot)}.test.ts`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Collision suffixing: the first occurrence keeps the bare id, later ones get
|
|
60
|
+
* `~2`, `~3`, … Keyed per file so a worker running many files never crosses
|
|
61
|
+
* streams. Reset per file by `setup.ts` when the file finishes.
|
|
62
|
+
*/
|
|
63
|
+
export class ItIdAllocator {
|
|
64
|
+
seen = new Map();
|
|
65
|
+
allocate(scenarioFile, title) {
|
|
66
|
+
const base = itRequirementId(scenarioFile, title);
|
|
67
|
+
const n = (this.seen.get(base) ?? 0) + 1;
|
|
68
|
+
this.seen.set(base, n);
|
|
69
|
+
return n === 1 ? base : `${base}~${n}`;
|
|
70
|
+
}
|
|
71
|
+
reset() {
|
|
72
|
+
this.seen.clear();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Explicit override for the current test.
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
let explicitId = null;
|
|
79
|
+
/**
|
|
80
|
+
* Attach a hand-authored requirement id to the CURRENT test. The per-`it`
|
|
81
|
+
* ledger row for this test is recorded under `id` instead of the derived
|
|
82
|
+
* title id. Returns the usual `driver.describe`-style message so the call
|
|
83
|
+
* doubles as the assertion message:
|
|
84
|
+
*
|
|
85
|
+
* expect(x, req('openwop.auth.subject-link.leaver-deny', 'auth-profiles.md §Subject linking', 'deactivation MUST deny')).toBe(true)
|
|
86
|
+
*
|
|
87
|
+
* The id MUST be listed in `conformance/requirements.json` (the generator
|
|
88
|
+
* collects `req(` first-argument literals); an unlisted id fails the registry
|
|
89
|
+
* check, so a hand id cannot drift from the registry silently.
|
|
90
|
+
*/
|
|
91
|
+
export function req(id, specSection, requirement) {
|
|
92
|
+
explicitId = id;
|
|
93
|
+
return `${specSection}: ${requirement}`;
|
|
94
|
+
}
|
|
95
|
+
/** setup.ts reads and clears the override after each test. */
|
|
96
|
+
export function takeExplicitRequirementId() {
|
|
97
|
+
const id = explicitId;
|
|
98
|
+
explicitId = null;
|
|
99
|
+
return id;
|
|
100
|
+
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* Pure functions, no I/O, so `runner-ledger.test.ts` can pin them without a
|
|
26
26
|
* host or a vitest subprocess.
|
|
27
27
|
*/
|
|
28
|
+
import { scenarioFileOfItId } from './requirement-ids.js';
|
|
28
29
|
import { PROFILE_FLOOR_SCENARIOS } from './profiles.js';
|
|
29
30
|
import { requirementIdForScenario, requirementIdForPrefix, requirementsFor } from './requirement-registry.js';
|
|
30
31
|
import { UNCLASSIFIED_RETURN_DETAIL } from './soft-skip.js';
|
|
@@ -233,6 +234,27 @@ document) {
|
|
|
233
234
|
}
|
|
234
235
|
rows.push(row);
|
|
235
236
|
}
|
|
237
|
+
// Per-`it` rows (suite 1.153.0): every ledger entry keyed `openwop.it.<file>.<slug>`
|
|
238
|
+
// becomes its own bundle row, attributed to its scenario file. Additive — the
|
|
239
|
+
// file-level and prefix rows above are unchanged, and the floors still key on
|
|
240
|
+
// them. This is the granularity RFC 0148 §A describes and the G8 fix.
|
|
241
|
+
const emitted = new Set(rows.map((r) => r.requirementId));
|
|
242
|
+
for (const e of [...ledger].sort((a, b) => a.requirementId.localeCompare(b.requirementId))) {
|
|
243
|
+
const file = scenarioFileOfItId(e.requirementId);
|
|
244
|
+
// Attribute only to files this run reported on: a worker's ledger can carry
|
|
245
|
+
// rows from files outside the certified set (the suite's own lib tests, or
|
|
246
|
+
// a filtered run), and those are not evidence about the host.
|
|
247
|
+
if (file === null || emitted.has(e.requirementId) || !reportStates.has(file))
|
|
248
|
+
continue;
|
|
249
|
+
emitted.add(e.requirementId);
|
|
250
|
+
rows.push({
|
|
251
|
+
requirementId: e.requirementId,
|
|
252
|
+
scenarioId: file,
|
|
253
|
+
disposition: e.disposition,
|
|
254
|
+
...(e.detail === undefined ? {} : { detail: e.detail }),
|
|
255
|
+
...(e.assertionCount === undefined ? {} : { assertionCount: e.assertionCount }),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
236
258
|
const totals = { executedPass: 0, executedFail: 0, skipped: 0, inapplicable: 0, blocked: 0 };
|
|
237
259
|
for (const r of rows) {
|
|
238
260
|
if (r.disposition === 'executed-pass')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.153.0",
|
|
4
4
|
"description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"fixtures",
|
|
19
19
|
"fixtures.md",
|
|
20
20
|
"coverage.md",
|
|
21
|
+
"requirements.json",
|
|
22
|
+
"requirement-aliases.json",
|
|
21
23
|
"api",
|
|
22
24
|
"schemas",
|
|
23
25
|
"vectors",
|