Skip to content

PluginConfigValidator can never run: PluginLoader calls its own validatePluginConfig(metadata) with no config, and a plugin factory closes over its config so the kernel never receives it #11982

Description

@os-zhuang

Found while implementing #11637 (the REST server config seam). Filed, not fixed — #11637's declared surface is packages/rest/src/rest-server.ts, and this is a packages/core mechanism with its own gate family.

#11637 offered "declare configSchema on the REST plugin and let the kernel's plugin-config-validator do it" as one of its two candidate fixes. Measuring that candidate is what turned this up: the mechanism cannot run at all, for any plugin, so the candidate was structurally unavailable and #11637 landed the seam-side parse instead.

What was measured

On origin/main @ 7899f5745.

1. The one call site passes no config. packages/core/src/plugin-loader.ts:157-159:

if (metadata.configSchema) {
    this.validatePluginConfig(metadata);   // <- no second argument
}

and :406-419:

private validatePluginConfig(plugin: PluginMetadata, config?: any): void {
    if (!plugin.configSchema) return;
    if (config === undefined) {
         // In loadPlugin, we often don't have the config yet.
         // We skip validation here or valid against empty object if schema allows?
         // For now, let's keep the logging behavior but note it's delegating
         this.logger.debug(`Plugin ${plugin.name} has configuration schema (config validation postponed)`);
         return;
    }
    this.configValidator.validatePluginConfig(plugin, config);
}

config is always undefined here, so the early return always fires. "Postponed" to nothing: git grep validatePluginConfig finds no other caller outside PluginConfigValidator's own unit test.

2. There is nothing for it to postpone to. kernel.use(plugin) (packages/core/src/kernel.ts:198) hands pluginLoader.loadPlugin(plugin) the Plugin object only. A plugin factory captures its config in a closure — createRestApiPlugin(config) is the model — so no config value ever crosses into the kernel, and PluginMetadata carries no field for one. Even a correct validatePluginConfig(metadata, config) call would have nothing to pass.

3. No kernel plugin in the repo declares configSchema. git grep -n "configSchema:" packages -- ':!**/dist/**' ':!**/CHANGELOG.md' returns 20 hits and none is a kernel Plugin: they are automation node-executor JSON schemas (service-automation/src/builtin/*), the datasource driver catalog, plugin-approvals' approval-node descriptor, and spec/test declarations. So PluginConfigValidator — ~200 lines with its own unit test, its own formatZodErrors, validatePartialConfig and getDefaultConfig — has zero live consumers.

Why it matters

PluginMetadata.configSchema reads, at every author's call site, as the declared way to validate a plugin's configuration. It is documented as such (packages/core/ADVANCED_FEATURES.md:304, ADR-0025's "Config" section: "PluginConfigValidator validates plugin config against the plugin's schema"). A plugin author who declares one gets a debug line and no validation — the declared-not-enforced shape, one layer up from the one #11637 closes.

Not prejudged

Three shapes, and the choice is a real call:

Worth measuring before choosing: whether ADR-0025's manifest-driven load path ("configuration" in the plugin manifest) has a live call site that would have a config in hand, since that is the one caller for which the mechanism was designed.


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 26, 2026
  2. os-warren commented on Aug 26, 2026

    @os-warren
    Collaborator

    Claimed and dispatched — as a measurement, with implementation conditional

    • Session: session_01W6HFzyH98W1YaQXhJUJt6o (seat post [PM seat] domain:engine — ⏳ vacant #6367)
    • Branch: claude/issue-11982-plugin-config-validator-measure
    • Comments re-read in the same action as the claim: one, triage's measure-first charter. No prior claim by any other session.

    Triage's fork goes to the dev unchanged and unsoftened, because it decides whether this dispatch produces code at all:

    • Zero live manifest-path caller → the choice reduces to retire-vs-wire on a documented, published mechanism. Deleting a published capability is a maintainer floor. The seat reports the measurement, this card moves to the decision inbox with it attached, ⛔ nothing is implemented.
    • A live caller exists → "fix the call and leave the surface" is a plain repair the dev may land here, with a pin that a declared configSchema actually executes.

    I have told the dev in as many words that returning a measurement and no PR is a successful outcome on this card. A measure-first charter that quietly rewards shipping code is not a measure-first charter, and a dev who feels pressure to produce a diff will find a route to justify one.

    One discipline point carried in explicitly, and I am the reason it is worth spelling out. Triage notes that #11332 already records the manifest configuration block having zero reads of its container — evidence toward "no live caller" — and says it must be re-taken on the current ref, not inherited. That is exactly the rule I broke today on another card: I repeated a #5499 freeze constraint out of a card body without opening the anchor, and it had been dissolved for fifteen days. Only the dev's own re-check caught it. So this brief says it twice: re-measure #11332's reading yourself, and do not take my framing of the fork as the reading of triage's comment either.

    Serial check: lands in packages/core/src/plugin-loader.ts + security/plugin-config-validator.ts. The sibling engine card in flight, #12032, is on health-monitor.ts. No overlap, no hold.


    Generated by Claude Code

  3. os-warren commented on Aug 26, 2026

    @os-warren
    Collaborator

    Measurement returned — branchOfFork: zero-live-caller, no PR, nothing implemented

    Triage's fork sends this card to the decision inbox. No branch was created, no worktree, no code. Every read below was taken with git grep <ref> against origin/main @ 7bd6447f413d36d1f4f0112042de32beefa3e67c (the card's facts were measured on 7899f5745; re-taken, not inherited).

    The one question

    Does ADR-0025's manifest-driven load path (the "configuration" block in the plugin manifest) have any live call site that would hold a config at loadPlugin time?

    Verdict: ZERO. Not merely "the block is unread" — there is no executable path from a manifest to loadPlugin at all, and the shape that would carry a config does not exist in the type.

    Five probes, each with a positive control in the same sweep

    1. Reads of the configuration container — the decisive probe.

    Pattern Hits Note
    \.configuration\b (subject) 0 across packages apps scripts examples, excl. dist/, CHANGELOG.md, liveness/
    \.packaging\b (control) 1 packages/cli/src/commands/plugin/build.ts:126 — manifest.packaging ?? 'bundled'
    \.contributes\b (control) 50 live read at packages/objectql/src/engine.ts:4693-4695
    \.capabilities\b (control) 219 —

    .packaging is the load-bearing control: it is a sibling key of the same ManifestSchema with exactly one reader in the monorepo, found by the same regex over the same pathspec. A scan that surfaces a 1-hit sibling would have surfaced a 1-hit subject.

    The single .configuration hit outside that exclusion is packages/spec/liveness/manifest.json:88 — the ledger recording the zero, not a read of it.

    ⚠️ Reported as inconclusive, not as evidence: the bracket (["configuration"]) and destructure ({ configuration } =) probes returned 0 for the subject and 0 for both controls. No control fired, so that probe establishes nothing on its own. The dot-access probe above is what carries the reading.

    2. There is no manifest→loadPlugin path to hold a config. loadPlugin has exactly one production call site monorepo-wide — packages/core/src/kernel.ts:198, from async use(plugin: Plugin), which is single-arity. The .osplugin artifact format has a build and publish side only: readOspluginManifest consumers are packages/cli/src/commands/plugin/publish.ts:84 and its tests — nothing unpacks, imports, or instantiates one. That matches ADR-0025's own text: "What is missing is the distribution layer."

    3. The nearest thing to a config-driven load passes no config. packages/cli/src/commands/serve.ts:3056-3087 iterates config.plugins: a string is import()ed and imported.default || imported used as-is; a non-init object is wrapped in new AppPlugin(...) (a metadata bundle, not a config). serve.ts's own comment at :3079-3086 states the supported way to supply options is for the app to construct the plugin itself and let the name-supersede rule replace the CLI's instance — i.e. the closure model, restated as the contract.

    4. The type has no field for a config value. PluginMetadata extends Plugin (plugin-loader.ts:41-59) carries configSchema?: z.ZodSchema and no config value field; toPluginMetadata() (:356-366) merely casts the Plugin and defaults version. So even a corrected validatePluginConfig(metadata, config) has nothing in scope to pass — confirming the card's fact 2.

    5. No kernel Plugin declares configSchema — and I closed a gap the original grep had. The card measured configSchema: only; a class-based plugin would write configSchema = MySchema. I ran that form too:

    • configSchema = → 11 hits, all const configSchema = z.object({…}) locals inside plugin-config-validator.test.ts — the mechanism's own unit test.
    • Control for the = form: startupTimeout = → 3, version = → 292, name = → 2349, so the form is findable.

    Across all declaration forms, every real configSchema belongs to a different surface — automation node-executor descriptors (ADR-0018, JSON Schema literals), driver specs/catalog, a manifest extensions fixture. The only PluginMetadata in the entire repo that declares a Zod configSchema is the documentation example at packages/core/ADVANCED_FEATURES.md:304 — whose inline comment reads // Config is validated before init is called. The mechanism's sole consumer is the doc that promises it works.

    (Card fact-3 drift, verdict unchanged: configSchema: is now 27 hits under packages, not the card's 20. None is a kernel Plugin.)

    #11332's reading, re-taken rather than inherited

    Triage flagged this explicitly. #11332 (open, pm:blocked) records manifest.configuration at 0 reads of its container, measured on b9e9227e3. Re-measured independently here on 7bd6447 with my own controls: it holds. No stale premise found — the card's facts and #11332's reading both survive on the current ref.

    Why this is retire-vs-wire, and why I stopped

    PluginConfigValidator is a published capability: packages/core/src/index.ts:28 does export * from './security/index.js', which exports PluginConfigValidator and createPluginConfigValidator from the . entry point of @objectstack/core. It is real, working code (~200 lines: Zod .parse, formatZodErrors, validatePartialConfig, getDefaultConfig) with zero external importers. Documented in ADR-0025 §3.7 and ADVANCED_FEATURES.md:304.

    Per triage's fork and the standing floor — deleting a published capability is a maintainer decision — I implemented nothing. I did not write the retirement "so it's ready", and I did not write the wiring "as a proposal".

    For the decision inbox — the four axes

    A. Wire the config through (add a config field to Plugin/PluginMetadata, pass at loadPlugin).

    • Real business need: no measured pull. Zero plugins declare configSchema; every one of ~40 production kernel.use() calls already passes config as a constructor argument (new SecurityPlugin(appSecurityPluginOptions(config)), createCloudConnectionPlugin({…})) and works.
    • Long-term soundness: it is the only shape that lets a plugin's schema cover its whole config rather than the slice a seam reaches; but it duplicates a working mechanism (constructor args) with a second one.
    • AI-authored safety: a kernel-enforced schema at load time is structurally stronger than per-seam parsing — real upside here.
    • Startup scope: expands a declaration surface with no consumer. Weakest axis.

    B. Retire it (ADR-0049 enforce-or-remove; tombstone configSchema, delete PluginConfigValidator).

    • Real business need: consistent with the measurement — a capability with zero consumers kept alive by a docstring.
    • Long-term soundness: removes a published API and the ADR-0025 §3.7 "Config" answer, which the distribution layer may want when it lands.
    • AI-authored safety: this is the sharpest axis. Today the surface actively misleads — an author (or a code-generating model) reading ADVANCED_FEATURES.md:304 writes configSchema next to // Config is validated before init is called and gets a debug line. Removing the surface removes the false promise; leaving it is the failure mode the platform most wants to avoid.
    • Startup scope: strongest fit — core capabilities first, no sunk-cost exemption.

    C. Fix the call and leave the surface. The measurement removes this option's premise: there is no caller that holds a config, so "pass config where one exists" has no where. It would move the postponement, not close it. Triage gated this branch on a live caller existing; it does not.

    My recommendation: B (retire), with one caveat the maintainer should weigh. Three of four axes favour it, and the misleading-docs axis is the one with a concrete cost today. The caveat is ADR-0025: if the distribution layer is genuinely next, configSchema is the ADR's designated answer for §3.7 Config, and retiring it means re-deciding then. If ADR-0025's distribution layer is not near-term, that argument is sunk cost. Either route, ADVANCED_FEATURES.md:304's // Config is validated before init is called should not survive the decision — it is false on the current ref regardless of which way this goes.

    Verification

    No gates run, no ablations, no changeset — nothing was changed, so there was nothing to verify and no skip-changeset label to apply. Reporting that as nothing rather than dressing it as a green run.

    Out-of-scope finding

    Claim status (rule 2)

    Claim intact and untouched: assignee os-warren, claim comment from session_01W6HFzyH98W1YaQXhJUJt6o. No competing claim from another session. Nothing on this issue was modified by me except this report comment.

    Channel note

    The seat's REST channel to GitHub is unavailable — both api.github.com/search/issues and the repo-scoped .../issues list endpoint return HTTP 403 ("GitHub access is not enabled for this session") with the seat's $GITHUB_TOKEN. My dispatch discipline prefers REST over the MCP GraphQL pool for de-dup searches; with REST closed I spent one narrow MCP search_issues call for the #12587 de-dup check and am declaring the fallback rather than making it silently.


    Generated by Claude Code


    Generated by Claude Code

  4. os-warren commented on Aug 26, 2026

    @os-warren
    Collaborator

    os-dev-report

    (Marker repair: the HTML-comment form of this marker was stripped by the GitHub body sanitizer on the full report above — comment 5425584149 — which would have made it invisible to a scan keyed on that marker. No comment-edit path is available from this seat (REST returns 403), so the machine-readable payload is re-posted here under the literal-text marker instead. The prose report above is the full version; this is the parseable one.)

    {
      "issue": 11982,
      "branch": null,
      "pr": null,
      "status": "measured-no-code",
      "branchOfFork": "zero-live-caller",
      "premise_still_valid": true,
      "measurement": {
        "question": "Does ADR-0025's manifest-driven load path (the 'configuration' block in the plugin manifest) have any live call site that would hold a config at loadPlugin time?",
        "ref": "origin/main @ 7bd6447f413d36d1f4f0112042de32beefa3e67c (card measured 7899f5745; re-taken, not inherited)",
        "method": "Five independent probes via git grep against the ref (no worktree, read-only). Each zero-claim paired with a positive control in the SAME sweep over an IDENTICAL pathspec. Both declaration forms tested (object-literal 'name:' and class-property 'name ='), closing a gap in the card's original grep.",
        "controls": [
          "SUBJECT .configuration = 0 hits; CONTROL .packaging = 1 hit (cli/src/commands/plugin/build.ts:126) - a sibling key of the SAME ManifestSchema with exactly one reader, so a 1-hit subject would have surfaced",
          "CONTROL .contributes = 50 hits, live read at objectql/src/engine.ts:4693-4695",
          "CONTROL .capabilities = 219 hits",
          "'=' form controls: startupTimeout= 3, version= 292, name= 2349 - proves class-property assignments are findable",
          "PluginMetadata sibling-field controls: startupTimeout live at core/src/kernel.ts:596, healthCheck 104 hits",
          "NEGATIVE/INCONCLUSIVE, reported as such: bracket and destructure probes returned 0 for subject AND 0 for both controls - no control fired, so that probe establishes nothing and is not counted as evidence",
          "METHOD TRAP CAUGHT: an early pathspec 'packages/*/src' matched NOTHING while exiting 0; caught only because the control failed to fire. Rerun with corrected pathspec."
        ],
        "hits": {
          "manifest_configuration_container_reads": 0,
          "loadPlugin_production_call_sites": 1,
          "loadPlugin_call_site": "packages/core/src/kernel.ts:198 via use(plugin: Plugin) - single-arity, no config parameter",
          "osplugin_install_or_load_consumers": 0,
          "osplugin_consumers_found": "build + publish only (readOspluginManifest at cli/src/commands/plugin/publish.ts:84 and tests)",
          "kernel_plugins_declaring_configSchema": 0,
          "configSchema_colon_form_hits": 27,
          "configSchema_equals_form_hits": 11,
          "configSchema_equals_all_in": "packages/core/src/security/plugin-config-validator.test.ts - the mechanism's own unit test locals",
          "only_PluginMetadata_with_zod_configSchema_in_repo": "packages/core/ADVANCED_FEATURES.md:304 - the DOCUMENTATION example, commented '// Config is validated before init is called'"
        },
        "verdict": "ZERO live manifest-path caller. Stronger than 'the block is unread': there is no executable path from a manifest to loadPlugin at all, and PluginMetadata carries no config-value field, so even a corrected two-argument call has nothing in scope to pass. All three card facts re-verified on the current ref."
      },
      "staleReadingsFound": [
        "NONE that change the verdict. #11332's reading (manifest.configuration = 0 reads of container, measured b9e9227e3) RE-TAKEN independently on 7bd6447 with own controls: it HOLDS. Not inherited.",
        "Card fact-3 numeric drift, verdict unchanged: 'configSchema:' is now 27 hits under packages, not the card's 20. None is a kernel Plugin.",
        "Card line numbers from 7899f5745 still accurate on 7bd6447: call site at plugin-loader.ts:158, private method at :406-419.",
        "Card completeness gap (not staleness): the card grepped 'configSchema:' only, which would miss class-property 'configSchema ='. Ran that form too; conclusion survives the stronger probe."
      ],
      "clause2": "Claim intact and untouched. Assignee os-warren, claim comment from session_01W6HFzyH98W1YaQXhJUJt6o. No competing claim from any other session. No assignee change, no label change, no PR. Nothing on this issue modified except my two report comments. No unexpected third-party state observed.",
      "gates": {
        "run": [],
        "reason": "Nothing was changed - no branch, no worktree, no file edits - so there was nothing to verify. dispatch-gates.mjs not run (its input is a changeset; there is none). No changeset and no skip-changeset label, because nothing publishes. Reported as nothing rather than dressed up as a green run."
      },
      "ablations": [],
      "openQuestions": [
        {
          "question": "Retire-vs-wire on PluginConfigValidator / PluginMetadata.configSchema - a documented, PUBLISHED capability (core/src/index.ts:28 re-exports it from the '.' entry point of @objectstack/core). Maintainer floor: deleting a published capability is not a dev call.",
          "options": [
            "A. Wire the config through - add a config field to Plugin/PluginMetadata, pass it at loadPlugin. Real business need: NO measured pull (zero plugins declare configSchema; ~40 production kernel.use() calls already pass config as constructor args and work). Long-term: only shape covering a plugin's WHOLE config, but duplicates a working mechanism. AI-safety: kernel-enforced load-time schema is structurally stronger than per-seam parsing - real upside. Startup scope: expands a declaration surface with no consumer - weakest axis.",
            "B. Retire under ADR-0049 enforce-or-remove - tombstone configSchema, delete PluginConfigValidator. Real need: matches the measurement (zero consumers, kept alive by a docstring). Long-term: removes the ADR-0025 section 3.7 Config answer the distribution layer may want. AI-safety: SHARPEST axis - the surface actively misleads today; an author or code-generating model reading ADVANCED_FEATURES.md:304 writes configSchema beside '// Config is validated before init is called' and gets a debug line. Startup scope: strongest fit, no sunk-cost exemption.",
            "C. Fix the call, leave the surface - premise REMOVED by this measurement: no caller holds a config, so 'pass config where one exists' has no 'where'. Would move the postponement, not close it. Triage gated this branch on a live caller; there is none."
          ],
          "recommendation": "B (retire), with one caveat for the maintainer. Three of four axes favour it and the misleading-docs axis carries a concrete cost today. Caveat: if ADR-0025's distribution layer is genuinely near-term, configSchema is the ADR's designated section 3.7 answer and retiring means re-deciding later; if it is not near-term, that is sunk cost. EITHER WAY: ADVANCED_FEATURES.md:304's '// Config is validated before init is called' is false on the current ref and should not survive the decision."
        }
      ],
      "out_of_scope_findings": [
        "filed as #12587: PluginMetadata.hotReloadable is declared (core/src/plugin-loader.ts:58) and documented (ADVANCED_FEATURES.md:374) with exactly 2 occurrences repo-wide and ZERO reads - HotReloadManager.reloadPlugin() gates only on its own reloadConfigs map, so hotReloadable:false is hot-reloaded identically to true. Control: sibling startupTimeout read live at kernel.ts:596. Unassigned, labels finding + domain:engine, no pm:queue."
      ],
      "channelNote": "The seat's REST channel to GitHub is unavailable: both /search/issues and the repo-scoped /issues list endpoint return HTTP 403 ('GitHub access is not enabled for this session') with the seat's GITHUB_TOKEN. Dispatch discipline prefers REST over the MCP GraphQL pool for de-dup searches; with REST closed, one narrow MCP search_issues call was spent on the #12587 de-dup check. Declared rather than done silently."
    }

    Generated by Claude Code


    Generated by Claude Code

  5. removed their assignment
    on Aug 26, 2026
  6. os-warren commented on Aug 26, 2026

    @os-warren
    Collaborator

    → Decision inbox. Measurement complete, nothing implemented, and the fork is a maintainer floor.

    Triage's charter routed this: zero live manifest-path caller ⇒ retire-vs-wire on a documented, published capability ⇒ not a dev call. The measurement came back zero-live-caller, so the seat reports and stops. pm:dispatched removed, assignee cleared, needs-user-decision applied.

    The verdict is stronger than "the block is unread"

    There is no executable path from a manifest to loadPlugin at all, and PluginMetadata carries configSchema?: z.ZodSchema but no config-value field — so even a corrected two-argument call has nothing in scope to pass. I verified that last part directly on origin/main: the interface declares configSchema, signature, healthCheck, startupTimeout, hotReloadable, and no config value.

    I re-ran the load-bearing control myself, and it holds — twice over

    The control design is the good part of this report. .configuration is compared against .packaging — a sibling key of the same ManifestSchema with exactly one reader monorepo-wide — swept by the same regex over the same pathspec. If the subject had even one reader, that sweep would have surfaced it.

    CONTROL  manifest.packaging   → packages/cli/src/commands/plugin/build.ts:126
                                    const packaging = manifest.packaging ?? 'bundled';
    SUBJECT  manifest.configuration → 0 code readers
    

    And the subject's only hit anywhere is independent corroboration neither of us went looking for — a liveness-ledger note in packages/spec/liveness/manifest.json:88 that already recorded it:

    "Dead by container: manifest.configuration has ZERO reads anywhere in objectstack or objectui … The describe() promises a settings surface the plugin 'exposes to the user via UI/ENV'; nothing renders or resolves it."

    ⚠️ Worth saying plainly: my first attempt at that control matched nothing while exiting 0 — a bad pathspec — and I caught it only because the control failed to fire. That is the same trap the dev caught mid-run and reported, and it is the whole reason a zero-claim without a firing control is not evidence. Two of us hit it on the same probe within the hour.

    What the dev also caught, and reported rather than absorbed


    The ruling needed

    Retire-vs-wire on PluginConfigValidator / PluginMetadata.configSchema — published from the . entry point of @objectstack/core (src/index.ts:28 → ./security/index.js), zero external importers.

    Option C from the card is off the table, and by measurement rather than by preference: "fix the call and leave the surface" assumed a caller that holds a config, and there is none. There is no where for "pass config where one exists".

    A · Wire it through B · Retire (ADR-0049)
    measured pull none — zero plugins declare configSchema; ~40 production kernel.use() calls already pass config as a constructor argument and work matches the measurement: zero consumers, kept alive by a docstring
    long-term the only shape covering a plugin's whole config rather than the slice a seam reaches removes ADR-0025 §3.7's designated Config answer, which a future distribution layer may want
    AI-safety kernel-enforced load-time schema beats per-seam parsing — real upside sharpest axis: the surface misleads today
    startup scope expands a declaration surface with no consumer strongest fit, no sunk-cost exemption

    The dev recommends B, with a caveat I think is the actual decision: if ADR-0025's distribution layer is near-term, configSchema is its designated answer and retiring means re-deciding then; if it is not near-term, that is sunk cost. That is a roadmap question only you can answer, and it is why this is here rather than in a PR.

    ⚠️ One item that is true under either ruling

    packages/core/ADVANCED_FEATURES.md:304 carries the documentation example — a PluginMetadata with a zod configSchema, annotated:

    // Config is validated before init is called
    

    That sentence is false on the current ref, and it is the concrete cost of the AI-safety axis: an author, or a code-generating model reading that file, writes configSchema beside a comment promising validation and receives a debug line. It should not survive the decision in either direction. I am folding it into this card rather than filing a competing one that a ruling would moot — but it needs no roadmap answer, so it can be approved independently of the fork above.

    Adjacent finding, filed separately

    #12587 — PluginMetadata.hotReloadable is declared and documented with zero reads: HotReloadManager.reloadPlugin() gates only on its own reloadConfigs map, so a plugin declaring hotReloadable: false is hot-reloaded identically to true. Control fired (sibling startupTimeout read live at kernel.ts:596). Same interface, same shape, one field over — visible in the block I quoted above.

    No branch, no worktree, no gates run, no changeset — nothing changed, so there was nothing to verify, and it is reported as nothing rather than dressed up as a green run.


    Generated by Claude Code

  7. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    Decision-inbox duty (triage seat, session session_01QpW4ykRUFq1DzigRGgq8oy): standard four-prism block backfill. The measurement and analysis above (comments 5425584149 / 5425623162) are complete but carry the four-axis analysis in English; per the 2026-08-19 ruling new decision analyses are written in Chinese. Content below serializes the existing analysis — no new judgment added, evidence unchanged.

    四棱卡面块(决策待裁)

    背景一句话:PluginConfigValidator / PluginMetadata.configSchema 是 @objectstack/core 主入口发布的能力,实测零消费者、零可执行路径(manifest → loadPlugin 无通路;PluginMetadata 无 config 值字段;唯一声明 configSchema 的 PluginMetadata 是文档示例)。选项 C(修调用留表面)已被测量移除前提。

    选项 A — wire(接线兑现):给 Plugin/PluginMetadata 加 config 字段并在 loadPlugin 传入。
    选项 B — retire(ADR-0049 退役):墓碑 configSchema,删除 PluginConfigValidator。

    • 实际业务需求:A 无实测拉力——零插件声明 configSchema,约 40 处生产 kernel.use() 全部经构造函数传 config 且工作正常;B 与测量一致(零消费者,仅靠一行文档注释维持存活)。
    • 项目长远合理性:A 是唯一能让 schema 覆盖插件全量 config 的形状,但与既有构造函数机制重复造第二条缝;B 移除 ADR-0025 §3.7 指定的 Config 答案——若分发层真在近期路线图上,届时需重新决策。
    • 防 AI 写代码犯错:最尖锐的一轴——ADVANCED_FEATURES.md:304 的示例旁写着 // Config is validated before init is called,该句在当前 ref 上为假:作者或代码生成模型照写 configSchema 以为有校验,实际只得到一行 debug 日志。B 删掉虚假承诺;A 也能兑现但为零用户建新行为面。
    • 创业阶段不扩散需求:A 为无消费者的声明面扩容(最弱一轴);B 最贴合——已发布零消费能力不因沉没成本豁免。

    推荐:B(retire),三轴半指向 B;唯一反向砝码是 ADR-0025 分发层的路线图时点,这只有维护者能答。两向皆须:ADVANCED_FEATURES.md:304 的那句虚假注释不应在任一裁决下幸存。呈报建议:与 #12587(同接口姊妹字段 hotReloadable,同零读者形状,推荐同为 retire)同批裁。


    Generated by Claude Code

  8. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer ruling recorded — Option B: retire configSchema / PluginConfigValidator under ADR-0049

    Provenance: maintainer, 2026-08-27, PM chat decision-inbox batch 5 (adjudication session session_01DKWDdUJ2XNRESVVWUvcpnh), verbatim: 「同意」 — accepting recommendation B, presented paired with #12587 and with the ADR-0025 lever stated explicitly (if the plugin distribution layer were near-term, A was the alternative; the maintainer took B).

    Ruled: retire under ADR-0049 enforce-or-remove — remove PluginMetadata.configSchema, delete PluginConfigValidator / createPluginConfigValidator (~200 lines plus their unit test), and update ADR-0025 §3.7 to record the retirement: re-declaring a kernel-owned config-validation surface is a fresh decision for the day the distribution layer actually lands, with this card's zero-caller measurement as its starting evidence. Either-way item, binding: ADVANCED_FEATURES.md:304's // Config is validated before init is called is false on the current ref and dies in the same PR. TS interface field — no Zod tombstone route, no silent-strip hazard; deletion is visible only as a type error to setters, of which there are none.

    ⛔ C stays measured-out (no caller holds a config); A declined (zero pull; ~40 production plugins pass config via constructors and work).

    Dispatch notes: Clause-②: yes — removes a published capability from @objectstack/core's main entry; contract-review tier. Companion #12587 (same interface, hotReloadable) is ruled B in the same batch — the lane may fold the two into one family dispatch if the five fold gates pass (same shape, same package, both ruled, each independently pinnable, exclusion list stated).

    State: needs-user-decision → pm:queue.


    Generated by Claude Code

  9. self-assigned this
    on Aug 27, 2026
  10. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01LZbWd2jNV1FErXTPSS4Dry
    Branch: claude/issue-11982-retire-plugin-config-schema
    Worktree: objectstack-issue-11982
    Domain: domain:engine
    File surface: packages/core/src/plugin-loader.ts, packages/core/src/security/plugin-config-validator.ts (+ its test), packages/core/src/security/index.ts (export barrel), packages/core/ADVANCED_FEATURES.md, docs/adr/0025-plugin-package-distribution.md (stop on breach; explain in the report)
    Container & model: M family dispatch, mode:subagent, model: claude-fable-5
    Clause-②: yes
    Serial constraints cleared: ⚠️ NOT fully clear — one live overlap, declared rather than assumed. PR #12652 (#11968, ACCEPTED by this seat, awaiting an enqueue blocked on GraphQL rate limit) edits packages/core/src/security/index.ts, the same barrel this retirement must edit to unpublish PluginConfigValidator. Different regions (an ADR-citation prose line vs an export removal), so this is sequenced, ⛔ not blocked: the dev merges origin/main before opening the PR and re-verifies the barrel. No other open PR touches this surface. packages/spec is untouched by this card and stays the spec seat's.

    Chain head of a folded family dispatch: this card + #12587. One branch, one worktree, one changeset, one queue slot; each member keeps its own claim comment, its own Fixes line and its own commit.

    Fold gates — all five measured, ⛔ not asserted

    Gate Reading
    ① same defect shape, same fix Both are PluginMetadata fields that are declared, documented, and read by nothing, both ruled retire under ADR-0049 in the same adjudication batch. ⛔ Not "same keyword" — same mechanism.
    ② same package / region Both live in packages/core, both on the same interface, PluginMetadata.
    ③ every member already ruled Both ruled 2026-08-27 (5434930483, 5434928113-batch). ⛔ Zero unruled members — this gate is the load-bearing one and it is why the fold is legitimate.
    ④ each independently verifiable #11982: configSchema gone + PluginConfigValidator/createPluginConfigValidator deleted + ADR-0025 §3.7 updated + the false comment dead. #12587: hotReloadable gone + HotReloadManager.reloadPlugin no longer implies it. Separate criteria; the batch cannot silently under-deliver.
    ⑤ exclusion list stated ⛔ Do NOT touch these PluginMetadata siblings, which look like family and are not: startupTimeout — measured READ LIVE (kernel.ts 4 hits, kernel.test.ts 8) and it is the positive control for the whole zero-read claim; signature and healthCheck — not ruled, not measured, not in scope.

    The ruling — ⛔ not re-adjudicable

    Maintainer, 2026-08-27, decision-inbox batch 5, verbatim, ⛔ untranslated:

    同意

    accepting recommendation B — retire under ADR-0049, presented paired with #12587 and with the ADR-0025 lever stated explicitly (had the plugin distribution layer been near-term, A was the alternative; the maintainer took B).

    Binding, from the ruling text: remove PluginMetadata.configSchema; delete PluginConfigValidator / createPluginConfigValidator and their unit test; update ADR-0025 §3.7 to record the retirement so re-declaring a kernel-owned config-validation surface is a fresh decision the day the distribution layer lands. Either-way item, binding: ADVANCED_FEATURES.md's // Config is validated before init is called is false on the current ref and dies in the same PR. ⛔ Option C stays measured-out (no caller holds a config). ⛔ Option A declined (zero pull; ~40 production plugins pass config via constructors and work).

    🚨 Governed surface — draft only

    docs/adr/** is a governed surface. ⛔ Never flip ready, never enqueue, never auto-merge. The PR stays draft for the maintainer to hand-merge; hand-merge is the review record. ⚠️ The ADR file is docs/adr/0025-plugin-package-distribution.md — verified by git ls-tree on origin/main; the "ADR-0025-plugin-architecture.md" spelling does not exist and an earlier probe of mine used it wrongly.

    Premises — measured on origin/main at fetch time, with controls

    • validatePluginConfig → 3 hits in packages/core/src/plugin-loader.ts, 6 in the validator's own test. No other caller.
    • hotReloadable → 1 hit in packages/core/src/security-adjacent core sources: the declaration itself.
    • Control fired: startupTimeout → kernel.ts 4, kernel.test.ts 8. The probe can see a live reader, so the zeros above are readings.

    ⛔ Confirm or refute each of these before writing code, rather than inheriting them. They were measured by the PM, not by you, and a premise in a brief carries the same burden as a claim in a review. premise_still_valid: false with no PR is a legitimate and valuable delivery.


    Generated by Claude Code

  11. claude commented on Aug 27, 2026

    @claude
    Contributor

    Dev claim — folded family dispatch, chain head (#11982 + #12587)

    • Session: session_01LZbWd2jNV1FErXTPSS4Dry — dev seat (os-dev subagent, mode:subagent; the PM claim above is the dispatching seat of the same session, so the session ID matches by design — this comment records that a dev ran)
    • Branch: claude/issue-11982-retire-plugin-config-schema (already pushed as write probe)
    • Worktree: objectstack-issue-11982
    • Comments re-read to the last one in the same action as this claim: no competing dev claim from any other session or branch.
    • Executing the recorded ruling (Option B, retire under ADR-0049): remove PluginMetadata.configSchema, delete PluginConfigValidator / createPluginConfigValidator and their unit test, unpublish from packages/core/src/security/index.ts, update ADR-0025 section 3.7, and remove the false ADVANCED_FEATURES.md validation comment. Exclusion list honoured: startupTimeout, signature, healthCheck untouched.
    • PR will be draft and must stay draft (docs/adr/** governed surface — maintainer hand-merge).

    Generated by Claude Code

  12. claude commented on Aug 27, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 11982,
      "family": [11982, 12587],
      "status": "done",
      "branch": "claude/issue-11982-retire-plugin-config-schema",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12689",
      "pr_state": "DRAFT and must stay draft — docs/adr/0025-plugin-package-distribution.md is a governed surface; maintainer hand-merge is the review record. Never flip ready, never enqueue, never arm auto-merge.",
      "premise_still_valid": true,
      "summary": "Both ruled retirements landed on one branch, one commit per card, plus a pin-relocation commit and a PM-directed docs-drift commit: PluginMetadata.configSchema + PluginConfigValidator/createPluginConfigValidator deleted and unpublished with ADR-0025 section 3.7 recording the retirement and the false ADVANCED_FEATURES sentence dead (#11982); PluginMetadata.hotReloadable removed with its doc line (#12587); three hand-written docs pages aligned. Exclusion list (startupTimeout, signature, healthCheck) untouched; packages/spec untouched; content/docs/releases untouched. Every PM premise re-verified at base 4bd6faa2 before editing — the two extra validatePluginConfig hits the brief did not name (packages/rest) are prose comments, not callers.",
      "clause2": "yes — removes a published capability from @objectstack/core's main entry. Claim intact: my dev claim comments (5436310822, 5436312507) sit under the PM's on both cards; no competing claim; nothing on either card modified by me except my comments; no unexpected third-party state on the PR (the appended bare footer after the body PATCH is the documented platform behaviour, not another actor).",
      "files_changed": [
        "packages/core/src/plugin-loader.ts",
        "packages/core/src/security/plugin-config-validator.ts (deleted)",
        "packages/core/src/security/plugin-config-validator.test.ts (deleted)",
        "packages/core/src/security/index.ts",
        "packages/core/ADVANCED_FEATURES.md",
        "docs/adr/0025-plugin-package-distribution.md",
        "packages/core/src/plugin-loader.retired-fields.pin.test.ts (new, runtime pins)",
        "packages/rest/src/plugin-metadata-retired-fields.pin.test.ts (new, compile-time published-surface pins)",
        ".changeset/retire-plugin-metadata-inert-fields.md (new)",
        "content/docs/plugins/anatomy.mdx",
        "content/docs/plugins/index.mdx",
        "content/docs/protocol/kernel/index.mdx"
      ],
      "docs_drift": {
        "edited": ["content/docs/plugins/anatomy.mdx (retired-field example replaced with constructor-owned config)", "content/docs/plugins/index.mdx (Configuration Validation bullet + example half removed; signature kept — live)", "content/docs/protocol/kernel/index.mdx (Configuration Management snippet + fail-fast callout rewritten to the retirement + self-parse seam)"],
        "no_change_with_reason": ["content/docs/automation/flows.mdx — its configSchema is the ADR-0018 node-executor schema, different surface", "content/docs/getting-started/quick-reference.mdx — its PluginMetadata is spec plugin-validator.zod.ts's locally-declared homonym, live", "content/docs/protocol/kernel/plugin-spec.mdx — teaches the surviving self-parse pattern; its phantom manifest configSchema file-map row predates this diff, filed as #12690"],
        "release_owned_read_only": "content/docs/releases/v17.mdx NOT edited. All four configSchema mentions are other surfaces — quoted, line ~532: 'A node's config stays an open record: it is per-node-type, owned by the executor's configSchema and the conversion layer' (ADR-0018); line ~582: 'An earlier version of this note said the driver's own configSchema did that, which was wrong for two releases: the field existed, nothing read it' (driver, historical); ~1949 and ~2242 are flow-designer/ADR-0018. VERDICT: the page is a historical record of shipped, unrelated surfaces; it does not advertise PluginMetadata.configSchema as a v17 capability, so this retirement falsifies nothing there and no separate routing is needed.",
        "blind_spot_sweep": "hand-written docs grepped for PluginConfigValidator / createPluginConfigValidator / ADVANCED_FEATURES / hotReloadable: zero hits outside releases/. Behavioural phrasings (config validat*, validated before init, hot-reload wording): only connector providerConfig (live, unrelated) and the auto-generated references/ tree (spec HotReloadConfig and the already-tombstoned manifest.loading — regenerated surfaces, not hand-edited). The two no-anchor changed files (ADVANCED_FEATURES.md, security/index.ts) were swept by identifier: no hand-written page mirrors either."
      },
      "gates": {
        "derived_by": "node scripts/pm/dispatch-gates.mjs, no paths, twice — first at commit 8f6fb6fc, re-derived after the docs pass at 67da0f09 (stderr names objectstack-ai/objectstack both times); union executed in full at a6c83156, docs families at 67da0f09, ratchets + tests re-run at the FINAL head 973e858b",
        "union_at_a6c83156": "adr-anchors 0 · changeset-gate-self-tests 0 · cross-package-test-inputs 0 · doc-authoring 0 · doc-formula-expressions 0 · kernel-hook-pairs 0 · objectql-double-limit 0 · objectui-changeset 0 · page-declaration-shape 0 · pm-governed-merges 0 · pm-half-states 0 · published-files 0 · slot-lookup 0 · test-source-alias 0 · type-source-resolution 0 · nul-bytes 0 · adr-0087-registration 0 · adr-links 0 · changeset-no-major 0 · ci-filter-parity 0 · comment-mask-adoption 0 · cross-package-test-inputs(ci) 0 · empty-changeset 0 · plugin-teardown-shape 0 · docs-affected 0 · docs-drift-comment 0 · release-rehearsal-self-test 0 · query-options-erasure 0 · type-check-coverage 0 · type-check-debt 0 · engine-double-contract 0 · where-matcher 0 · rest check:test-typecheck 0",
        "docs_families_at_67da0f09": "doc-frontmatter 0 · doc-route-spelling 0 · docs-section-name 0 · doc-security-posture 0 · spec check:docs 0 · spec check:empty-state 0 · spec check:liveness 0 · spec check:skill-examples 0 · spec check:strictness-ledger 0 · spec check:variant-docs 0 · dispatcher-error-vocabulary 0 · doc-anchors 0 · docs-audit-scope 0 · docs-redirects 0 · docs-single-h1 0 · published-readme-links 0 · react-page-adapter-contract 0 · role-word 0 · doc-authoring 0 · doc-formula-expressions 0 · docs-affected 0 · docs-drift-comment 0 · ci-filter-parity 0 · cross-package-test-inputs 0 · nul-bytes 0 (the final merge 0d4a6a85 brought no content/docs changes, so these readings carry to the final head)",
        "ratchets_at_final_973e858b": "type-check-coverage 0 · type-check-debt 0 (own line: 31 ledger entries re-measured, 1687 raw errors, none above recorded) · rest check:test-typecheck 0 (own OK line) · engine-double-contract 0 · where-matcher 0 · query-options-erasure 0 · core vitest 0 (40 files / 999 tests passed)",
        "not_measured": "scripts/pm/check-half-states.mjs exit 3 = PREREQUISITE NOT MET on this seat (container token is a 14-byte proxy placeholder; the tool refuses to sweep anonymously; its own text: the result says NOTHING about the board). NOT green, NOT red — CI runs it with credentials. One earlier batch attempt exited 99 (verify-lock queue-timeout, NOT MEASURED) and was re-run to completion with the kept slot.",
        "exit_code_discipline": "every gate ran via a runner capturing $? immediately, no pipes before capture; verdicts quoted from each gate's own printed line"
      },
      "tests": "Final head 973e858b: pnpm --filter @objectstack/core test = 'Test Files  40 passed (40)' / 'Tests  999 passed (999)'; rest pin file vitest 1 file / 3 tests passed (at 67da0f09; rest untouched by the final merge); rest check:test-typecheck OK line at 973e858b. Dependency closure built first via turbo under os-verify-lock (VERDICT command-exit 0).",
      "ablations": [
        {
          "name": "shipping channel — cross-package compile pin (prediction recorded in ablation-prediction-2.txt BEFORE mutation)",
          "mutation": "re-added 'hotReloadable?: boolean;' to PluginMetadata in packages/core/src/plugin-loader.ts, then REBUILT core dist — the pin reads the built d.ts, so the rebuild is stated and was performed on BOTH legs",
          "on_disk_confirmation": "src anchor grep 0 pre / 1 post; dist preflight grep on dist/index.d.ts 0 pre-build / 1 post-build (mutation leg) and back to 0 after the restore rebuild (restore leg); control startupTimeout stayed 3 in dist on both legs",
          "predicted_direction": "MORE diagnostics: check:test-typecheck RED with exactly 1 error in src/plugin-metadata-retired-fields.pin.test.ts (TS2578 unused directive); configSchema directive stays satisfied",
          "observed": "exactly as predicted — gate exit 1, its own text: 'src/plugin-metadata-retired-fields.pin.test.ts: 1 type error(s) in a file the ledger does not cover'; restore leg: git diff HEAD empty, anchors 0, gate back to its OK line exit 0",
          "trap": "restore + rebuild in trap EXIT INT TERM with absolute paths"
        },
        {
          "name": "first channel (superseded residence, evidence kept) — same-package DEBT-ratchet pin",
          "mutation": "same field re-add, no dist in the loop (tsc reads src); prediction in ablation-prediction.txt",
          "observed": "tsc error count 98 to 99, the one new error TS2578 at the hotReloadable directive; restore proven by git hash-object equal to the HEAD blob (5bacc628...)"
        }
      ],
      "deviations": [
        "Pin residence changed mid-run: check:type-check-coverage refused @ts-expect-error pins in packages/core (no typecheck script — phantom pin; PHANTOM_PIN_DEBT closed to new entries). Compile pins moved to packages/rest whose tsconfig.test.json program is run by its typecheck script and reads core's BUILT d.ts. This adds one test file in packages/rest beyond the claims' declared file surface — declared here per stop-on-breach; no non-test rest file touched.",
        "Four commits + two merge commits instead of strictly one per card: commit 3 is the pin-relocation repair (first two commits were already pushed; force-push forbidden), commit 4 is the PM-directed docs-drift pass. Each card's substance remains its own commit.",
        "ADR-0025 edits go slightly beyond the ruled section 3.7 sentence: three factual inventory mentions of the retired validator and the hotReloadable inventory item were aligned so the ADR does not contradict its own record; forward-looking design prose deliberately kept. Enumerated in the PR body for the hand-merge review.",
        "check:pm-half-states NOT MEASURED on this seat (see gates.not_measured).",
        "Channel: REST repo-scoped probe answered 200 on this seat, so all GitHub reads/writes rode REST + git; the public-page payload channel covered issue bodies but its front timeline page truncated at 28 items (hasNextPage true), so comments were read via REST. De-dup for the two new filings used complete REST enumeration (326 open issues, pages read to exhaustion, positive control fired); PM's dedup readings were used as standing facts, not re-run."
      ],
      "mcp_calls": "0 — no mcp__github__* call was made in this run; every GitHub operation used the container REST channel (probe 200) or git",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12688: packages/core/REFACTORING_SUMMARY.md section 5 claims config validation that never ran and now names the retired mechanism (unassigned, finding + domain:engine)",
        "filed as #12690: content/docs/protocol/kernel/plugin-spec.mdx documents a manifest configSchema file-map key ManifestSchema never declared — pre-existing, not falsified by this diff (unassigned, finding + domain:docs)"
      ]
    }
    

    Generated by Claude Code

  13. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPT — PR #12689 (family: #11982 + #12587)

    Reviewer of record: domain:engine seat, session session_01LZbWd2jNV1FErXTPSS4Dry (R1). Verified against the diff, ⛔ not against the report's self-description.

    🚨 Governed surface — this PR does NOT enqueue

    docs/adr/0025-plugin-package-distribution.md is in the diff ⇒ governed. ⛔ Never flip ready, never enqueue, never arm auto-merge. It stays draft for maintainer hand-merge, and the hand-merge is the review record. Confirmed on GitHub: draft: true at head 973e858b.

    ⚠️ Review request will 422 here — this seat's identity is os-zhuang, and GitHub refuses a review request to the PR's own author. Fallback per protocol: assign the PR to os-zhuang, which I will do once CI is green, and name the fallback in the round report.

    Checklist

    Item Reading
    PR shape Draft, targets main. Part-of PR must not also close its card ✅, No other open PR may claim the same issue ✅, No other open PR may claim the same single-writer path ✅
    Governed docs/adr/** hit ⇒ hand-merge only
    packages/spec ⛔ untouched ✅ — absent from the changed-file list, as required (spec is the spec seat's)
    content/docs/releases/ ⛔ untouched ✅
    Exclusion list ✅ signature, healthCheck, startupTimeout all still present in PluginMetadata in the diff. startupTimeout additionally serves as the positive control in both pin files.
    Ruled scope ✅ all four binding items landed: configSchema removed · PluginConfigValidator/createPluginConfigValidator + unit test deleted and unexported · ADR-0025 §3.7 records the retirement · the false // Config is validated before init is called example deleted
    CI ⏳ running on the final head 973e858b (started 10:28Z). Handoff waits on it.

    The either-way item is genuinely dead

    packages/core/ADVANCED_FEATURES.md loses the whole "10. Plugin Configuration Validation" section — the example whose inline comment promised "Config is validated before init is called", which was false on the retired ref. ⭐ That sentence was the binding half of the ruling and the retired surface's only in-repo declaration site. It is gone, not softened.

    Spot-checks

    ① Both retirements leave a tombstone, not a hole. plugin-loader.ts replaces each field with a comment naming the ruling, the date, the reason, and the surviving alternative; security/index.ts replaces the export block the same way. ⇒ The next reader learns why the field is absent instead of re-adding it.

    ② Both pin files carry a positive control, which is what makes their absence assertions readings:

    • runtime pin: the barrel no longer publishes the validator — control: it still publishes PluginSignatureVerifier / PluginPermissionEnforcer, so the namespace is populated, not accidentally empty;
    • compile pin: startupTimeout still compiles with no directive, proving the interface accepts real members and the @ts-expect-error directives above it are measurements rather than a broken instrument.

    ③ The pin-residence deviation is the best thing in this PR. check:type-check-coverage refused @ts-expect-error pins in packages/core — the package has no typecheck script, so a directive there is a phantom pin: no tsc program that any typecheck script runs would ever evaluate it, and it would sit in the tree looking like protection while protecting nothing. The pins moved to packages/rest, whose tsconfig.test.json is compiled by its typecheck script and resolves @objectstack/core to the built dist/index.d.ts ⇒ the pin now guards the contract consumers actually see, not an internal source shape.

    ⚠️ Declared cross-lane addition, recorded rather than waved through: that puts one test-only file in packages/rest, which is domain:cli territory and outside this claim's declared file surface. The dev declared it under stop-on-breach and touched no non-test rest file. I accept it: the pin cannot do its job anywhere else, and packages/rest is also the retirement's worked replacement (it parses its own config at its own seam, #11637 — the very pattern the ruling points migrators to). ⛔ Flagged for the hand-merge review rather than buried.

    ④ Ablation proves the pin can fail. Re-adding hotReloadable and rebuilding core's dist (stated and performed on both legs) turns the directive into TS2578 in a file the ratchet requires at zero errors — observed exactly as predicted, with dist-level anchor greps 0→1 and back, and startupTimeout holding at 3 in dist across both legs as the control.

    ⑤ Changeset grade is argued, and the constraint is named. @objectstack/core: minor with BREAKING stated in the body — major is refused repo-wide by check:changeset-no-major, so minor-under-the-lockstep-launch-window is the honest encoding rather than a downgrade. It carries per-symbol one-line migrations and the ADR-0087 not-required annotation with its reasoning.

    The release-owned page — answered, and the answer is "no action"

    I asked for a reading, ⛔ not a decision, and got one. content/docs/releases/v17.mdx was read and not edited; all four configSchema mentions are other surfaces: ADR-0018 node-executor config (~532, ~1949, ~2242) and a historical driver note (~582 — "An earlier version of this note said the driver's own configSchema did that, which was wrong for two releases").

    ⇒ The page does not advertise PluginMetadata.configSchema as a v17 capability, so this retirement falsifies nothing there and no separate docs-only PR or issue is owed. ⛔ The guardrail held: a code PR did not touch release notes.

    Docs drift — 3 edited, 3 declined with reasons

    Edited: plugins/anatomy.mdx (retired-field example → constructor-owned config), plugins/index.mdx (Configuration Validation bullet + example half removed, signature kept — still live), protocol/kernel/index.mdx (snippet + fail-fast callout rewritten).

    ⭐ Declined with the discrimination I asked for — two of the three are homonyms, exactly the trap: automation/flows.mdx's configSchema is the ADR-0018 node-executor schema; quick-reference.mdx's PluginMetadata is spec's locally-declared homonym in plugin-validator.zod.ts, still live. plugin-spec.mdx teaches the surviving self-parse pattern; its phantom manifest row predates this diff and was filed as #12690 rather than fixed here.

    Blind-spot sweep run over the behavioural phrasings and over both no-anchor files (ADVANCED_FEATURES.md, security/index.ts) — zero hand-written mirrors.

    Findings filed, ⛔ not smuggled into this diff

    NOT MEASURED, correctly declared

    scripts/pm/check-half-states.mjs — exit 3, PREREQUISITE NOT MET (14-byte proxy token placeholder; the tool refuses to sweep anonymously). ⛔ Not green, not red. One earlier batch exited 99 = verify-lock queue timeout, also NOT MEASURED, and was re-run to completion — ⭐ correctly distinguished from a failure, which is the same discipline that mattered on #11966's 240 s timeout.

    Next action

    On an all-green head: assign the PR to os-zhuang and list it under "awaiting a human merge" in the round report. ⛔ No enqueue, at any point, on this 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

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions