Skip to content

Seed datasets have no locale axis — an app shipping demo data for two language markets has to select seeds at config-assembly time, which bakes the choice into dist and leaves the other locale's rows behind #16510

Closed

Description

@hotlong

Symptom

SeedSchema can gate a dataset on environment but not on locale. An app that wants to ship
a Chinese demo dataset and an English one — the same records, different display strings — has no
declarative way to say which applies, so the selection has to happen in application code before the
platform ever sees the seed.

The schema is strictObject, so an app cannot add the key itself:

  • packages/spec/src/data/seed.zod.ts:33 — SeedSchema = lazySchema(() => strictObject({…}));
    fields are object / externalId / mode / env / records plus the package-provenance keys.
    The alias table maps environment and environments onto env; there is no locale spelling.
  • packages/spec/src/data/seed-loader.zod.ts — SeedLoaderConfigSchema carries env,
    organizationId and identity, but no locale.
  • packages/metadata-protocol/src/seed-loader.ts:2486 — filterByEnv(datasets, env) is the only
    dataset-level filter; :490 is its single call site.

Verified against the source tree at c383352, and against the published @objectstack/spec@17.2.0
d.ts as installed in a consuming app. A repo-wide grep for locale across the seed sources returns
one unrelated hit (a pinyin-search comment in examples/app-showcase/src/data/seed/index.ts:111).

Why this lands on the platform

Per references/app-platform-boundary.md, third row of the deciding table: the subject is
platform behaviour and the cost lands on the app
— the missing filter axis is the loader's, and
the workaround is paid for in every app that needs it.

The second question — if a second app needed this, would it copy the implementation? — is
answered structurally rather than by a second consumer already in hand: any marketplace app
installed into an org whose locale differs from the author's has the same problem, and the person
installing it cannot be expected to know an app-private environment variable exists. One real
consumer today
(see below); the generalisation argument is the marketplace install path, not a
second app already written.

What the app-side workaround costs

objectstack-ai/kpi currently selects between two seed profiles with an app-invented environment
variable read at module-evaluation time:

// src/data/index.ts
export const SEED_PROFILE = (process.env.OS_SEED_PROFILE ?? 'default').trim().toLowerCase();
export const KpiSeedData = SEED_PROFILE === 'software' ? SoftwareSeedData : DefaultSeedData;

Because the choice is made while the config is assembled rather than while seeds are loaded, two
defects follow that the app cannot fix from where it sits:

  1. The selection is cached in the build output. Switching profiles requires deleting dist,
    otherwise the previous profile is still in effect. The app's README has to document this.
  2. The other profile's rows stay in the database. Every profile is upsert and the loader only
    writes, so switching on a non-empty database leaves both datasets resident — two org trees and
    two plans in one database. The app's README has to tell operators to use a fresh database file.

Both are consequences of filtering in the wrong layer. A loader-side filter is evaluated at load
time, so the dist caching problem does not arise; and the loader already holds externalId and
the _packageId / _provenance keys, so it is the only layer that could reconcile rows belonging
to a locale that is no longer active.

Expected capability

Add locale to SeedSchema as a filter axis exactly parallel to env, honoured by the loader:

  • SeedSchema.locale?: string[] — omitted means "applies to every locale", matching how env
    defaults to all environments.
  • SeedLoaderConfig.locale?: string — resolved from the runtime's configured locale, overridable
    the way env is.
  • filterByEnv becomes the composition of both axes; a dataset applies when it passes env and
    locale.

Deliberately out of scope: the platform does not translate anything. The app still authors both
record sets. The platform gains only the axis to select between them.

Scope note

The spec field and the loader filter have to land together. A locale key accepted by the schema
but ignored by the loader is a declared-not-enforced surface — the shape this repo routes to the
enforce-or-remove channel — and it would be worse than the current state, because an app would
write the key and silently get both datasets.

