Skip to content

Survey: which of step 3's claimed platform rules actually fire here, at what severity, and which local assertions are vacuous (epic #1579, step 3 — the half that needs no release) #1613

Closed

Description

@os-steve

Read-only survey. ⛔ Deletes nothing, changes no test, ships no metadata change. Its output is a table, a set of verdicts, and any upstream gap cards it earns.

Split out of #1582–#1587 (epic #1579 step 3), each of which carries one Blocked-by: #1581 over two separable halves. Same shape as the #1581/#1596 split, applied to its six siblings rather than banked once.

Why this is not blocked

#1582's Method step 1, verbatim: "inject one known defect of that rule's class into the metadata on a committed tree, run pnpm lint (strict), confirm exit 1 naming that rule id, restore by blob hash."

⭐ The evidence is the rule id appearing in the output. The exit code is packaging. An injected defect of a warning-severity rule still names its rule id on the pinned 17.3.0 today, at exit 0. Only Method step 2 — deleting the local assertion — genuinely needs --strict, because retiring a local guard is safe only once the platform rule actually reds CI.

⇒ The proof half runs now. The deletion half stays on #1582–#1587, Blocked-by #1581.

⭐ The finding this card exists to get, and it may unblock real work today

The census records @objectstack/lint@17.3.0 shipping ~250 rules — 161 error / 119 warning / 11 advisory — repo-wide. ⛔ That is not a per-family figure and this card must not assume it is.

But if it holds even partly: objectstack lint exits 1 on an error-severity rule today, with no --strict and no #1581. A local assertion duplicating an error-severity platform rule is therefore retirable now, not after a release.

⇒ Output 1 partitions step 3 into work that is already unblocked and work that genuinely waits. That partition is the point of this card. Everything else is in service of it.

The work, in this order — the cheap output first, because it partitions the rest

1. The severity map — all six families, mandatory.
For every rule id claimed by #1582 through #1587, read its shipped severity out of the pinned @objectstack/lint in node_modules, ⛔ not from platform source and ⛔ not from the census's repo-wide 161/119/11. Report one table: rule id · family · severity · which local file claims it.
⚠️ A claimed rule id that does not exist in the pinned package is its own finding — report it; it means a local assertion is credited to a rule that cannot retire it.

2. Vacuity verdict — F1's candidate files.
⛔ Before any ablation. #1611 established this failure mode by measurement: test/demo-staffing.test.ts and test/flow-variable-conditions.test.ts both assert "nothing in this tree does X", and when the tree they walked went empty they passed vacuously — green while inspecting nothing, 61 generated cases down to 47.

For each F1 candidate assertion, establish it is non-vacuous: it inspects a non-empty set, and it goes red on a planted defect. Report the verdict per assertion.

⚠️ This is not a nicety, it breaks step 3's method. A vacuous local assertion passes "the platform rule fires, so this duplicate can go" trivially — it fires on nothing, so nothing contradicts deleting it — while establishing nothing about whether the platform covers the case. Delete it and the coverage was already gone; keep it and you keep a guard that inspects an empty set. ⛔ Either way the ablation told you nothing while looking like it told you something.

3. The firing ablation — F1 only, error-severity rules first.
Inject one known defect per rule id, run pnpm lint, confirm the rule id is named in the output, restore by blob hash. Record the exit code as a separate column — it is what says whether that rule needs #1581 at all.
⛔ Restore must be verified by observation (git status clean, git hash-object equals git rev-parse HEAD:<path>), ⛔ never by a command's exit code alone.
F1 only in this card: prove the method on one family before spending it on six.

4. Gaps filed upstream.
A claimed rule that does not fire on this repo is a platform finding, not a licence to delete. File it in objectstack-ai/objectstack (unassigned, citing rule id and site), leave the local assertion, report it here.
⚠️ These are the reason this card is worth running now rather than inside #1582: each gap needs an upstream card filed, developed, merged and published before the family card depending on it can finish. Found late, that is two serial release cycles instead of one.

Acceptance

A single report comment on this card carrying: the severity map (all six families), the unblocked/blocked partition it implies, F1's per-assertion vacuity verdicts, F1's per-rule firing results with exit codes, and the list of upstream cards filed.

⛔ No deletions. No test changes. No metadata changes. If the survey makes a deletion look obviously right, that is #1582's to do, under its own Blocked-by.

⛔ Not in this card

Provenance

