Skip to content

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants - #13724

Merged
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn
Aug 31, 2026
Merged

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants#13724
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13419 — slice 2 of the 2026-08-31 ruling: 执行要点 2's determination, and 执行要点 3's warning half.

⛔ Deliberately not a closing keyword. 执行要点 2's out-of-repo half and all of 要点 3's fold deletion are unstarted; merging this must close nothing.

Step 1 — the determination: the two name-folds are a fixture collision, not grants

Slice 1's census reports exactly two name-fold dependencies, both cross_scope. Their position half is declared by examples/app-crm/src/security/sales-positions.ts; their permission-set half comes from the vendored packages/metadata/src/__fixtures__/hotcrm-17.1-built-permissions.artifact.json.

The determination is based on load paths, never on file location — "it lives under __fixtures__/" is a hint, not evidence.

controlresult
Negative — non-test loaders of the artifact0. Scanning packages/ examples/ apps/ scripts/ tools/ .github/ for the basename, the repo-relative fixtures directory, and the glob shape (__fixtures__ on the same line as hotcrm or .artifact): 4 references, 2 test + 2 declared instrument, 0 loader
Positive — the walk reached a known readerpackages/metadata/src/plugin-artifact-forward-conversion.test.ts, classified TEST. A zero from a walk that visited nothing is refused
Reverse (live, real tree) — can the scan report at all?A non-test file importing the artifact was written into packages/runtime/src/, confirmed on disk (210 bytes, 1 named reference). Gate exited 1 naming that file. Probe removed; gate back to 0
Reverse (adversarial inputs)15 self-test cases: composition entries, seed paths and build scripts classify as loader; a bare glob sweep matches; __fixtures__ and hotcrm on separate lines does not
Premise — is the fold still there to guard?The artifact still declares permission sets sales_rep / sales_manager; app-crm still declares positions of both names. Either moving reddens the gate rather than passing quietly

⇒ Nothing composes that artifact into a deployment. The two "dependencies" are a collision between an example app's position names and a test fixture's permission-set names — not grants any deployment holds. Materialising junction rows for them would mint two authorizations nothing intends, which is the opposite of what the ruling protects. 执行要点 2's in-repo materialisation worklist is empty, and scripts/check-position-name-fold-loaders.mjs (wired into lint.yml) keeps it empty on purpose: wire the artifact into a real composition and CI reddens, instead of two live grants appearing with nothing said.

The measured / NOT MEASURED boundary

Measured: every in-repo load path for the vendored artifact, across the six scanned roots, for all three spellings a loader can use (literal path, directory, glob).

NOT MEASURED — unchanged from slice 1, and this slice adds nothing to either side:

  • HotCRM's own same-name folds. The vendored artifact carries that app's permission sets but not its position roster, so those folds are NOT MEASURED, not zero — which is why the fold itself is not deleted here.
  • Positions and permission sets created at runtime (Setup, admin UI).
  • Marketplace artifacts not vendored here; per-organization state.

Step 2 — 执行要点 3's warning half

A permission set granted only because a position of the same name resolved by name, with no junction row, now emits a position_name_fold_grant warning. 要点 5 names this as the permitted shape: 「任何行为差异只能表现为拒绝/告警,永不静默改变解析结果」.

The predicate reports position P when a set named P resolved, P was not already requested through the governed channel (junction row or direct assignment — both arrive as set names in context.permissions), and P is not a baseline name in force regardless of the fold.

⚠️ It is keyed on the pair (position N, set N) specifically — slice 1's finding. sales_manageris junction-bound (to crm_sales_user) and folds onto its own same-name set anyway; a predicate asking "is this position bound to anything?" would report neither real fold while looking complete.

Both directions pinned, from the census's own classification

  • MUST FIRE:sales_rep, sales_manager.
  • ⛔ MUST NOT FIRE: the 13 junction bindings (everyone → member_default included), and the 7 inert positions the census prints under a heading saying so — platform_admin, org_owner, org_admin, org_member, guest, finance, legal. Plus org_admin against the real set organization_admin (a near-miss, one underscore away), the baseline case, and the pair once 执行要点 2 materialises it.

A false positive on a built-in identity is this slice's most expensive failure mode, so it is pinned rather than assumed. Three ablations, each direction predicted first, each mutation confirmed on disk and each restore proven byte-identical to the HEAD blob:

ablationpredictedmeasured
remove the report callMUST-FIRE red, MUST-NOT green4 failed / 24 passed — exactly the MUST-FIRE cases
drop the collision clauseevery built-in identity falsely warns19 failed / 9 passed — all 7 inert positions, the organization_admin near-miss, and all 11 non-fold junction bindings
drop the governed-channel clausethe forward-looking negatives break2 failed / 26 passed — the materialised-pair pin and the baseline pin

A fourth attempt silently missed its anchor; the disk-landing guard voided that reading rather than reporting a green from an unmutated tree.

