Skip to content

feat(spec,lint): resolve action → flow and widget → dataset references against the release artifact - #25617

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25514-artifact-flow-dataset-refs
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25514-artifact-flow-dataset-refs

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #25514

Clause-②: yes (widening)
Metadata-protocol: added
Ruling-ref: 6377677280

What

One release artifact carries several packages (ADR-0130 D4). Two per-package checks judged a reference against one package, so they refused a target that a sibling package of the same artifact provides:

  1. packages/spec, defect 1. validateCrossReferences built its flow scope from the stack's own flows alone. A type: 'flow' action whose target names a sibling package's flow was refused with "which is not defined in flows." This applied in all three positions: top-level actions[], objects[].actions[], and inline page actions.
  2. packages/lint, defect 2. widget-dataset-unknown (validateWidgetBindings) built its datasets map from stack.datasets alone. It ignored the packages[] resolution context that the CLI's per-package pass already supplies.

Following ruling 6377677280 (option A), both reference classes now resolve against the artifact. A flow or dataset that no package of the artifact defines stays refused, exactly as before. Scope is same-artifact only. Cross-artifact resolution is #25403's ADR and is not touched here.

Contract (public-surface widening)

  • New DefineStackOptions.artifactFlows?: readonly string[] (packages/spec/src/stack.zod.ts). It lists the flow names the other packages of the artifact provide. A flow action's target resolves against own flows ∪ these names. It mirrors artifactObjects.
    • It widens a check that already runs and never switches one on. A stack with no flows of its own keeps the plugin leniency (flowNames.size === 0), unchanged.
    • Declared means enforced. Every target that resolved only through the list is recorded on the stack defineStack returns. The record is a non-enumerable Symbol.for('objectstack.stack.artifact-flow-claims') property beside the provenance mark, so it is not a schema key and not in dist/objectstack.json. composeStacks step 3b (collectArtifactFlowClaimErrors) refuses the composition when no input defines a recorded target. The refusal uses the same STACK_CROSS_REFERENCE_INVALID/422 envelope and the byte-identical per-finding message. A flow is "defined by an input" when it is in that input's top-level flows or, for a nested multi-package artifact, in a packages[].manifest.flows body.
    • Why a record and not the artifactObjects re-run. Re-running the flow rule over every input at composition would also newly refuse a dangling target in an input built with strict: false. That is a narrowing this card does not declare. The record checks exactly the promise the option made. An input that did not opt in cannot newly fail at composition, strict or not. That case is pinned.
    • Boundary, the same as artifactObjects: a one-package composition, or a stack that is never composed, leaves the claim unverified.
  • widget-dataset-unknown reads packagesOf(stack) (the existing packages/lint reader) and adds each body's datasets to the map, the way validate-object-references.ts widens rung ① with artifactProvidedObjectNames. This is resolution context only. Dataset-level checks (measure-aggregate-incoherent) still walk stack.datasets alone, and the stack's own declaration wins a shared name. packages/cli is untouched: packageBodyAsStack already passes packages[].
  • Runtime publish gate (packages/metadata-protocol/src/runtime-authoring-gate.ts): neither widened nor broken. Its judged stack never carries packages (no packages key in runtime-gate.ts / runtime-authoring-gate.ts), so packagesOf answers [] and the rule behaves exactly as before. Its tests are green (below).

Booted measurement (ruling note 2)

I boot-tested a temporary (uncommitted) extension of examples/app-multi-package:

  • orders (type: module) ships the flow crm_order_stamp (start → create_record on crm_order → end) and the dataset crm_order_metrics.
  • core (type: app) has its own flow crm_core_noop, so the flow check runs at all. It has crm_account.actions[stamp_orders] → type 'flow', target 'crm_order_stamp', a dashboard widget order_totals bound to crm_order_metrics, and defineStack(..., { artifactFlows: ['crm_order_stamp'] }). Its requires is ['automation'].
$ PROBE_NO_CLAIM=1 objectstack validate        # option omitted: today's behaviour
  ✗ Action 'stamp_orders' on object 'crm_account' references flow 'crm_order_stamp' which is not defined in flows.
exit 1
$ objectstack validate                          # with artifactFlows
  → Running author-time rules per package (2)...
  ✓ Validation passed                           exit 0
$ objectstack build  → dist/objectstack.json packages[]:
  com.example.multi.orders {"flows":["crm_order_stamp"],"datasets":["crm_order_metrics"],"dashboards":[],"actions":[]}
  com.example.multi.core   {"flows":["crm_core_noop"],"datasets":[],"dashboards":["crm_overview→crm_order_metrics"],"actions":["crm_account.stamp_orders→crm_order_stamp"]}
$ objectstack dev --fresh -p 38517              # AutomationServicePlugin + AnalyticsServicePlugin up
GET  /api/v1/data/crm_order                     → {"records":[],"total":0}
POST /api/v1/actions/crm_account/stamp_orders/W79i9VpR-IiyJjlz
  → HTTP 200 {"success":true,"data":{"success":true,"flowLabel":"Stamp Order","summary":{"acted":1,"nodes":[{"nodeId":"start","status":"success"},{"nodeId":"create_probe_order","nodeType":"create_record","status":"success","acted":1}]}}}
GET  /api/v1/data/crm_order                     → 1 record: {"name":"flow-started-from-core-action","amount":42}
GET  /api/v1/meta/dashboard/crm_overview        → widget {"id":"order_totals","dataset":"crm_order_metrics"}, packageId com.example.multi.core
POST /api/v1/analytics/dataset/query {"datasetName":"crm_order_metrics","selection":{"measures":["order_count","order_amount"],"dimensions":["name"]}}
  → HTTP 200 {"rows":[{"name":"flow-started-from-core-action","order_count":1,"order_amount":42},{"name":"manual-order","order_count":1,"order_amount":8}],"object":"crm_order", "sql":"SELECT name AS \"name\", COUNT(*) …, SUM(amount) … FROM \"crm_order\" GROUP BY name"}

So the app package's action starts the module package's flow, and the app package's widget's dataset (owned by the module) returns its rows. The server was stopped by its recorded PID, and the probe edits were restored (git status clean).

Tests

  • New packages/spec/src/stack-artifact-flow-crossref.test.ts (9): both directions per class across all three positions. A sibling flow is accepted at defineStack and at composeStacks. An unlisted flow is refused at defineStack. A claimed flow that no input defines is refused at composeStacks with the per-stack message, envelope and header. A nested multi-package input resolves. Fences: no own flows means no check, a non-opted strict: false input cannot newly fail, and no new enumerable key is added.
  • New packages/lint/src/validate-widget-bindings.artifact-datasets.test.ts (6): a sibling dataset is accepted, a dataset no package declares is refused, the single-package answer is unchanged, a resolved sibling dataset's measures are judged, a sibling's dataset-level findings are not judged on this package's pass, and the rule table reaches the per-package pass.
  • Ablations (fix committed first; restored by ablation-replace.mjs, blob == HEAD, git diff HEAD empty):
    • spec artifactFlows?.has(target) → never matches: 5/9 red.
    • spec ...collectArtifactFlowClaimErrors(stacks), deleted: 1/9 red (the compose-time refusal).
    • lint packages loop reads a non-key, then rebuilt (ablation-dist-preflight marker present in 4 dist files): 3/6 red. The real door objectstack validate on the probe exits 1 with HotCRM's exact defect-2 line, package 'com.example.multi.core' — dashboard "crm_overview" › widget "order_totals": dataset "crm_order_metrics" does not resolve to a declared dataset. (rule widget-dataset-unknown). After the restore and rebuild (marker absent from all 20 dist files), the door exits 0 and 6/6 pass.
  • Package suites at 5cf84d58 (pre-merge): spec pnpm test 715 files / 21696 passed, pnpm typecheck 0; lint pnpm test 159 files / 7243 passed, pnpm typecheck 0. The CLI unit tier (vitest run --project unit) ran 327/327 files: 325 on the first run, plus published-subpath-console / published-subpath-hook-body, which needed packages/cli itself built and then passed, 29/29. metadata-protocol protocol.dashboard-dataset-publish-gate, protocol.runtime-gate-stored-universe and protocol-publish-drafts-closure ran 3 files / 24 passed. The CLI integration tier is left to CI; the diff touches no integration-tier file.
  • After merging origin/main (e649c2d91e, the Version Packages commit) at head 45b786764e: spec build plus check:generated show all 14 generated artifacts up to date. Spec flow and artifact cross-ref tests: 39 passed. Lint build plus both widget-binding test files: 184 passed.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 113 commands, with an identical derivation on 45b786764e. All 113 exit 0. Four first returned PREREQUISITE NOT MET (exit 3) or hit my own runner timeout, and passed once the prerequisite was met:

  • check-plugin-teardown-shape --self-test: shallow clone, fixed with git fetch --unshallow.
  • spec check:skill-examples and check:dual-build-cjs-loads: unbuilt sibling dist/, fixed by building those packages.
  • check:type-check-debt: my 420 s cap; rerun uncapped, it exits 0.

--ran reconciliation: 113 derived, 113 run, 0 NOT-MEASURED (a derived zero: every line carries its exit code). Re-run on 45b786764e: the --base origin/main changeset gates, check:type-check-coverage, check:nul-bytes, check:doc-authoring, check:issue-citations and spec check:api-surface all exit 0. Local-only families also exit 0: check:dev-prereqs and check:skill-frame-freshness. Dogfood battery: n/a (no ! in the title, no (narrowing) arm).

Acceptance notes

  • The card's HotCRM acceptance rows (hotcrm#2270's patch validating unchanged, and top_products returning 4 rows) need a released @objectstack/* carrying this. They are measured downstream, not here. HotCRM must add artifactFlows to its app package's defineStack call. The dataset half needs no HotCRM change.
  • The release is in progress: this PR stays draft. It is not to be readied, queued or merged until the maintainer lifts that.
  • content/docs/getting-started/examples.mdx gains one bullet beside the existing artifactObjects paragraph.

Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 13 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/getting-started/examples.mdx (via artifactFlows (symbol, a field of interface DefineStackOptions))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via validateWidgetBindings (symbol, a top-level function))
  • content/docs/releases/v17/17-1.mdx (via validateWidgetBindings (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: defineStack (literal, 74 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 10b8250ad5af331beed721694dca6e43df6d16b8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 56d41b252e302762e1cb39ec610fd5e779e8036f — the merge of head 45b786764e97c05431a870693d2345b247833163 into base 10b8250ad5af331beed721694dca6e43df6d16b8, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 56d41b252e302762e1cb39ec610fd5e779e8036f && git checkout 56d41b252e302762e1cb39ec610fd5e779e8036f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 10b8250ad5af331beed721694dca6e43df6d16b8 45b786764e97c05431a870693d2345b247833163 && git checkout -B drift-repro 10b8250ad5af331beed721694dca6e43df6d16b8 && git merge --no-ff 45b786764e97c05431a870693d2345b247833163

node scripts/docs-audit/affected-docs.mjs --json 10b8250ad5af331beed721694dca6e43df6d16b8

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 10b8250ad5af331beed721694dca6e43df6d16b8 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #25514 · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T09:58Z · head 45b786764e · report 6382768493 · seat review at TIER_DEFAULT. The clause-② enqueue gate hits on both limbs (path: packages/spec/src/stack.zod.ts; declaration: Clause-②: yes), so a same-shape ## Contract review PASS at CONTRACT_REVIEW_TIER on the landing head is still owed. needs:contract-review is added in this stroke.

  1. Shape. Draft, base main, first line Fixes #25514, no other closing keyword. Clause-②: yes (widening), Metadata-protocol: added and Ruling-ref: 6377677280 are line-initial in the body. The changeset carries both declarations, with @objectstack/spec and @objectstack/lint at minor. Six files, +523/−16 (get_files): stack.zod.ts, validate-widget-bindings.ts, two new test files, the changeset, and content/docs/getting-started/examples.mdx. No governed path, no content/docs/releases/. The PR author is objectstack-fleet[bot] (the relay).
  2. Ruling, measured on the diff. Ruling A asks that a reference resolve against the artifact, and that a name no package defines stay refused. Defect 1: one checkFlowTarget now serves the three positions (top-level, object-bound, inline page). A target in artifactFlows passes only where it used to be refused (own flows non-empty), and is recorded as a claim. composeStacks refuses a claim that no input defines (collectArtifactFlowClaimErrors, reading each input's flows and its packages[] bodies), with the same per-finding message. A target in neither scope is refused as before. Defect 2: widget-dataset-unknown adds the packages[] bodies' datasets to the resolution map only; dataset-level checks still walk stack.datasets; the stack's own name wins. Ruling note (2), the booted measurement, is in the PR body: the sibling flow started from the action (HTTP 200, one row written), and the sibling dataset returned 2 rows to the widget query.
  3. Gates. Check-runs on 45b786764e at this record: 25 success, 6 skipped, 9 in progress, 0 failed. Local, per the report: --ran 113/113 with no NOT MEASURED; spec 715 files and lint 159 files green; cli unit 327/327; metadata-protocol's three publish-gate files 24/24. Ablations: spec claim branch 5/9 red, compose pass 1/9 red, lint loop 3/6 red plus objectstack validate exiting 1 at the door. Landing waits for every check green and the contract-review PASS.
  4. Deviations accepted. (a) content/docs/getting-started/examples.mdx (+9) is outside the claim's declared surface. It is disclosed in the PR's Acceptance notes but not as a report deviation. It is accepted as the page that restates the multi-package split rules. I checked its two sentences against the diff: "in a composition of two or more packages, composeStacks refuses a claimed flow that no package defines" matches collectArtifactFlowClaimErrors and the option's own boundary; "os build, os validate and os lint resolve it against every package of the artifact, and still refuse a dataset that no package declares" matches the lint hunk on the per-package pass, which carries packages[]. (b) The verification shape is a recorded claim, not the artifactObjects-style re-run (the report's open_questions item). Accepted: a re-run would newly refuse a strict: false input, an undeclared narrowing. The contract review answers it as a boundary flag. (c) One origin/main merge (e649c2d91e), re-tested. mcp_calls 0; relay writes 2.
  5. Out-of-scope outcomes.
  6. Landing. Not governed, 539 changed lines. Once the CONTRACT_REVIEW_TIER PASS is on record and every check on the then-current head is green, the seat sends pr_ready + automerge_enable through the relay.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 45b786764e97c05431a870693d2345b247833163
Local-runs: none
Scope: packages/spec/src/stack.zod.ts + packages/spec/src/stack-provenance.ts, packages/spec/src/stack-artifact-flow-crossref.test.ts, packages/lint/src/validate-widget-bindings.ts, packages/lint/src/validate-widget-bindings.artifact-datasets.test.ts, packages/lint/src/object-graph.ts (packagesOf, recordsOf; not in the diff), packages/cli/src/utils/artifact-packages.ts (packageBodyAsStack; not in the diff), packages/metadata-protocol/src/runtime-gate.ts and runtime-authoring-gate.ts (not in the diff), .changeset/25514-artifact-flow-dataset-refs.md, content/docs/getting-started/examples.mdx. The remainder of the diff outside the schema face is the lane seat's ACCEPT at TIER_DEFAULT (card comment 6382832757).

Inputs, and nothing else: card #25514 (body and every comment; ruling of record 6377677280; dev report 6382768493), PR #25617 (body, file list, net diff against main at merge-base e649c2d91e), and the check-runs on the head. Read-only: nothing built, run or re-run.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named right or wrong:

  1. defineStack accept set — widened, opt-in, byte-identical without the option. RIGHT. validateCrossReferences takes artifactFlows and flowClaims; one checkFlowTarget closure serves the three positions (actions[], objects[].actions[], inline page actions). Its guard — no target, own flows empty, or target in own flows ⇒ return — is the former inline condition inverted, so the plugin leniency and every refusal that exists today are unchanged; a target in artifactFlows is accepted and recorded where it was refused. flowNames has no other consumer at the head (lines 4267 to 4280 only), so no flow-reference site is left on the old scope.
  2. Refusal text — byte-identical. RIGHT. flowTargetMessage(subject, target) reproduces the three former literals exactly (Action 'x', Action 'x' on object 'o', the inline where), and the new test pins all three strings. No runtime string carries a tracker number.
  3. Public surface — one additive member. RIGHT. DefineStackOptions.artifactFlows?: readonly string[], optional, on an exported interface. DefineStackOptions has no Zod mirror (the only options schema is ComposeStacksOptionsSchema) and defineStack hands options straight to buildDefinedStack, so the key is neither stripped nor refused. No api-surface/ shard moves: the snapshot records that an export exists, not its members, and TypeScript Type Check — the job hosting every spec generated-artifact gate — is green on the head.
  4. The claim record — not a schema key, not data, not published. RIGHT. Symbol.for('objectstack.stack.artifact-flow-claims'), non-enumerable, non-writable, non-configurable, stamped after markStackProvenance. markStackProvenance always returns an extensible object (the fresh parse result, or a shallow copy of a non-extensible one), so the Object.isExtensible early return in recordArtifactFlowClaims is unreachable for the producer's own output — a dead branch, not a live fail-open. The readers (artifactFlowClaimsOf, providedFlowNames, collectArtifactFlowClaimErrors) are module-private; nothing new leaves the package entry. The fence test pins Object.keys and JSON.stringify unchanged.
  5. composeStacks — a new refusal reachable only through the new option; not a narrowing. RIGHT. Step 3b appends collectArtifactFlowClaimErrors(stacks): a recorded claim no input provides is refused with STACK_CROSS_REFERENCE_INVALID / 422 and the per-stack message. Before this diff such a stack never reached composition (it was refused at defineStack), so nothing previously accepted becomes refused. The existing collectArtifactCrossReferenceErrors re-runs only the two object rules (collectSeedDataObjectErrors, collectPermissionGrantObjectErrors), never validateCrossReferences, so an opted-in sibling flow target is not re-refused at composition. The single-input early return (stacks.length === 1) precedes step 3b, so the docblock's "a one-package composition leaves the claim unverified" is true of the code. A copy that drops the symbol drops the provenance mark with it, and step 0 refuses an unmarked input (StackProvenanceMissingError) — the hunk's "composeStacks already refuses such inputs" holds.
  6. Nested multi-package input. RIGHT. providedFlowNames reads flows and packages[].manifest.flows; preservePackageEntries emits { manifest: assemblePackageBody(stack) } and assembledPackageBodyShape carries every concat collection, flows included, so a preserve-composed input whose flattened flows step 8 deleted still provides them. Pinned.
  7. widget-dataset-unknown (lint) — resolution widened, judged set unchanged. RIGHT. The datasets map gains each packagesOf(stack) entry's manifest.datasets, own declaration first (!datasets.has). The dataset-level loop (measure-aggregate-incoherent) still iterates recordsOf(stack.datasets); widget-level checks judge the resolved sibling dataset's measures and dimensions (pinned). packagesOf throws on packages: null, but the same rule table already calls it on the same stack (validate-object-references.ts), so no door gains a refusal. The CLI's packageBodyAsStack already spreads packages: artifactPackageEntries onto each per-package stack — packages/cli is correctly untouched.
  8. Runtime publish gate — unchanged. RIGHT. Neither runtime-gate.ts nor runtime-authoring-gate.ts builds a packages key on the judged stack (the only matches are prose paths), so packagesOf answers an empty list there and the rule behaves as before.
  9. Docs. RIGHT. content/docs/getting-started/examples.mdx gains one bullet beside the artifactObjects paragraph, in the hand-written tree; content/docs/releases/v17/17-5.mdx, the only other artifactObjects mention, is RELEASE-OWNED and correctly untouched. No governed path in the file list; no skills/** text to keep in step.

No derived judgment in the diff is wrong.

② Semver level

.changeset/25514-artifact-flow-dataset-refs.md declares '@objectstack/spec': minor and '@objectstack/lint': minor — exactly the two released packages the diff moves (both 18.1.0, neither private). The two declarations are line-initial in the changeset and in the PR body and agree: the Clause-②: line reads yes (widening) and the Metadata-protocol: line reads added. added is right — a new key an author writes in a defineStack call, and a widget binding newly accepted — and a kind takes at least minor on a moved package, so the level matches what the diff publishes. The (widening) arm is right: judgment 5's compose-time refusal is reachable only through the new option, so no (narrowing) arm is owed — unlike #18202, whose re-run design declared a strict: false narrowing. No BREAKING banner and no ADR-0087 marker are owed. Check Changeset is green on the head.

③ Boundary flags

Dev report 6382768493 — one open_questions entry and two out_of_scope_findings — plus the ruling's two notes and the card's acceptance rows, each answered or escalated:

  • Open question — a recorded claim (A, implemented) vs the artifactObjects-style re-run (B): A is right; answered, not escalated. B would newly refuse a dangling flow target in a strict: false input at composition — a narrowing this card's Clause-②: yes (widening) does not carry and ruling 6377677280 did not ask for. The reason the artifactObjects pass gives for not recording (a composition-time concern on ObjectStackDefinitionSchema) does not apply to a non-enumerable symbol beside the provenance mark. The record holds exactly what the option let through; it is lost only together with the mark, and composition refuses an unmarked input. The leniency path (own flows empty) records no claim because nothing was relied on — consistent. Two mechanisms in one family is an implementation difference each docblock explains, not a contract fork: accept set, envelope and message are one shape.
  • Finding 1 — indexObjectGraph reads stack.objects alone, so on the per-package pass a widget bound to a sibling dataset whose object is also sibling-owned has its dashboard-filter and widget-filter field checks skipped as unknowable, never refused. Not a defect at any door (a skip is not a wrong answer) and the existing rung-① design of the per-package pass; the union run judges the union's objects. Prime Directive chore: version packages #10 puts it in the PR's acceptance notes, which the dev could not amend (body write-once): the owning seat should append it there. Not escalated.
  • Finding 2 — HotCRM must add artifactFlows to its app package's defineStack call. The card's acceptance row 1 says the hotcrm#2270 patch "validates and builds unchanged"; the flow half needs that one line, the dataset half none. The card's own Ask names the pattern ("the way artifactObjects is verified"), which is opt-in by construction: at defineStack time a package cannot see its siblings, and deferring the per-stack refusal to composition would widen defineStack for every single-package stack, which no ruling asked for. The PR body's acceptance notes disclose it. Carrier: the hotcrm#2270 seat; the maintainer can object in one word. Not escalated.
  • Ruling note 1 (same-artifact only): honoured — collectArtifactFlowClaimErrors resolves against the composition's own inputs; nothing reaches across artifacts, and [Decision] 一个应用能否声明依赖另一个应用并引用其对象(ADR-0019 推迟的应用间依赖) #25403's territory is untouched.
  • Ruling note 2 (one booted measurement): present in the PR body, on a temporary extension of examples/app-multi-package — the app package's action started the module package's flow (HTTP 200, one crm_order row written) and the app widget's module-owned dataset answered its rows. The card's HotCRM rows are measured downstream on a release, as the ruling says.
  • Card acceptance "a regression pin per class, both directions": stack-artifact-flow-crossref.test.ts (9) and validate-widget-bindings.artifact-datasets.test.ts (6) pin accept and refuse for both classes, the three flow positions included.
  • Check-runs on the head, read at 2026-10-24T10:21Z: every concluded run is success or skipped; none failed. Still not concluded at this reading: Test Core (3/6), Test Core (4/6), Test Core (5/6) — all in_progress. Test Core is a required context, so this PASS does not make the head green by itself; the owning seat lands only once every required run concludes success.
  • The maintainer's no-landing word stands. The PR is draft under 「正在发版本,你可以派发但是不要合并。」; this record is the CONTRACT_REVIEW_TIER review of record for this head and lifts nothing else. Nothing here readies, queues or arms it.

Implemented-by: session_011SU6BLnw3XLU8nKfnWMYVG
Reviewed-by: session_01PC4NPXPgPmpDRG9DdDnyXT

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 24, 2026 10:34
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 24, 2026 10:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit cc10e74 Oct 24, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25514-artifact-flow-dataset-refs branch October 24, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants