@bongos/core 1.19.159 → 1.19.161
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 +20 -10
- package/modules/ideas/templates.js +137 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/idea_templates.mjs +139 -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.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.161",
|
|
6
|
+
"core_contract": "1.19.161",
|
|
7
|
+
"source_commit": "206359b5cf85472d7b4def6050a2766f001569ea",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-16T16:
|
|
9
|
+
"built_at": "2026-08-16T16:41:02.340Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 305,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1259,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1585,
|
|
20
|
+
"tree_sha256": "67b93c7d90a238fa234b0e809aee4545bf5190f1cd5f86513a294c4adb4244db",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -3479,6 +3479,11 @@
|
|
|
3479
3479
|
"mode": "0000644",
|
|
3480
3480
|
"sha256": "6c57608d1eae4159e967f9b471b20cb4c518c2b4acdf9d5ab88cad2afaf7c197"
|
|
3481
3481
|
},
|
|
3482
|
+
{
|
|
3483
|
+
"path": "modules/ideas/templates.js",
|
|
3484
|
+
"mode": "0000644",
|
|
3485
|
+
"sha256": "d2816db988207f9f2c9305971ebca9bcd87d62e594facaea115d9f6db4b5d9f0"
|
|
3486
|
+
},
|
|
3482
3487
|
{
|
|
3483
3488
|
"path": "modules/lifecycle/CLAUDE.md",
|
|
3484
3489
|
"mode": "0000644",
|
|
@@ -4192,12 +4197,12 @@
|
|
|
4192
4197
|
{
|
|
4193
4198
|
"path": "package-lock.json",
|
|
4194
4199
|
"mode": "0000644",
|
|
4195
|
-
"sha256": "
|
|
4200
|
+
"sha256": "8bd2f7c052801d8b86e46c98ea962ecb4b23164a82ae96282e9789fb64da10c9"
|
|
4196
4201
|
},
|
|
4197
4202
|
{
|
|
4198
4203
|
"path": "package.json",
|
|
4199
4204
|
"mode": "0000644",
|
|
4200
|
-
"sha256": "
|
|
4205
|
+
"sha256": "113553d6b43e63c5c34c77934179cb762626a81c13d63865e02de74c8c361633"
|
|
4201
4206
|
},
|
|
4202
4207
|
{
|
|
4203
4208
|
"path": "public-docs/index.html",
|
|
@@ -5792,7 +5797,7 @@
|
|
|
5792
5797
|
{
|
|
5793
5798
|
"path": "src/module-api.js",
|
|
5794
5799
|
"mode": "0000644",
|
|
5795
|
-
"sha256": "
|
|
5800
|
+
"sha256": "9c0be130573a1045b0a96d8db4b8ede624b304cdb5095c209fbec1c9c0d1248c"
|
|
5796
5801
|
},
|
|
5797
5802
|
{
|
|
5798
5803
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6904,6 +6909,11 @@
|
|
|
6904
6909
|
"mode": "0000644",
|
|
6905
6910
|
"sha256": "07cacc97990f0e775342f8b30171c87128e0acb1abd38d64c683c3f257927ebd"
|
|
6906
6911
|
},
|
|
6912
|
+
{
|
|
6913
|
+
"path": "tests/idea_templates.mjs",
|
|
6914
|
+
"mode": "0000644",
|
|
6915
|
+
"sha256": "3360dcfcccd6ada6375124108a2570d84172977046aa47e1704d4be303ad41f3"
|
|
6916
|
+
},
|
|
6907
6917
|
{
|
|
6908
6918
|
"path": "tests/idea_triage.mjs",
|
|
6909
6919
|
"mode": "0000644",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// modules/ideas/templates.js — the Full Idea template declarations (task 1002917,
|
|
2
|
+
// BV1.R141, goal 1000062).
|
|
3
|
+
//
|
|
4
|
+
// THIS FILE IS WHAT 'base-v1' MEANS. ideas_001_grades.sql stores `field_values`
|
|
5
|
+
// jsonb against a `template_key` and deliberately declares nothing about shape —
|
|
6
|
+
// its header says "R141 declares what 'base-v1' means". This is that declaration:
|
|
7
|
+
// the five base fields, in the owner's own wording from the Holy Workshop Trinity
|
|
8
|
+
// brief, in the order they display.
|
|
9
|
+
//
|
|
10
|
+
// THE TEMPLATE IS DATA, READ BY BOTH VALIDATION AND SCORING. Capture (R143)
|
|
11
|
+
// validates a submitted `field_values` against a template with
|
|
12
|
+
// `validateFieldValues`; the deterministic completeness scorer (R142) walks the
|
|
13
|
+
// same declaration (`fields`, each field's `substanceMin`) instead of hard-coding
|
|
14
|
+
// five names of its own. Swapping the template changes what both accept — no
|
|
15
|
+
// migration, no code change in either consumer. That property is the task's
|
|
16
|
+
// done-when, and tests/idea_templates.mjs proves it by injecting a different
|
|
17
|
+
// registry and watching validation follow it.
|
|
18
|
+
//
|
|
19
|
+
// DISPLAY ORDER COMES FROM HERE, NEVER FROM THE ROW. jsonb does not preserve key
|
|
20
|
+
// order (the schema comment calls this out), so any renderer that iterates
|
|
21
|
+
// Object.keys(field_values) shows the five fields in whatever order Postgres
|
|
22
|
+
// chose. `fieldOrder(template)` is the one legal source of order.
|
|
23
|
+
//
|
|
24
|
+
// THE PER-PROJECT SEAM, NOT THE FEATURE (owner philosophy, recorded on goal
|
|
25
|
+
// 1000062: the best possible simple base, already designed to be altered).
|
|
26
|
+
// Every function takes an optional `registry`, defaulting to the built-in one.
|
|
27
|
+
// A project-defined template (R156, a spike — deliberately NOT built here) will
|
|
28
|
+
// resolve its own registry and pass it through; nothing below cares where a
|
|
29
|
+
// registry came from. No customization UI, no config plumbing — just the seam.
|
|
30
|
+
//
|
|
31
|
+
// Same-module import: DEFAULT_TEMPLATE_KEY and the payload cap live in inbox.js
|
|
32
|
+
// (declared there for the route validators, mirroring the DB constraints) — one
|
|
33
|
+
// source, so this file can never drift from what the store accepts.
|
|
34
|
+
|
|
35
|
+
'use strict';
|
|
36
|
+
|
|
37
|
+
const { DEFAULT_TEMPLATE_KEY, FIELD_VALUES_MAX } = require('./inbox');
|
|
38
|
+
|
|
39
|
+
// The five base fields, verbatim from the owner's brief. `key` is the stable
|
|
40
|
+
// snake_case identity `field_values` is stored under (renaming a label is free;
|
|
41
|
+
// renaming a key is a data migration — that is why they differ). `substanceMin`
|
|
42
|
+
// is the character floor the R142 scorer treats as "this field was actually
|
|
43
|
+
// answered, not padded past" — declared here as data so the scorer stays
|
|
44
|
+
// template-agnostic; validation deliberately does NOT enforce it (a thin answer
|
|
45
|
+
// is a scoring problem, not a capture error — scoring never blocks capture).
|
|
46
|
+
const BASE_TEMPLATE = Object.freeze({
|
|
47
|
+
key: 'base-v1',
|
|
48
|
+
label: 'Full Idea',
|
|
49
|
+
fields: Object.freeze([
|
|
50
|
+
Object.freeze({ key: 'purpose', label: 'Purpose of Idea', required: true, substanceMin: 40 }),
|
|
51
|
+
Object.freeze({ key: 'big_picture', label: 'What does the Big Picture look like', required: true, substanceMin: 40 }),
|
|
52
|
+
Object.freeze({ key: 'why_had', label: 'Why did you have the Idea', required: true, substanceMin: 40 }),
|
|
53
|
+
Object.freeze({ key: 'why_matters', label: 'Why does the Idea Matter', required: true, substanceMin: 40 }),
|
|
54
|
+
Object.freeze({ key: 'fit', label: 'How does the Idea Fit into the big picture of the project', required: true, substanceMin: 40 }),
|
|
55
|
+
]),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// The built-in registry. One entry today; a second built-in template would be
|
|
59
|
+
// added here. Frozen so a consumer can never mutate the base declaration into
|
|
60
|
+
// a de-facto fork — a different shape is a different registry (the seam above).
|
|
61
|
+
const BUILTIN_TEMPLATES = Object.freeze({
|
|
62
|
+
[BASE_TEMPLATE.key]: BASE_TEMPLATE,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Sanity pin, not a runtime branch: the store's default key and this file's
|
|
66
|
+
// base declaration are two statements of the same fact, and requiring them to
|
|
67
|
+
// agree at load time turns a rename-drift into a boot failure instead of a
|
|
68
|
+
// silent "every new row validates against a template that does not exist".
|
|
69
|
+
if (BASE_TEMPLATE.key !== DEFAULT_TEMPLATE_KEY) {
|
|
70
|
+
throw new Error(`templates: BASE_TEMPLATE.key (${BASE_TEMPLATE.key}) !== inbox DEFAULT_TEMPLATE_KEY (${DEFAULT_TEMPLATE_KEY})`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Resolve a template by key. Null for an unknown key — the caller decides
|
|
74
|
+
// whether that is a 400 (capture) or a render-as-raw fallback (display).
|
|
75
|
+
function templateFor(key, registry = BUILTIN_TEMPLATES) {
|
|
76
|
+
if (typeof key !== 'string' || !key) return null;
|
|
77
|
+
return registry[key] || null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The one legal source of field display order (see header). Returns key order
|
|
81
|
+
// as DECLARED, never as stored.
|
|
82
|
+
function fieldOrder(template) {
|
|
83
|
+
return template.fields.map((f) => f.key);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate a submitted field_values object against a template. PURE — no DB, no
|
|
87
|
+
// clock — so capture (R143) can 400 with named fields before any query runs, and
|
|
88
|
+
// the tests exercise every path without a harness.
|
|
89
|
+
//
|
|
90
|
+
// Returns { ok, template, errors } where each error is { field, reason }:
|
|
91
|
+
// unknown_template — templateKey resolves to nothing in the registry
|
|
92
|
+
// not_an_object — field_values is not a plain object
|
|
93
|
+
// unknown_field — a key the template does not declare (refused, never
|
|
94
|
+
// silently stored: an unknown key in jsonb is invisible
|
|
95
|
+
// debt the scorer and renderer would each trip on later)
|
|
96
|
+
// not_a_string — a declared field carrying a non-string value
|
|
97
|
+
// missing_required — a required field absent or blank (whitespace = blank)
|
|
98
|
+
// too_large — serialized payload over the store's byte cap (mirrored
|
|
99
|
+
// from inbox.js so the refusal is a named 400 here rather
|
|
100
|
+
// than a constraint-violation 500 there)
|
|
101
|
+
//
|
|
102
|
+
// Substance (substanceMin) is deliberately NOT validated — thin answers score
|
|
103
|
+
// low (R142); they do not fail capture.
|
|
104
|
+
function validateFieldValues(fieldValues, { templateKey = DEFAULT_TEMPLATE_KEY, registry = BUILTIN_TEMPLATES } = {}) {
|
|
105
|
+
const template = templateFor(templateKey, registry);
|
|
106
|
+
if (!template) return { ok: false, template: null, errors: [{ field: null, reason: 'unknown_template' }] };
|
|
107
|
+
if (!fieldValues || typeof fieldValues !== 'object' || Array.isArray(fieldValues)) {
|
|
108
|
+
return { ok: false, template, errors: [{ field: null, reason: 'not_an_object' }] };
|
|
109
|
+
}
|
|
110
|
+
const errors = [];
|
|
111
|
+
const declared = new Set(fieldOrder(template));
|
|
112
|
+
for (const key of Object.keys(fieldValues)) {
|
|
113
|
+
if (!declared.has(key)) errors.push({ field: key, reason: 'unknown_field' });
|
|
114
|
+
else if (typeof fieldValues[key] !== 'string') errors.push({ field: key, reason: 'not_a_string' });
|
|
115
|
+
}
|
|
116
|
+
for (const f of template.fields) {
|
|
117
|
+
if (!f.required) continue;
|
|
118
|
+
const v = fieldValues[f.key];
|
|
119
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
120
|
+
// a non-string already carries not_a_string above; don't double-report it
|
|
121
|
+
if (typeof v !== 'string' && v !== undefined) continue;
|
|
122
|
+
errors.push({ field: f.key, reason: 'missing_required' });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (errors.length === 0 && Buffer.byteLength(JSON.stringify(fieldValues), 'utf8') > FIELD_VALUES_MAX) {
|
|
126
|
+
errors.push({ field: null, reason: 'too_large' });
|
|
127
|
+
}
|
|
128
|
+
return { ok: errors.length === 0, template, errors };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
BASE_TEMPLATE,
|
|
133
|
+
BUILTIN_TEMPLATES,
|
|
134
|
+
templateFor,
|
|
135
|
+
fieldOrder,
|
|
136
|
+
validateFieldValues,
|
|
137
|
+
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.161",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.161",
|
|
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.19.
|
|
3
|
+
"version": "1.19.161",
|
|
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/src/module-api.js
CHANGED
|
@@ -745,8 +745,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
745
745
|
// landed since 1.19.157 with no explicit bump. run 31958753412. (task 1002620)
|
|
746
746
|
// 1.19.159 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
747
747
|
// landed since 1.19.158 with no explicit bump. run 31958999646. (task 1002620)
|
|
748
|
+
// 1.19.160 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
749
|
+
// landed since 1.19.159 with no explicit bump. run 31959145128. (task 1002620)
|
|
750
|
+
// 1.19.161 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
751
|
+
// landed since 1.19.160 with no explicit bump. run 31959299652. (task 1002620)
|
|
748
752
|
// ---------------------------------------------------------------------------
|
|
749
|
-
const CORE_VERSION = '1.19.
|
|
753
|
+
const CORE_VERSION = '1.19.161'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
750
754
|
|
|
751
755
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
752
756
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// tests/idea_templates.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002917 (BV1.R141, goal 1000062) — the Full Idea base template.
|
|
4
|
+
//
|
|
5
|
+
// The claim these tests protect: THE TEMPLATE IS DATA. Validation follows the
|
|
6
|
+
// declaration, order comes from the declaration, and swapping the registry
|
|
7
|
+
// changes what is accepted with no code change — that is the task's done-when,
|
|
8
|
+
// and the "swap" test at the bottom is the proof (the R156 per-project seam).
|
|
9
|
+
//
|
|
10
|
+
// Pure module — no DB, no doorway, no harness. inbox.js is required transitively
|
|
11
|
+
// (templates.js reads DEFAULT_TEMPLATE_KEY / FIELD_VALUES_MAX from it), and it
|
|
12
|
+
// destructures `pool` off the doorway at require time without querying, so a
|
|
13
|
+
// bare require is safe here the same way tests/idea_grades.mjs relies on.
|
|
14
|
+
//
|
|
15
|
+
// Run: node tests/idea_templates.mjs
|
|
16
|
+
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const T = require('../modules/ideas/templates.js');
|
|
22
|
+
const { DEFAULT_TEMPLATE_KEY } = require('../modules/ideas/inbox.js');
|
|
23
|
+
|
|
24
|
+
let passed = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
function test(name, fn) {
|
|
27
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
28
|
+
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A complete, well-formed base payload — reused across tests.
|
|
32
|
+
const FULL = {
|
|
33
|
+
purpose: 'A concrete purpose, written out.',
|
|
34
|
+
big_picture: 'What the world looks like once this exists.',
|
|
35
|
+
why_had: 'The observation that sparked it.',
|
|
36
|
+
why_matters: 'Why anyone should spend hours on it.',
|
|
37
|
+
fit: 'Where it sits in the project arc.',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ── the declaration ─────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
test('base-v1 declares exactly the five owner fields, in brief order', () => {
|
|
43
|
+
assert.equal(T.BASE_TEMPLATE.key, DEFAULT_TEMPLATE_KEY, 'the base key IS the store default');
|
|
44
|
+
assert.deepEqual(T.fieldOrder(T.BASE_TEMPLATE),
|
|
45
|
+
['purpose', 'big_picture', 'why_had', 'why_matters', 'fit']);
|
|
46
|
+
for (const f of T.BASE_TEMPLATE.fields) {
|
|
47
|
+
assert.ok(f.label && f.required === true, `${f.key} carries a label and is required`);
|
|
48
|
+
assert.ok(Number.isInteger(f.substanceMin) && f.substanceMin > 0,
|
|
49
|
+
`${f.key} declares the substance floor the R142 scorer reads`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('the declaration is frozen — a consumer cannot fork it in place', () => {
|
|
54
|
+
assert.throws(() => { T.BASE_TEMPLATE.fields.push({ key: 'rogue' }); });
|
|
55
|
+
assert.throws(() => { 'use strict'; T.BUILTIN_TEMPLATES['base-v1'] = null; });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('templateFor: base key resolves, unknown/blank/non-string do not', () => {
|
|
59
|
+
assert.equal(T.templateFor('base-v1'), T.BASE_TEMPLATE);
|
|
60
|
+
assert.equal(T.templateFor('no-such-template'), null);
|
|
61
|
+
assert.equal(T.templateFor(''), null);
|
|
62
|
+
assert.equal(T.templateFor(null), null);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ── validation follows the declaration ──────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
test('a complete five-field payload validates', () => {
|
|
68
|
+
const r = T.validateFieldValues(FULL);
|
|
69
|
+
assert.equal(r.ok, true);
|
|
70
|
+
assert.deepEqual(r.errors, []);
|
|
71
|
+
assert.equal(r.template, T.BASE_TEMPLATE);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('an unknown field is refused, never silently stored', () => {
|
|
75
|
+
const r = T.validateFieldValues({ ...FULL, budget: 'not a base field' });
|
|
76
|
+
assert.equal(r.ok, false);
|
|
77
|
+
assert.deepEqual(r.errors, [{ field: 'budget', reason: 'unknown_field' }]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('a missing or blank required field is named, per field', () => {
|
|
81
|
+
const { fit, ...four } = FULL;
|
|
82
|
+
let r = T.validateFieldValues(four);
|
|
83
|
+
assert.deepEqual(r.errors, [{ field: 'fit', reason: 'missing_required' }]);
|
|
84
|
+
r = T.validateFieldValues({ ...FULL, purpose: ' ' });
|
|
85
|
+
assert.deepEqual(r.errors, [{ field: 'purpose', reason: 'missing_required' }]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('a non-string value is refused once, not double-reported', () => {
|
|
89
|
+
const r = T.validateFieldValues({ ...FULL, why_had: 42 });
|
|
90
|
+
assert.deepEqual(r.errors, [{ field: 'why_had', reason: 'not_a_string' }]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('a non-object payload and an unknown template are structured refusals', () => {
|
|
94
|
+
assert.deepEqual(T.validateFieldValues(null).errors, [{ field: null, reason: 'not_an_object' }]);
|
|
95
|
+
assert.deepEqual(T.validateFieldValues([]).errors, [{ field: null, reason: 'not_an_object' }]);
|
|
96
|
+
const r = T.validateFieldValues(FULL, { templateKey: 'nope' });
|
|
97
|
+
assert.deepEqual(r.errors, [{ field: null, reason: 'unknown_template' }]);
|
|
98
|
+
assert.equal(r.template, null);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('a payload over the store byte cap is a named refusal, not a DB 500', () => {
|
|
102
|
+
const r = T.validateFieldValues({ ...FULL, purpose: 'x'.repeat(40000) });
|
|
103
|
+
assert.deepEqual(r.errors, [{ field: null, reason: 'too_large' }]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('substance is scored, not gated: a thin answer still validates', () => {
|
|
107
|
+
const thin = Object.fromEntries(Object.keys(FULL).map((k) => [k, 'ok']));
|
|
108
|
+
assert.equal(T.validateFieldValues(thin).ok, true,
|
|
109
|
+
'scoring never blocks capture — substanceMin is the scorer\'s, not the validator\'s');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── the done-when: swap the registry, behaviour follows (the R156 seam) ─────
|
|
113
|
+
|
|
114
|
+
test('a different registry changes what is accepted — no code change', () => {
|
|
115
|
+
const custom = {
|
|
116
|
+
'econ-v1': {
|
|
117
|
+
key: 'econ-v1',
|
|
118
|
+
label: 'Economics Idea',
|
|
119
|
+
fields: [
|
|
120
|
+
{ key: 'market', label: 'The market', required: true, substanceMin: 20 },
|
|
121
|
+
{ key: 'unit_econ', label: 'Unit economics', required: false, substanceMin: 20 },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const opts = { templateKey: 'econ-v1', registry: custom };
|
|
126
|
+
// the custom shape validates…
|
|
127
|
+
assert.equal(T.validateFieldValues({ market: 'builders paying for modules' }, opts).ok, true);
|
|
128
|
+
// …the BASE fields are now the unknown ones…
|
|
129
|
+
const r = T.validateFieldValues({ purpose: 'base field' }, opts);
|
|
130
|
+
assert.equal(r.ok, false);
|
|
131
|
+
assert.ok(r.errors.some((e) => e.field === 'purpose' && e.reason === 'unknown_field'));
|
|
132
|
+
// …and order comes from the new declaration
|
|
133
|
+
assert.deepEqual(T.fieldOrder(custom['econ-v1']), ['market', 'unit_econ']);
|
|
134
|
+
// the built-in registry is untouched by any of it
|
|
135
|
+
assert.equal(T.validateFieldValues(FULL).ok, true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log(`\nidea_templates: ${passed} passed, ${failed} failed`);
|
|
139
|
+
if (failed > 0) process.exit(1);
|