⛔ Boundaries held

  • The name fold is intactconst requested = [...positions, ...explicitPermissionSets]; (relocated to security-plugin.ts:4687 by this diff's own insertions; located by symbol, never by line number).
  • resolve-authz-context.ts is not touched — 0 files in the diff.
  • No resolution result changes. The census re-run after every edit is byte-identical to the pre-change run, and the MUST-FIRE cases assert the resolved set list is unchanged.

Verification

Union re-run at final head f0408bad1, on the tree that ships.

  • 79 gate families derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (re-derived after the docs page entered the diff, which added 20 families the first derivation did not name). 77 green, 0 red, 2 NOT MEASURED.
  • NOT MEASURED (exit 3, PREREQUISITE NOT MET — neither a pass nor a red):check-test-completeness (grades a saved turbo test log CI supplies). check:dual-build-cjs-loads and check:i18n and check:type-check-debt each hit a build prerequisite first; all three were cleared by building the closure each names and then ran green.
  • pnpm lintfull repoeslint . --no-inline-config, green. No narrowing claimed.
  • @objectstack/plugin-security: typecheck green, and --listFiles confirms both edited files are really in the tsc program (1 hit each) — the check is a measurement, not a vacuous green. Full suite 93 files / 1722 tests passed.
  • Three gates went red on this diff and were repaired, not routed around: check-system-context-census (pure anchor line rot from the insertion; repaired with its own documented --fix), check:doc-authoring (a tracker id in a runtime string — stripped to an adjacent comment, per the 2026-08-12 ruling), and check:engine-double-contract (a new pinned double — removed instead, the stub never needed findOne, so no ledger row was added).
  • The new gate script incurred both bare-root obligations; four REFUSE-WIDE verdicts are recorded with measured precision (packages 5562/5625, examples 240/243, apps 36/40, scripts 295/298 — the population is not a subset of each root, it is the root).

Tier

Dispatched not clause ②, and the actual diff agrees: no path touches packages/spec/src/**, no governed surface, and the content limb does not fire — no accept/reject result changes and no published symbol is added (the event constant is deliberately not exported).

Changeset: @objectstack/plugin-security patch — a new operator-visible warning. The scripts/, .github/ and content/docs/ halves publish nothing from any package.

Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs


Generated by Claude Code

@github-actionsgithub-actionsBot added size/l ci/cd documentation Improvements or additions to documentation tests tooling labels Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 6 documentable anchor(s).

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

  • content/docs/permissions/field-level-security.mdx(via SecurityPlugin (symbol))
  • content/docs/permissions/index.mdx(via SecurityPlugin (symbol))
  • content/docs/plugins/packages.mdx(via SecurityPlugin (symbol))
  • content/docs/ui/forms.mdx(via SecurityPlugin (symbol))

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

  • content/docs/releases/implementation-status.mdx(via SecurityPlugin (symbol))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 14 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb — the merge of head f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 into base cc837dbfec53401c03b2d89071c8b4bee1e5ac97, 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 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb && git checkout 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cc837dbfec53401c03b2d89071c8b4bee1e5ac97 f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 && git checkout -B drift-repro cc837dbfec53401c03b2d89071c8b4bee1e5ac97 && git merge --no-ff f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658
node scripts/docs-audit/affected-docs.mjs --json cc837dbfec53401c03b2d89071c8b4bee1e5ac97

⚠️ 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-steve
os-steve marked this pull request as ready for review August 31, 2026 12:47
@os-steve
os-steve enabled auto-merge August 31, 2026 12:47
@os-steve
os-steve added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 936aa2dAug 31, 2026
35 checks passed
@os-steve
os-steve deleted the claude/issue-13419-materialise-and-warn branch August 31, 2026 13:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddocumentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants by claude[bot] · Pull Request #13724 · objectstack-ai/objectstack · GitHub
Skip to content

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants - #13724

Merged
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn
Aug 31, 2026
Merged

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants#13724
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13419 — slice 2 of the 2026-08-31 ruling: 执行要点 2's determination, and 执行要点 3's warning half.

⛔ Deliberately not a closing keyword. 执行要点 2's out-of-repo half and all of 要点 3's fold deletion are unstarted; merging this must close nothing.

Step 1 — the determination: the two name-folds are a fixture collision, not grants

Slice 1's census reports exactly two name-fold dependencies, both cross_scope. Their position half is declared by examples/app-crm/src/security/sales-positions.ts; their permission-set half comes from the vendored packages/metadata/src/__fixtures__/hotcrm-17.1-built-permissions.artifact.json.

The determination is based on load paths, never on file location — "it lives under __fixtures__/" is a hint, not evidence.

controlresult
Negative — non-test loaders of the artifact0. Scanning packages/ examples/ apps/ scripts/ tools/ .github/ for the basename, the repo-relative fixtures directory, and the glob shape (__fixtures__ on the same line as hotcrm or .artifact): 4 references, 2 test + 2 declared instrument, 0 loader
Positive — the walk reached a known readerpackages/metadata/src/plugin-artifact-forward-conversion.test.ts, classified TEST. A zero from a walk that visited nothing is refused
Reverse (live, real tree) — can the scan report at all?A non-test file importing the artifact was written into packages/runtime/src/, confirmed on disk (210 bytes, 1 named reference). Gate exited 1 naming that file. Probe removed; gate back to 0
Reverse (adversarial inputs)15 self-test cases: composition entries, seed paths and build scripts classify as loader; a bare glob sweep matches; __fixtures__ and hotcrm on separate lines does not
Premise — is the fold still there to guard?The artifact still declares permission sets sales_rep / sales_manager; app-crm still declares positions of both names. Either moving reddens the gate rather than passing quietly

⇒ Nothing composes that artifact into a deployment. The two "dependencies" are a collision between an example app's position names and a test fixture's permission-set names — not grants any deployment holds. Materialising junction rows for them would mint two authorizations nothing intends, which is the opposite of what the ruling protects. 执行要点 2's in-repo materialisation worklist is empty, and scripts/check-position-name-fold-loaders.mjs (wired into lint.yml) keeps it empty on purpose: wire the artifact into a real composition and CI reddens, instead of two live grants appearing with nothing said.

The measured / NOT MEASURED boundary

Measured: every in-repo load path for the vendored artifact, across the six scanned roots, for all three spellings a loader can use (literal path, directory, glob).

NOT MEASURED — unchanged from slice 1, and this slice adds nothing to either side:

  • HotCRM's own same-name folds. The vendored artifact carries that app's permission sets but not its position roster, so those folds are NOT MEASURED, not zero — which is why the fold itself is not deleted here.
  • Positions and permission sets created at runtime (Setup, admin UI).
  • Marketplace artifacts not vendored here; per-organization state.

Step 2 — 执行要点 3's warning half

A permission set granted only because a position of the same name resolved by name, with no junction row, now emits a position_name_fold_grant warning. 要点 5 names this as the permitted shape: 「任何行为差异只能表现为拒绝/告警,永不静默改变解析结果」.

The predicate reports position P when a set named P resolved, P was not already requested through the governed channel (junction row or direct assignment — both arrive as set names in context.permissions), and P is not a baseline name in force regardless of the fold.

⚠️ It is keyed on the pair (position N, set N) specifically — slice 1's finding. sales_manageris junction-bound (to crm_sales_user) and folds onto its own same-name set anyway; a predicate asking "is this position bound to anything?" would report neither real fold while looking complete.

Both directions pinned, from the census's own classification

  • MUST FIRE:sales_rep, sales_manager.
  • ⛔ MUST NOT FIRE: the 13 junction bindings (everyone → member_default included), and the 7 inert positions the census prints under a heading saying so — platform_admin, org_owner, org_admin, org_member, guest, finance, legal. Plus org_admin against the real set organization_admin (a near-miss, one underscore away), the baseline case, and the pair once 执行要点 2 materialises it.

A false positive on a built-in identity is this slice's most expensive failure mode, so it is pinned rather than assumed. Three ablations, each direction predicted first, each mutation confirmed on disk and each restore proven byte-identical to the HEAD blob:

ablationpredictedmeasured
remove the report callMUST-FIRE red, MUST-NOT green4 failed / 24 passed — exactly the MUST-FIRE cases
drop the collision clauseevery built-in identity falsely warns19 failed / 9 passed — all 7 inert positions, the organization_admin near-miss, and all 11 non-fold junction bindings
drop the governed-channel clausethe forward-looking negatives break2 failed / 26 passed — the materialised-pair pin and the baseline pin

A fourth attempt silently missed its anchor; the disk-landing guard voided that reading rather than reporting a green from an unmutated tree.

⛔ Boundaries held

  • The name fold is intactconst requested = [...positions, ...explicitPermissionSets]; (relocated to security-plugin.ts:4687 by this diff's own insertions; located by symbol, never by line number).
  • resolve-authz-context.ts is not touched — 0 files in the diff.
  • No resolution result changes. The census re-run after every edit is byte-identical to the pre-change run, and the MUST-FIRE cases assert the resolved set list is unchanged.

Verification

Union re-run at final head f0408bad1, on the tree that ships.

  • 79 gate families derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (re-derived after the docs page entered the diff, which added 20 families the first derivation did not name). 77 green, 0 red, 2 NOT MEASURED.
  • NOT MEASURED (exit 3, PREREQUISITE NOT MET — neither a pass nor a red):check-test-completeness (grades a saved turbo test log CI supplies). check:dual-build-cjs-loads and check:i18n and check:type-check-debt each hit a build prerequisite first; all three were cleared by building the closure each names and then ran green.
  • pnpm lintfull repoeslint . --no-inline-config, green. No narrowing claimed.
  • @objectstack/plugin-security: typecheck green, and --listFiles confirms both edited files are really in the tsc program (1 hit each) — the check is a measurement, not a vacuous green. Full suite 93 files / 1722 tests passed.
  • Three gates went red on this diff and were repaired, not routed around: check-system-context-census (pure anchor line rot from the insertion; repaired with its own documented --fix), check:doc-authoring (a tracker id in a runtime string — stripped to an adjacent comment, per the 2026-08-12 ruling), and check:engine-double-contract (a new pinned double — removed instead, the stub never needed findOne, so no ledger row was added).
  • The new gate script incurred both bare-root obligations; four REFUSE-WIDE verdicts are recorded with measured precision (packages 5562/5625, examples 240/243, apps 36/40, scripts 295/298 — the population is not a subset of each root, it is the root).

Tier

Dispatched not clause ②, and the actual diff agrees: no path touches packages/spec/src/**, no governed surface, and the content limb does not fire — no accept/reject result changes and no published symbol is added (the event constant is deliberately not exported).

Changeset: @objectstack/plugin-security patch — a new operator-visible warning. The scripts/, .github/ and content/docs/ halves publish nothing from any package.

Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs


Generated by Claude Code

@github-actionsgithub-actionsBot added size/l ci/cd documentation Improvements or additions to documentation tests tooling labels Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 6 documentable anchor(s).

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

  • content/docs/permissions/field-level-security.mdx(via SecurityPlugin (symbol))
  • content/docs/permissions/index.mdx(via SecurityPlugin (symbol))
  • content/docs/plugins/packages.mdx(via SecurityPlugin (symbol))
  • content/docs/ui/forms.mdx(via SecurityPlugin (symbol))

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

  • content/docs/releases/implementation-status.mdx(via SecurityPlugin (symbol))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 14 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb — the merge of head f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 into base cc837dbfec53401c03b2d89071c8b4bee1e5ac97, 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 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb && git checkout 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cc837dbfec53401c03b2d89071c8b4bee1e5ac97 f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 && git checkout -B drift-repro cc837dbfec53401c03b2d89071c8b4bee1e5ac97 && git merge --no-ff f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658
node scripts/docs-audit/affected-docs.mjs --json cc837dbfec53401c03b2d89071c8b4bee1e5ac97

⚠️ 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-steve
os-steve marked this pull request as ready for review August 31, 2026 12:47
@os-steve
os-steve enabled auto-merge August 31, 2026 12:47
@os-steve
os-steve added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 936aa2dAug 31, 2026
35 checks passed
@os-steve
os-steve deleted the claude/issue-13419-materialise-and-warn branch August 31, 2026 13:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddocumentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants by claude[bot] · Pull Request #13724 · objectstack-ai/objectstack · GitHub
Skip to content

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants - #13724

Merged
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn
Aug 31, 2026
Merged

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants#13724
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13419 — slice 2 of the 2026-08-31 ruling: 执行要点 2's determination, and 执行要点 3's warning half.

⛔ Deliberately not a closing keyword. 执行要点 2's out-of-repo half and all of 要点 3's fold deletion are unstarted; merging this must close nothing.

Step 1 — the determination: the two name-folds are a fixture collision, not grants

Slice 1's census reports exactly two name-fold dependencies, both cross_scope. Their position half is declared by examples/app-crm/src/security/sales-positions.ts; their permission-set half comes from the vendored packages/metadata/src/__fixtures__/hotcrm-17.1-built-permissions.artifact.json.

The determination is based on load paths, never on file location — "it lives under __fixtures__/" is a hint, not evidence.

controlresult
Negative — non-test loaders of the artifact0. Scanning packages/ examples/ apps/ scripts/ tools/ .github/ for the basename, the repo-relative fixtures directory, and the glob shape (__fixtures__ on the same line as hotcrm or .artifact): 4 references, 2 test + 2 declared instrument, 0 loader
Positive — the walk reached a known readerpackages/metadata/src/plugin-artifact-forward-conversion.test.ts, classified TEST. A zero from a walk that visited nothing is refused
Reverse (live, real tree) — can the scan report at all?A non-test file importing the artifact was written into packages/runtime/src/, confirmed on disk (210 bytes, 1 named reference). Gate exited 1 naming that file. Probe removed; gate back to 0
Reverse (adversarial inputs)15 self-test cases: composition entries, seed paths and build scripts classify as loader; a bare glob sweep matches; __fixtures__ and hotcrm on separate lines does not
Premise — is the fold still there to guard?The artifact still declares permission sets sales_rep / sales_manager; app-crm still declares positions of both names. Either moving reddens the gate rather than passing quietly

⇒ Nothing composes that artifact into a deployment. The two "dependencies" are a collision between an example app's position names and a test fixture's permission-set names — not grants any deployment holds. Materialising junction rows for them would mint two authorizations nothing intends, which is the opposite of what the ruling protects. 执行要点 2's in-repo materialisation worklist is empty, and scripts/check-position-name-fold-loaders.mjs (wired into lint.yml) keeps it empty on purpose: wire the artifact into a real composition and CI reddens, instead of two live grants appearing with nothing said.

The measured / NOT MEASURED boundary

Measured: every in-repo load path for the vendored artifact, across the six scanned roots, for all three spellings a loader can use (literal path, directory, glob).

NOT MEASURED — unchanged from slice 1, and this slice adds nothing to either side:

  • HotCRM's own same-name folds. The vendored artifact carries that app's permission sets but not its position roster, so those folds are NOT MEASURED, not zero — which is why the fold itself is not deleted here.
  • Positions and permission sets created at runtime (Setup, admin UI).
  • Marketplace artifacts not vendored here; per-organization state.

Step 2 — 执行要点 3's warning half

A permission set granted only because a position of the same name resolved by name, with no junction row, now emits a position_name_fold_grant warning. 要点 5 names this as the permitted shape: 「任何行为差异只能表现为拒绝/告警,永不静默改变解析结果」.

The predicate reports position P when a set named P resolved, P was not already requested through the governed channel (junction row or direct assignment — both arrive as set names in context.permissions), and P is not a baseline name in force regardless of the fold.

⚠️ It is keyed on the pair (position N, set N) specifically — slice 1's finding. sales_manageris junction-bound (to crm_sales_user) and folds onto its own same-name set anyway; a predicate asking "is this position bound to anything?" would report neither real fold while looking complete.

Both directions pinned, from the census's own classification

  • MUST FIRE:sales_rep, sales_manager.
  • ⛔ MUST NOT FIRE: the 13 junction bindings (everyone → member_default included), and the 7 inert positions the census prints under a heading saying so — platform_admin, org_owner, org_admin, org_member, guest, finance, legal. Plus org_admin against the real set organization_admin (a near-miss, one underscore away), the baseline case, and the pair once 执行要点 2 materialises it.

A false positive on a built-in identity is this slice's most expensive failure mode, so it is pinned rather than assumed. Three ablations, each direction predicted first, each mutation confirmed on disk and each restore proven byte-identical to the HEAD blob:

ablationpredictedmeasured
remove the report callMUST-FIRE red, MUST-NOT green4 failed / 24 passed — exactly the MUST-FIRE cases
drop the collision clauseevery built-in identity falsely warns19 failed / 9 passed — all 7 inert positions, the organization_admin near-miss, and all 11 non-fold junction bindings
drop the governed-channel clausethe forward-looking negatives break2 failed / 26 passed — the materialised-pair pin and the baseline pin

A fourth attempt silently missed its anchor; the disk-landing guard voided that reading rather than reporting a green from an unmutated tree.

⛔ Boundaries held

  • The name fold is intactconst requested = [...positions, ...explicitPermissionSets]; (relocated to security-plugin.ts:4687 by this diff's own insertions; located by symbol, never by line number).
  • resolve-authz-context.ts is not touched — 0 files in the diff.
  • No resolution result changes. The census re-run after every edit is byte-identical to the pre-change run, and the MUST-FIRE cases assert the resolved set list is unchanged.

Verification

Union re-run at final head f0408bad1, on the tree that ships.

  • 79 gate families derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (re-derived after the docs page entered the diff, which added 20 families the first derivation did not name). 77 green, 0 red, 2 NOT MEASURED.
  • NOT MEASURED (exit 3, PREREQUISITE NOT MET — neither a pass nor a red):check-test-completeness (grades a saved turbo test log CI supplies). check:dual-build-cjs-loads and check:i18n and check:type-check-debt each hit a build prerequisite first; all three were cleared by building the closure each names and then ran green.
  • pnpm lintfull repoeslint . --no-inline-config, green. No narrowing claimed.
  • @objectstack/plugin-security: typecheck green, and --listFiles confirms both edited files are really in the tsc program (1 hit each) — the check is a measurement, not a vacuous green. Full suite 93 files / 1722 tests passed.
  • Three gates went red on this diff and were repaired, not routed around: check-system-context-census (pure anchor line rot from the insertion; repaired with its own documented --fix), check:doc-authoring (a tracker id in a runtime string — stripped to an adjacent comment, per the 2026-08-12 ruling), and check:engine-double-contract (a new pinned double — removed instead, the stub never needed findOne, so no ledger row was added).
  • The new gate script incurred both bare-root obligations; four REFUSE-WIDE verdicts are recorded with measured precision (packages 5562/5625, examples 240/243, apps 36/40, scripts 295/298 — the population is not a subset of each root, it is the root).

Tier

Dispatched not clause ②, and the actual diff agrees: no path touches packages/spec/src/**, no governed surface, and the content limb does not fire — no accept/reject result changes and no published symbol is added (the event constant is deliberately not exported).

Changeset: @objectstack/plugin-security patch — a new operator-visible warning. The scripts/, .github/ and content/docs/ halves publish nothing from any package.

Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs


Generated by Claude Code

@github-actionsgithub-actionsBot added size/l ci/cd documentation Improvements or additions to documentation tests tooling labels Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 6 documentable anchor(s).

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

  • content/docs/permissions/field-level-security.mdx(via SecurityPlugin (symbol))
  • content/docs/permissions/index.mdx(via SecurityPlugin (symbol))
  • content/docs/plugins/packages.mdx(via SecurityPlugin (symbol))
  • content/docs/ui/forms.mdx(via SecurityPlugin (symbol))

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

  • content/docs/releases/implementation-status.mdx(via SecurityPlugin (symbol))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 14 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb — the merge of head f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 into base cc837dbfec53401c03b2d89071c8b4bee1e5ac97, 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 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb && git checkout 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cc837dbfec53401c03b2d89071c8b4bee1e5ac97 f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 && git checkout -B drift-repro cc837dbfec53401c03b2d89071c8b4bee1e5ac97 && git merge --no-ff f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658
node scripts/docs-audit/affected-docs.mjs --json cc837dbfec53401c03b2d89071c8b4bee1e5ac97

⚠️ 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-steve
os-steve marked this pull request as ready for review August 31, 2026 12:47
@os-steve
os-steve enabled auto-merge August 31, 2026 12:47
@os-steve
os-steve added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 936aa2dAug 31, 2026
35 checks passed
@os-steve
os-steve deleted the claude/issue-13419-materialise-and-warn branch August 31, 2026 13:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddocumentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants by claude[bot] · Pull Request #13724 · objectstack-ai/objectstack · GitHub
Skip to content

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants - #13724

Merged
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn
Aug 31, 2026
Merged

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants#13724
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13419 — slice 2 of the 2026-08-31 ruling: 执行要点 2's determination, and 执行要点 3's warning half.

⛔ Deliberately not a closing keyword. 执行要点 2's out-of-repo half and all of 要点 3's fold deletion are unstarted; merging this must close nothing.

Step 1 — the determination: the two name-folds are a fixture collision, not grants

Slice 1's census reports exactly two name-fold dependencies, both cross_scope. Their position half is declared by examples/app-crm/src/security/sales-positions.ts; their permission-set half comes from the vendored packages/metadata/src/__fixtures__/hotcrm-17.1-built-permissions.artifact.json.

The determination is based on load paths, never on file location — "it lives under __fixtures__/" is a hint, not evidence.

controlresult
Negative — non-test loaders of the artifact0. Scanning packages/ examples/ apps/ scripts/ tools/ .github/ for the basename, the repo-relative fixtures directory, and the glob shape (__fixtures__ on the same line as hotcrm or .artifact): 4 references, 2 test + 2 declared instrument, 0 loader
Positive — the walk reached a known readerpackages/metadata/src/plugin-artifact-forward-conversion.test.ts, classified TEST. A zero from a walk that visited nothing is refused
Reverse (live, real tree) — can the scan report at all?A non-test file importing the artifact was written into packages/runtime/src/, confirmed on disk (210 bytes, 1 named reference). Gate exited 1 naming that file. Probe removed; gate back to 0
Reverse (adversarial inputs)15 self-test cases: composition entries, seed paths and build scripts classify as loader; a bare glob sweep matches; __fixtures__ and hotcrm on separate lines does not
Premise — is the fold still there to guard?The artifact still declares permission sets sales_rep / sales_manager; app-crm still declares positions of both names. Either moving reddens the gate rather than passing quietly

⇒ Nothing composes that artifact into a deployment. The two "dependencies" are a collision between an example app's position names and a test fixture's permission-set names — not grants any deployment holds. Materialising junction rows for them would mint two authorizations nothing intends, which is the opposite of what the ruling protects. 执行要点 2's in-repo materialisation worklist is empty, and scripts/check-position-name-fold-loaders.mjs (wired into lint.yml) keeps it empty on purpose: wire the artifact into a real composition and CI reddens, instead of two live grants appearing with nothing said.

The measured / NOT MEASURED boundary

Measured: every in-repo load path for the vendored artifact, across the six scanned roots, for all three spellings a loader can use (literal path, directory, glob).

NOT MEASURED — unchanged from slice 1, and this slice adds nothing to either side:

  • HotCRM's own same-name folds. The vendored artifact carries that app's permission sets but not its position roster, so those folds are NOT MEASURED, not zero — which is why the fold itself is not deleted here.
  • Positions and permission sets created at runtime (Setup, admin UI).
  • Marketplace artifacts not vendored here; per-organization state.

Step 2 — 执行要点 3's warning half

A permission set granted only because a position of the same name resolved by name, with no junction row, now emits a position_name_fold_grant warning. 要点 5 names this as the permitted shape: 「任何行为差异只能表现为拒绝/告警,永不静默改变解析结果」.

The predicate reports position P when a set named P resolved, P was not already requested through the governed channel (junction row or direct assignment — both arrive as set names in context.permissions), and P is not a baseline name in force regardless of the fold.

⚠️ It is keyed on the pair (position N, set N) specifically — slice 1's finding. sales_manageris junction-bound (to crm_sales_user) and folds onto its own same-name set anyway; a predicate asking "is this position bound to anything?" would report neither real fold while looking complete.

Both directions pinned, from the census's own classification

  • MUST FIRE:sales_rep, sales_manager.
  • ⛔ MUST NOT FIRE: the 13 junction bindings (everyone → member_default included), and the 7 inert positions the census prints under a heading saying so — platform_admin, org_owner, org_admin, org_member, guest, finance, legal. Plus org_admin against the real set organization_admin (a near-miss, one underscore away), the baseline case, and the pair once 执行要点 2 materialises it.

A false positive on a built-in identity is this slice's most expensive failure mode, so it is pinned rather than assumed. Three ablations, each direction predicted first, each mutation confirmed on disk and each restore proven byte-identical to the HEAD blob:

ablationpredictedmeasured
remove the report callMUST-FIRE red, MUST-NOT green4 failed / 24 passed — exactly the MUST-FIRE cases
drop the collision clauseevery built-in identity falsely warns19 failed / 9 passed — all 7 inert positions, the organization_admin near-miss, and all 11 non-fold junction bindings
drop the governed-channel clausethe forward-looking negatives break2 failed / 26 passed — the materialised-pair pin and the baseline pin

A fourth attempt silently missed its anchor; the disk-landing guard voided that reading rather than reporting a green from an unmutated tree.

⛔ Boundaries held

  • The name fold is intactconst requested = [...positions, ...explicitPermissionSets]; (relocated to security-plugin.ts:4687 by this diff's own insertions; located by symbol, never by line number).
  • resolve-authz-context.ts is not touched — 0 files in the diff.
  • No resolution result changes. The census re-run after every edit is byte-identical to the pre-change run, and the MUST-FIRE cases assert the resolved set list is unchanged.

Verification

Union re-run at final head f0408bad1, on the tree that ships.

  • 79 gate families derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (re-derived after the docs page entered the diff, which added 20 families the first derivation did not name). 77 green, 0 red, 2 NOT MEASURED.
  • NOT MEASURED (exit 3, PREREQUISITE NOT MET — neither a pass nor a red):check-test-completeness (grades a saved turbo test log CI supplies). check:dual-build-cjs-loads and check:i18n and check:type-check-debt each hit a build prerequisite first; all three were cleared by building the closure each names and then ran green.
  • pnpm lintfull repoeslint . --no-inline-config, green. No narrowing claimed.
  • @objectstack/plugin-security: typecheck green, and --listFiles confirms both edited files are really in the tsc program (1 hit each) — the check is a measurement, not a vacuous green. Full suite 93 files / 1722 tests passed.
  • Three gates went red on this diff and were repaired, not routed around: check-system-context-census (pure anchor line rot from the insertion; repaired with its own documented --fix), check:doc-authoring (a tracker id in a runtime string — stripped to an adjacent comment, per the 2026-08-12 ruling), and check:engine-double-contract (a new pinned double — removed instead, the stub never needed findOne, so no ledger row was added).
  • The new gate script incurred both bare-root obligations; four REFUSE-WIDE verdicts are recorded with measured precision (packages 5562/5625, examples 240/243, apps 36/40, scripts 295/298 — the population is not a subset of each root, it is the root).

Tier

Dispatched not clause ②, and the actual diff agrees: no path touches packages/spec/src/**, no governed surface, and the content limb does not fire — no accept/reject result changes and no published symbol is added (the event constant is deliberately not exported).

Changeset: @objectstack/plugin-security patch — a new operator-visible warning. The scripts/, .github/ and content/docs/ halves publish nothing from any package.

Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs


Generated by Claude Code

@github-actionsgithub-actionsBot added size/l ci/cd documentation Improvements or additions to documentation tests tooling labels Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 6 documentable anchor(s).

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

  • content/docs/permissions/field-level-security.mdx(via SecurityPlugin (symbol))
  • content/docs/permissions/index.mdx(via SecurityPlugin (symbol))
  • content/docs/plugins/packages.mdx(via SecurityPlugin (symbol))
  • content/docs/ui/forms.mdx(via SecurityPlugin (symbol))

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

  • content/docs/releases/implementation-status.mdx(via SecurityPlugin (symbol))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 14 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb — the merge of head f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 into base cc837dbfec53401c03b2d89071c8b4bee1e5ac97, 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 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb && git checkout 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cc837dbfec53401c03b2d89071c8b4bee1e5ac97 f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 && git checkout -B drift-repro cc837dbfec53401c03b2d89071c8b4bee1e5ac97 && git merge --no-ff f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658
node scripts/docs-audit/affected-docs.mjs --json cc837dbfec53401c03b2d89071c8b4bee1e5ac97

⚠️ 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-steve
os-steve marked this pull request as ready for review August 31, 2026 12:47
@os-steve
os-steve enabled auto-merge August 31, 2026 12:47
@os-steve
os-steve added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 936aa2dAug 31, 2026
35 checks passed
@os-steve
os-steve deleted the claude/issue-13419-materialise-and-warn branch August 31, 2026 13:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddocumentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants by claude[bot] · Pull Request #13724 · objectstack-ai/objectstack · GitHub
Skip to content

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants - #13724

Merged
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn
Aug 31, 2026
Merged

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants#13724
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13419 — slice 2 of the 2026-08-31 ruling: 执行要点 2's determination, and 执行要点 3's warning half.

⛔ Deliberately not a closing keyword. 执行要点 2's out-of-repo half and all of 要点 3's fold deletion are unstarted; merging this must close nothing.

Step 1 — the determination: the two name-folds are a fixture collision, not grants

Slice 1's census reports exactly two name-fold dependencies, both cross_scope. Their position half is declared by examples/app-crm/src/security/sales-positions.ts; their permission-set half comes from the vendored packages/metadata/src/__fixtures__/hotcrm-17.1-built-permissions.artifact.json.

The determination is based on load paths, never on file location — "it lives under __fixtures__/" is a hint, not evidence.

controlresult
Negative — non-test loaders of the artifact0. Scanning packages/ examples/ apps/ scripts/ tools/ .github/ for the basename, the repo-relative fixtures directory, and the glob shape (__fixtures__ on the same line as hotcrm or .artifact): 4 references, 2 test + 2 declared instrument, 0 loader
Positive — the walk reached a known readerpackages/metadata/src/plugin-artifact-forward-conversion.test.ts, classified TEST. A zero from a walk that visited nothing is refused
Reverse (live, real tree) — can the scan report at all?A non-test file importing the artifact was written into packages/runtime/src/, confirmed on disk (210 bytes, 1 named reference). Gate exited 1 naming that file. Probe removed; gate back to 0
Reverse (adversarial inputs)15 self-test cases: composition entries, seed paths and build scripts classify as loader; a bare glob sweep matches; __fixtures__ and hotcrm on separate lines does not
Premise — is the fold still there to guard?The artifact still declares permission sets sales_rep / sales_manager; app-crm still declares positions of both names. Either moving reddens the gate rather than passing quietly

⇒ Nothing composes that artifact into a deployment. The two "dependencies" are a collision between an example app's position names and a test fixture's permission-set names — not grants any deployment holds. Materialising junction rows for them would mint two authorizations nothing intends, which is the opposite of what the ruling protects. 执行要点 2's in-repo materialisation worklist is empty, and scripts/check-position-name-fold-loaders.mjs (wired into lint.yml) keeps it empty on purpose: wire the artifact into a real composition and CI reddens, instead of two live grants appearing with nothing said.

The measured / NOT MEASURED boundary

Measured: every in-repo load path for the vendored artifact, across the six scanned roots, for all three spellings a loader can use (literal path, directory, glob).

NOT MEASURED — unchanged from slice 1, and this slice adds nothing to either side:

  • HotCRM's own same-name folds. The vendored artifact carries that app's permission sets but not its position roster, so those folds are NOT MEASURED, not zero — which is why the fold itself is not deleted here.
  • Positions and permission sets created at runtime (Setup, admin UI).
  • Marketplace artifacts not vendored here; per-organization state.

Step 2 — 执行要点 3's warning half

A permission set granted only because a position of the same name resolved by name, with no junction row, now emits a position_name_fold_grant warning. 要点 5 names this as the permitted shape: 「任何行为差异只能表现为拒绝/告警,永不静默改变解析结果」.

The predicate reports position P when a set named P resolved, P was not already requested through the governed channel (junction row or direct assignment — both arrive as set names in context.permissions), and P is not a baseline name in force regardless of the fold.

⚠️ It is keyed on the pair (position N, set N) specifically — slice 1's finding. sales_manageris junction-bound (to crm_sales_user) and folds onto its own same-name set anyway; a predicate asking "is this position bound to anything?" would report neither real fold while looking complete.

Both directions pinned, from the census's own classification

  • MUST FIRE:sales_rep, sales_manager.
  • ⛔ MUST NOT FIRE: the 13 junction bindings (everyone → member_default included), and the 7 inert positions the census prints under a heading saying so — platform_admin, org_owner, org_admin, org_member, guest, finance, legal. Plus org_admin against the real set organization_admin (a near-miss, one underscore away), the baseline case, and the pair once 执行要点 2 materialises it.

A false positive on a built-in identity is this slice's most expensive failure mode, so it is pinned rather than assumed. Three ablations, each direction predicted first, each mutation confirmed on disk and each restore proven byte-identical to the HEAD blob:

ablationpredictedmeasured
remove the report callMUST-FIRE red, MUST-NOT green4 failed / 24 passed — exactly the MUST-FIRE cases
drop the collision clauseevery built-in identity falsely warns19 failed / 9 passed — all 7 inert positions, the organization_admin near-miss, and all 11 non-fold junction bindings
drop the governed-channel clausethe forward-looking negatives break2 failed / 26 passed — the materialised-pair pin and the baseline pin

A fourth attempt silently missed its anchor; the disk-landing guard voided that reading rather than reporting a green from an unmutated tree.

⛔ Boundaries held

  • The name fold is intactconst requested = [...positions, ...explicitPermissionSets]; (relocated to security-plugin.ts:4687 by this diff's own insertions; located by symbol, never by line number).
  • resolve-authz-context.ts is not touched — 0 files in the diff.
  • No resolution result changes. The census re-run after every edit is byte-identical to the pre-change run, and the MUST-FIRE cases assert the resolved set list is unchanged.

Verification

Union re-run at final head f0408bad1, on the tree that ships.

  • 79 gate families derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (re-derived after the docs page entered the diff, which added 20 families the first derivation did not name). 77 green, 0 red, 2 NOT MEASURED.
  • NOT MEASURED (exit 3, PREREQUISITE NOT MET — neither a pass nor a red):check-test-completeness (grades a saved turbo test log CI supplies). check:dual-build-cjs-loads and check:i18n and check:type-check-debt each hit a build prerequisite first; all three were cleared by building the closure each names and then ran green.
  • pnpm lintfull repoeslint . --no-inline-config, green. No narrowing claimed.
  • @objectstack/plugin-security: typecheck green, and --listFiles confirms both edited files are really in the tsc program (1 hit each) — the check is a measurement, not a vacuous green. Full suite 93 files / 1722 tests passed.
  • Three gates went red on this diff and were repaired, not routed around: check-system-context-census (pure anchor line rot from the insertion; repaired with its own documented --fix), check:doc-authoring (a tracker id in a runtime string — stripped to an adjacent comment, per the 2026-08-12 ruling), and check:engine-double-contract (a new pinned double — removed instead, the stub never needed findOne, so no ledger row was added).
  • The new gate script incurred both bare-root obligations; four REFUSE-WIDE verdicts are recorded with measured precision (packages 5562/5625, examples 240/243, apps 36/40, scripts 295/298 — the population is not a subset of each root, it is the root).

Tier

Dispatched not clause ②, and the actual diff agrees: no path touches packages/spec/src/**, no governed surface, and the content limb does not fire — no accept/reject result changes and no published symbol is added (the event constant is deliberately not exported).

Changeset: @objectstack/plugin-security patch — a new operator-visible warning. The scripts/, .github/ and content/docs/ halves publish nothing from any package.

Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs


Generated by Claude Code

@github-actionsgithub-actionsBot added size/l ci/cd documentation Improvements or additions to documentation tests tooling labels Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 6 documentable anchor(s).

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

  • content/docs/permissions/field-level-security.mdx(via SecurityPlugin (symbol))
  • content/docs/permissions/index.mdx(via SecurityPlugin (symbol))
  • content/docs/plugins/packages.mdx(via SecurityPlugin (symbol))
  • content/docs/ui/forms.mdx(via SecurityPlugin (symbol))

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

  • content/docs/releases/implementation-status.mdx(via SecurityPlugin (symbol))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 14 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb — the merge of head f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 into base cc837dbfec53401c03b2d89071c8b4bee1e5ac97, 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 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb && git checkout 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cc837dbfec53401c03b2d89071c8b4bee1e5ac97 f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 && git checkout -B drift-repro cc837dbfec53401c03b2d89071c8b4bee1e5ac97 && git merge --no-ff f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658
node scripts/docs-audit/affected-docs.mjs --json cc837dbfec53401c03b2d89071c8b4bee1e5ac97

⚠️ 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-steve
os-steve marked this pull request as ready for review August 31, 2026 12:47
@os-steve
os-steve enabled auto-merge August 31, 2026 12:47
@os-steve
os-steve added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 936aa2dAug 31, 2026
35 checks passed
@os-steve
os-steve deleted the claude/issue-13419-materialise-and-warn branch August 31, 2026 13:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddocumentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants by claude[bot] · Pull Request #13724 · objectstack-ai/objectstack · GitHub
Skip to content

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants - #13724

Merged
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn
Aug 31, 2026
Merged

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants#13724
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13419 — slice 2 of the 2026-08-31 ruling: 执行要点 2's determination, and 执行要点 3's warning half.

⛔ Deliberately not a closing keyword. 执行要点 2's out-of-repo half and all of 要点 3's fold deletion are unstarted; merging this must close nothing.

Step 1 — the determination: the two name-folds are a fixture collision, not grants

Slice 1's census reports exactly two name-fold dependencies, both cross_scope. Their position half is declared by examples/app-crm/src/security/sales-positions.ts; their permission-set half comes from the vendored packages/metadata/src/__fixtures__/hotcrm-17.1-built-permissions.artifact.json.

The determination is based on load paths, never on file location — "it lives under __fixtures__/" is a hint, not evidence.

controlresult
Negative — non-test loaders of the artifact0. Scanning packages/ examples/ apps/ scripts/ tools/ .github/ for the basename, the repo-relative fixtures directory, and the glob shape (__fixtures__ on the same line as hotcrm or .artifact): 4 references, 2 test + 2 declared instrument, 0 loader
Positive — the walk reached a known readerpackages/metadata/src/plugin-artifact-forward-conversion.test.ts, classified TEST. A zero from a walk that visited nothing is refused
Reverse (live, real tree) — can the scan report at all?A non-test file importing the artifact was written into packages/runtime/src/, confirmed on disk (210 bytes, 1 named reference). Gate exited 1 naming that file. Probe removed; gate back to 0
Reverse (adversarial inputs)15 self-test cases: composition entries, seed paths and build scripts classify as loader; a bare glob sweep matches; __fixtures__ and hotcrm on separate lines does not
Premise — is the fold still there to guard?The artifact still declares permission sets sales_rep / sales_manager; app-crm still declares positions of both names. Either moving reddens the gate rather than passing quietly

⇒ Nothing composes that artifact into a deployment. The two "dependencies" are a collision between an example app's position names and a test fixture's permission-set names — not grants any deployment holds. Materialising junction rows for them would mint two authorizations nothing intends, which is the opposite of what the ruling protects. 执行要点 2's in-repo materialisation worklist is empty, and scripts/check-position-name-fold-loaders.mjs (wired into lint.yml) keeps it empty on purpose: wire the artifact into a real composition and CI reddens, instead of two live grants appearing with nothing said.

The measured / NOT MEASURED boundary

Measured: every in-repo load path for the vendored artifact, across the six scanned roots, for all three spellings a loader can use (literal path, directory, glob).

NOT MEASURED — unchanged from slice 1, and this slice adds nothing to either side:

  • HotCRM's own same-name folds. The vendored artifact carries that app's permission sets but not its position roster, so those folds are NOT MEASURED, not zero — which is why the fold itself is not deleted here.
  • Positions and permission sets created at runtime (Setup, admin UI).
  • Marketplace artifacts not vendored here; per-organization state.

Step 2 — 执行要点 3's warning half

A permission set granted only because a position of the same name resolved by name, with no junction row, now emits a position_name_fold_grant warning. 要点 5 names this as the permitted shape: 「任何行为差异只能表现为拒绝/告警,永不静默改变解析结果」.

The predicate reports position P when a set named P resolved, P was not already requested through the governed channel (junction row or direct assignment — both arrive as set names in context.permissions), and P is not a baseline name in force regardless of the fold.

⚠️ It is keyed on the pair (position N, set N) specifically — slice 1's finding. sales_manageris junction-bound (to crm_sales_user) and folds onto its own same-name set anyway; a predicate asking "is this position bound to anything?" would report neither real fold while looking complete.

Both directions pinned, from the census's own classification

  • MUST FIRE:sales_rep, sales_manager.
  • ⛔ MUST NOT FIRE: the 13 junction bindings (everyone → member_default included), and the 7 inert positions the census prints under a heading saying so — platform_admin, org_owner, org_admin, org_member, guest, finance, legal. Plus org_admin against the real set organization_admin (a near-miss, one underscore away), the baseline case, and the pair once 执行要点 2 materialises it.

A false positive on a built-in identity is this slice's most expensive failure mode, so it is pinned rather than assumed. Three ablations, each direction predicted first, each mutation confirmed on disk and each restore proven byte-identical to the HEAD blob:

ablationpredictedmeasured
remove the report callMUST-FIRE red, MUST-NOT green4 failed / 24 passed — exactly the MUST-FIRE cases
drop the collision clauseevery built-in identity falsely warns19 failed / 9 passed — all 7 inert positions, the organization_admin near-miss, and all 11 non-fold junction bindings
drop the governed-channel clausethe forward-looking negatives break2 failed / 26 passed — the materialised-pair pin and the baseline pin

A fourth attempt silently missed its anchor; the disk-landing guard voided that reading rather than reporting a green from an unmutated tree.

⛔ Boundaries held

  • The name fold is intactconst requested = [...positions, ...explicitPermissionSets]; (relocated to security-plugin.ts:4687 by this diff's own insertions; located by symbol, never by line number).
  • resolve-authz-context.ts is not touched — 0 files in the diff.
  • No resolution result changes. The census re-run after every edit is byte-identical to the pre-change run, and the MUST-FIRE cases assert the resolved set list is unchanged.

Verification

Union re-run at final head f0408bad1, on the tree that ships.

  • 79 gate families derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (re-derived after the docs page entered the diff, which added 20 families the first derivation did not name). 77 green, 0 red, 2 NOT MEASURED.
  • NOT MEASURED (exit 3, PREREQUISITE NOT MET — neither a pass nor a red):check-test-completeness (grades a saved turbo test log CI supplies). check:dual-build-cjs-loads and check:i18n and check:type-check-debt each hit a build prerequisite first; all three were cleared by building the closure each names and then ran green.
  • pnpm lintfull repoeslint . --no-inline-config, green. No narrowing claimed.
  • @objectstack/plugin-security: typecheck green, and --listFiles confirms both edited files are really in the tsc program (1 hit each) — the check is a measurement, not a vacuous green. Full suite 93 files / 1722 tests passed.
  • Three gates went red on this diff and were repaired, not routed around: check-system-context-census (pure anchor line rot from the insertion; repaired with its own documented --fix), check:doc-authoring (a tracker id in a runtime string — stripped to an adjacent comment, per the 2026-08-12 ruling), and check:engine-double-contract (a new pinned double — removed instead, the stub never needed findOne, so no ledger row was added).
  • The new gate script incurred both bare-root obligations; four REFUSE-WIDE verdicts are recorded with measured precision (packages 5562/5625, examples 240/243, apps 36/40, scripts 295/298 — the population is not a subset of each root, it is the root).

Tier

Dispatched not clause ②, and the actual diff agrees: no path touches packages/spec/src/**, no governed surface, and the content limb does not fire — no accept/reject result changes and no published symbol is added (the event constant is deliberately not exported).

Changeset: @objectstack/plugin-security patch — a new operator-visible warning. The scripts/, .github/ and content/docs/ halves publish nothing from any package.

Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs


Generated by Claude Code

@github-actionsgithub-actionsBot added size/l ci/cd documentation Improvements or additions to documentation tests tooling labels Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 6 documentable anchor(s).

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

  • content/docs/permissions/field-level-security.mdx(via SecurityPlugin (symbol))
  • content/docs/permissions/index.mdx(via SecurityPlugin (symbol))
  • content/docs/plugins/packages.mdx(via SecurityPlugin (symbol))
  • content/docs/ui/forms.mdx(via SecurityPlugin (symbol))

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

  • content/docs/releases/implementation-status.mdx(via SecurityPlugin (symbol))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 14 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb — the merge of head f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 into base cc837dbfec53401c03b2d89071c8b4bee1e5ac97, 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 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb && git checkout 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cc837dbfec53401c03b2d89071c8b4bee1e5ac97 f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 && git checkout -B drift-repro cc837dbfec53401c03b2d89071c8b4bee1e5ac97 && git merge --no-ff f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658
node scripts/docs-audit/affected-docs.mjs --json cc837dbfec53401c03b2d89071c8b4bee1e5ac97

⚠️ 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-steve
os-steve marked this pull request as ready for review August 31, 2026 12:47
@os-steve
os-steve enabled auto-merge August 31, 2026 12:47
@os-steve
os-steve added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 936aa2dAug 31, 2026
35 checks passed
@os-steve
os-steve deleted the claude/issue-13419-materialise-and-warn branch August 31, 2026 13:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddocumentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants by claude[bot] · Pull Request #13724 · objectstack-ai/objectstack · GitHub
Skip to content

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants - #13724

Merged
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn
Aug 31, 2026
Merged

Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants#13724
os-steve merged 4 commits into
mainfrom
claude/issue-13419-materialise-and-warn

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13419 — slice 2 of the 2026-08-31 ruling: 执行要点 2's determination, and 执行要点 3's warning half.

⛔ Deliberately not a closing keyword. 执行要点 2's out-of-repo half and all of 要点 3's fold deletion are unstarted; merging this must close nothing.

Step 1 — the determination: the two name-folds are a fixture collision, not grants

Slice 1's census reports exactly two name-fold dependencies, both cross_scope. Their position half is declared by examples/app-crm/src/security/sales-positions.ts; their permission-set half comes from the vendored packages/metadata/src/__fixtures__/hotcrm-17.1-built-permissions.artifact.json.

The determination is based on load paths, never on file location — "it lives under __fixtures__/" is a hint, not evidence.

controlresult
Negative — non-test loaders of the artifact0. Scanning packages/ examples/ apps/ scripts/ tools/ .github/ for the basename, the repo-relative fixtures directory, and the glob shape (__fixtures__ on the same line as hotcrm or .artifact): 4 references, 2 test + 2 declared instrument, 0 loader
Positive — the walk reached a known readerpackages/metadata/src/plugin-artifact-forward-conversion.test.ts, classified TEST. A zero from a walk that visited nothing is refused
Reverse (live, real tree) — can the scan report at all?A non-test file importing the artifact was written into packages/runtime/src/, confirmed on disk (210 bytes, 1 named reference). Gate exited 1 naming that file. Probe removed; gate back to 0
Reverse (adversarial inputs)15 self-test cases: composition entries, seed paths and build scripts classify as loader; a bare glob sweep matches; __fixtures__ and hotcrm on separate lines does not
Premise — is the fold still there to guard?The artifact still declares permission sets sales_rep / sales_manager; app-crm still declares positions of both names. Either moving reddens the gate rather than passing quietly

⇒ Nothing composes that artifact into a deployment. The two "dependencies" are a collision between an example app's position names and a test fixture's permission-set names — not grants any deployment holds. Materialising junction rows for them would mint two authorizations nothing intends, which is the opposite of what the ruling protects. 执行要点 2's in-repo materialisation worklist is empty, and scripts/check-position-name-fold-loaders.mjs (wired into lint.yml) keeps it empty on purpose: wire the artifact into a real composition and CI reddens, instead of two live grants appearing with nothing said.

The measured / NOT MEASURED boundary

Measured: every in-repo load path for the vendored artifact, across the six scanned roots, for all three spellings a loader can use (literal path, directory, glob).

NOT MEASURED — unchanged from slice 1, and this slice adds nothing to either side:

  • HotCRM's own same-name folds. The vendored artifact carries that app's permission sets but not its position roster, so those folds are NOT MEASURED, not zero — which is why the fold itself is not deleted here.
  • Positions and permission sets created at runtime (Setup, admin UI).
  • Marketplace artifacts not vendored here; per-organization state.

Step 2 — 执行要点 3's warning half

A permission set granted only because a position of the same name resolved by name, with no junction row, now emits a position_name_fold_grant warning. 要点 5 names this as the permitted shape: 「任何行为差异只能表现为拒绝/告警,永不静默改变解析结果」.

The predicate reports position P when a set named P resolved, P was not already requested through the governed channel (junction row or direct assignment — both arrive as set names in context.permissions), and P is not a baseline name in force regardless of the fold.

⚠️ It is keyed on the pair (position N, set N) specifically — slice 1's finding. sales_manageris junction-bound (to crm_sales_user) and folds onto its own same-name set anyway; a predicate asking "is this position bound to anything?" would report neither real fold while looking complete.

Both directions pinned, from the census's own classification

  • MUST FIRE:sales_rep, sales_manager.
  • ⛔ MUST NOT FIRE: the 13 junction bindings (everyone → member_default included), and the 7 inert positions the census prints under a heading saying so — platform_admin, org_owner, org_admin, org_member, guest, finance, legal. Plus org_admin against the real set organization_admin (a near-miss, one underscore away), the baseline case, and the pair once 执行要点 2 materialises it.

A false positive on a built-in identity is this slice's most expensive failure mode, so it is pinned rather than assumed. Three ablations, each direction predicted first, each mutation confirmed on disk and each restore proven byte-identical to the HEAD blob:

ablationpredictedmeasured
remove the report callMUST-FIRE red, MUST-NOT green4 failed / 24 passed — exactly the MUST-FIRE cases
drop the collision clauseevery built-in identity falsely warns19 failed / 9 passed — all 7 inert positions, the organization_admin near-miss, and all 11 non-fold junction bindings
drop the governed-channel clausethe forward-looking negatives break2 failed / 26 passed — the materialised-pair pin and the baseline pin

A fourth attempt silently missed its anchor; the disk-landing guard voided that reading rather than reporting a green from an unmutated tree.

⛔ Boundaries held

  • The name fold is intactconst requested = [...positions, ...explicitPermissionSets]; (relocated to security-plugin.ts:4687 by this diff's own insertions; located by symbol, never by line number).
  • resolve-authz-context.ts is not touched — 0 files in the diff.
  • No resolution result changes. The census re-run after every edit is byte-identical to the pre-change run, and the MUST-FIRE cases assert the resolved set list is unchanged.

Verification

Union re-run at final head f0408bad1, on the tree that ships.

  • 79 gate families derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (re-derived after the docs page entered the diff, which added 20 families the first derivation did not name). 77 green, 0 red, 2 NOT MEASURED.
  • NOT MEASURED (exit 3, PREREQUISITE NOT MET — neither a pass nor a red):check-test-completeness (grades a saved turbo test log CI supplies). check:dual-build-cjs-loads and check:i18n and check:type-check-debt each hit a build prerequisite first; all three were cleared by building the closure each names and then ran green.
  • pnpm lintfull repoeslint . --no-inline-config, green. No narrowing claimed.
  • @objectstack/plugin-security: typecheck green, and --listFiles confirms both edited files are really in the tsc program (1 hit each) — the check is a measurement, not a vacuous green. Full suite 93 files / 1722 tests passed.
  • Three gates went red on this diff and were repaired, not routed around: check-system-context-census (pure anchor line rot from the insertion; repaired with its own documented --fix), check:doc-authoring (a tracker id in a runtime string — stripped to an adjacent comment, per the 2026-08-12 ruling), and check:engine-double-contract (a new pinned double — removed instead, the stub never needed findOne, so no ledger row was added).
  • The new gate script incurred both bare-root obligations; four REFUSE-WIDE verdicts are recorded with measured precision (packages 5562/5625, examples 240/243, apps 36/40, scripts 295/298 — the population is not a subset of each root, it is the root).

Tier

Dispatched not clause ②, and the actual diff agrees: no path touches packages/spec/src/**, no governed surface, and the content limb does not fire — no accept/reject result changes and no published symbol is added (the event constant is deliberately not exported).

Changeset: @objectstack/plugin-security patch — a new operator-visible warning. The scripts/, .github/ and content/docs/ halves publish nothing from any package.

Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs


Generated by Claude Code

@github-actionsgithub-actionsBot added size/l ci/cd documentation Improvements or additions to documentation tests tooling labels Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 6 documentable anchor(s).

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

  • content/docs/permissions/field-level-security.mdx(via SecurityPlugin (symbol))
  • content/docs/permissions/index.mdx(via SecurityPlugin (symbol))
  • content/docs/plugins/packages.mdx(via SecurityPlugin (symbol))
  • content/docs/ui/forms.mdx(via SecurityPlugin (symbol))

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

  • content/docs/releases/implementation-status.mdx(via SecurityPlugin (symbol))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 14 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb — the merge of head f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 into base cc837dbfec53401c03b2d89071c8b4bee1e5ac97, 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 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb && git checkout 5e864f73d5fa1dc5e47f14d856647c98dbf08cbb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cc837dbfec53401c03b2d89071c8b4bee1e5ac97 f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658 && git checkout -B drift-repro cc837dbfec53401c03b2d89071c8b4bee1e5ac97 && git merge --no-ff f0408bad1c3c0e20a1d6d5e2e908d9b477fa7658
node scripts/docs-audit/affected-docs.mjs --json cc837dbfec53401c03b2d89071c8b4bee1e5ac97

⚠️ 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-steve
os-steve marked this pull request as ready for review August 31, 2026 12:47
@os-steve
os-steve enabled auto-merge August 31, 2026 12:47
@os-steve
os-steve added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 936aa2dAug 31, 2026
35 checks passed
@os-steve
os-steve deleted the claude/issue-13419-materialise-and-warn branch August 31, 2026 13:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddocumentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude