Skip to content

[finding] SendTemplateInput.org is declared ("Tenant id for org-overlay resolution (when supported)") but no implementation reads it #11832

Description

@os-warren

Found while executing #11741 (widening SendEmailInput/SendTemplateInput with organizationId).

Observation

packages/spec/src/contracts/email-service.ts declares on SendTemplateInput:

/** Tenant id for org-overlay resolution (when supported). */
org?: string;

Measured on claude/issue-11741-sendemail-organization-id (base dce6a019f): zero readers. @objectstack/plugin-email — the only IEmailService implementation in this repo — never consults input.org anywhere (email-service.ts, template-loader.ts, email-plugin.ts; grep for a bare org read comes back empty apart from a prose comment in template-loader.ts:69 about overlay rows). Template resolution keys on (name, locale) only.

Declared ≠ enforced (Prime Directive #10): a caller passing org today gets no org-overlay resolution and no error — the key is silently inert. The "(when supported)" hedge in the TSDoc is the declaration admitting it.

Why it matters slightly more after #11741

#11741 adds organizationId (the sys_email.organization_id delivery stamp, pass-through) beside the inert org (declared as template resolution overlay). Two org-shaped keys on one input, one of which does nothing, is exactly the shape that invites an AI author to pick the wrong one. The #11741 TSDoc cross-references the distinction, but the long-term fix is for org to be either implemented (org-overlay template rows, if there is measured pull) or retired.

Disposition options for triage

  • Retire org (ADR-0049 enforce-or-remove; it is a contracts interface key, not an authorable metadata key, so the retirement kit is lighter — no liveness ledger surface).
  • Implement org-overlay template resolution and make the declaration true (only with a real business pull; startup scope discipline argues against building it speculatively).

Not verified here: whether anything outside this repo (e.g. cloud) passes or reads org.

Refs: #11741 · #11303

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 24, 2026
  2. claude commented on Aug 25, 2026

    @claude
    Contributor

    Concentrated triage batch: finding → pm:queue + domain:spec, Task — SendTemplateInput.org is declared with a "(when supported)" hedge and has zero readers (re-verified: no bare org read in plugin-email beyond a prose comment), while #11741 establishes organizationId as the read channel. Retire org via the spec-property-retirement / enforce-or-remove route (the playbook governs tombstones, forms, docs and pins). Clause-②: yes at claim (an input-contract member is removed) — contract-review tier per spec-lane rules; if the retirement route's ledger check finds a reader the grep missed, stop and report.


    Generated by Claude Code

  3. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 25, 2026
  4. added theissue type on Aug 25, 2026
  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 25, 2026
  6. self-assigned this
    on Aug 25, 2026
  7. os-litant commented on Aug 25, 2026

    @os-litant
    Contributor

    Claim: PM loop round R6
    Session: session_01NDGG54XF5gbTLdQzCtnaVV
    Branch: claude/issue-11832-retire-sendtemplate-org
    Worktree: objectstack-issue-11832
    Domain: domain:spec
    File surface: packages/spec/src/contracts/email-service.ts (SendTemplateInput.org region ONLY — organizationId member untouched) + email-service tests + regenerated api-surface artifacts if moved + consumer call-site cleanup (examples) + changeset; conditional migrations tail-append only if the ADR-0087 gate demands it (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: claude-fable-5 — dispatch-gates 2026-08-25 @0b04839: "no path-derived mandate … floor sonnet · default opus · ceiling fable … a card changing contract accept/reject behaviour or widening the public surface is claude-fable-5"; clause-② content leg (published contract member removal) ⇒ fable mandatory
    Clause-②: yes
    Serial constraints cleared: sole writer of contracts/email-service.ts this batch (prior writer PR #11839 for #11741 MERGED; #11741 CLOSED — verified). Conditional registry.ts tail-append shared with #11842/#12039 under the maintainer's parallel waiver (2026-08-25, chat, verbatim: 「并行5个子任务,加快进度」) + 4-point substitute discipline. Route: retire per ADR-0049 enforce-or-remove; premise (zero readers of input.org) is falsifiable — a found reader forks back as needs_decision, never a silent implement. No pin assertions on sibling behavior.


    Generated by Claude Code

  8. os-litant commented on Aug 25, 2026

    @os-litant
    Contributor
    {
      "issue": 11832,
      "status": "done",
      "branch": "claude/issue-11832-retire-sendtemplate-org",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12114",
      "premise_still_valid": true,
      "summary": "Retired SendTemplateInput.org per ADR-0049 enforce-or-remove. Premise re-verified at claim (base 8d60dcd): zero readers and zero writers of the member in-repo (plugin-email resolves on (name, locale) only; no sendTemplate call site passes org; RenderTemplateInput never carried it; objectui has zero SendTemplateInput references); the member SHIPPED in published @objectstack/spec@17.2.0 (dist/contracts/index.d.ts:6318 in the npm tarball). Removal = member + JSDoc deleted with self-contained tombstone prose in the interface; compile-time pin (@ts-expect-error, bidirectional via TS2578) in email-service.test.ts; D3 SemanticMigration send-template-input-org-retired as one-file shard entries/semantic/18.send-template-input-org-retired.ts + gen:migration-registry (no D2 — call-time input bag, the options.upsert precedent; spec-changes.json/upgrade-guide byte-unchanged by construction, projections stop at PROTOCOL_MAJOR 17); changeset @objectstack/spec minor with BREAKING prose + adr-0087: registered (runtime-interface-only is structurally refused for contracts/ symbols and the body carries a FROM→TO, so registered is the only honest disposition). organizationId (#11741) untouched semantically; no consumer cleanup needed anywhere. Draft PR at final head 6a8a947 after pre-PR origin/main merge; delta vs main is exactly the 5 intended files (+156/−5).",
      "tests": "All at final head 6a8a947, via scripts/pm/os-verify-lock.sh, exits captured before any pipe. spec vitest src/contracts/email-service.test.ts src/migrations/migrations.test.ts: 'Test Files 2 passed (2) / Tests 109 passed (109)'; spec full typecheck green incl. 'check:test-typecheck: OK — test layer compiles; 55 file(s)/263 error(s) held' (unchanged). plugin-email (downstream consumer of rebuilt narrowed dist; direction = it declares @objectstack/spec in dependencies): 'Test Files 27 passed (27) / Tests 429 passed (429)' + tsc --noEmit clean. example-showcase test/email-template-locale.test.ts: 'Test Files 1 passed (1) / Tests 6 passed (6)'. Reverse verification from committed state, both legs' mutations proven on disk by anchored grep (tsc reads src via tsconfig.test.json — no dist leg, so no rebuild owed): mutated (org re-added) moved the program 263 ledger-held → 264, the +1 being exactly 'src/contracts/email-service.test.ts(71,7): error TS2578: Unused @ts-expect-error directive'; restored leg grep-absent then back to 'check:test-typecheck: OK' at 263.",
      "gates": "At 6a8a947 (re-run after the pre-PR merge), each read from its own verdict line: check:generated 'All 14 generated artifacts are up to date' (api-surface/export-origins/docs unchanged — interface-member removal is invisible to existence-level snapshots, expected for this route); check-adr-0087-registration '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … registered send-template-input-org-retired (new here)'; green: check:nul-bytes, check:merge-driver, check:published-files, check:slot-lookup, check:spec-parsed-alias, check:cross-package-test-inputs, check:changeset-gate-self-tests, check:objectui-changeset, check:query-options-erasure, check:test-source-alias, check:type-source-resolution, check:engine-double-contract, check:where-matcher, check:type-check-coverage, check-changeset-no-major, check-ci-filter-parity, check-empty-changeset, check-plugin-teardown-shape, spec check:liveness/check:empty-state/check:variant-docs/check:strictness-ledger, lint check:doc-formula-expressions. Dispatch-gates re-derived from the actual change set (script's own derivation, no hand-fed paths; answer attributed to objectstack tree) — the newly derived families beyond the dispatch list were all run and are in the green list above. Remote CI: in_progress at delivery (report at draft-PR time per standing rule).",
      "files_changed": [
        ".changeset/send-template-input-org-retired.md",
        "packages/spec/src/contracts/email-service.test.ts",
        "packages/spec/src/contracts/email-service.ts",
        "packages/spec/src/migrations/entries/semantic/18.send-template-input-org-retired.ts",
        "packages/spec/src/migrations/registry.ts"
      ],
      "line_budget": "n/a — diff touches no skills/** (published skill packages untouched)",
      "deviations": [
        "SendTemplateInput.organizationId TSDoc: one clause rewritten — the claim said organizationId untouched, but its final sentence cross-referenced {@link SendTemplateInput.org}, left dangling and misleading by the removal; rewritten to past-tense retired-member phrasing. Semantics untouched; SendEmailInput.organizationId untouched byte-for-byte. Named in the PR body.",
        "check-dev-prereqs reds locally: 'The workspace is not built — 1 unmet precondition, not a list of problems' (fresh targeted worktree; the gate's own text says it is not a finding). CI builds first.",
        "check:type-check-debt not run repo-wide — declared narrowing with measurement: its population is the DEBT/TEST_DEBT ledgers in scripts/check-type-check-coverage.mjs; @objectstack/spec graduated (#5286) and the @objectstack/spec-monorepo root program excludes packages/, so none of this PR's 5 paths is an input to any ledgered tsc program; the only tsc programs reading them are spec's own three, run in full (green above).",
        "Repo-wide pnpm lint / remaining gate farm: CI-owned (standing narrowing).",
        "Channel deviation: this session's egress proxy 403s repo-scoped api.github.com REST paths ('GitHub access is not enabled for this session'; /rate_limit reachable, git push/fetch normal), so the dispatch's REST-channel prescriptions were unavailable; single-issue reads/writes went through MCP issue_read/add_issue_comment/create_pull_request (the banned MCP list_issues/search_issues were not used). Duplicate-search leaned on the PM's dispatch-time dedup reading + local grep + git ls-remote, per the increment-only rule."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  9. os-litant commented on Aug 25, 2026

    @os-litant
    Contributor

    os-dev-report

    (Supersedes the previous comment, whose HTML-comment marker the body sanitizer stripped; JSON identical.)

    {
      "issue": 11832,
      "status": "done",
      "branch": "claude/issue-11832-retire-sendtemplate-org",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12114",
      "premise_still_valid": true,
      "summary": "Retired SendTemplateInput.org per ADR-0049 enforce-or-remove. Premise re-verified at claim (base 8d60dcd): zero readers and zero writers of the member in-repo (plugin-email resolves on (name, locale) only; no sendTemplate call site passes org; RenderTemplateInput never carried it; objectui has zero SendTemplateInput references); the member SHIPPED in published @objectstack/spec@17.2.0 (dist/contracts/index.d.ts:6318 in the npm tarball). Removal = member + JSDoc deleted with self-contained tombstone prose in the interface; compile-time pin (@ts-expect-error, bidirectional via TS2578) in email-service.test.ts; D3 SemanticMigration send-template-input-org-retired as one-file shard entries/semantic/18.send-template-input-org-retired.ts + gen:migration-registry (no D2 — call-time input bag, the options.upsert precedent; spec-changes.json/upgrade-guide byte-unchanged by construction, projections stop at PROTOCOL_MAJOR 17); changeset @objectstack/spec minor with BREAKING prose + adr-0087: registered (runtime-interface-only is structurally refused for contracts/ symbols and the body carries a FROM→TO, so registered is the only honest disposition). organizationId (#11741) untouched semantically; no consumer cleanup needed anywhere. Draft PR at final head 6a8a947 after pre-PR origin/main merge; delta vs main is exactly the 5 intended files (+156/−5).",
      "tests": "All at final head 6a8a947, via scripts/pm/os-verify-lock.sh, exits captured before any pipe. spec vitest src/contracts/email-service.test.ts src/migrations/migrations.test.ts: 'Test Files 2 passed (2) / Tests 109 passed (109)'; spec full typecheck green incl. 'check:test-typecheck: OK — test layer compiles; 55 file(s)/263 error(s) held' (unchanged). plugin-email (downstream consumer of rebuilt narrowed dist; direction = it declares @objectstack/spec in dependencies): 'Test Files 27 passed (27) / Tests 429 passed (429)' + tsc --noEmit clean. example-showcase test/email-template-locale.test.ts: 'Test Files 1 passed (1) / Tests 6 passed (6)'. Reverse verification from committed state, both legs' mutations proven on disk by anchored grep (tsc reads src via tsconfig.test.json — no dist leg, so no rebuild owed): mutated (org re-added) moved the program 263 ledger-held → 264, the +1 being exactly 'src/contracts/email-service.test.ts(71,7): error TS2578: Unused @ts-expect-error directive'; restored leg grep-absent then back to 'check:test-typecheck: OK' at 263.",
      "gates": "At 6a8a947 (re-run after the pre-PR merge), each read from its own verdict line: check:generated 'All 14 generated artifacts are up to date' (api-surface/export-origins/docs unchanged — interface-member removal is invisible to existence-level snapshots, expected for this route); check-adr-0087-registration '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … registered send-template-input-org-retired (new here)'; green: check:nul-bytes, check:merge-driver, check:published-files, check:slot-lookup, check:spec-parsed-alias, check:cross-package-test-inputs, check:changeset-gate-self-tests, check:objectui-changeset, check:query-options-erasure, check:test-source-alias, check:type-source-resolution, check:engine-double-contract, check:where-matcher, check:type-check-coverage, check-changeset-no-major, check-ci-filter-parity, check-empty-changeset, check-plugin-teardown-shape, spec check:liveness/check:empty-state/check:variant-docs/check:strictness-ledger, lint check:doc-formula-expressions. Dispatch-gates re-derived from the actual change set (script's own derivation, no hand-fed paths; answer attributed to objectstack tree) — the newly derived families beyond the dispatch list were all run and are in the green list above. Remote CI: in_progress at delivery (report at draft-PR time per standing rule).",
      "files_changed": [
        ".changeset/send-template-input-org-retired.md",
        "packages/spec/src/contracts/email-service.test.ts",
        "packages/spec/src/contracts/email-service.ts",
        "packages/spec/src/migrations/entries/semantic/18.send-template-input-org-retired.ts",
        "packages/spec/src/migrations/registry.ts"
      ],
      "line_budget": "n/a — diff touches no skills/** (published skill packages untouched)",
      "deviations": [
        "SendTemplateInput.organizationId TSDoc: one clause rewritten — the claim said organizationId untouched, but its final sentence cross-referenced {@link SendTemplateInput.org}, left dangling and misleading by the removal; rewritten to past-tense retired-member phrasing. Semantics untouched; SendEmailInput.organizationId untouched byte-for-byte. Named in the PR body.",
        "check-dev-prereqs reds locally: 'The workspace is not built — 1 unmet precondition, not a list of problems' (fresh targeted worktree; the gate's own text says it is not a finding). CI builds first.",
        "check:type-check-debt not run repo-wide — declared narrowing with measurement: its population is the DEBT/TEST_DEBT ledgers in scripts/check-type-check-coverage.mjs; @objectstack/spec graduated (#5286) and the @objectstack/spec-monorepo root program excludes packages/, so none of this PR's 5 paths is an input to any ledgered tsc program; the only tsc programs reading them are spec's own three, run in full (green above).",
        "Repo-wide pnpm lint / remaining gate farm: CI-owned (standing narrowing).",
        "Channel deviation: this session's egress proxy 403s repo-scoped api.github.com REST paths ('GitHub access is not enabled for this session'; /rate_limit reachable, git push/fetch normal), so the dispatch's REST-channel prescriptions were unavailable; single-issue reads/writes went through MCP issue_read/add_issue_comment/create_pull_request (the banned MCP list_issues/search_issues were not used). Duplicate-search leaned on the PM's dispatch-time dedup reading + local grep + git ls-remote, per the increment-only rule."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  10. added
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Aug 25, 2026
  11. os-litant commented on Aug 25, 2026

    @os-litant
    Contributor

    ACCEPT — PR #12114 (R6, reviewer of record: spec seat, session session_01NDGG54XF5gbTLdQzCtnaVV).

    Checklist conclusion (verified against GitHub + local git diff of the branch): draft ✓ · base main ✓ · Fixes #11832 first line ✓ · 5 files +156/−5 = member removal + tombstone, compile-time pin test, D3 semantic shard, regenerated migration registry, BREAKING-minor changeset with adr-0087: registered (gate verdict quoted in the PR) ✓ · no governed path ✓. Spot readings: tombstone is self-contained (dated, names why organizationId is NOT a replacement, and pins any revival to a NEW ruling); SendEmailInput.organizationId byte-untouched; the one declared deviation — rewriting SendTemplateInput.organizationId's now-dangling {@link} clause to past tense — is correct and was flagged, not taken silently.

    Premise (third-tier, falsifiable): HELD — zero readers and zero writers re-measured at claim across plugin-email, all sendTemplate call sites, examples, docs, and objectui; the member's shipped status (spec@17.2.0 dist) was verified rather than assumed, which is what makes registered the honest ADR-0087 disposition. The fork clause never triggered. Consumer-package readings delivered per the lane's standing commitment: plugin-email 27 files / 429 tests, showcase locale suite 6 tests, spec contracts+migrations 109 tests — all green at final head 6a8a947. Rejection-surface evidence: bidirectional TS2578 pin proven 263→264→263 with on-disk mutation anchors.

    Contract review (clause-② content leg: published contract member removal): fuse reading get_session.external_metadata.last_served_model = claude-fable-5 = CONTRACT_REVIEW_TIER (this seat, self-dispatched-card path). Verdict: PASS — removal narrows the compile-time surface exactly to the documented dead member; runtime accept/reject untouched (nothing ever parsed org); migration prescription present (D3, no D2 — call-time input bag precedent); startup-focus and ADR-0049 both satisfied. Clearing needs:contract-review both carriers in this write batch.

    Next: ready + merge-queue once EVERY check on the PR is green (CI started 09:35Z, in progress at review time — flip rides the next check-in). Sibling note honored: #12039's dev re-merges main after this lands (shared migrations registry, per its dispatch clause).


    Generated by Claude Code

  12. removed
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seat

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions