Uh oh!
There was an error while loading. Please reload this page.
docs(tooling): state check-spec-symbol-derivation's export-filter boundary, with the measured population - #6284
Conversation
…ndary in its header Both scanners skip any statement without an `export` modifier, so the header's account of the failure class — read as fully covered by rule 1 plus rule 2 — excluded module-local declarations entirely. objectui#5652 was a specimen neither rule could see: three hand mirrors declared under the spec's own export names, one with an inverted contract arm, with the gate green throughout. Records the measured cost of the filter (objectui#5899, on a76b18c against @objectstack/spec@17.2.0): rule 1 18 -> 47, rule 2 20 -> 22, 30 distinct additional declarations, 22 of them real mirrors and 12 with a divergence measurable today. Comment-only; the filter itself is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
yinlianghui-tw
commented
Aug 25, 2026
PM review — ACCEPT, and the measurement is decision-grade. ⭐ Ruling: A (split), with the reasoning below on the record.Reviewed by the The measurement — exactly what the card said the decision hangs onRule 1: 18 → 47 (+29). Rule 2: 20 → 22 (+2). 30 distinct declarations, every one verified non-exported, baseline a strict subset of relaxed. Classified by reading each site and probing shapes with ⭐ The instrumented-copy method deserves naming: a generator that asserts each anchor is present exactly once and exits 2 on a miss — and it did miss once, surfacing as a loud TypeError rather than a silent green run. That is the difference between an instrument and a hope. Probe archived to scratchpad, deleted before commit, PR is 1 file, comment-only proven mechanically (+73/−0, 0 non-comment lines). Ruling on open question 1 — A, split, adoptedThe evidence forces it, as you argued:
Open question 2 — A governed, correctlyThe card body's own sequencing ("that number decides…") outranks the triage comment's fix-first ordering, and you flagged the conflict instead of silently picking. The concrete vindication: ordering B would have dropped both filters before anyone knew rule 1 and rule 2 have different answers — the split only exists because the measurement came first. Nothing from B's ordering is lost; its disposition step is now fully prepared in this PR's census. The three filed findings — right altitude#6285 (user-visible label drift), #6286 (a hand-copied server cap whose own expiry condition the spec has met), #6287 (a local Also noted
Follow-upThe implementation card for the split is filed as a successor; this PR (header correction) arms independently once CI concludes. ⛔ Not armed yet — CI to conclude first. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Part of #5899
The card's own sequencing: the decision (one-line change / scoped change / documented non-goal) hangs on a number nobody had measured. This PR takes the measurement and lands the header correction the card owes unconditionally. It does not touch the export filter — that is the population change the card explicitly routes through a decision, and the ruling stays with PM/triage.
Premise: still valid
scripts/check-spec-symbol-derivation.mjsskips every non-exported declaration in both scanners. The guard ishasExportModifier, once per scanner — at lines 828 and 924 today (the card said 827/923; the file has drifted one line, so the mechanism is stated by scanner rather than by line number in the header text).The #5652 specimen is fixed and therefore does not appear in the population below:
packages/react/src/spec-bridge/bridges/form-view.tsnow importsFormFieldInput,FormSection,FormViewfrom@objectstack/spec/ui.Also worth naming for whoever rules on the filter: the current behaviour is pinned by a named test —
scripts/__tests__/check-spec-symbol-derivation.test.ts›rule 2 stays green on everything that is not the defect› "an unexported declaration — it publishes no surface to be mistaken for the spec". Any filter change must rewrite that pin, not merely delete a line.The measurement
Method: an instrumented copy of the script (not a mutation of the tracked file), with
hasExportModifierforced true behind an env var, plus a--probe-jsonmode emitting every finding pre-ALLOW with file/line/kind and the real export status. Run twice — relaxed off, relaxed on — and diffed. The copy was archived and deleted before the commit; the working tree carries only the comment diff.Measured on
a76b18cf2(branch base) against@objectstack/spec@17.2.0.Every one of the 31 additional findings was verified non-exported; the baseline set is a strict subset of the relaxed set (no finding moved or disappeared).
With the filter relaxed the gate exits 1, reporting 29 rule-1 sites across 8 packages and 2 rule-2 sites.
The population, classified by reading every site
Classification is
real-mirror(same concept as the spec symbol, hand-restated) /legitimate-local(different concept sharing a name, or already derived) /unclear. Shapes were checked against the spec's own schemas bysafeParseprobe, not by name pattern.Rule 1 — 29 additional
ObjectLikehooks/useTrackRouteAsRecent.ts:30{name, label?}is a strict subset of spec/system'sObjectLike; same "minimal object metadata for a helper" concept, no divergence todayAppLikeutils/appRoute.ts:18name: string; this declaresname?: unknown. Adds_packageId. Relaxes a spec-required keyObjectLikeutils/deriveRelatedLists.ts:97name?optional where the spec requires it; addslist?, aMetadataProvidermerge artifact the spec does not modelSearchResultviews/SearchResultsPage.tsx:36{hits, totalHits, processingTimeMs, facets}; this is one result row{id, label, href, type, description}. "A row is not a response" — the reasoning already in ALLOW forInboxNotificationAdminScopeviews/metadata-admin/PermissionAdvancedFacets.tsx:82AdminScopeSchema(6/6); a local instancesafeParses clean against itRemoteTable.../datasource/DatasourceResourcePage.tsx:126columnCount; this makes it optional and dropsrowCountEstimate. It types the very service response it readsActionParam.../inspectors/ActionDefaultInspector.tsx:266ActionParamSchemakeys (probed against the strict schema); a hand subset plus a[k: string]: unknowncatch-allDashboardWidget.../inspectors/DashboardDefaultInspector.tsx:57= DashboardWidgetSchema & { id: string }, whereDashboardWidgetSchemais imported from@object-ui/types— derived, one hop outside where rule 1 looksField.../inspectors/DashboardWidgetInspector.tsx:548Fieldis a field-type builder. Rule 1 records functions withderived: falseunconditionally — rule 2'sisRendererLikeskip is not applied hereDimension.../inspectors/DatasetDefaultInspector.tsx:105DimensionSchemais the semantic-layer cube dimension: it requiressqland rejects bothfieldanddateGranularity, which are this shape's own keys. Different vocabularies; the spec exports noDatasetSchemato bind againstFlowEdge.../inspectors/FlowEdgeInspector.tsx:49FlowEdgeSchema(7/7), instancesafeParses clean — and its own doc comment says "Mirroring the spec keeps the read side honest (#3202)". #5652's shape verbatimFlowNode.../inspectors/FlowNodeInspector.tsx:60description?, which the spec's.strict()FlowNodeSchemaREJECTS (unrecognized_keys: ["description"]); alsolabel?optional where the spec requires itFlowEdge.../inspectors/FlowNodeInspector.tsx:69condition?: unknownhere vscondition?: ExpressionInputthereField.../inspectors/ObjectDefaultInspector.tsx:329ActionParam.../previews/ActionPreview.tsx:47options/helpText/defaultValuehere, no index signature,label?: string | {en?}vslabel?: unknown)ModelConfig.../previews/AgentPreview.tsx:53ModelConfigSchemais a model registry record (id, version, capabilities, limits, pricing, endpoint, apiKey, …); this is an agent's model selection{provider, model, temperature, maxTokens}FlowNode.../previews/FlowPreview.tsx:57= FlowDesignerNode— a pure alias to the canvas's own type. Its doc comment records that restating the shape is exactly what was removed (#3172/#3202)FlowEdge.../previews/FlowPreview.tsx:58= FlowDesignerEdge, as row 17FlowRuntimeStateviews/studio-design/StudioDesignSurface.tsx:3153enabledandbound; this relaxes both to optional and dropsstatus/triggerType/object— while its own comment says it comes fromGET /automation/_status, the contract's endpointSelectOptionrenderers/basic/metadata-viewer.tsx:114SelectOptionSchemakeys;visibleWhendropped.@object-ui/typesalready carries an ALLOWedSelectOptiondialect this file could useSortDirectionrenderers/complex/data-table.tsx:66'asc' | 'desc' | null— spec/sharedSortDirectionEnumrestated by hand plus a UI-onlynull. Derivable asSpecSortDirection | nullCONTEXT_TOKEN_SUGGESTIONSutils/filter-tokens.ts:80CONTEXT_TOKEN_SUGGESTIONSin@objectstack/spec"). 25 lines above it, the same file explains that its neighbourCONTEXT_TOKENSwas converted to a re-export precisely because "the copy was byte-identical, so every value comparison and every behavioural test passed while it sat here". The strongest specimen in the population, and the only one both rules flagisContextTokenutils/filter-tokens.ts:109isContextToken(token: string): boolean; this is the same predicate narrowed to a TS type guard. The type-predicate return is a real reason the spec's cannot be used verbatim; the membership logic is a copynormalizeFilterOperatorsrc/index.ts:134normalizeFilterOperator(op: unknown): string; this is a second normalizer over a hand alias table returningstring | null. Two normalizers disagreeing about filter operators is the silent-over-fetch class this function's own comment describes (objectstack#3948)RecordAlertPropsrenderers/record-alert.tsx:119RecordAlertPropswhose keys areseverity, title, body, visible, icon, action, dismissible, dismissKey— exactly this interface'sschema.propertieskey set, restated by hand, and again in the flat legacy armEXPLAIN_BATCH_MAX_RECORD_IDShooks/useRecordCrudVerdicts.ts:74@objectstack/spec@17.0.0-rc.6… exports neither the constant nor the request/response types; the pin bump (objectui#4636) supersedes this declaration". The pin is now 17.2.0, and@objectstack/spec/securityexportsEXPLAIN_BATCH_MAX_RECORD_IDS = 200TreeConfigObjectTree.tsx:71TreeConfigSchema(4/4); instancesafeParses cleanUserFilterFieldSchemazod/objectql.zod.ts:216UserFilterFieldSchema'sUserFiltersSchemazod/objectql.zod.ts:263elementdeliberately narrowed to['dropdown','tabs'](ADR-0053 authoring narrowing — the spec still accepts'toggle', confirmed bysafeParse). Documented at the declaration, but a hand mirror, and not in ALLOWRule 2 — 2 additional
CONTEXT_TOKEN_SUGGESTIONSutils/filter-tokens.ts:80CURATED_CAPABILITY_LABELSwidgets/CapabilityMultiSelectField.tsx:82@objectstack/spec/securityPLATFORM_CAPABILITIES.)" The spec has 8 platform capabilities; this handSethas 7 —manage_sharingis missing, so that capability silently loses its localized label. (The dot→underscore spellings are not drift:labelFordoesname.replace(/\./g,'_')before the lookup)Totals — the numbers that decide the card's question
Recommendation (the ruling stays with PM/triage)
Rule 2: a genuine one-line change. Its entire additional population is 2 declarations, both real mirrors, one (
CURATED_CAPABILITY_LABELS) with measured drift against the spec. Dropping the filter inscanFileForClaimscosts zero legitimate-local fallout and needs no ALLOW entries — only the named pin test above rewritten. This is the cheapest real win on the card.Rule 1: a scoped change, not a one-liner and not a non-goal. Dropping the filter there turns the gate red on 29 sites at once, which is the "ALLOW map with dozens of entries is not a guard" bar the header itself sets. But the 8 legitimate-locals are not 8 irreducible waivers — 4 of them fall out of two structural narrowings the script already knows how to make:
isRendererLiketo rule 1 too. It is already written and already used by rule 2, on exactly this judgement ("a component that RENDERS the spec's shape is not a second declaration of it"), and three existing ALLOW entries make the same call. Retires rows 9 and 14. Rule 1 currently records every function withderived: falseunconditionally, which is what drags local components in.type X = SomeLocalTyperestates nothing; it is the change the tree already made in Flow 分支编辑器提交时新建的边没有id—— 这才是「设计器产出、spec 拒绝」的真实形状 #3202. Retires rows 17, 18 and 8.That leaves 4 different-concept name collisions (row 4
SearchResult, row 10Dimension, row 16ModelConfig, and whichever of rows 1/21 triage judges deliberate) wanting one reasoned ALLOW entry each — which is precisely the governance the ALLOW map exists for — plus a--ledger-regenerated DEBT block for the 22 real mirrors, which is the sanctioned shrink-only form for exactly this backlog.So the scoped shape is: narrow rule 1 structurally (2 changes), then drop both filters, seed DEBT mechanically, and write 4 ALLOW reasons. Not "dozens of entries".
Either way, several entries above deserve issues on their own merits regardless of what happens to the filter — row 26 (expired by its own stated condition), row 31 (a capability missing its label), row 12 (a type admitting a key the contract rejects), and the two duplicate pairs. Those are recorded in the report, not fixed here.
What this PR actually changes
Comment-only, one file, 73 insertions, 0 deletions. Mechanically proven:
The header now carries a section stating the export-filter boundary explicitly, citing #5652 as the specimen neither rule could see and #5899 as the decision, with every figure anchored to
a76b18cf2and@objectstack/spec@17.2.0and a note to re-take rather than edit in place — the #6260/#6274 disciplinescripts/invoked-as.mjsalready records for its own measured section.Verification — all on
e333623e0node scripts/check-spec-symbol-derivation.mjsbefore✅ spec symbol derivation: 1304 files scanned against 4959 spec export names; 13 declared dialects, 3 untriaged collisions in 1 packages.node scripts/check-spec-symbol-derivation.mjsafter✅ spec alignment claims: 2 declared deliberate copies, 18 unbacked claims in 5 packages.pnpm exec vitest run scripts/__tests__ --reporter=verboseTest Files 77 passed (77)/Tests 2207 passed (2207)— includingcheck-spec-symbol-derivation.test.tscollected by name, all of its cases greenpnpm type-check:scriptstsc -p tsconfig.scripts.json, no diagnosticspnpm lint:root(unnarrowed)✖ 28 problems (0 errors, 28 warnings)— all pre-existing, none in the edited filepnpm check:control-bytes✅ check-control-bytes: OK (scanned 5178 tracked text file(s); skipped 85 binary).grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'→ no hitspnpm check:entry-guard✓ check:entry-guard: 47 scripts/ file(s) — no entry guard outside the baseline; 0 file(s) still hand-type onepnpm check:skills-paths✅ check-skills-paths: OK (93/94 stated path(s) resolve across 18 guide file(s); 1 baselined).pnpm check:doc-fences✅ check:doc-fences — every TypeScript block in 223 document(s) is fenced ts/tsx/typescript…pnpm check:esm-specifiers,check:node-esm-load,check:shell-escape-residueEvery exit code was captured by redirect before any pipe.
Changeset: none owed, on the presence gate's own verdict, not on my judgement:
There is no
skip-changesetlabel in this repo — confirmed, and not created.Scope fence honoured
The export filter is untouched.
git diffis one file, comment-only, 0 non-comment lines. The instrumented copy used for the measurement never entered the commit.Note for triage: this issue's triage comment reads "Fix: drop/narrow the export-modifier guard … then enumerate and disposition the fallout population", which is the opposite ordering from the card body's own ("that number decides whether this is a one-line change, a scoped change, or a documented non-goal") and from the dispatch. I followed measure-first and am flagging the conflict rather than picking a side silently. The measurement above is what either ordering needed.
Generated by Claude Code