Epic PM session_01DuzfS5chho38Yx1jxx9DEj, on the maintainer's standing instruction 「不要等发版,hotcrm能做的开发可以先做」 (2026-09-05). Audit: #1579 comment 5555488224; the vacuity amendment: #1579 comment 5555623529. The vacuity failure mode was found by #1604's dev in PR #1611 and is measured there, ⛔ not hypothesised here.

Refs: #1579 (epic) · #1582–#1587 (the deletion halves) · #1581 (what they still wait for) · #1604 / PR #1611 (where the vacuity finding came from) · objectstack#15936 (step 4, which the totality half waits on).

Activity

  1. added
    pm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 6, 2026
  2. self-assigned this
    on Sep 6, 2026
  3. os-steve commented on Sep 6, 2026

    @os-steve
    CollaboratorAuthor

    Claim: session_01DuzfS5chho38Yx1jxx9DEj · branch claude/issue-1613-step3-rule-survey

    PM dispatch (epic #1579 seat, step 3 survey half). The dev inherits this claim — it checks that this newest Claim: names its branch, posts no second claim, and ⛔ never writes the assignee field.

    Unblocked as of now. This card's only sequencing constraint was PR #1611, which rewrote two of F1's five file groups (test/flow-decision-authority.test.ts, test/flow-filter-today-token.test.ts) — measuring F1 before it landed would have guaranteed a stale survey. #1611 merged at 23:53:19Z; main carries it at 9924ee1a. Verified from main's log with a working control, ⛔ not from the PR's merged field alone.

    ⚠️ main has moved twice more since (a0362a37, #1612). Cut from whatever origin/main reads when you start and say which SHA that was — this epic has been bitten four times by a measurement that was correct at its base and stale by review.


    Generated by Claude Code

  4. claude commented on Sep 6, 2026

    @claude
    Contributor

    os-dev-report

    Step-3 rule survey — the severity map, the partition, F1's vacuity verdicts and F1's firing ablation

    Base SHA: a0362a37 (fix(ratchet): exempt a ruled ceiling from opportunistic tightening (#1612)). origin/main was re-read at the start of this run and again at the end; it did not move during the survey. Branch claude/issue-1613-step3-rule-survey, worktree-only, no commits, no push, no PR. The claim comment 5555670304 names this branch; no second claim was posted and the assignee field was not touched.

    Read-only, as ruled. Nothing was deleted, no test changed, no metadata changed. Every write to the tree was an injection restored by blob hash in the same run. Final state: git status clean, HEAD == a0362a37, git rev-list --count BASE..HEAD == 0.

    Everything below is measured against the pinned @objectstack/lint@17.3.0 and @objectstack/spec@17.3.0 in node_modules, never the platform source tree, and never the census's repo-wide 161/119/11.

    0. The harness, and the control every negative rests on

    Clean-tree baseline, pnpm lint --json at a0362a37:

    passed=true  errors=0  warnings=17  suggestions=12   exit 0
    warning  component-props-invalid      x16
    warning  component-props-unknown-key  x1
    suggestion  approval-approvers-may-resolve-empty x4 · rollup/missing-summary x2
                object/missing-name-field x2 · relationship/line-items-inline-edit x2
                relationship/line-item-should-be-master-detail x2
    

    Per injection the harness: reads git rev-parse HEAD:PATH; applies a python edit that asserts its anchor occurrence count before and its marker count after; refuses to measure if the on-disk blob is unchanged; runs pnpm lint --json and the owning test file; then restores with git checkout HEAD -- PATH and proves the restore by git hash-object equalling the HEAD blob and git status being clean for that path. A trap … EXIT INT TERM carries the restore on any abort. Heavy runs went through scripts/pm/os-verify-lock.sh; verdicts read from its VERDICT command-exit line.

    ⚠️ Two injections were no-ops and are reported as such rather than silently retried. (1) A first list.tabs injection used member shape { key, label }, which ViewTabSchema refuses outright — the config threw and lint returned an error envelope, not a rule verdict. Redone with the legal { name, label }. (2) A first flow-date-equality-filter injection added a stage_entry_date key to a filter that already had one; the bundler warned Duplicate key and the effective metadata was unchanged. Redone by replacing the existing entry. Both first attempts proved nothing and neither is counted below.

    1. The severity map — all six families, from the pinned package

    95 distinct rule ids are claimed across #1582 to #1587. 94 of them exist in the pinned @objectstack/lint@17.3.0. The one that does not is i18n/missing-* (F4), and that is not a defect: it is synthesised by the CLI, not the lint package — foldCoverageIssues in @objectstack/cli builds the id as i18n/missing- plus the coverage issue's source. So no claimed id is credited to a rule that does not exist. That is the one finding step 1 was told to look for, and it is negative.

    Severities were extracted structurally from the shipped dist/index.js: brace-matched to the object literal carrying each rule: key, reading only that literal's own severity:, then hand-resolved for the sites that delegate to a registry wrapper default or a conditional. Calibration: 15 of these ids were also measured by injection below, and the static reading matched the observed severity 15 times out of 15.

    F1 — flow / predicate / readonly-write (#1582)

    rule id shipped severity claimed by
    flow-inert-node-condition warning (no per-finding severity; lintFlowPatterns wrapper default) test/flow-decision-authority.test.ts
    flow-decision-unconditional-branch warning (wrapper default) same
    flow-multiple-default-edges warning (wrapper default) same
    flow-default-edge-with-condition error same
    liveness-dead-property warning — but unreachable, see §5 test/view-tab-label-inert.test.ts
    liveness-live-elsewhere-property warning — but unreachable, see §5 same
    flow-update-readonly-field error test/readonly-write-semantics.test.ts
    flow-update-readonly-when-field warning same
    hook-api-update-readonly-field error — but unreachable, see §5 same
    hook-api-update-readonly-when-field warning — but unreachable, see §5 same
    action-api-update-readonly-when-field warning same
    filter-token-unknown error — flow surface unreachable, see §5 test/flow-filter-today-token.test.ts (A)
    flow-time-relative-descriptor-invalid error same
    flow-time-relative-antipattern warning (wrapper default) same
    flow-date-equality-filter warning (wrapper default) same
    visibility-predicate-syntax error the predicate group
    visibility-predicate-unknown-function error same
    visibility-bare-identifier error same
    predicate-path-unresolved error — unreachable here, see §5 same
    predicate-path-unrooted error — unreachable here, see §5 same
    expression-invalid per finding; parse and resolution failures are error, some sub-cases warning same
    validation-rule-regex-uncompilable error same

    F2 — reference integrity (#1583)

    rule id severity rule id severity
    page-field-unknown error when the position is queried, else warning nav-target-unresolved warning
    page-field-unprovisioned warning nav-object-unservable error
    page-section-group-unknown warning nav-object-ungranted warning
    list-view-field-unknown position-dependent — see note dashboard-action-route-unresolved warning
    list-view-field-dotted error (always) dashboard-action-target-undefined error
    view-ref-form-target-missing warning action-name-undefined error
    view-ref-nav-view-missing error action-no-placement warning
    component-props-unknown-key warning form-section-group-unknown warning
    component-props-invalid warning field-group-undeclared warning
    default-agent-outside-roster warning field-group-empty warning
    default-agent-legacy-alias warning field-group-shadowed warning
    sort-field-unknown error ai-skill-tool-unresolved warning
    sort-field-unprovisioned warning ai-skill-surface-mismatch error
    searchable-field-unknown error capability-reference-unknown warning
    view-key-collision warning filter-token-unknown error

    ⚠️ list-view-field-unknown has no single severity. validateListViewFieldRefs carries a per-position table: error at the load-bearing positions (columns, filterableFields, grouping.fields, userFilters.fields, kanban.groupByField, calendar.startDateField, gantt.startDateField/endDateField/titleField, gantt.quickFilters, and every filter key) and warning at the inert ones (hiddenFields, fieldOrder, rowColor.field, kanban.columns, most gallery/timeline scalars). A retirement decision on test/view-references.test.ts has to be taken per position, not per rule id. test/smoke.test.ts is credited to os validate structural checks rather than rule ids, so it has no row here.

    F3 — analytics / dataset (#1584)

    chart-dataset-unknown, chart-field-unknown, chart-measure-unknown, widget-measure-unknown, widget-dataset-unknown, widget-sortby-unselected, dataset-field-unknown, filter-preset-comparand, filter-token-unknown, dataset-include-unknown, dataset-field-not-included — all error (11).
    chart-axis-not-selected, measure-aggregate-incoherent — warning (2).

    F3 is the family with the highest error share: 11 of 13.

    F4 — i18n (#1585)

    translation-target-unknown, translation-option-key-unknown, translation-section-name-missing — all warning.
    i18n/missing-* — synthesised by the CLI; error only where computeI18nCoverage itself says error (the default locale), otherwise warning; --i18n-strict promotes the non-default locales. ⚠️ On this repo pnpm lint also hides 2,319 platform-built-in i18n issues by default (--include-platform reveals them), so any figure quoted for this family has to say whether the fold is on.

    F4 has zero error-severity rule ids. Its whole retirement depends on #1581 or on --i18n-strict, which is exactly what scripts/check-lint-i18n-gate.mjs's own header says.

    F5 — security / sharing / org axis (#1586)

    error (15): security-owd-unset, security-owd-alias, security-fls-unqualified-key, security-controlled-by-parent-no-relation, security-controlled-by-parent-ambiguous-relation, security-anchor-high-privilege, rls-predicate-unenforceable, rls-predicate-unparseable, rls-predicate-over-budget, sharing-rule-object-not-shareable, sharing-rule-object-controlled-by-parent, sharing-rule-unlowerable-condition, sharing-rule-runtime-variable-condition, org-axis-permission-inheritance, org-axis-cross-org-bu-grant.
    warning (1): security-master-detail-ungranted.
    ⚠️ security-private-no-readscope is info, which os lint renders as a suggestion. It never gates, under --strict or otherwise, and it is the only claimed id in any family at that level. Given #1586's own instruction that a security assertion is deleted only when its rule is proved to fire, this row needs naming before that card starts.

    F6 — hook / action / flow write shape (#1587)

    error (2): flow-node-write-unknown-field, flow-runas-unscoped.
    mixed (2): flow-template-unknown-field and flow-template-lookup-traversal are error when the path sits inside a filter and warning otherwise (inFilter ? 'error' : 'warning').
    warning (7): hook-body-write-unknown-field, hook-body-write-unprovisioned-anchor, hook-body-source-unparseable, action-body-write-unknown-field, action-body-source-unparseable, action-record-write-discarded, flow-multi-write-unfiltered.
    ⚠️ The three hook-body-* ids are also structurally unreachable on this repo — see §5 and objectstack#16095. test/hook-write-shape.test.ts is credited entirely to those three, so that row is blocked twice over.

    2. ⭐ The partition — how much of step 3 is already unblocked

    objectstack lint on the pinned 17.3.0 has no --strict flag at all (the only strict flag is --i18n-strict), and its exit is one line: if (errors.length > 0) process.exit(1). So an error-severity rule gates today. Confirmed by measurement, not by reading: eight separate injections in §4 produced exit 1.

    By rule id, across all six families (97 rows, 95 distinct ids; a rule claimed by two families counts once per family):

    family claimed ids error warning mixed info
    F1 22 12 10 0 0
    F2 30 9 19 2 0
    F3 13 11 2 0 0
    F4 4 0 4 0 0
    F5 17 15 1 0 1
    F6 11 2 7 2 0
    total 97 49 43 4 1

    Half of step 3's claimed rules — 49 of 97 — are error severity and gate with no --strict and no #1581. F3 (11/13) and F5 (15/17) are almost entirely unblocked; F4 (0/4) is entirely blocked; F1 and F6 are mixed.

    ⚠️ But severity is only the first filter, and on F1 it is not the binding one. Measuring F1 end to end (§4, §5) turns 12 error-severity ids into a much smaller retirable set:

    F1 verdict ids what it means for #1582
    error, fires here, and a local assertion actually duplicates it 5 flow-update-readonly-field, visibility-predicate-syntax, visibility-predicate-unknown-function, visibility-bare-identifier, expression-invalid — retirable now, no release needed
    error, fires here, but no local assertion covers it 3 flow-default-edge-with-condition, flow-time-relative-descriptor-invalid, validation-rule-regex-uncompilable — nothing to retire; the platform is ahead of the tests here
    warning, fires here 7 genuinely waits on #1581
    does not fire here at all 6 (+1 surface) upstream cards filed; local assertions stay; #1581 is irrelevant to them

    So F1's honest partition is 5 unblocked / 7 waiting on #1581 / 7 blocked on an upstream fix, not 12 / 10. ⛔ The same discount must be applied to the other five families before their numbers are trusted — this card only measured F1, and F6's hook-body-* row is already known to be in the third bucket by the same mechanism.

    3. Vacuity verdicts — F1's candidate files, before any firing ablation

    Two independent halves per file: the size of the set it inspects, measured by a probe importing the repo's own modules; and whether it goes red on a planted defect, measured by injection.

    file inspected set (measured) its own floor red on a planted defect verdict
    test/flow-decision-authority.test.ts 24 decision sites >= 15 + a named loop-body site YES non-vacuous
    test/view-tab-label-inert.test.ts 14 bundles / 14 list blocks / 54 listViews entries 12 named objects + > 30 YES non-vacuous
    test/readonly-write-semantics.test.ts 22 cases, executed on a real engine (not a sweep) n/a YES non-vacuous
    test/flow-filter-today-token.test.ts 10 {TODAY()} sites across 6 flows > 0 YES non-vacuous
    test/view-predicate-dialect.test.ts 49 predicates collected vs 21 authored on disk >= authored YES non-vacuous
    test/object-validation-predicates.test.ts 32 script rules / 41 predicates > 15 / > 20 YES non-vacuous
    test/flow-condition-totality.test.ts 14 record/previous condition sites >= 9 + 3 named shapes YES non-vacuous
    test/flow-variable-conditions.test.ts 26 variable-reading condition sites >= 10 YES non-vacuous
    test/line-item-conventions.test.ts 18 object source files > 10 YES non-vacuous

    All nine are non-vacuous. #1611's failure mode is not present anywhere in F1 today. Every one of these files already carries an explicit coverage-floor assertion, and every floor sits below the measured population with real margin.

    Two observations worth carrying forward rather than acting on:

    • The deep walk is doing heavy lifting. flowGraphDeep reaches 293 nodes across the 28 flows where a top-level-only walk reaches 186 — a walk that stopped at config.body would be inspecting 63% of the tree while looking identical.
    • test/flow-condition-totality.test.ts is the one F1 sweep that still walks top-level flow.nodes / flow.edges only. Measured today that costs nothing: 14 record/previous condition sites either way, and the set of missed sites is empty. But the unfiltered counts are 35 top-level vs 49 deep, so 14 condition sites already live only inside regions. The day a record-change condition is authored inside a loop or try_catch, this file goes quietly blind in exactly the Contain a failing item to its own loop iteration: ten sweeps stop aborting mid-batch (42 flow-loop-body-uncontained to 0) #1611 way. ⛔ Not filed and not touched — it is a live guard with no defect today, and it belongs to whoever next opens that file.

    4. The firing ablation — F1, exit code as its own column

    Per rule id: one known defect of that class injected into the committed tree, pnpm lint --json run, the rule id looked for by name, the owning test file run, then restore verified by hash. Injections are listed in the order run; error-severity rules first was the intent, but the file-by-file order below reads better and the results are order-independent.

    rule id fired? severity observed lint exit count the local assertion, on the same defect
    flow-inert-node-condition yes warning 0 1 RED — no decision node carries the inert singular config.condition
    flow-decision-unconditional-branch yes warning 0 1 sweep GREEN, mechanism test RED (see note)
    flow-multiple-default-edges yes warning 0 1 sweep GREEN, mechanism test RED
    flow-default-edge-with-condition yes error 1 1 GREEN — the file has no assertion of this shape
    liveness-dead-property NO — 0 0 RED
    liveness-live-elsewhere-property NO — 0 0 RED
    flow-update-readonly-field yes error 1 1 RED — escalate_case writes only user-writable columns
    flow-update-readonly-when-field yes warning 0 4 GREEN
    hook-api-update-readonly-field NO on a handler hook; yes on a body.source hook error 1 (body.source) / 0 (handler) 1 n/a
    hook-api-update-readonly-when-field NO — same gate, not separately injected warning (declared) — — n/a
    action-api-update-readonly-when-field yes warning 0 1 n/a
    filter-token-unknown yes in a view filter; NO in a flow filter error 1 (view) / 0 (flow) 1 GREEN
    flow-time-relative-descriptor-invalid yes error 1 1 GREEN
    flow-time-relative-antipattern yes warning 0 1 GREEN
    flow-date-equality-filter yes warning 0 1 GREEN
    visibility-predicate-syntax yes error 1 8 RED — 6 of 11 cases
    visibility-predicate-unknown-function yes error 1 8 RED — 5 of 11 cases
    visibility-bare-identifier yes error 1 8 RED — 6 of 11 cases
    predicate-path-unresolved NO — see §5 — — 0 —
    predicate-path-unrooted NO — see §5 — — 0 —
    expression-invalid yes error 1 1 RED — 4 of 78 cases
    validation-rule-regex-uncompilable yes error 1 1 GREEN

    ⭐ Eight rule ids exit 1 today with no --strict and no #1581. The card's central hypothesis holds, and it holds by measurement.

    ⚠️ The duplication claim is weaker than the severity claim, in both directions. Six rows fire while the local assertion stays green, and three of those are cases the local file was never asserting: flow-default-edge-with-condition, flow-time-relative-descriptor-invalid and validation-rule-regex-uncompilable are platform coverage the tests never had. Conversely, flow-decision-unconditional-branch and flow-multiple-default-edges fire on defects that the metadata sweeps in test/flow-decision-authority.test.ts do not catch — only that file's live-engine mechanism test reds. So "the platform rule fires ⇒ the local assertion is a duplicate" is not sound on this evidence; the assertion has to be shown to cover the same predicate, and on F1 it often does not.

    One concrete example, measured: the local sweep every decision node actually decides something catches a decision with zero gated out-edges. The platform rule catches a decision with a mix of gated and ungated. Neither subsumes the other, and the fully-inert case is a platform gap (objectstack#16093). Deleting that assertion on the strength of the rule id would have removed real coverage.

    ⚠️ validation-rule-regex-uncompilable fires, at error, exit 1 — but the repo currently authors zero format validations with a regex (grep over src/objects/*.object.ts finds none; the control for that grep is type: 'script', which finds 32). There is nothing here for it to duplicate in either direction.

    5. The negatives, each with its working control

    Every "does not fire" below is paired with something that does fire in the same harness, same command, same run design.

    (a) The fully-inert decision — objectstack#16093. Removing the condition from check_not_nudged's only out-edge leaves a decision that branches on nothing. Lint stays at the baseline 17 warnings, exit 0, no new rule id. Control: four other injections into the same node and its edges each named their rule id. Cause, read from the shipped dist: const gated = outs.filter(e => e.condition || e.isDefault === true); if (gated.length === 0) continue;.

    (b) liveness-dead-property / liveness-live-elsewhere-property are unreachable — objectstack#16094. A schema-legal list.tabs on crm_account reds the local test and produces no liveness-* finding. Control: externalSharingModel on the same object fires liveness-planned-property at warning. Cause: shouldWarn requires authorWarn === true || status === 'experimental', and enumerating all 36 ledger files gives 636 live · 90 dead · 14 planned · 5 experimental · 1 live-elsewhere, with the opt-in set being exactly ten rows, none of them dead or live-elsewhere. Separately, view.json records list.tabs as status: live, which contradicts #1307's premise; that question is raised on the upstream card, not settled here.

    (c) The whole hook-body family is unreachable for handler-authored hooks — objectstack#16095. The same statement (ctx.api.object('crm_case').update({ id, is_escalated: true }), is_escalated being readonly: true) injected twice: as a handler function it produces nothing, exit 0; as body: { language: 'js', source } it produces hook-api-update-readonly-field at error, exit 1. Cause: if (!isRec5(body) || body.language !== 'js') return;. Population, from the normalized stack: 39 hooks, 39 handlers, 0 bodies — versus 31 actions of which 24 carry body.source. This blocks test/readonly-write-semantics.test.ts (F1) and all three ids on test/hook-write-shape.test.ts (F6).

    (d) filter-token-unknown does not walk flows — objectstack#16096. The identical string '{TOMORROW()}' in a view filter gives error, exit 1; in opportunity_stagnation's query_stalled filter it gives nothing, exit 0. Cause: validateFilterTokens's roots are objects · views · reports · datasets · pages · apps. filter-token-unknown is claimed by F1, F2 and F3, and its flow half is unreachable for all three.

    (e) predicate-path-unresolved / predicate-path-unrooted — a mis-credit, not a platform gap, so nothing was filed. validatePredicatePathRefs opens each form site with const schemaId = schemaIdOf(site.view); if (!schemaId) continue;, and schemaIdOf requires data.provider === 'schema'. These rules are about metadata-editing forms (Studio), not app-object forms. hotcrm has 0 occurrences of schemaId in src/ — control for that grep: provider: in src/views/ returns 76 hits, all provider: 'object'. Confirmed by the three visibility injections, which fired visibility-* at error while never producing a predicate-path-* finding. ⇒ The bare-identifier and unresolvable-path hazards on this repo's forms are covered, by visibility-bare-identifier and visibility-predicate-*. #1582's table credits test/view-predicate-dialect.test.ts to two rule ids that cannot apply to it; the correction belongs on that card, and ⛔ no upstream card was filed, because asking the platform to widen a deliberately metadata-scoped rule would be the wrong ask.

    6. Upstream cards filed

    All four in objectstack-ai/objectstack, unassigned, each citing the rule id, the site, the measurement and its control:

    card subject
    objectstack#16093 flow-decision-unconditional-branch skips the decision that gates on nothing (gated.length === 0 short-circuit)
    objectstack#16094 liveness-dead-property / liveness-live-elsewhere-property unreachable on 17.3.0 — 90 dead + 1 live-elsewhere rows, none with authorWarn; plus view.json's list.tabs: live vs #1307
    objectstack#16095 every hook-body-* / hook-api-update-readonly-* rule is gated on body.language === 'js' — 39/39 of this repo's hooks are handlers
    objectstack#16096 filter-token-unknown never walks flows

    Duplicate search was run for each before filing. ⚠️ Channel switch, declared: the container's REST proxy refuses GET /search/issues (HTTP 403, "sessions are bound to their configured repositories"), while repo-scoped REST reads return 200 for both repos. So dedupe used targeted MCP search_issues calls instead — four of them, each returning non-zero, on-topic results (38 / 17 / 4 / 6 hits), which is the working control that the search was not silently returning zero. Nearest existing cards, all checked and none duplicating: #15429 (open) is the multi-guarded-edge partition problem, not the zero-guarded-edge case #16093 covers; #14057 (closed) fixed describe()'s live-elsewhere throw but not its reachability; #13838 / #13718 (open) are about hook-body lowering from os build's side, not the lint door.

    7. Housekeeping

    {
      "issue": 1613,
      "status": "done",
      "branch": "claude/issue-1613-step3-rule-survey",
      "pr": null,
      "premise_still_valid": true,
      "base_sha": "a0362a37",
      "summary": "Read-only survey delivered as this comment plus four upstream cards. 94 of 95 claimed rule ids exist in the pinned 17.3.0 (the exception, i18n/missing-*, is CLI-synthesised, not missing). objectstack lint has no --strict flag and exits 1 on any error-severity rule today: 49 of 97 claimed rows are error severity, so roughly half of step 3 needs no release. F3 (11/13) and F5 (15/17) are almost entirely unblocked; F4 (0/4) is entirely blocked. All nine F1 candidate files are non-vacuous by measurement. F1's ablation shows 8 rule ids exiting 1 today, but only 5 have a local assertion that actually duplicates them, and 6 more do not fire here at all.",
      "severity_map": { "F1": "12 error / 10 warning", "F2": "9 error / 19 warning / 2 position-dependent", "F3": "11 error / 2 warning", "F4": "0 error / 4 warning", "F5": "15 error / 1 warning / 1 info", "F6": "2 error / 7 warning / 2 inFilter-conditional" },
      "partition_F1_measured": { "retirable_now": 5, "no_local_assertion_to_retire": 3, "waits_on_1581": 7, "blocked_on_upstream": 6 },
      "upstream_filed": ["objectstack#16093", "objectstack#16094", "objectstack#16095", "objectstack#16096"],
      "mcp_calls": 9
    }

    Generated by Claude Code

  5. os-steve commented on Sep 6, 2026

    @os-steve
    CollaboratorAuthor

    ✅ ACCEPT — and the survey's central finding overturns the premise I filed this card on. That is the best possible outcome for it.

    Epic PM session_01DuzfS5chho38Yx1jxx9DEj, 00:5xZ. Base a0362a37.

    Verified independently

    • objectstack#16095 read in full — carries the measurement, a genuine working control (the byte-identical statement in a body: { language: 'js' } hook fires hook-api-update-readonly-field at error, exit 1, while the handler shape produces nothing), and a population count (39 hooks / 39 handlers / 0 bodies).
    • No branch was pushed. claude/issue-1613-* sorts between claude/hotcrm-saas-deployment-ffi3v1 and claude/issue-508-bulk-selectedids-restore — both present in the listing, and the region between them is empty. ⭐ The negative is observed in the right place, not off the end of a page. No PR, no commits, no surviving branch: the read-only contract held.

    ⛔ I was wrong, and the correction is the deliverable

    I filed this card arguing: error-severity rules gate today, so their local duplicates are retirable now — if the census's 161-of-250 holds, a substantial share of step 3 was never release-blocked.

    The severity half of that is true — 49 of 97 claimed rows are error severity. The conclusion I drew from it is not. Measured end to end on F1, of 12 error-severity ids:

    count
    genuinely retirable now 5
    rule fires but no local assertion covers it — nothing to retire, the platform is ahead of the tests 3
    do not fire here at all → blocked on upstream cards, ⛔ #1581 irrelevant to them 6 ids + 1 surface

    ⇒ Severity is the first filter and it is not the binding one. "49 of 97" is an upper bound that a discount of roughly this shape has to be applied to before anyone plans against it. ⛔ Nobody may quote the 49 without the discount.

    ⭐ The finding beyond the brief — step 3's method is unsound, independently of vacuity

    #1582 Method step 2: "Delete only the local assertions whose rule fired."

    That inference does not hold, and the survey measured why rather than arguing it. flow-decision-unconditional-branch and flow-multiple-default-edges fire on defects the corresponding local sweeps do not catch — and the local sweep catches a case the platform rule structurally skips (if (gated.length === 0) continue, so a decision gating on nothing at all is the one case the rule cannot see; now objectstack#16093).

    ⇒ Neither subsumes the other. Deleting on the rule id alone would have removed real coverage — six rows fire while the owning local assertion stays green.

    Step 3's method is amended to three gates. Delete only when all three hold:

    1. Non-vacuous — the local assertion inspects a non-empty set and goes red on its own defect. (From PR Contain a failing item to its own loop iteration: ten sweeps stop aborting mid-batch (42 flow-loop-body-uncontained to 0) #1611's measured 61-vs-47.)
    2. Reachable — the platform rule fires on this repo at all.
    3. Subsuming — the platform rule fires on the local assertion's own defect, not merely on some defect of that rule's class.

    Gate 3 is the new one and it is the one that was missing. A rule that fires on a defect of its class tells you nothing about whether it covers this assertion.

    Rulings on the three open questions

    Q1 — security-private-no-readscope ships at info, so it never gates, under --strict or otherwise. → Option A. The local assertion is permanently non-retirable and the row comes off #1586's table.
    ⛔ C is the trap and is refused explicitly: #1581 promotes warnings, not info/suggestions, so a card claiming "strict covers it" would be false for this row.
    On B (file upstream asking for a severity re-derivation): ⛔ not now. Under ruling A nothing waits on the answer — the local assertion simply stays — so the critical-path argument that justified this whole card does not apply here, and filing without a stated position on what the severity should be is the noise this lane's charter warns against. If #1586's dev develops that position, it files then.

    Q2 — list-view-field-unknown has per-position severity, not per-rule. → Option A, per position. C would silently drop coverage on the inert positions that only exit 0; B blocks the load-bearing half for no reason.
    ⇒ ⭐ The severity map's unit is (rule id, position), not rule id. Same shape applies to flow-template-unknown-field and flow-template-lookup-traversal (F6, error inside a filter / warning outside) and page-field-unknown (F2). Carried into the F2–F6 continuation.

    Q3 — flow-condition-totality.test.ts still walks top-level only. → recorded, ⛔ not filed and ⛔ not touched. I depart slightly from the recommendation: "costs nothing today" is a property of the current metadata, not of the test — the unfiltered counts are 35 top-level vs 49 deep, so 14 condition sites already live only inside regions, and #1611 just demonstrated this exact shape costing 14 cases in silence. Recording it on #1582's body costs one line; rediscovering it costs an incident. No card, no edit.

    Also accepted

    ⇒ Card complete. F2–F6 continue in a follow-up, which must apply the same end-to-end discount before any of their counts are trusted.


    Generated by Claude Code

  6. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    Upstream ruling — objectstack-ai/objectstack#16094 (director seat, decision batch #60, 2026-09-06)

    Ruled: ledger dead / live-elsewhere verdicts will imply the author warning (no per-row opt-in), after a ledger audit (objectstack#16362) that also re-derives the view.json list.tabs verdict against the shipped switcher — the contradiction with test/view-tab-label-inert.test.ts (hotcrm#1307) is settled there, on the platform side. Keep the local stand-in assertion until a released @objectstack/lint carries the flip; if #16362 finds list.tabs genuinely live, the local test's premise is the side that changes.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

pm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions