@bongos/core 1.20.22 → 1.20.24
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/.bongos-core.json +23 -13
- package/docs/architecture.md +14 -0
- package/docs/module-api-changelog.md +4 -0
- package/migrations/core_265_module_assessments.sql +97 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/module_assessments_migration.mjs +96 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.20.
|
|
6
|
-
"core_contract": "1.20.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.20.24",
|
|
6
|
+
"core_contract": "1.20.24",
|
|
7
|
+
"source_commit": "64e82c9e53ba033fa0a010b81ce5a1530730df4f",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-30T18:38:12.477Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 558,
|
|
13
13
|
"agent_docs_stubbed": 25,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2643,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3227,
|
|
20
|
+
"tree_sha256": "d2625091be4e2f62dd4ef689d1ae4db2c14ae9fd89b7491059734525b36b140a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2257,7 +2257,7 @@
|
|
|
2257
2257
|
{
|
|
2258
2258
|
"path": "docs/architecture.md",
|
|
2259
2259
|
"mode": "0000644",
|
|
2260
|
-
"sha256": "
|
|
2260
|
+
"sha256": "4149011e96acb427e32a55b0e48026ad0aa5a5ac2d3fa5054da9e5ef0e4f9a3f"
|
|
2261
2261
|
},
|
|
2262
2262
|
{
|
|
2263
2263
|
"path": "docs/branding-contract.md",
|
|
@@ -2782,7 +2782,7 @@
|
|
|
2782
2782
|
{
|
|
2783
2783
|
"path": "docs/module-api-changelog.md",
|
|
2784
2784
|
"mode": "0000644",
|
|
2785
|
-
"sha256": "
|
|
2785
|
+
"sha256": "76bebe410cae7c9eeda4bcc19c45a31595de4d7a45cb1f125cd742be9d0ea97b"
|
|
2786
2786
|
},
|
|
2787
2787
|
{
|
|
2788
2788
|
"path": "docs/modules-contract.md",
|
|
@@ -3959,6 +3959,11 @@
|
|
|
3959
3959
|
"mode": "0000644",
|
|
3960
3960
|
"sha256": "2f578b536ed624b204fc201c0a5e8033a2417c189c9caaa8f2148408f76ae481"
|
|
3961
3961
|
},
|
|
3962
|
+
{
|
|
3963
|
+
"path": "migrations/core_265_module_assessments.sql",
|
|
3964
|
+
"mode": "0000644",
|
|
3965
|
+
"sha256": "ea887c7bcd2863e69121bfcfa36cc62fedc77c5bd5a052e5868e2f7c99cbc103"
|
|
3966
|
+
},
|
|
3962
3967
|
{
|
|
3963
3968
|
"path": "modules/agents/CLAUDE.md",
|
|
3964
3969
|
"mode": "0000644",
|
|
@@ -8907,12 +8912,12 @@
|
|
|
8907
8912
|
{
|
|
8908
8913
|
"path": "package-lock.json",
|
|
8909
8914
|
"mode": "0000644",
|
|
8910
|
-
"sha256": "
|
|
8915
|
+
"sha256": "cc2bd4f2449af9d3c504aa278de3c2cb3601b64d23fd2156b38764b726395217"
|
|
8911
8916
|
},
|
|
8912
8917
|
{
|
|
8913
8918
|
"path": "package.json",
|
|
8914
8919
|
"mode": "0000644",
|
|
8915
|
-
"sha256": "
|
|
8920
|
+
"sha256": "f57e0105f2c5994b176ded81625dbf0dc0e6f9a4b5ca26cf4e2eba6a9215af23"
|
|
8916
8921
|
},
|
|
8917
8922
|
{
|
|
8918
8923
|
"path": "public-docs/index.html",
|
|
@@ -8932,7 +8937,7 @@
|
|
|
8932
8937
|
{
|
|
8933
8938
|
"path": "release-notes.json",
|
|
8934
8939
|
"mode": "0000644",
|
|
8935
|
-
"sha256": "
|
|
8940
|
+
"sha256": "0ff22595a15543713edcced44a6c4cf66150bcedfdc4b242b6e83d9c45418dfa"
|
|
8936
8941
|
},
|
|
8937
8942
|
{
|
|
8938
8943
|
"path": "scripts/bongos-mcp.js",
|
|
@@ -11047,7 +11052,7 @@
|
|
|
11047
11052
|
{
|
|
11048
11053
|
"path": "src/module-api.js",
|
|
11049
11054
|
"mode": "0000644",
|
|
11050
|
-
"sha256": "
|
|
11055
|
+
"sha256": "3482788e77daee0cd538f507f349ab0ac522566ea19600c3d3163101bc7c1abb"
|
|
11051
11056
|
},
|
|
11052
11057
|
{
|
|
11053
11058
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13984,6 +13989,11 @@
|
|
|
13984
13989
|
"mode": "0000644",
|
|
13985
13990
|
"sha256": "9183d78e4d195428afbee8c71746e12db2455f8529ac1223d690a7b8f7ded538"
|
|
13986
13991
|
},
|
|
13992
|
+
{
|
|
13993
|
+
"path": "tests/module_assessments_migration.mjs",
|
|
13994
|
+
"mode": "0000644",
|
|
13995
|
+
"sha256": "bb71efed35265acaf82f475cbe5221c63eda8a0af452fe4154dfe05fcb7c920a"
|
|
13996
|
+
},
|
|
13987
13997
|
{
|
|
13988
13998
|
"path": "tests/module_asset_subdomain.mjs",
|
|
13989
13999
|
"mode": "0000644",
|
package/docs/architecture.md
CHANGED
|
@@ -258,6 +258,20 @@ module_entitlements(id, module_key, holder_kind IN (builder|instance), holder_re
|
|
|
258
258
|
-- keeping the row.
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
+
Module assessments (migration `core_265`, task 1003814 — [ADR 0343](adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md) + [ADR 0347](adr/0347-every-store-module-ships-a-how-to.md) D5; per module VERSION, append-only history, the current row is the newest; never reads price):
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
module_assessment_signals(id, version_id → store_module_versions, part IN (security|tests|
|
|
265
|
+
install|reliability|tester_feedback|docs), outcome IN (scored|passed|failed|
|
|
266
|
+
no_data|pending|not_scored), score 0-100 iff scored, sample_size, reason,
|
|
267
|
+
detail jsonb, measured_at) -- security is pass/fail only (the D2 gate);
|
|
268
|
+
-- "no data yet" is an outcome, never a zero (D3/D5)
|
|
269
|
+
module_assessment_scores(id, version_id, kind IN (computed|override), overall 0-100 | NULL,
|
|
270
|
+
security_passed, is_new (D5 "New" label), formula, signal_ids bigint[],
|
|
271
|
+
override_by, override_reason, computed_at) -- an override names who + why
|
|
272
|
+
-- and is a new row on the computed history, never an edit (D6)
|
|
273
|
+
```
|
|
274
|
+
|
|
261
275
|
Code: `src/bongos/module-entitlements.js` (`grantEntitlement`, `recordAcquired`, `revokeEntitlement`, `checkEntitlement`, `listEntitlements`). Read routes (own-scoped, `requireBuilder`): `GET /store/entitlements`, `GET /store/modules/:key/entitlement`. Install (task 1003785) grants a free module through `POST /store/modules/:key/acquire`; the buy action (area 8) will grant a paid one.
|
|
262
276
|
|
|
263
277
|
Bongos views: `claimable_tasks` (rebuilt in 006 to use SELECT t.*; rebuilt again in 014 so the new `discipline` column flows through), `version_progress` (rebuilt in 006 to count only leaf tasks for forward-compat with task hierarchy), `tasks_session_fit` (new in 006: derives `fits_session_modes[]` and `credits_per_minute`; rebuilt in 014 for the same SELECT t.* refresh), `task_grades_with_context` (new in 016: joins task_grades × tasks × builders × cost_log so the V4 optimizer has quality-per-dollar in one read).
|
|
@@ -2701,5 +2701,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2701
2701
|
landed since 1.20.20 with no explicit bump. run 36735857856. (task 1002620)
|
|
2702
2702
|
1.20.22 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2703
2703
|
landed since 1.20.21 with no explicit bump. run 36743446010. (task 1002620)
|
|
2704
|
+
1.20.23 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2705
|
+
landed since 1.20.22 with no explicit bump. run 36747567551. (task 1002620)
|
|
2706
|
+
1.20.24 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2707
|
+
landed since 1.20.23 with no explicit bump. run 36759918201. (task 1002620)
|
|
2704
2708
|
---------------------------------------------------------------------------
|
|
2705
2709
|
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
-- core_265_module_assessments.sql — what the module assessment produces: each
|
|
2
|
+
-- part's result, the composed overall score, and the history of both.
|
|
3
|
+
--
|
|
4
|
+
-- task 1003814 (goal 1000091, working area 5). ADR 0343 settled the score: a
|
|
5
|
+
-- Security gate, then the plain average of the other parts that have data;
|
|
6
|
+
-- ADR 0347 D5 adds a Docs part that is shown but never averaged in. These two
|
|
7
|
+
-- tables are the storage only — the signals that fill them (tasks 1003791-3,
|
|
8
|
+
-- 1003806, 1004367), the composition (1003794) and the Metic+ override route
|
|
9
|
+
-- (1003796) are their own tasks and write here.
|
|
10
|
+
--
|
|
11
|
+
-- Everything is scoped per module VERSION (store_module_versions.id), not per
|
|
12
|
+
-- module: a new version is a new thing to judge, which is what lets assessment
|
|
13
|
+
-- re-run on publish.
|
|
14
|
+
--
|
|
15
|
+
-- module_assessment_signals one row per measurement of one part of one
|
|
16
|
+
-- version. APPEND-ONLY: a re-run adds a row, never
|
|
17
|
+
-- UPDATEs one, so a part's score can be seen to move.
|
|
18
|
+
-- The CURRENT result of a part is the newest row per
|
|
19
|
+
-- (version, part). `outcome` keeps "no data yet"
|
|
20
|
+
-- distinct from a low score (ADR 0343 D3/D5 — a missing
|
|
21
|
+
-- part is never a zero):
|
|
22
|
+
-- scored a 0-100 score (Tests, Install,
|
|
23
|
+
-- Reliability, Tester feedback, Docs)
|
|
24
|
+
-- passed / the Security gate (D2) — pass/fail,
|
|
25
|
+
-- failed never a number, never averaged
|
|
26
|
+
-- no_data nothing to measure yet (no installs, no
|
|
27
|
+
-- testers, a module that declares no tests
|
|
28
|
+
-- — task 1003791 says that is not a fail)
|
|
29
|
+
-- pending measurement under way ("Docs: pending")
|
|
30
|
+
-- not_scored the measurement could not run
|
|
31
|
+
-- `sample_size` is how many installs / testers / test
|
|
32
|
+
-- cases the number rests on, so two friendly testers
|
|
33
|
+
-- cannot outweigh two hundred real ones (task 1003806).
|
|
34
|
+
-- module_assessment_scores one row per composition of a version's overall
|
|
35
|
+
-- score. APPEND-ONLY history; the CURRENT score is the
|
|
36
|
+
-- newest row per version. `overall` is NULL when no
|
|
37
|
+
-- averaged part has data. `security_passed` NULL means
|
|
38
|
+
-- the gate has not run. `is_new` is ADR 0343 D5's label
|
|
39
|
+
-- (no Install or Reliability data yet). `signal_ids`
|
|
40
|
+
-- names the exact signal rows the number was composed
|
|
41
|
+
-- from, so "why did I score 71?" is answerable from the
|
|
42
|
+
-- row. `kind = 'override'` is a Metic+ correction
|
|
43
|
+
-- (ADR 0343 D6, task 1003796): it must say who and why,
|
|
44
|
+
-- and it is a new row on top of the computed history,
|
|
45
|
+
-- never an edit of it.
|
|
46
|
+
--
|
|
47
|
+
-- Nothing here reads price: free and paid are assessed identically (D6).
|
|
48
|
+
-- Additive only (new tables), so forward-safe. Idempotent: safe to re-run.
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS module_assessment_signals (
|
|
51
|
+
id bigserial PRIMARY KEY,
|
|
52
|
+
version_id bigint NOT NULL REFERENCES store_module_versions(id),
|
|
53
|
+
part text NOT NULL
|
|
54
|
+
CHECK (part IN ('security', 'tests', 'install', 'reliability',
|
|
55
|
+
'tester_feedback', 'docs')),
|
|
56
|
+
outcome text NOT NULL
|
|
57
|
+
CHECK (outcome IN ('scored', 'passed', 'failed', 'no_data',
|
|
58
|
+
'pending', 'not_scored')),
|
|
59
|
+
score smallint CHECK (score BETWEEN 0 AND 100),
|
|
60
|
+
sample_size integer CHECK (sample_size >= 0),
|
|
61
|
+
reason text,
|
|
62
|
+
detail jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
63
|
+
measured_at timestamptz NOT NULL DEFAULT now(),
|
|
64
|
+
-- a number exactly when the outcome is 'scored'
|
|
65
|
+
CHECK ((outcome = 'scored') = (score IS NOT NULL)),
|
|
66
|
+
-- Security is a gate (ADR 0343 D2): pass/fail only; no other part passes or fails
|
|
67
|
+
CHECK ((part = 'security') = (outcome IN ('passed', 'failed'))
|
|
68
|
+
OR outcome IN ('no_data', 'pending', 'not_scored'))
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_module_assessment_signals_current
|
|
72
|
+
ON module_assessment_signals (version_id, part, measured_at DESC, id DESC);
|
|
73
|
+
|
|
74
|
+
CREATE TABLE IF NOT EXISTS module_assessment_scores (
|
|
75
|
+
id bigserial PRIMARY KEY,
|
|
76
|
+
version_id bigint NOT NULL REFERENCES store_module_versions(id),
|
|
77
|
+
kind text NOT NULL DEFAULT 'computed'
|
|
78
|
+
CHECK (kind IN ('computed', 'override')),
|
|
79
|
+
overall smallint CHECK (overall BETWEEN 0 AND 100),
|
|
80
|
+
security_passed boolean,
|
|
81
|
+
is_new boolean NOT NULL,
|
|
82
|
+
formula text NOT NULL,
|
|
83
|
+
signal_ids bigint[] NOT NULL DEFAULT '{}',
|
|
84
|
+
override_by bigint REFERENCES builders(id),
|
|
85
|
+
override_reason text,
|
|
86
|
+
computed_at timestamptz NOT NULL DEFAULT now(),
|
|
87
|
+
-- an override names who made it and why; a computed row names neither
|
|
88
|
+
CHECK ((kind = 'override') = (override_by IS NOT NULL)),
|
|
89
|
+
CHECK ((kind = 'computed' AND override_reason IS NULL)
|
|
90
|
+
OR (kind = 'override' AND length(btrim(coalesce(override_reason, ''))) > 0))
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_module_assessment_scores_current
|
|
94
|
+
ON module_assessment_scores (version_id, computed_at DESC, id DESC);
|
|
95
|
+
|
|
96
|
+
INSERT INTO schema_migrations (version) VALUES ('core_265_module_assessments')
|
|
97
|
+
ON CONFLICT (version) DO NOTHING;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.24",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.24",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.24",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8188,5 +8188,17 @@
|
|
|
8188
8188
|
"id": "1002818",
|
|
8189
8189
|
"text": "When a project invites someone, that person now sees the invitation on their Cloud Bongos projects page, and signing in to the project is what accepts it. Taking an invite back removes it there too. Inviting from the platform'"
|
|
8190
8190
|
}
|
|
8191
|
+
],
|
|
8192
|
+
"1.20.23": [
|
|
8193
|
+
{
|
|
8194
|
+
"id": "1003814",
|
|
8195
|
+
"text": "Module scores now have a place to live: each quality check's result per module version, the overall score, and its full history, with 'no data yet' never counted as zero."
|
|
8196
|
+
}
|
|
8197
|
+
],
|
|
8198
|
+
"1.20.24": [
|
|
8199
|
+
{
|
|
8200
|
+
"id": "1004428",
|
|
8201
|
+
"text": "The plan for the new project-startup experience is agreed and written down. New projects begin with a demo-or-create choice and seven planet questions, and each project picks its own colours and logo. While a project is being"
|
|
8202
|
+
}
|
|
8191
8203
|
]
|
|
8192
8204
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.20.
|
|
78
|
+
const CORE_VERSION = '1.20.24'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// tests/module_assessments_migration.mjs — the module assessment tables (task
|
|
2
|
+
// 1003814, ADR 0343 + ADR 0347 D5). DB-free: the unit gate has no Postgres, so
|
|
3
|
+
// this pins the rules the owner decided directly in the migration text, where a
|
|
4
|
+
// later edit that loosened one would otherwise land silently. The live apply
|
|
5
|
+
// runs in the PG lane (scripts/verify-gds-only-schema.sh) and at deploy.
|
|
6
|
+
//
|
|
7
|
+
// Run: node tests/module_assessments_migration.mjs
|
|
8
|
+
|
|
9
|
+
import { test } from 'node:test';
|
|
10
|
+
import { strict as assert } from 'node:assert';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
const FILE = 'core_265_module_assessments.sql';
|
|
19
|
+
const SQL = readFileSync(path.join(ROOT, 'migrations', FILE), 'utf8');
|
|
20
|
+
// Comments stripped so an assertion can only be satisfied by real DDL.
|
|
21
|
+
const DDL = SQL.replace(/--.*$/gm, '');
|
|
22
|
+
|
|
23
|
+
// The body of one CREATE TABLE, up to its closing ");".
|
|
24
|
+
function table(name) {
|
|
25
|
+
const m = new RegExp(`CREATE TABLE IF NOT EXISTS ${name} \\(([\\s\\S]*?)\\n\\);`).exec(DDL);
|
|
26
|
+
assert.ok(m, `${name} is created`);
|
|
27
|
+
return m[1];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('both tables exist, idempotently', () => {
|
|
31
|
+
table('module_assessment_signals');
|
|
32
|
+
table('module_assessment_scores');
|
|
33
|
+
assert.doesNotMatch(DDL, /CREATE TABLE (?!IF NOT EXISTS)/, 'every CREATE TABLE is IF NOT EXISTS');
|
|
34
|
+
assert.doesNotMatch(DDL, /CREATE INDEX (?!IF NOT EXISTS)/, 'every CREATE INDEX is IF NOT EXISTS');
|
|
35
|
+
assert.match(DDL, /INSERT INTO schema_migrations \(version\) VALUES \('core_265_module_assessments'\)\s+ON CONFLICT \(version\) DO NOTHING;/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('assessment is per module VERSION, not per module', () => {
|
|
39
|
+
for (const t of ['module_assessment_signals', 'module_assessment_scores']) {
|
|
40
|
+
assert.match(table(t), /version_id\s+bigint NOT NULL REFERENCES store_module_versions\(id\)/, t);
|
|
41
|
+
assert.doesNotMatch(table(t), /module_key/, `${t} is not keyed by module`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('the parts are exactly ADR 0343 D1 plus the ADR 0347 Docs part', () => {
|
|
46
|
+
assert.match(table('module_assessment_signals'),
|
|
47
|
+
/CHECK \(part IN \('security', 'tests', 'install', 'reliability',\s+'tester_feedback', 'docs'\)\)/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('"no data yet" is an outcome, never a zero (ADR 0343 D3/D5)', () => {
|
|
51
|
+
const t = table('module_assessment_signals');
|
|
52
|
+
assert.match(t, /'no_data'/);
|
|
53
|
+
assert.match(t, /'pending'/);
|
|
54
|
+
assert.match(t, /'not_scored'/);
|
|
55
|
+
assert.match(t, /score\s+smallint CHECK \(score BETWEEN 0 AND 100\)/);
|
|
56
|
+
assert.match(t, /CHECK \(\(outcome = 'scored'\) = \(score IS NOT NULL\)\)/, 'a number only when scored');
|
|
57
|
+
assert.doesNotMatch(t, /score\s+smallint[^,\n]*NOT NULL/, 'score must be nullable');
|
|
58
|
+
assert.doesNotMatch(t, /score[^,\n]*DEFAULT 0/, 'no default-to-zero');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('security is a pass/fail gate, and nothing else passes or fails (ADR 0343 D2)', () => {
|
|
62
|
+
assert.match(table('module_assessment_signals'),
|
|
63
|
+
/CHECK \(\(part = 'security'\) = \(outcome IN \('passed', 'failed'\)\)\s+OR outcome IN \('no_data', 'pending', 'not_scored'\)\)/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('history is kept: no uniqueness that would force an overwrite, nothing cascades', () => {
|
|
67
|
+
assert.doesNotMatch(DDL, /UNIQUE/, 'a re-run adds a row rather than replacing one');
|
|
68
|
+
assert.doesNotMatch(DDL, /ON DELETE CASCADE/);
|
|
69
|
+
assert.match(DDL, /ON module_assessment_signals \(version_id, part, measured_at DESC, id DESC\)/);
|
|
70
|
+
assert.match(DDL, /ON module_assessment_scores \(version_id, computed_at DESC, id DESC\)/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('an override names who and why; a computed row names neither (ADR 0343 D6)', () => {
|
|
74
|
+
const t = table('module_assessment_scores');
|
|
75
|
+
assert.match(t, /CHECK \(kind IN \('computed', 'override'\)\)/);
|
|
76
|
+
assert.match(t, /CHECK \(\(kind = 'override'\) = \(override_by IS NOT NULL\)\)/);
|
|
77
|
+
assert.match(t, /kind = 'override' AND length\(btrim\(coalesce\(override_reason, ''\)\)\) > 0/);
|
|
78
|
+
assert.match(t, /kind = 'computed' AND override_reason IS NULL/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('a score records what it was composed from, and the New label (ADR 0343 D5)', () => {
|
|
82
|
+
const t = table('module_assessment_scores');
|
|
83
|
+
assert.match(t, /signal_ids\s+bigint\[\] NOT NULL/);
|
|
84
|
+
assert.match(t, /formula\s+text NOT NULL/);
|
|
85
|
+
assert.match(t, /is_new\s+boolean NOT NULL/);
|
|
86
|
+
assert.match(t, /overall\s+smallint CHECK \(overall BETWEEN 0 AND 100\)/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('price is never an input (ADR 0343 D6)', () => {
|
|
90
|
+
assert.doesNotMatch(DDL, /price|credits|cents/i);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('the migration is forward-safe under the lint gate', () => {
|
|
94
|
+
const lint = require('../scripts/gds/lint-migrations.js');
|
|
95
|
+
assert.deepEqual(lint.scanMigrationSql(SQL).unsafe || [], []);
|
|
96
|
+
});
|