Skip to content

Commit fd289be

Browse files
Fifth Rule 3 population (function declarations) widened, censused ids stripped, and the gate states its own scan boundary; hot-reload refusal ids stripped (#13298)
* feat(tooling): fifth population — plain function declarations join Rule 3's sink pass, and the gate states its own scan boundary The fifth widening (#13156's A half, adjudicated 2026-08-29): a plain `function` DECLARATION is transparent to the climb exactly when its NAME is consumed from a recognised customer-facing position — the same narrow clause as the fourth population, one declaration form over, never an unconditional crawl of function bodies. Its literals get their own `functionDeclared` bucket with its own blindness floor, so the declaration clause rotting cannot hide behind the arrow members. The C half (shared with the family member card): the gate's output now prints Rule 3's scan boundary — the root, the not-scanned siblings with the deferral pointer, and the recognised sink-shape list — derived from the constants the scan reads, pinned derived-vs-derived in --self-test. At this commit the widened gate is deliberately RED on the real tree (reproduce-first): 4 violations — 2 functionDeclared, 2 message via the new closure into declarations. The strip is the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N * fix(spec): strip internal tracker ids from function-declaration-built refusal prose, re-pin the twins The strip half of the fifth-population adjudication (2026-08-29, Class-1), under the keep-the-ADR-id charter: 17 ids across 8 spec sources — the 15 the broad declaration census finds on current origin/main (materially fewer than the relayed 47/51/14, measured before today's main moved) plus the 2 hoisted ruling consts the widened sink closure newly reaches (SUBMIT_REDIRECT_RULING, FORM_VIEW_FEATURES_RULING). ADR ids, protocol versions, error codes and ruling dates stay; ids that were the whole parenthetical take the parenthetical with them; load-bearing internal references move to adjacent comments. Twins re-pinned red-then-green, never weakened: 42 assertions across 7 spec test files red before this commit, 607/607 green after, each re-pin a customer-resolvable anchor from the NEW text plus negative id pins at the two ruling-const doors. The objectql and service-analytics twins flagged by the id sweep pin sources OUTSIDE the scanned root and are deliberately untouched (evidence for the cross-package revival census). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N * fix(core): strip tracker ids from HotReloadManager's author-facing refusal messages, re-pin the twins The member half of the 2026-08-29 family adjudication: the ruled strip is the plugin-registration refusal's tracker id (the id the #13151-verified twins at hot-reload.test.ts:259,302,310 pin); the sibling id in the same refusal table's watchPatterns entry and in the startWatching() removal notice is stripped in the same stroke under the bounded same-class extension — same defect family, same keep-the-ADR-id remedy, same file, same twin suite, declared in the PR body. ADR-0049, the spec/core versions and the scheduleReload migration call stay as the customer-resolvable anchors. Twins red-then-green, never weakened: 8 assertions red before this commit (10/18 passing), 18/18 after; each id pin re-pinned to a content anchor from the NEW text plus negative id pins at all three doors — mirroring the negative pin the spec-side parse door has carried since its own strip. Repo-wide section-7 sweep for the stripped id set: the only other test literal is an expect failure-label, not a content pin. Root extension of the doc-authoring gate's Rule 3 is deliberately NOT part of this change (deferred by the adjudication; the gate's new boundary output is what keeps that deferral visible). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N * test(spec): re-pin the two compose-stacks predicate twins the id sweep's callee filter missed Found by RUNNING the full mention set rather than trusting the sweep's expect-callee filter: both pins spell the id check inside a .some() predicate, so the nearest enclosing call is w.includes, not expect. Same re-pin discipline — anchored on the warning's own prescription words (COMPOSE_KEY_DISPOSITIONS / 'cannot be composed'), red-then-green (2 red in the consumer batch, 35/35 green after), never weakened. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5b3ff63 commit fd289be

20 files changed

Lines changed: 345 additions & 70 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Customer-facing refusal and warning messages built inside plain `function` declarations (and two hoisted ruling consts) no longer cite internal tracker ids. The teaching stays; customer-resolvable anchors stay — ADR ids, protocol versions, error codes such as `INVALID_FILTER / 400`, and ruling dates — while the `#NNNN` tokens, which resolve to nothing for a refused author, are gone. The doc-authoring gate now recognises function declarations as text sinks (its fifth population, with its own blindness floor) and prints its own scan boundary, so the next boundary move is visible from the gate's output.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/core": patch
3+
---
4+
5+
`HotReloadManager`'s refusal messages — the plugin-registration doors for retired `stateStrategy` values and removed config keys, and the `startWatching()` removal notice — no longer cite internal tracker ids. The prescriptions keep their customer-resolvable anchors (ADR-0049 enforce-or-remove, the `@objectstack/spec` / `@objectstack/core` versions, and the `scheduleReload` migration call); the `#NNNN` tokens, which resolve to nothing for the host author reading the refusal, are gone.

‎packages/core/src/hot-reload.test.ts‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,14 @@ describe('[#12340] stateStrategy refusal', () => {
256256
// doors (parse vs registration) and are deliberately not shared.
257257
const m = caught?.message ?? '';
258258
expect(m).toContain(retired);
259-
expect(m).toContain('#12340');
260-
expect(m).toContain('ADR-0049');
259+
expect(m).toContain('ADR-0049 enforce-or-remove');
261260
expect(m).toContain('were removed');
262261
expect(m).toContain("Use 'memory'");
263262
expect(m).toContain('p'); // locates the offending plugin
263+
// The negative twin (#13179's strip): the prescription anchors on the
264+
// ADR and the version — never on a tracker id the refused author
265+
// cannot resolve. Mirrors the spec-side door's own pin.
266+
expect(m).not.toMatch(/(?<![#&])#\d{3,5}(?![0-9A-Za-z])/);
264267
});
265268
}
266269

@@ -299,15 +302,16 @@ describe('[#12340] stateStrategy refusal', () => {
299302
expect(caught?.code).toBe('VALIDATION_ERROR');
300303
expect(caught?.status).toBe(400);
301304
expect(caught?.message).toContain('distributedConfig');
302-
expect(caught?.message).toContain('#12340');
305+
expect(caught?.message).toContain('ADR-0049 enforce-or-remove');
303306
expect(caught?.message).toContain('nothing ever read it');
307+
expect(caught?.message).not.toMatch(/(?<![#&])#\d{3,5}(?![0-9A-Za-z])/);
304308
});
305309

306310
it('refuses even when hot reload is disabled', () => {
307311
// The door must not depend on `enabled`: a false declaration is false
308312
// whether or not the feature is switched on.
309313
const cfg = { ...configWith('disk'), enabled: false } as HotReloadConfigParsed;
310-
expect(() => mgr.registerPlugin('p', cfg)).toThrow(/#12340/);
314+
expect(() => mgr.registerPlugin('p', cfg)).toThrow(/were removed/);
311315
});
312316

313317
for (const live of ['memory', 'none'] as const) {
@@ -377,18 +381,20 @@ describe('[#12428] startWatching refusal and the watch-handle removal', () => {
377381
// The prescription's load-bearing facts, by CONTENT — this message is the
378382
// whole migration document for whoever hits it.
379383
const m = caught?.message ?? '';
380-
expect(m).toContain('#12428');
381-
expect(m).toContain('ADR-0049');
384+
expect(m).toContain('ADR-0049 enforce-or-remove');
382385
expect(m).toContain('never watched');
383386
expect(m).toContain('scheduleReload');
384387
expect(m).toContain('p'); // locates the offending plugin
388+
// The negative twin (#13179's strip, extended to this door's sibling id):
389+
// anchored on the ADR and the migration call, never on a tracker id.
390+
expect(m).not.toMatch(/(?<![#&])#\d{3,5}(?![0-9A-Za-z])/);
385391
});
386392

387393
it('refuses startWatching for an UNREGISTERED plugin too', () => {
388394
// The old body early-returned when the plugin was unknown or disabled, so
389395
// the lie was conditional. The refusal must not be: the method never
390396
// worked for anyone, in any state.
391-
expect(() => mgr.startWatching('never-registered')).toThrow(/#12428/);
397+
expect(() => mgr.startWatching('never-registered')).toThrow(/never watched/);
392398
});
393399

394400
it('refuses a leftover watchPatterns at registration', () => {
@@ -406,16 +412,17 @@ describe('[#12428] startWatching refusal and the watch-handle removal', () => {
406412
expect(caught?.code).toBe('VALIDATION_ERROR');
407413
expect(caught?.status).toBe(400);
408414
expect(caught?.message).toContain('watchPatterns');
409-
expect(caught?.message).toContain('#12428');
415+
expect(caught?.message).toContain('ADR-0049 enforce-or-remove');
410416
expect(caught?.message).toContain('nothing ever read it');
417+
expect(caught?.message).not.toMatch(/(?<![#&])#\d{3,5}(?![0-9A-Za-z])/);
411418
});
412419

413420
it('refuses watchPatterns even when hot reload is disabled', () => {
414421
// The door must not depend on `enabled` — a false declaration is false
415422
// whether or not the feature is switched on.
416423
expect(() =>
417424
mgr.registerPlugin('p', liveConfig({ enabled: false, watchPatterns: ['a/**'] }))
418-
).toThrow(/#12428/);
425+
).toThrow(/nothing ever read it/);
419426
});
420427

421428
it('still registers a config that does not carry the retired key', () => {

‎packages/core/src/hot-reload.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const HONOURED_STATE_STRATEGIES = ['memory', 'none'] as const;
4545
*/
4646
const RETIRED_STATE_STRATEGY_GUIDANCE =
4747
"'disk' and 'distributed' were removed from HotReloadConfig.stateStrategy in "
48-
+ '@objectstack/spec 18 (#12340, ADR-0049 enforce-or-remove) — neither was ever '
48+
+ '@objectstack/spec 18 (ADR-0049 enforce-or-remove) — neither was ever '
4949
+ "implemented. Both wrote to the same in-memory Map as 'memory' and reported it "
5050
+ 'only at debug level, so a host that asked for durable or cluster-replicated '
5151
+ 'state got process-local memory and no error. '
@@ -111,7 +111,7 @@ const RETIRED_HOT_RELOAD_KEYS: ReadonlyArray<readonly [string, string]> = [
111111
[
112112
'distributedConfig',
113113
"'distributedConfig' was removed from "
114-
+ 'HotReloadConfig in @objectstack/spec 18 (#12340, ADR-0049 '
114+
+ 'HotReloadConfig in @objectstack/spec 18 (ADR-0049 '
115115
+ 'enforce-or-remove) — nothing ever read it. A provider, endpoints, a key '
116116
+ 'prefix, a TTL and a replication factor could all be declared and no '
117117
+ "connection was ever opened. It left with the stateStrategy: 'distributed' "
@@ -121,7 +121,7 @@ const RETIRED_HOT_RELOAD_KEYS: ReadonlyArray<readonly [string, string]> = [
121121
[
122122
'watchPatterns',
123123
"'watchPatterns' was removed from HotReloadConfig in @objectstack/spec 18 "
124-
+ '(#12428, ADR-0049 enforce-or-remove) — nothing ever read it. Its only two '
124+
+ '(ADR-0049 enforce-or-remove) — nothing ever read it. Its only two '
125125
+ 'uses were log lines: no watcher was ever constructed from it, so an author '
126126
+ 'could declare a glob and no file change ever triggered a reload. File '
127127
+ 'watching is the HOST\'s job in this host-driven library. Delete the key, '
@@ -322,7 +322,7 @@ export class HotReloadManager {
322322
startWatching(pluginName: string): never {
323323
throw hotReloadRefusal(
324324
`[HotReload] Plugin '${pluginName}': startWatching() never watched `
325-
+ 'anything and was removed in @objectstack/core 18 (#12428, ADR-0049 '
325+
+ 'anything and was removed in @objectstack/core 18 (ADR-0049 '
326326
+ "enforce-or-remove). It logged 'File watching started' at info level "
327327
+ 'while no watcher was ever constructed, so no file change could ever '
328328
+ 'trigger a reload. File watching is the HOST\'s job in this '

‎packages/spec/src/compose-stacks-key-loss.test.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ describe('#5005 rule 2 — conflicting values throw a prescriptive error', () =>
158158
// ─── Rule 3 — unhandled keys warn ───────────────────────────────────
159159

160160
describe('#5005 rule 3 — a key with no declared rule warns', () => {
161-
it('warns once, names the key and points at #5005', () => {
161+
it('warns once, names the key and carries the declare-a-rule prescription', () => {
162162
// A key the schema does not declare: reaches composeStacks only via
163163
// `strict: false`, which is exactly how a NEW key looks before someone
164164
// remembers to teach the composer about it.
@@ -167,11 +167,13 @@ describe('#5005 rule 3 — a key with no declared rule warns', () => {
167167

168168
const composed = composeStacks([a, b]) as Record<string, unknown>;
169169

170-
// One warning that both names the key AND points at #5005 — not two
171-
// unrelated ones (`defineStack` also warns about undeclared keys).
170+
// One warning that both names the key AND carries the prescription — not
171+
// two unrelated ones (`defineStack` also warns about undeclared keys).
172+
// Anchored on the prescription's own words, never on a tracker id the
173+
// author cannot resolve (#13156's strip).
172174
const warnings = warnSpy.mock.calls.map((c) => String(c[0]));
173175
expect(
174-
warnings.some((w) => w.includes('composeStacks') && w.includes("'futureThing'") && w.includes('#5005')),
176+
warnings.some((w) => w.includes('composeStacks') && w.includes("'futureThing'") && w.includes('COMPOSE_KEY_DISPOSITIONS')),
175177
).toBe(true);
176178
// …and it is composed by the default rule rather than dropped.
177179
expect(composed.futureThing).toEqual({ enabled: true });
@@ -195,7 +197,7 @@ describe('#5005 rule 3 — a key with no declared rule warns', () => {
195197
expect(
196198
warnSpy.mock.calls
197199
.map((c) => String(c[0]))
198-
.some((w) => w.includes('composeStacks') && w.includes("'views'") && w.includes('#5005')),
200+
.some((w) => w.includes('composeStacks') && w.includes("'views'") && w.includes('cannot be composed')),
199201
).toBe(true);
200202
});
201203

‎packages/spec/src/data/date-range-presets.test.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ describe('date-range preset vocabulary (#4614, re-homed by #8793)', () => {
6767
expect(message).toContain('$gte'); // the position it sat in
6868
expect(message).toContain('{30_days_ago}'); // the spelling that works
6969
expect(message).toContain('2026-01-15'); // the ISO alternative
70-
expect(message).toContain('#8793'); // attributable from the error alone
70+
// Attributable from the error alone by the customer-resolvable sentence —
71+
// never by a tracker id (#13156's strip).
72+
expect(message).toContain('Refused at authoring time so the error surfaces where the filter is written.');
73+
expect(message).not.toMatch(/(?<![#&])#\d{3,5}(?![0-9A-Za-z])/);
7174
// A calendar preset prescribes its window pair.
7275
const window = bareDateRangePresetComparandMessage('this_week', '$lt');
7376
expect(window).toContain('{week_start}');

‎packages/spec/src/data/date-range-presets.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,9 @@ export function bareDateRangePresetComparandMessage(
128128
+ `understood by the dashboard date-filter positions (dateRange.defaultRange, a date `
129129
+ `global filter's defaultValue), where the console lowers it to {date-macro} bounds `
130130
+ `before querying. As a bare "${operator}" comparand nothing resolves it: a declared `
131-
+ `datetime/date field refuses the query at the engine (INVALID_FILTER / 400, #8690), `
131+
+ `datetime/date field refuses the query at the engine (INVALID_FILTER / 400), `
132132
+ `and any other column compares the literal string. Write the date-macro window `
133133
+ `instead — e.g. ${window} — or an ISO date such as "2026-01-15". `
134-
+ `Refused at authoring time so the error surfaces where the filter is written (#8793).`
134+
+ `Refused at authoring time so the error surfaces where the filter is written.`
135135
);
136136
}

‎packages/spec/src/data/default-value-shape.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export function defaultValueTokenIssue(
198198
`Field "${name}" (${def.type}): the default ${JSON.stringify(dv)} is the runtime token \`current_user\` — `
199199
+ `resolved by the engine to the acting user's \`sys_user.id\` at insert time — and a \`${def.type}\` field `
200200
+ `cannot hold one${lookupAside}. It is legal only on a \`user\` field or a \`lookup\` with `
201-
+ "`reference: 'sys_user'` (#4560 records what happens when that id lands anywhere else). Use one of "
201+
+ "`reference: 'sys_user'` — anywhere else the resolved id is dead data at best. Use one of "
202202
+ 'those, or write a literal record id.'
203203
);
204204
}

‎packages/spec/src/data/field.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -919,7 +919,7 @@ describe('FieldSchema', () => {
919919
const issue = r.error.issues.find((i) => i.message.includes('last_30_days'));
920920
expect(issue, 'the preset refusal must surface through relatedListFilter').toBeTruthy();
921921
expect(issue!.path).toEqual(['relatedListFilter', 'created_at', '$gte']);
922-
expect(issue!.message).toContain('#8793');
922+
expect(issue!.message).toContain('Refused at authoring time so the error surfaces where the filter is written.');
923923
}
924924
});
925925

‎packages/spec/src/data/filter-comparand-shape.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ function nonListComparandError(
222222
`"${op}" tests membership of a list — write ${shapePreview([value])} for a single value` +
223223
(alternative ? `, or use ${alternative} to compare against it` : '') +
224224
`. Authoring spellings: ${spellings.join(', ')}. The filter was NOT applied, and an ` +
225-
`unapplied filter would have returned the UNFILTERED result set (#5869).`,
225+
`unapplied filter would have returned the UNFILTERED result set.`,
226226
);
227227
}
228228

@@ -247,7 +247,7 @@ function malformedRangeComparandError(
247247
`value array. Received ${describeOperand(value)} (${shapePreview(value)}) at ${path}. ` +
248248
`A range needs exactly two bounds, in order; the authoring spelling that lowers to ` +
249249
`"$between" is "between". The filter was NOT applied, and an unapplied filter would have ` +
250-
`returned the UNFILTERED result set (#5869).`,
250+
`returned the UNFILTERED result set.`,
251251
);
252252
}
253253

0 commit comments

Comments
 (0)