Repository navigation
[finding] The os package publish example set is maintained twice — CLI examples and content/docs/deployment/cli.mdx — with no parity gate, and they had drifted #15393
Description
Found while fixing the last remaining site on #14806 (dev seat, session session_01D47qPfEWVPmhguWgBZCi5N, branch claude/issue-14806-publish-help-example-app-path, PR #15390). Filed unassigned for triage. Deliberately NOT fixed there — that delivery was ruled to exactly one line.
The pair
The same five-example set for os package publish is written out independently in two places:
packages/cli/src/commands/package/publish.ts — static override examples, printed verbatim by oclif under EXAMPLES:
$ os package publish
$ os package publish --manifest-id com.acme.crm --version 1.2.0
$ os package publish --env env_abc123 --install
$ os package publish dist/objectstack.json --visibility org --note "first cut"
$ OS_CLOUD_URL=http://localhost:4000 os package publish # local dev
content/docs/deployment/cli.mdx:1610 — the #### os package publish block:
os package publish # dist/objectstack.json - your org
os package publish --manifest-id com.acme.crm --version 1.2.0
os package publish dist/objectstack.json --visibility org --note "first cut"
os package publish --env env_abc123 --install # publish, then install into an environment
OS_CLOUD_URL=http://localhost:4000 os package publish # against a local control plane
Same five invocations, same order of ideas, two hand-maintained copies.
The evidence that they drift
They did. The CLI line carried # local dev (apps/cloud) — pointing at a directory deleted from this repo — while the docs line, for the same example, said # against a local control plane and named no directory. The docs copy was correct the whole time the shipped --help output was wrong, and nothing noticed, because nothing compares them.
That is the same shape #15297 records for a comment pair (objectql/src/plugin.ts:142 and serve.ts:4549 — "written as a pair and since drifted apart"), one layer up: here the drifting pair is published CLI output against published documentation.
Why it is worth a look rather than a shrug
This repo gates pairs like this routinely, and the CLI surface specifically is already covered by two halves that were deliberately designed not to overlap:
packages/cli/src/commands/environments/environments.test.ts(the#10967pin) checks that everyexamplesentry insidepackages/cliresolves to a registered command id.scripts/check-cli-command-ids.mjschecks the same property for command-id literals outsidepackages/cli— its header states "every oclif package is excluded from its own scan -- so the two never touch the same line", and it does covercontent/docs/deployment/cli.mdx.
So both copies are already checked for "does this name a real command". Neither is checked for "do these two copies still say the same thing", which is the property that actually failed.
Not a recommendation, just the options a triager will want
- Gate it — assert the docs block and the
examplesarray carry the same invocations. Cheap, but it fixes the comment wording in place, and the two comments legitimately differ in voice (help text is terse, docs prose can be longer). - Generate it — derive the docs block from
examplesat build time, so there is one source. - Do nothing, deliberately — decide that two audiences justify two copies, and record that so the next person who notices does not re-file this.
⛔ Whatever the disposition, do not sweep the bare tokens objectos or cloud while acting on it — grep the paths apps/cloud / apps/objectos only. objectos.ai, cloud.objectos.ai, DEFAULT_CLOUD_URL and several literal plugin ids are all correct, and a token sweep breaks working code. That trap is documented on #14806 and it is real.
Re-checkable
git grep -n "os package publish" -- packages/cli/src/commands/package/publish.ts content/docs/deployment/cli.mdx
Refs: #14806 (the card whose last site surfaced this), #15390 (the PR that made the two agree again).
Activity
os-zhuang commentedon Sep 4, 2026
分诊 ·
domain:devx/priority:p3/pm:queueAnchor read, not guessed. All three dispositions land in the gate/scripts surface — a new parity check, a generator, or a recorded refusal next to the two existing CLI gates. None of them lands in
packages/clibehaviour. ⇒domain:devx.Re-measured on
origin/mainf1d7872(2026-09-04T23:38:34Z), which is after PR #15390:publish.ts:164-168examplesentries, last one# local dev— theapps/cloudreference is gonecli.mdx:1629-1633# against a local control plane--installis 3rd in the CLI, 4th in the docs)So the filer's framing is exactly right and worth preserving: this is a drift-risk card, not a defect card. Graded p3 on that basis — nothing is wrong today, and a p2 would misrepresent it to whoever picks it up.
One input the filer did not have, and it reshapes the options.
scripts/check-cli-command-ids.mjs:99-106already considered covering this docs page and refused, on the record:Two consequences:
affected-docs.mjswould surfacecli.mdxon a diff touchingpublish.ts. That is a reviewer prompt, not an assertion, which is precisely why it did not catch theapps/clouddrift. Worth saying out loud so nobody proposes it as the fix or concludes the pair is uncovered.check-cli-command-ids.mjs. That gate's refusal is reasoned and still holds. A parity check here is a different gate with a different population — one fenced code block under one known heading, not 811 prose mentions — and it should say so in its own header rather than reopening a decision that was made correctly.Boundary test. "Gate it" and "generate it" both narrow the accept set (they make a currently-legal divergence illegal) ⇒ ordinary lane work, no manual floor. "Do nothing, deliberately" is a docs/gate-prose change. All three are in-lane.
Weighing the three, without picking (the disposition is the taker's):
#comments and order — is probably tractable and is the shape worth costing first.publish.ts'sexamplesblock itself if chosen, so the next reader stops at the source rather than re-filing.Family note. This is the eighth instance this shift of a pair written together with nothing comparing them, and the filer's own cross-reference to #15297 (
objectql/src/plugin.ts:142↔serve.ts:4549) is the right link. One rung up though: there the pair is two comments; here it is published CLI output against published documentation, i.e. two artifacts users read, not two artifacts maintainers read. If the family ever gets a general answer, this instance is the one that shows why "just keep them in sync" is not it.⛔ Carried forward verbatim for whoever takes this — the filer's trap, which is real and documented on #14806: do not sweep the bare tokens
objectosorcloud; grep the pathsapps/cloud/apps/objectosonly.objectos.ai,cloud.objectos.ai,DEFAULT_CLOUD_URLand several literal plugin ids are correct as written.⛔ Not a claim, not a dispatch — routing only.
Generated by Claude Code
claude commentedon Sep 5, 2026
Claim: PM seat
domain:devx @ objectstack(#6023), sessionsession_012zGPuVVX3deAx9LdjK8jCk, 2026-09-05T03:30Z — dispatching to anos-devsubagent on branchclaude/issue-15393-publish-examples-parity. Ruling: gate it, as a NEW narrow parity gate (⛔ not an extension ofscripts/check-cli-command-ids.mjs, whose recorded refusal at:99-106stands — its population is 811 prose mentions; this one's is ONE fenced block under one known heading): the invocation SET of the docs block under#### \os package publish`incontent/docs/deployment/cli.mdxmust equal the set instatic override examplesofpackages/cli/src/commands/package/publish.ts, compared modulo the leading$, trailing#comments, whitespace and ORDER (the two voices may differ; the invocations may not). Built so a second (command, heading) pair is one table row. Header states why it is a different gate. ⛔ Do not sweep the bare tokensobjectos/cloud(the #14806 trap). Hot files:cli.mdxandpublish.tsFREE of this lane;package.json/lint.ymlare touched by other seats' ready PRs #15392/#15331 on different hunks — merge-tree before ACCEPT.pm:queue→pm:dispatched`, assignee set.Generated by Claude Code
claude commentedon Sep 5, 2026
os-dev-report
{
"issue": 15393,
"status": "done",
"branch": "claude/issue-15393-publish-examples-parity",
"pr": "#15721",
"premise_still_valid": true,
"summary": "Gated it, narrowly, as ruled. New scripts/check-cli-examples-parity.mjs asserts that the invocation SET of the fenced block under a named heading equals the SET in a named oclif
examplesarray, modulo the leading$prompt, trailing#comments, whitespace and ORDER; population is a TABLE with today exactly one row (packages/cli/src/commands/package/publish.ts examples paired with content/docs/deployment/cli.mdx#### os package publish), a second pair being one more row. Wired as check:cli-examples-parity (--self-test && gate) in root package.json and as one step in lint.yml'sLint & Repo Gatesjob, immediately after the CLI command-id step. The header states in its own words that this is a DIFFERENT gate from check-cli-command-ids with a DIFFERENT population and does not reopen that gate's recorded prose refusal; that file is untouched, as are publish.ts and cli.mdx. ONE definitional call the ruling did not pre-decide, and it is the reviewer's question: measured on origin/main 54e2369, the docs fence carries SIX lines and the examples array FIVE - the extra docs line isos compile, a setup step, not a sixth way to invoke the command. So the row's command phrase (derived from the command file path plus the owning package's declared oclif.bin, never typed twice) selects which fence lines are compared; a fence line that does not invoke it is a CONTEXT line - counted, named in the verdict, not compared. Under that rule the two sides are EQUAL today (5 == 5) and no change to either file is needed. Under the literal alternative (compare every fence line) the gate would red today and the repair would land in cli.mdx, which this lane holds read-only - so if the PM rules that way, this comes back as a decision rather than a patch. The asymmetry is deliberate and runs the other way on the CLI side: an examples entry that does not invoke the command is a FINDING, never a silently skipped line. Note on card state: the assignee was already set by the PM (baozhoutao) and the claim comment 5549054449 is in place; I set no labels on the card and touched no assignee.","tests": "Final head 374a148 (worktree clean; every reading below is from that head unless marked). Exit codes captured BEFORE any pipe (cmd > file 2>&1; EXIT=$?). [1] node scripts/check-cli-examples-parity.mjs --self-test -> exit 0, verdict line: 'check-cli-examples-parity self-test: 49 cases pass (normalisation of prompts, comments and alignment; the string-literal examples reader and its refusals; the heading/fence locator; set equality over real scratch trees with two rows; and the real exit codes 0 / 1 from a child process).' Three batteries under a declared roster + floor: 22 predicate cases, 16 over real scratch trees on disk (including a SECOND table row judged in the same run as the first, one green one red, each finding attributed to its own row), 8 at PROGRAM level - real trees, the real file spawned as a child process, real exit codes: equal-sets-different-order-different-comments -> 0; an invocation missing on either side -> 1 naming it; a zero-invocation docs side -> 1 saying 'ZERO invocations'; a moved heading -> 1; an absent examples symbol -> 1. [2] pnpm check:cli-examples-parity on the real tree -> exit 0, verdict naming the row: 'os-package-publish: 5
os package publishinvocation(s) in packages/cli/src/commands/package/publish.ts (examples) == 5 in the block at content/docs/deployment/cli.mdx:1627 under ####os package publish(1 context line(s) skipped)' and 'check-cli-examples-parity: 1 declared pair(s) agree as SETS (order, prompts and trailing comments normalised away).' --list prints both five-element sets as identical strings plus the one context line. [3] ABLATION - trap-guarded (trap on EXIT INT TERM), absolute paths from git rev-parse --show-toplevel, mutation proven on disk in BOTH directions by anchor/marker grep counts and git hash-object, not by an editor exit code. No build/dist leg exists for this gate and none is needed: it is a source-reading script run directly by node, with no dist consumer - so the rebuild step that a dist-resolved ablation owes does not apply here, and that is stated rather than skipped. Removed the trailing-comment normalisation (' text = stripTrailingComment(text);', exactly one occurrence, replaced by a marker comment): HEAD blob 63fbfe9e3e1f2f778a182a085632f66b52e09446 == pre-mutation hash; anchor count before 1 / after 0; marker count after 1; mutated hash 3e0f725008da9b91b40dfed04d9ffc7a56714081. ABLATED_PRODUCTION_EXIT=1 with 6 findings (every annotated line stops matching - the predicted direction, RED, observed); ABLATED_SELFTEST_EXIT=1. Restore by git checkout HEAD -- ABSOLUTE_PATH (never a bare checkout): restored hash 63fbfe9e3e1f2f778a182a085632f66b52e09446 == HEAD blob, git diff HEAD empty, RESTORED_PRODUCTION_EXIT=0 with the green verdict above. [4] DISPATCH DERIVATION, before/after, exit 0 each, --repo objectstack-ai/objectstack asserted and held: node scripts/pm/dispatch-gates.mjs --commands packages/cli/src/commands/package/publish.ts -> BEFORE 0 occurrences of cli-examples-parity, AFTER 'pnpm check:cli-examples-parity'; same command for content/docs/deployment/cli.mdx -> BEFORE 0, AFTER 'pnpm check:cli-examples-parity'. Derived for BOTH sides, which is what the table's path literals buy. [5] THE DERIVED FAMILY, re-derived on the final head after git fetch origin main (node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack, exit 0, changeset read from the merge base 54e2369 - 3 paths: .github/workflows/lint.yml, package.json, scripts/check-cli-examples-parity.mjs; identical family to the earlier derivation, 53 commands). ALL 53 run under OS_VERIFY_LOCK_SLOT=issue-15393 via scripts/pm/os-verify-lock.sh; VERDICT line: 'os-verify-lock: VERDICT command-exit 0 - held the lock 665s (11m05s) - waited 403s (6m43s)'. 50 of 53 exit 0, including check:cli-examples-parity, check:cli-command-ids ('411 command-id literal(s) across 121 file(s) outside packages/cli all resolve'), check-self-test-wired (+ --self-test), check-self-test-workflow-commands (+ --self-test), check:ci-filter-parity, check:watch-hint-literal, check:declared-population-live, check:pm-dispatch-gates ('dispatch-gates self-test: 1445 cases pass.'), check:nul-bytes, check-aggregator-roster, check-declaration-mirrors, check-step-collectors, check-whole-set-label-write, check:required-contexts, check:parse-guard, check:entry-guard, check:turbo-task-graph, check:comment-mask-corpus, check:closing-keyword-parity. [5a] check:cli-command-ids REDDED on the first draft and was fixed, not baselined: two self-test fixtures were quoted phrases opening with a real oclif bin name and resolving to nothing ('os package publish-draft ...', 'os nope'). Reworded to a neutral binary for the word-boundary case and a plain heading for the absent-heading case; green since, on the final head. [6] NOT MEASURED, by name: (a) pnpm check:dual-build-cjs-loads and pnpm check:type-check-debt both exit 3, PREREQUISITE NOT MET - they read built output and this tree has no dist/. Not a pass and not a finding; neither is implicated by this diff (both derived from package.json). (b) The workflow-variable families dispatch-gates itself prints as NOT MEASURED (check-shard-attestation, check-test-completeness, the --union-into limb of check-cross-package-test-inputs) - their argv carries values that exist only on a runner. (c) node scripts/check-required-contexts.mjs --verify-required-set exited 2 (NOT VERIFIED, HTTP 401) inside the batch because Node fetch bypassed the session proxy; re-run at the final head as NODE_OPTIONS=--use-env-proxy it exits 0 and reads the live set - that reading produced the out-of-scope finding below. [7] REPO-WIDE LINT: narrowed, and the narrowing is MEASURED, three pieces. (i) Population from eslint's own flat config, not from my guess: run over the three changed paths, eslint reports 'File ignored because no matching configuration was supplied' for package.json and .github/workflows/lint.yml - they are outside its population entirely. (ii) Count from --format json: 3 files passed in, 1 linted, 0 errors and 0 warnings on scripts/check-cli-examples-parity.mjs, exit 0. (iii) Invariance for untouched files: the flat config enables NO type-aware linting (no parserOptions.project anywhere in eslint.config.mjs; its comment at line 328 states the posture), so this diff cannot move the verdict on any file it does not contain. [8] Byte hygiene: pnpm check:nul-bytes exit 0, and grep -naP over the diff and the PR body for C0/C1/DEL control bytes found none. PR body read back from the API: 10323 bytes sent, got.startswith(sent) true, the only appended bytes being GitHub's own session-URL footer block.","mcp_calls": "1 - one targeted search_issues, for the out-of-scope dedup below. Everything else went over repo-scoped REST (probe: GET /issues/15393 -> HTTP 200) and git.",
"open_questions": [
{
"question": "Should a docs fence line that does not invoke the row's command be treated as CONTEXT (skipped, counted, named) or as part of the compared set? The shipped gate takes the first reading; today's block carries
os compileas such a line.","options": [
"A. CONTEXT (shipped): the row's derived command phrase selects the compared lines. The two sides are equal today (5 == 5), no source file changes, and a docs block stays free to show the setup step a reader must run first.",
"B. Compare every fence line: strictly simpler rule, but it reds today's tree, and the only repair is editing content/docs/deployment/cli.mdx (drop or move
os compile) - a file this lane holds read-only, and a change that makes the block worse to paste.","C. Compare every fence line but let a row declare an allow-list of context lines: keeps B's strictness with an escape hatch, at the cost of a hand-kept list per row that nothing re-derives - the shape this repo's own gates warn about."
],
"recommendation": "A, because it is the only reading under which the gate's claim is true of what the two artefacts ARE: an oclif
examplesarray is by construction a list of invocations of one command, while a docs fence is a teaching transcript. B asserts something false about docs blocks and its repair lands in a file the ruling fenced off. If the PM prefers B, this is a needs_decision, not a patch - say so and I will bring it back."},
{
"question": "The ruling said to STOP with needs_decision if the two sets differ today. Under the shipped rule they do NOT differ; under a literal line-by-line reading they do, by the one
os compileline. Is shipping the gate (green today, disclosed here) the right call, or did the ruling intend a stop?","options": [
"A. Ship (done): the gate is green on origin/main, no source file was edited to make it so, and the definitional refinement is stated in the gate header, the PR body and here.",
"B. Treat the six-vs-five reading as the STOP condition, close the PR unmerged and re-rule."
],
"recommendation": "A, because the stop clause exists to prevent two things - silently editing publish.ts/cli.mdx to force agreement, and landing a gate that is red on main. Neither happened. The PR is draft and the call is disclosed in three places, so B remains available at zero cost."
}
],
"out_of_scope_findings": [
"already tracked as #15233 (open, finding/domain:devx/priority:p2) - NOT re-filed:
Governed Surface Queue Guardis required on main but pinned by no REQUIRED_CONTEXTS row (direction B in check-required-contexts --verify-required-set, which is report-only by design, exit 0). Surfaced while re-running that gate with NODE_OPTIONS=--use-env-proxy at the final head; dedup was the one MCP search_issues call."]
}
Generated by Claude Code
claude commentedon Sep 5, 2026
Ruling on the dev's two open questions — PM seat
domain:devx @ objectstack(#6023), sessionsession_012zGPuVVX3deAx9LdjK8jCk. Anchored here on the card; PR #15721 carries the ACCEPT.Q1 — a docs fence line that does not invoke the row's command: A, CONTEXT (as shipped). An oclif
examplesarray is by construction a list of invocations of one command; a docs fence is a teaching transcript that may show the step a reader runs first (os compileatcli.mdx:1628). The gate's claim — "the invocation SETS agree" — is true of what the two artefacts are only under A. B would assert a falsehood about docs blocks and its only repair lands incli.mdx, which this card holds read-only; C is a hand-kept per-row list nothing re-derives. The asymmetry is right: a CLIexamplesentry that does not invoke the command is a FINDING (cli-example-*), and a context line is counted and named in the verdict so it cannot hide.Q2 — ship vs stop: A, ship. The stop clause existed to prevent silently editing
publish.ts/cli.mdxinto agreement and landing a gate red onmain. Neither happened: no source file changed, the gate is green onorigin/main(5 == 5, 1 context line named), and the refinement is stated in the header, the PR body and the report.Not widened:
check-cli-command-ids.mjsuntouched, its recorded refusal stands; the population is a one-row TABLE.Generated by Claude Code
claude commentedon Sep 5, 2026
LANDED — PR #15721 merged as
ef6744930(origin/mainnoweca504724). Card closed byFixes.Probe on re-fetched
origin/main(PM seat, plain node):node scripts/check-cli-examples-parity.mjs→ EXIT=0,✓ check-cli-examples-parity: 1 declared pair(s) agree as SETS (order, prompts and trailing comments normalised away).--self-test→ EXIT=0,✓ check-cli-examples-parity self-test: 49 cases pass (…)node scripts/pm/dispatch-gates.mjs --commands packages/cli/src/commands/package/publish.ts --repo objectstack-ai/objectstack→ the derived union namespnpm check:cli-examples-parity(1 hit).This landing frees
.github/workflows/lint.ymland rootpackage.jsonin this lane's serial-file queue (#15537 + #15569 become dispatchable). Strippingpm:dispatchedand the assignee in the same action.Generated by Claude Code