Environment

  • @objectstack/spec / @objectstack/runtime / @objectstack/cli 17.2.0
  • Source tree c383352
  • Consuming app: objectstack-ai/kpi (Apache-2.0, public)

Duplicate check

Searched seed data locale language variant demo data per locale i18n seed selection and
seed loader env filter records upsert in this repo. The two locale hits are unrelated — #9135
(auth email locale) and #5419 (console UI language). The control search surfaces real seed-loader
cards (#16488, #14403, #3434), so the negative result on locale is a real absence rather than a
search that matched nothing.

Activity

  1. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    Triage note — filed, not claimed. Reading taken 2026-09-07T06:2xZ.

    File surface (read from the source tree at c383352, not inferred from the title):

    File Change
    packages/spec/src/data/seed.zod.ts:33 add locale to SeedSchema (strictObject, so an app cannot add it)
    packages/spec/src/data/seed-loader.zod.ts add locale to SeedLoaderConfigSchema
    packages/metadata-protocol/src/seed-loader.ts:2486 filterByEnv becomes the composition of both axes; single call site at :490

    Cross-domain, deliberately not split. The surface spans packages/spec (domain:spec) and
    packages/metadata-protocol (domain:engine). It should stay one card: a locale key accepted by
    the schema but ignored by the loader is a declared-not-enforced surface, and an app writing the key
    would silently get both datasets — strictly worse than today, where the app at least knows it has to
    select the dataset itself.

    Per the cross-domain exception path this needs the triage seat to designate one lane PM. Noting for
    whoever picks it up: packages/spec belongs to the spec seat under the sole-owner rule, which the
    cross-lane direct-pickup path does not waive.

    Why this was not dispatched by the seat that filed it. Seat readings at the time of filing —
    domain:spec (#6017) green with in-flight dev 3/3 and produce timestamped 2026-09-07T05:20Z, no
    handover brief; domain:engine (#6367) green. Round mutual-exclusion therefore stands down. The
    card is left open, pm:queue, no assignee, which is the state the owning lane consumes.

    Origin. Reported from objectstack-ai/kpi, an Apache-2.0 app on @objectstack/spec@17.2.0,
    where the workaround described in the body is currently in production use. That app carries the
    interim fixture; it will be removed when this lands.


    Generated by Claude Code

  2. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01QhMsYoDpoMGeQLf24eTawP
    Branch: claude/issue-16510-seed-locale-axis
    Worktree: objectstack-issue-16510
    Domain: domain:spec (lead; packages/spec sole-owner rule) + domain:engine (cross-domain exception path, single PR)
    File surface: packages/spec/src/data/, packages/metadata-protocol/src/seed-loader.ts (stop on breach; explain in the report)
    Container & model: M, mode:cloud, model: claude-opus-5 (contract-review tier, forced by Clause-② yes)
    Clause-②: yes
    Serial constraints cleared: #16488 touches the same file (packages/metadata-protocol/src/seed-loader.ts) but a disjoint region — externalIdKey/externalIdLabel diagnostic rendering, not filterByEnv; it is open with no assignee and not dispatched, so no in-flight overlap. No other open card names the seed schema or the dataset filter.

    Clause-② rationale: the card adds a key to SeedSchema, which is a strictObject — a spelling that is rejected today becomes accepted, and the published contract surface widens. Both halves of the Clause-② test, so this goes to the contract-review tier.

    Cross-domain note: dispatched as one card rather than split, per the scope note in the body — a locale key accepted by the schema but ignored by the loader would be a declared-not-enforced surface.

    Maintainer ruling authorising this dispatch: "和你相关的任务你负责派发" (2026-09-07, in session). This supersedes the round mutual-exclusion stand-down recorded in the triage note above.


    Generated by Claude Code

  3. self-assigned this
    on Sep 7, 2026
  4. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    Dispatched — PM loop round 1, 2026-09-07T10:42Z.

    Backend mode:cloud
    Dev session session_013r78utTbiWqxghcuRJxfZf
    Model claude-opus-5 (contract-review tier, forced by Clause-②: yes)
    Outcome branch claude/issue-16510-seed-locale-axis
    Source objectstack-ai/objectstack (push authorisation at birth via source_url)

    The dispatch order carries: the os-dev definition as the production-side contract (dev instructed to stop and ask if .claude/agents/os-dev.md is absent rather than improvise); the established file surface with stop-on-breach; the Clause-② consequences (changeset bump level for a new optional field on a published schema, strictness ledger, contract test, the two seed docs pages); the test shape to mirror (packages/metadata-protocol/src/seed-loader-env-scope.test.ts, the existing env-axis precedent); and the serial constraint against #16488 with the breach condition spelled out.

    Self-driving clause included: two legal turn endings only — pushed branch + draft PR + report, or a detailed hard blocker. Open design choices are the dev's to settle and record under open_questions rather than stopping to ask. Foreground execution required.

    Explicit non-goals passed to the dev: no translation, no per-locale record merging, and no reconciliation of rows already written under a different locale. That last one is a genuine follow-up — the PM will file it as its own card if the dev's report says it is needed.

    PM will subscribe to the draft PR as soon as it exists.


    Generated by Claude Code

  5. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    os-dev-report

    {
      "issue": 16510,
      "status": "done",
      "branch": "claude/issue-16510-seed-locale-axis",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16592",
      "premise_still_valid": true,
      "summary": "Added `Seed.locale?: string[]` (BCP-47 tags, optional, `.min(1)`, aliases `locales`/`language`/`languages`) and `SeedLoaderConfig.locale?: string`, and made the seed loader compose the two scope axes by conjunction: `filterDatasets` = `filterByLocale(filterByEnv(...))`, replacing the single `filterByEnv` call site. `filterByLocale`/`datasetAllowsLocale`/`isLocaleScopedDataset` mirror the `env` helpers down to the reporting posture. Both halves land together, so the key is never accepted-and-ignored by the loader. The one thing NOT in this PR is the runtime PRODUCER: no first-party call site passes `config.locale` yet (those three `seedLoader.load(request)` sites live in packages/runtime/src/app-plugin.ts, outside the declared file surface), so on the default boot path the axis is inert — signposted by a new `warnOnUnresolvedLocaleScope` that names every locale-scoped dataset it let through, and recorded honestly in the liveness ledger as `experimental` rather than `live`.",
      "tests": "All foreground, heavy runs through scripts/pm/os-verify-lock.sh, slot issue-16510. (1) Closure: `pnpm --filter '@objectstack/metadata-protocol^...' build --concurrency=2` -> VERDICT command-exit 0. (2) Generated: `pnpm --filter @objectstack/spec build && check:generated --fix` -> 3 of 15 stale (skill references, content/docs/references/**, liveness/state-counts.md), all three regenerated, VERDICT command-exit 0. (3) New+precedent tests: 'Test Files 2 passed (2) / Tests 29 passed (29)'. (4) `pnpm --filter @objectstack/spec test && typecheck` -> 'Test Files 460 passed (460) / Tests 12778 passed (12778)', VERDICT command-exit 0. (5) `pnpm --filter @objectstack/metadata-protocol test` -> 'Test Files 170 passed | 2 skipped (172) / Tests 2448 passed | 10 skipped (2458)'; typecheck VERDICT command-exit 0. (6) ABLATION — subject resolves through SOURCE (the test imports './seed-loader.js', a relative specifier inside the package), so no dist leg applies; mutation = early `return true` above the `normalizeLocaleTag(locale)` read in `datasetAllowsLocale`. On-disk proof: marker occurrences 1, HEAD blob 7772349ec06deaa63b19a54a0b7f42011fcbfe5d vs mutated blob 5ea1048d3883b16f98129a8bdb00442c7053aa34. MUTANT leg: mutant-exit=1, 'Tests 8 failed | 8 passed (16)'. RESTORE leg: restored blob 7772349ec06deaa63b19a54a0b7f42011fcbfe5d == HEAD blob, marker absent. RESTORED leg: restored-exit=0, 'Tests 16 passed (16)'. Direction observed was the expected one (turns red), and the 8 that stayed green are the unscoped / no-locale / env-only legs that do not read this predicate. (7) GATES: derived, not guessed — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at d621dec over 14 paths, 103 commands, ALL 103 run. Final: 102 pass, 1 not measurable. Three were red first pass and are fixed in the diff: check:system-context-census (6 problems; my +51 lines in seed-loader.ts rotted 3 anchors cited from content/docs/permissions/system-context.mdx — repaired by the gate's own --fix, now 'OK — 106 elevation read sites in 20 packages across 45 files, all anchored'); check:engine-double-contract (the new test pins 3 doubles the ledger lacked — `--write` reports '756 (file, verb) row(s), 3 added or grown, 0 lost', gate green); and check:skill-examples / check:dual-build-cjs-loads / check:type-check-debt, all three PREREQUISITE NOT MET (exit 3 for two) for want of built output — built the workspace ('72 successful, 72 total') and all three went green, with check:type-check-debt re-run at 8 GB after a 4 GB OOM: '--re-measure: OK — 5 ledger entr(ies) re-measured in 66.6s, 55 raw tsc error(s) total, none above its recorded number'. NOT MEASURED, not read as a pass: `node scripts/check-plugin-teardown-shape.mjs --self-test` refuses on a shallow clone ('cannot read the positive control at 621a487607881c66b2899b7e3477115229a156b4'); `git fetch --deepen 500` did not reach it; it is a checker-health self-test over the checker's own fixtures and my diff touches neither it nor plugin teardown shapes. (8) LINT: the repo-wide run, not a narrowed one — `pnpm lint` (eslint . --no-inline-config) VERDICT command-exit 0 in 66s at HEAD 35929cd3, so no narrowing to declare and no three-part narrowing evidence owed. (9) BYTES: check:nul-bytes green plus a direct control-character grep over every file in the diff — no hits.",
      "mcp_calls": "7 — issue_read get, issue_read get_comments, create_pull_request, issue_write update (labels), pull_request_read get x2 (body/label read-back), add_issue_comment. REST channel was probed first per the contract and is UNAVAILABLE in this container: `curl GET /repos/objectstack-ai/objectstack/issues/16510` returns http=403 'GitHub access is not enabled for this session', so the label write and both read-backs went through MCP. Declaring the channel switch.",
      "open_questions": [
        {
          "question": "Where should the loader get its locale when the host passes none? The env axis resolves itself from NODE_ENV inside `load()` precisely so call site seven cannot reopen framework#4704's hole; the locale axis has no equivalent source in this repo.",
          "options": [
            "A. Leave `config.locale` host-supplied, and make an unresolved locale LOUD — a named warning listing every locale-scoped dataset seeded anyway, plus an `experimental` liveness row that spells out the producer gap.",
            "B. Mint an `OS_I18N_LOCALE` env var and resolve inside the loader, mirroring resolveSeedEnvFromNodeEnv exactly.",
            "C. Have the loader read the stack's i18n config through IMetadataService."
          ],
          "recommendation": "A, and it is what shipped. B mints a declared-and-unset env var, which is the trade the seed loader's own NODE_ENV comment already refused for `OS_SEED_ENV`, and it would put the operator's locale in a different place from the app's `i18n.defaultLocale`. C is not reachable: ManifestSchema carries no i18n, and the loader's only metadata call today is `getObject` — the i18n config lives in the app plugin, not in the metadata store. So the honest producer is the runtime, which is outside this card's file surface (see out_of_scope_findings)."
        },
        {
          "question": "Should a dataset scoped `['zh']` match a runtime locale of `zh-CN`?",
          "options": [
            "A. No — exact tag match, case-insensitive only.",
            "B. Yes — fall back to the primary language subtag."
          ],
          "recommendation": "A, shipped. Case folding is normalization of an identifier that RFC 5646 defines as case-insensitive; primary-subtag fallback is a second matching dialect on our own contract, which Prime Directive #12 forbids. The author's remedy is to list both tags, and the loader's skip log names exactly what it dropped. Pinned by a test in both directions."
        },
        {
          "question": "Liveness status for the new key: `live` or `experimental`?",
          "options": [
            "A. `experimental`, with the consumer cited and the producer gap written into the row.",
            "B. `live`, with the consumer pointer and no `producer` field (which merely joins the #4837 worklist)."
          ],
          "recommendation": "A, shipped. `live` means authoring the property changes runtime behaviour, and on the first-party boot path today it does not. B is precisely the verdict `seed.env` carried before framework#4704 — the specimen the `producer` field was built from — and under-claiming is the safe error under Prime Directive #10. The row says to re-classify to `live` with a `producer` pointer when the runtime wiring lands."
        },
        {
          "question": "Reuse `LocaleSchema` from system/translation.zod, or declare the tag inline in data/seed.zod?",
          "options": [
            "A. Reuse LocaleSchema.",
            "B. Inline `z.string()` to keep the data namespace's dependency closure unchanged."
          ],
          "recommendation": "A, shipped, with its cost declared: the import enlarges the data skill's reference closure, so `gen:skill-refs` added one generated line to skills/objectstack-data/references/_index.md — a governed path under Prime Directive #14. It is a generated register row (+1 line; no SKILL.md touched, published skills total unchanged at 6858 lines), which PD#14's regenerated-register clause covers. One spelling of 'locale' in the spec was worth that over saving a governed path from the file list."
        }
      ],
      "out_of_scope_findings": [
        "PLEASE FILE (required for the axis to work on the default boot path): wire `config.locale` from the runtime's configured locale (`i18n.defaultLocale`) at the three `seedLoader.load(request)` sites in packages/runtime/src/app-plugin.ts and the draft-publish path in packages/metadata-protocol/src/protocol.ts, then flip packages/spec/liveness/seed.json's `locale` row from `experimental` to `live` with a `producer` pointer. This is the STOP-ON-BREACH report the dispatch asked for: the correct completion needs packages/runtime, which is outside the declared file surface (`packages/spec/src/data/` + seed-loader.ts), and app-plugin.ts is a hot 1800-line file no serial-constraint clearance covered, so I did not widen into it.",
        "PLEASE FILE (named in the dispatch as a real follow-up, deliberately not attempted): reconcile rows already written under a locale that is no longer active. The loader only ever writes, so switching markets on a non-empty database leaves both datasets resident — the loader is the only layer that could do it, since it holds externalId and the _packageId / _provenance keys.",
        "noted, not filed: `check-plugin-teardown-shape.mjs --self-test` cannot run on a shallow clone (its positive control is pinned to commit 621a487607881c66b2899b7e3477115229a156b4, unreachable after `git fetch --deepen 500`). Not a defect in the script and not in any contract — it is an environment shape, and CI checks out deep enough. Successor: none — no PR or person is heading into this file.",
        "noted, not filed: content/docs/permissions/system-context.mdx cites seed-loader.ts by LINE, so any edit to that 2,800-line file rots three anchors. The gate catches it and `--fix` repairs it mechanically, which is the system working; symbol anchors would make it not happen at all, but that is a citation-form migration nobody is mid-flight on. Successor: every future PR touching seed-loader.ts, which will hit the same green-after---fix loop."
      ]
    }

    Generated by Claude Code

  6. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    PM review — round 1 complete. Reading taken 2026-09-07T12:05Z on head 35929cd.

    CI green. 38 check runs, all success or skipped, none failed. Governed Surface Queue Guard
    green, No other open PR may claim the same issue green, Check Changeset green.
    mergeable_state has gone blocked → clean.

    Report accepted. Spot-checked rather than taken on trust: the three seedLoader.load(request)
    call sites the report names in packages/runtime/src/app-plugin.ts are real (:1249, :1326,
    :1642, confirmed by grep). The declared file surface held — nothing in the diff touches #16488's
    externalIdKey / externalIdLabel region, so the serial constraint cleared in the claim was
    respected. The one gate the dev could not run (check-plugin-teardown-shape --self-test, refuses on
    a shallow clone) is declared as not-measured rather than counted as a pass, which is the correct
    posture.

    One correction to the report's call-site list, for whoever implements the follow-up: it also names a
    draft-publish path in packages/metadata-protocol/src/protocol.ts. A grep at c383352 found nothing
    there. Re-derive the full list against current main rather than trusting either reading.

    Both requested follow-ups filed:

    Both carry Blocked-by: #16510. #16596 was the one I had initially declined to file for want of a
    decided owner; the report supplied the judgement (the loader is the only layer holding both
    externalId and the provenance keys), so it is filed with the remaining decisions written into it.

    What this card is now waiting on, and it is not the PM. PR #16592 is a draft carrying
    needs:contract-review, and it touches a governed path (skills/objectstack-data/references/_index.md,
    +1 generated line). Under the red lines the dispatching seat does not flip it ready, queue it, arm
    auto-merge, approve it, or merge it — the contract review is another seat's or the maintainer's.
    The needs:contract-review label is the inbox that carries it there; nothing further for me to flip.

    PM keeps the PR watched until it merges or closes.


    Generated by Claude Code

  7. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    Contract review — PASS (director seat, summon #17, CONTRACT_REVIEW_TIER); route is GOVERNED — draft stays, human merge

    Reviewed-by: session_01XesLUWmuhjuRwmU618AZ1M (director seat, GitHub hotlong). Tier fuse: get_session 13:24Z → external_metadata.last_served_model = claude-fable-5-1 = CONTRACT_REVIEW_TIER.
    Implemented-by: mode:cloud dev session session_013r78utTbiWqxghcuRJxfZf (branch claude/issue-16510-seed-locale-axis), dispatched by session_01QhMsYoDpoMGeQLf24eTawP. ⚠️ That claim (5569403849) records model: claude-opus-5 (contract-review tier …) — claude-opus-5 is TIER_DEFAULT, not CONTRACT_REVIEW_TIER; the implementation ran one step below the mandated tier, which is exactly what this review compensates. Different sessions ⇒ independent.
    Carrier: PR #16592 at head 35929cd379c6 — 16 files, +660/−6, read from the fetched head, ⛔ not from the PR body.

    ① Derived judgments — accept set and public face, per change

    change direction verdict
    SeedSchema.locale?: string[] — z.array(LocaleSchema).min(1).optional(), no default, on a strictObject accept set widens: a key refused today as unrecognized_keys is accepted correct — absence is the unrestricted spelling (locales have no enumerable universe to default to, unlike env's closed set of three); an empty array is refused. The feature decision itself is on the manual floor and its provenance is the maintainer's own dispatch on this card (5569403849, 「和你相关的任务你负责派发」)
    aliases locales / language / languages → locale in the alias table not a second accepted spelling: the aliased key is refused with a pointer to locale (pinned: parsed.success false, message names locale) — the same mechanism as environment(s) → env correct under the no-double-spelling rule
    SeedLoaderConfigSchema.locale?: string (LocaleSchema.optional()) loader config widens (non-strict z.object) correct
    loader: filterDatasets = filterByEnv ∧ filterByLocale; tags compared case-folded and otherwise exact (['zh'] does not match zh-CN); permissive when config.locale is absent but warnOnUnresolvedLocaleScope names every locale-scoped dataset it let through and the config key; info log names every dataset dropped additive; nothing changes for a dataset that does not author locale correct — no primary-subtag fallback (that would be the lenient consumer-side fallback Prime Directive #12 forbids); the absent-producer case is loud, so the framework#4704 shape is not repeated
    packages/spec/liveness/seed.json: locale row experimental, consumer cited, producer gap spelled out; authorable-surface/data.json gains both keys ledger honest about a consumer-closed / producer-open key correct — follow-ups #16595 (wire config.locale at the runtime call sites, flip the row to live) and #16596 (reconcile rows under an inactive locale) exist with Blocked-by: #16510
    generated: content/docs/references/** (3), skills/objectstack-data/references/_index.md (+1), engine-double-contract.pinned.json (+15), system-context.mdx anchors regenerated registers correct

    Reviewer note, not blocking: LocaleSchema is a bare z.string() (system/translation.zod.ts:12), so locale: [''] passes the schema. That is the pre-existing shape of LocaleSchema, outside this PR; an empty tag can never match a supplied config.locale, and the skip log names it.

    ② Semver

    @objectstack/spec minor · @objectstack/metadata-protocol minor — matches: a published schema and a loader config gain optional keys; behaviour is additive.

    ③ Boundary flags

    Four open_questions, each answered A by the dev; this seat concurs on all four: host-supplied locale made loud rather than a minted env var; exact tag match; experimental not live; reuse LocaleSchema at the cost of one generated register line. The PM review (5570347106) already corrected the protocol.ts call-site claim — nothing further.

    Route — GOVERNED, so PASS does not open the queue

    node scripts/pm/check-governed-merges.mjs --test over the 16 paths: GOVERNED — 1 of 16 (skills/objectstack-data/references/_index.md, skills/**); the generated-surface exception (#11705) did not lift it — the generator declared no output set, fail closed. ⇒ Prime Directive #14: the PR stays draft, no queue, no auto-merge; a human merge is the review record. This PASS clears the needs:contract-review carrier on the PR (the card never carried it) and the terminal four-step follows on the PR: needs-user-decision label, the final 「维护者速读」 comment, review request / assignment to the authorised accounts. CI on 35929cd: 38 check runs, all success/skipped.


    Generated by Claude Code

  8. hotlong commented on Sep 8, 2026

    @hotlong
    ContributorAuthor

    落地受阻记录 · PR #16592 第二次被踢出合并队列

    按签名四分支的「新签名」要求,在 PR 与本卡各留一份完整签名与初判。完整版在 PR #16592 评论 5577891324,这里留摘要以便本卡可独立追溯。

    签名: 01:48:11Z 以 CI_FAILURE 出队,但无任何测试失败、无任何 job 报 failure。批次 queue/main/pr-16592-be92d468… 的四个 merge_group workflow 三绿(Governed Surface Guard / Spec Liveness Check / Lint & Type Check),CI run 34175875839 为 cancelled——其 17 个 job 中 16 success,唯一非绿是 Build Core(job 101905084839):Build packages (excluding docs) 跑了 34m27s 未完成,其后 12 个 step 全 pending,01:47:53Z 被取消,18 秒后出队。

    对照: 同 workflow、晚 52 秒起跑的 #16573 该 job 8.6 分钟 success;本 PR 同代码在自己 head 上 8.5 分钟 success;本批次其它 job 均正常(Test Core 12.1m、Build Docs 5.6m)。⇒ 非全仓变慢、非本批次变慢、非本卡代码问题,是单 job 停摆。

    初判: 基础设施侧单 job 停摆。⛔ 不自行认定为 flaky、不加台账(只有人工能升级台账),⛔ 不反射式重投(本 PR 第二次被踢;第一次 14:11Z 的 MERGE_CONFLICT 已由返工第 1 轮真实修掉)。

    本卡交付物状态未受影响: PR clean、非草稿、批准有效、head ba564fcc 未动,15 个文件 +659/−5,契约复审 PASS 依然成立。卡的实现没有任何待办——挡住的纯粹是落地环节。

    下游 #16595 / #16596 的 Blocked-by: #16510 继续保持,待本 PR 合并后由我收口。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions