Skip to content

Retire the local tests that re-implement the platform's flow / predicate / readonly-write lint rules, once os lint --strict guards them (epic #1579, step 3, family F1) #1582

Closed

Description

@os-zhuang

Blocked-by: #1581

Sub-issue of the epic #1579; pm:epic. Filed by the director seat (objectstack#12708, summon #15).

⚠️ This card's table and method were corrected twice — by the #1613 survey (2026-09-06), then again by #1625's first-hand re-measurement (PR #1629). ⛔ Read both corrections before planning. Evidence: #1613 report 5555835078 · PM rulings 5555855395 · PR #1629's decline table, which is the newest and most precise.

Governing text. AGENTS.md rule 3 ("Tests in this repo pin this repo's own business facts and nothing else … ⛔ Do not grow a gate farm") and rule 1 ("⛔ Do not re-invent what the platform already owns").


⛔ METHOD — three gates, and gate 3 is PER CASE

The original ("delete only the local assertions whose rule fired") is unsound. Delete only when all three hold:

  1. Non-vacuous — the assertion inspects a non-empty set and reds on its own planted defect. (PR Contain a failing item to its own loop iteration: ten sweeps stop aborting mid-batch (42 flow-loop-body-uncontained to 0) #1611: two walkers passed green while inspecting nothing, 61 cases → 47.)
  2. Reachable — the platform rule fires on this repo at all.
  3. Subsuming — for EVERY CASE the assertion covers, ⛔ not for the assertion as a whole and ⛔ not for "a defect of the rule's class".

⚠️ Gate 3's granularity is the correction that matters most, and it was learned the expensive way. #1613 applied gate 3 per assertion and cleared these rows; #1629 re-measured per case and declined 10 of 21 — including four on this card. The same coarse-proxy error cost three successive over-counts: 49 → 21 → 11.

⇒ A rule that fires on one case an assertion covers, and is silent on another, is not a duplicate. It is partial coverage, and deleting the assertion loses the rest silently, with nothing going red.

⛔ A rule failing gate 2 or 3 is a platform finding: file upstream (unassigned, rule id + site, with a working control), leave the local assertion.


F1 status — corrected by PR #1629's first-hand re-measurement at ff43321

✅ Retired — landed in PR #1629

rule id local file note
flow-update-readonly-field test/readonly-write-semantics.test.ts group A only — the exact-write-set pin (toEqual([...])) is covered by no rule and stays byte-identical

⛔ DECLINED — previously listed here as "retirable now"; the re-measurement disproved all four

rule id why it stays
visibility-bare-identifier Fires on status == "qualified" (control) but goes silent once the identifier also appears inside a has() in the same predicate — ⚠️ the guarded-but-unprefixed spelling an author who half-remembers the totality rule actually writes. objectstack#16118
visibility-predicate-unknown-function Reds only the live-engine totality cases, whose defect — a predicate that parses, resolves, is record-bound and still aborts at runtime (#688 class) — no static rule sees
expression-invalid Same shape: reds only the null-guard sweeps and live-engine totality cases. The defect they exist for is an unguarded-but-valid predicate
visibility-predicate-syntax No standalone local assertion; its only counterpart is the parse guard inside the sweep kept for the row above

⛔ Blocked on an UPSTREAM card — #1581 is irrelevant to these

rule id why it cannot fire here upstream
liveness-dead-property · liveness-live-elsewhere-property shouldWarn needs authorWarn === true; the pinned ledger has 90 dead + 1 live-elsewhere and not one opts in objectstack#16094
hook-api-update-readonly-field · hook-api-update-readonly-when-field gated on body.language === 'js'; this repo is 39 hooks / 39 handlers / 0 bodies objectstack#16095
filter-token-unknown — flow surface only validateFilterTokens never walks flows; the view surface DOES fire (error, exit 1) objectstack#16096
flow-decision-unconditional-branch — fully-inert case only gated.length === 0 short-circuit objectstack#16093

⛔ MIS-CREDITED — remove these rows, they were never duplicates

predicate-path-unresolved and predicate-path-unrooted are metadata-editing-form rules (if (!schemaId) continue). Control: 0 occurrences of schemaId in src/, against 76 provider: occurrences. ⛔ Deliberately not filed upstream — asking the platform to widen a deliberately metadata-scoped rule would be the wrong ask, and the hazards they name are already covered here by visibility-bare-identifier / visibility-predicate-*.

⏳ Genuinely waiting on #1581 (warning severity)

flow-inert-node-condition, flow-multiple-default-edges, flow-time-relative-antipattern, flow-date-equality-filter, flow-update-readonly-when-field, action-api-update-readonly-when-field, and the totality half of test/flow-condition-totality.test.ts — the last also gated on objectstack#15936 (epic step 4).

Fires and exits 1 but no local assertion covers it, so there is nothing to retire — the platform is ahead of the tests: flow-default-edge-with-condition, flow-time-relative-descriptor-invalid, validation-rule-regex-uncompilable.


⚠️ Latent blind spot in this card's own file surface — recorded, ⛔ not a defect today

test/flow-condition-totality.test.ts is the one F1 sweep still walking top-level flow.nodes / flow.edges only. It costs nothing today — 14 record/previous condition sites either way — but the unfiltered counts are 35 top-level vs 49 deep, so 14 condition sites already live only inside regions. "Costs nothing" is a property of the current metadata, not of the test, and PR #1611 demonstrated this exact shape costing 14 cases in silence.

⇒ Whoever next opens that file should deepen it via test/helpers/flow-regions.ts (which #1611 added for precisely this). ⛔ Not this card's subject; no card filed.

Refs: #1579 · #1581 · #1613 · #1625 / PR #1629 (the re-measurement that corrected this table) · objectstack#16093 · #16094 · #16095 · #16096 · #16118 · objectstack#15935 · #15936 · #1543 / PR #1580.

Activity

  1. added
    pm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree
    on Sep 5, 2026
  2. added theissue type on Sep 5, 2026
  3. added and removed
    pm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree
    on Oct 8, 2026
  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Ruling: batch hotcrm-R74 item 4 · letter B · maintainer 「同意」 2026-10-08T03:15Z

    repo:hotcrm seat, session_012zh91QzFgePbkmuHnugLN3. Epic #1579 is dissolved by this ruling; the full reading and the destination of every child are in #1579's closing comment.

    Transition: no pm:* state → pm:blocked on the Blocked-by: #1581 line already in this body · pm:epic is removed in the same write · source: the maintainer's 「同意」 to batch hotcrm-R74, in this seat's chat.

    What happens when #1581 lands: this card is re-graded once, under the three-gate, per-case method (non-vacuous, reachable, subsuming: 5555869319, 5556310099). The surviving retirable rows of every family fold into ONE card; this card closes not_planned if it has none. This card is family F1, the flow / predicate / readonly-write family. Its totality row waited on objectstack#15936, which closed on 2026-10-06; the re-grade reads that card's outcome first.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round R74
    Session: session_012zh91QzFgePbkmuHnugLN3
    Account: os-zhuang (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-1582-family-regrade
    Worktree: hotcrm-issue-1582
    Domain: repo:hotcrm (single-lane repo, no domain:* taxonomy)
    Seat: repo:hotcrm#1
    File surface: none committed: a READ-ONLY measurement round over the test/** and scripts/** assertions that the 2026-09-06 surveys (#1613, #1621, #1637) credited to families F1–F6, plus #1805's qualified-FLS-key half. Temporary ablations are restored by blob hash; no commit, no PR. The branch is pushed empty as the marker for the one follow-up retirement card the ruling calls for (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default (dispatch-gates --tier --repo objectstack-ai/hotcrm answers "no path-derived mandate … the tier stays the PM's per-card judgment call")
    Clause-②: no
    Responsibility: n/a — not a defect card (family F1 flow / predicate / readonly-write)
    Thread-read: 6051438552
    Serial constraints cleared: folded family dispatch, chain head #1582, with members #1583 #1584 #1585 #1586 #1587 #1805, each carrying its own claim naming this branch. The five fold gates: ① one method (the three-gate, per-case re-grade of 5555869319 / 5556310099); ② one region (test/**, one worktree); ③ every member is ruled (hotcrm-R74 item 4 B); ④ each member is checkable on its own family's rows; ⑤ EXCLUDED: the 86 business-fact pin files, #1595's harness port, and step 6 (docs↔metadata drift, frozen). In flight elsewhere: #1907 (PR #2011, packaging files and nav/docs suites), which is read-only here. #2000 is blocked. Ruling-ref: hotcrm-R74 item 4 B (on #1579 and on each family card).


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    #1582 chain head, folded with #1583 #1584 #1585 #1586 #1587 #1805. This is a read-only re-grade of F1–F6 under three gates, with gate 3 checked per case, at hotcrm c529de2b (@objectstack/* 17.7.0, pnpm lint = objectstack lint --strict). Nothing was deleted, edited or committed, and no PR was opened. Branch claude/issue-1582-family-regrade is pushed empty as the marker for the follow-up card.

    Verdict table: F1–F6 re-grade at hotcrm c529de2b, @objectstack/* 17.7.0, pnpm lint = objectstack lint --strict

    How to read it. G1 means non-vacuous: the inspected set is non-empty and the assertion goes red on its own planted defect. G2 means reachable: the credited rule fires under --strict, with its severity and the lint exit code. G3 is checked per case: every case the assertion goes red on must also make some platform rule fire. A silent case blocks retirement. The one exception is a case where the local red is shown to be a false positive on metadata that is valid at runtime. Each such exception is named in its row.

    Every cell comes from one injection run against the committed tree (probe label in brackets). Each run did strict lint with --json, then ran the owning test file, then restored the file and proved the restore. Raw outputs are in scratchpad/issue-1582/r/LABEL.summary.txt.

    Clean-tree baseline: errors 0 · warnings 0 · suggestions 18 · failing 0 · strict true · hiddenPlatform 3396, exit 0.

    F1 (#1582): flow, predicate and readonly-write

    # test file › assertion rule id G1 G2 G3 (per case) verdict
    1 flow-decision-authority › no decision node carries the inert singular config.condition flow-inert-node-condition ✓ 24 decisions (deep) · red ✓ warning, exit 1 [f1-inert-top2, f1-inert-nested2] ✓ non-empty condition, top level and inside a loop/try region → flow-inert-node-condition. ✓ condition: '' → expression-invalid (error) [f1-inert-empty]. Silent only on condition: null [f1-inert-null]: a null states no predicate, so it falls outside the assertion's own rationale ("a copy … reads like the live predicate"). Flagged. retire
    2 flow-decision-authority › every decision node actually decides something flow-decision-unconditional-branch (blocked on objectstack#16093, now landed) ✓ ✓ warning, exit 1, top level and nested: "gates on NOTHING" [f1-fullyinert-top, f1-fullyinert-nested] ✗ a decision with zero out-edges is silent: exit 0, local red [f1-zero-outedges-nested]. The dist has if (outs.length === 0) continue keep: partial (deliberate skip; not filed, seat's call)
    3 flow-decision-authority (sweeps + mechanism tests) flow-multiple-default-edges n/a carried (#1613: warning fires) no sweep asserts multiple defaults. conditions[]-routability inspects an empty set (0 decisions declare conditions[]). Mechanism tests are live-engine nothing to retire
    4 view-tab-label-inert › no list or listViews block declares tabs (+ walk guard) liveness-dead-property · liveness-live-elsewhere-property (blocked on objectstack#16094, now landed) the defect can no longer reach the assertion rule never sees the key: spec 17.5.0 tombstones view.list.tabs (invalid_type, "was removed in @objectstack/spec 17.5.0 …"), and config load fails with exit 1 and no rule id [f1-tabs-list, f1-tabs-listviews]. Liveness control NOT MEASURED: RLS policy label is ledger-live in 17.7.0 [f1-liveness-ctl], and action.shortcut is also tombstoned [f1-liveness-ctl2] ✓ both cases (list, listViews) are refused by the schema, with a message more precise than the rule's (it names the key, the release and the replacement) retire, credit the spec schema (#1637 nav-target-unresolved precedent)
    5 readonly-write-semantics hook-api-update-readonly-field · -when-field (blocked on objectstack#16095, now landed) n/a partial: fires on a lowered handler only when written as a literal ctx.api.object(…) (error, exit 1) [f6-hook-readonly-ctxdirect]. Silent on this repo's mandated const api = ctx.api alias [f6-hook-readonly]: 25 alias declarations, 73 api.object( call sites, 0 direct no local sweep of hook writes in this file (it is live-engine writer semantics plus escalate pins) nothing to retire. Platform gap, to file (alias)
    6 readonly-write-semantics flow-update-readonly-when-field · action-api-update-readonly-when-field n/a carried (#1613: warning fires) no local sweep (structure re-read at c529de2) nothing to retire
    7 flow-filter-today-token filter-token-unknown, flow surface. The new id is flow-filter-token-unknown (objectstack#16096, landed) n/a ✓ error, exit 1 [f1-flow-filter-token] the only red is the live-engine ANSWER A run (behavioural). No static sweep nothing to retire
    8 flow-filter-today-token flow-time-relative-antipattern · flow-date-equality-filter · flow-time-relative-descriptor-invalid n/a carried carried: local green (#1613) nothing to retire
    9 view-predicate-dialect › references only namespaces the evaluator binds visibility-bare-identifier (declined, objectstack#16118 landed) · visibility-predicate-syntax ✓ 44 envelopes · red ✓ bare → error ×7 [f1-bare-ident]. ✓ has(x) && x == … now fires, error ×7 [f1-guarded-bare-ident]. ✓ unparseable → visibility-predicate-syntax, error ×7 [f1-unparseable-predicate] ✗ unbound dotted root foo.duplicate_of_type is silent: exit 0. Local red, and the engine throws "Unknown variable: foo" [f1-unbound-namespace] keep. Platform gap, to file
    10 view-predicate-dialect › TOTAL-on-engine sweeps visibility-predicate-unknown-function · expression-invalid n/a carried live-engine totality. The #15936 answer: the null-guard gate excludes view predicates (#7219) keep (unchanged)
    11 flow-condition-totality › totality half (null-guard gate; objectstack#15936 closed) ✓ ✗ unguarded record.rating in a flow condition gives exit 0 [f1-flow-unguarded]. Flow conditions are excluded by #4953 clause 3, per #15936's answer n/a keep, still blocked (#4953 clause 3; no new card)
    12 view-predicate-dialect predicate-path-unresolved · -unrooted n/a n/a n/a mis-credited (carried)

    F2 (#1583): reference integrity

    # test file › assertion rule id G1 G2 G3 (per case) verdict
    1 metadata-references › record:details / record:highlights / record:path only name real fields page-field-unknown ✓ 18 highlight fields, 3 path statusFields (0 explicit details fields) · red ✓ warning, exit 1 ✓ highlights [f2-hl-field] · path statusField [f2-path-field] · explicit details-section field [f2-details-fields] retire
    2 metadata-references › every page component's properties parse against its ComponentPropsMap entry component-props-unknown-key · component-props-invalid ✓ 54 components · red. The clean tree now has 0 of the old 16+1 warnings ✓ warning, exit 1 ✓ unknown key at top level [f2-props-unknown-top], at a 3-level nested related_list [f2-props-unknown-nested2], and in a slot page:header [f2-props-unknown-slot3]. ✓ invalid value at top level [f2-props-invalid-top2] and nested [f2-props-invalid-nested] retire
    3 field-groups-coverage › every field-level group resolves to a declared key field-group-undeclared ✓ 356 grouped fields · red ✓ warning, exit 1 [f2-fg-undeclared] ✓ retire
    4 field-groups-coverage › no declared group is left with no fields field-group-empty ✓ 85 groups · red ✓ warning, exit 1 [f2-fg-empty2] ✓ retire
    5 field-groups-coverage › no declared group is entirely hoisted into the highlight strip field-group-shadowed ✓ · red ✓ warning, exit 1, contact_info [f2-fg-shadowed] ✓ The rule models the same synthesized page, so #1704's reach question moves with it retire
    6 field-groups-coverage › every detail-page object declares fieldGroups none n/a n/a business pin (#1583 two-half reading) keep (not a candidate)
    7 skills-integrity › every tool name resolves ai-skill-tool-unresolved ✓ 22 tool refs · red ✓ warning, exit 1 ✓ plain name [f2-skill-plain] · action_ prefix [f2-skill-action] · wildcard [f2-skill-wildcard2] retire
    8 view-references › every form section field is a real field on the view object form-field-unknown (blocked on objectstack#16168, now landed) ✓ 374 form field refs · red ✓ warning, exit 1 ✓ default form [f2-form-field-default] · formViews object entry [f2-form-field-formviews] retire
    9 none form-section-group-unknown (objectstack#16168, landed) n/a ✓ warning, exit 1 [f2-form-group-unknown2] no local assertion goes red (97/97 green) nothing to retire (platform ahead)
    10 detail-section-dedup › every section still carries at least one field page-section-group-unknown ✓ · red ✓ warning, exit 1 [f2-page-section-group-unknown] ✗ section { name, fields: [] } is silent: exit 0, local red [f2-page-section-empty] keep. Platform gap, to file (low)
    11 view-references › kanban groupByField is a select field with options list-view-field-unknown ✓ ✓ missing field → error, exit 1 [f2-kanban-missing] ✗ real field with no options is still silent [f2-kanban-nooptions] keep (unchanged; not filed per #1629/#1583)
    12 authorization-coverage › every object nav item is readable nav-object-ungranted carried carried partial: posture, allowRead only (#1637) keep (carried)
    13 action-references › url-type widget/header action resolves dashboard-action-route-unresolved (objectstack#16169, landed) ✗ empty set: 0 header actions, 0 widget actionUrl moot moot nothing to retire
    14 the other F2 ids: list-view-field-dotted, searchable-field-unknown, view-ref-nav-view-missing, default-agent-*, view-key-collision, nav-object-unservable, action-no-placement, capability-reference-unknown, view-ref-form-target-missing, page/sort-field-unprovisioned, ai-skill-surface-mismatch, nav-target-unresolved, list-view-field-unknown warning positions as named carried carried local green, empty population, or mis-credit (#1621/#1637) nothing to retire (carried)

    F3 (#1584): analytics

    # test file › assertion rule id G1 G2 G3 (per case) verdict
    1 analytics-integrity › every report block names a defined dataset (A1) chart-dataset-unknown (blocked on objectstack#16105, now landed) ✓ 13 blocks · red ✓ error, exit 1 ✓ chartless matrix [f3-ds-chartless] · charted [f3-ds-charted] · joined block [f3-ds-joinedblock] retire
    2 analytics-integrity › every chart xAxis is a dimension (A3) chart-dimension-unknown (the corrected id; per-case gate 3 was still owed) ✓ 5 xAxis · red ✓ error, exit 1 [f3-xaxis] ✓ (joined blocks carry no chart) retire
    3 analytics-integrity › every rows / columns / values entry resolves (A4) chart-dimension-unknown · chart-measure-unknown (objectstack#16105) ✓ 19 rows+cols, 22 values · red ✓ error, exit 1 ✓ rows, columns and values on a chartless report [f3-rows-chartless, f3-cols-chartless, f3-values-chartless] · rows on a charted report [f2-rows-charted] · rows on a joined block [f3-rows-joinedblock] retire
    4 analytics-integrity › no filter comparand is a bare date-range PRESET name (A8) filter-preset-comparand (blocked on objectstack#16106, now landed) ✓ · red ✓ error, exit 1 ✓ bare (widget) [f3-preset-bare-widget] · $eq (widget) [f3-preset-eq-widget] · $in (report runtimeFilter) [f3-preset-in-report]. Ordering positions are refused by the schema with a precise message (#1621) retire
    5 dataset-granularity dataset-* · measure-aggregate-incoherent n/a n/a mis-credit (carried) nothing to retire

    F4 (#1585): i18n and the severity-promoting gate

    # test file › assertion rule id G1 G2 G3 (per case) verdict
    1 i18n-references › option translations are keyed by option VALUE translation-option-key-unknown ✓ 1496 option keys · red ✓ warning, exit 1 ✓ extra key in en [f4-optkey-extra] and in zh-CN [f4-optkey-zhcn] · English-label key [f4-optkey-englishlabel] · options on a non-select field [f4-optkey-nooptions-field] · options on an unknown field → translation-target-unknown [f4-optkey-unknownfield] retire
    2 i18n-references › translated object and field keys name real objects and fields translation-target-unknown (now error) ✓ · red ✓ error, exit 1: unknown object [f4-target-object3], unknown field [f4-target-field2] ✗ fields.name and fields.space on crm_case, which declares neither, are silent: exit 0, local red [f4-target-field-name, f4-target-field-space]. (organization_id is also silent, but it is an injected column, so that red is a local false positive [f4-target-field-orgid]) keep. Gap is uncertain (name/space are blanket-implicit in the rule)
    3 i18n-references › no _views entry for a view the stack does not ship translation-target-unknown ✓ 276 keys · red ✓ error, exit 1 [f4-target-views2] ✗ _views.web_to_case (a shipped formView key) is silent and the local goes red [f4-target-views-formview]. Not established whether that key renders keep
    4 i18n-references › no apps.*.navigation entry the app does not declare translation-target-unknown ✓ 103 nav keys · red ✓ error, exit 1 ✓ unknown nav id [f4-target-nav2] · unknown app [f4-target-navapp2] retire
    5 i18n-references › every section with a label carries a name translation-section-name-missing ✓ 69 labelled sections · red ✓ warning, exit 1 ✓ view form [f4-section-view] · formViews [f4-section-formviews2] · page record:details nested in tabs [f4-section-page] retire
    6 i18n-references › the completeness assertions (field label/help, option, section, view, action, widget, page, rail, empty-state, params) i18n/missing-* (folded by the CLI) ✓ · red ✓ a non-default-locale gap gives a warning, exit 1, on 12 ids [f4g-*] ✗ default-locale (en) gaps are silent by design, because the inline label is the en text. This repo's #679 convention requires explicit en entries, and the local goes red [f4-en-field-label, f4-en-view-label, f4-en-action-label] keep (repo convention pin)
    7 scripts/check-lint-i18n-gate.mjs + test/lint-i18n-gate.test.ts + lint:i18n-gate in verify and in both CI workflows i18n/missing-* n/a ✓ on each of 12 reachable ids (object, field, option, section, view, action, navigation, dashboard, widget, dataset, page, flow) the gate exit was 1 and strict lint exit was 1, with the same finding [f4g-*]. pnpm lint itself exits 1 on i18n/missing-navigation [pnpm-lint-ctl] ✓ Structural: foldCoverageIssues emits only error or warning, never suggestion, and the gate and pnpm lint share the same i18n flags (no --i18n-strict, no --include-platform). app: the description is not a coverage key and the label is schema-required, silent in both [f4g-app]. globalAction and picklist: empty populations (0 global actions, 0 picklists). metadataForm: hidden in both. Nothing the gate checks escapes strict. retire

    F5 (#1586): security, sharing and org axis (with #1805)

    # test file › assertion rule id G1 G2 G3 (per case) verdict
    1 authorization-coverage › every FLS key is object-qualified and names a real field (#1805) security-fls-unqualified-key · security-fls-unknown-field (grep -c = 1, control unqualified-key = 1) ✓ 21 FLS keys · red ✓ crm_account.description_nope → unknown-field, error, exit 1 [f5-fls-unknown-field]. ✓ crm_account. → unknown-field, error, empty-remainder branch [f5-fls-empty-remainder]. ✓ description → unqualified-key (not the new id), error [f5-fls-unqualified] ✗ only the unknown-object branch: no_such_object.description is silent, exit 0, local red [f5-fls-unknown-object]. This is deliberate per objectstack PR #16998 retire the unqualified and names-a-real-field branches; keep the unknown-object branch (#1805 steps 4–5)
    2 authorization-coverage › every RLS predicate is pushdownable and names real fields · › the first field of a using predicate exists rls-predicate-unenforceable + the new rls-predicate-unknown-field and rls-predicate-unknown-user-variable (declined; objectstack#16119 landed) ✓ 4 policies · red ✓ all error, exit 1: unknown field [f5-rls-unknown-field] · unknown leading field [f5-rls-unknown-leading] · current_user.nope [f5-rls-unknown-uservar] · nested path [f5-rls-nested-ctl] · function call [f5-rls-funcall] the unknown policy object case is silent at lint, but the kept, uncredited "every policy targets an object the same set grants" holds it (red) [f5-rls-unknown-object] retire both, on condition that the holder stays
    3 authorization-coverage › a sharing rule only targets an object whose OWD lets shares apply sharing-rule-object-not-shareable · -controlled-by-parent ✓ ✓ public_read_write → error ×2, exit 1 [f5-share-prw-ctl] ✗ a read share on public_read is still silent [f5-share-read-on-publicread] keep (unchanged; not filed per ruling)
    4 authorization-coverage › every business object is granted in at least one permission set security-master-detail-ungranted ✓ · red ✓ master-detail child → warning, exit 1 [f5-md-ungranted-ctl] ✗ non-MD nav object → nav-object-ungranted (another id) [f5-nonmd-ungranted]. ✗ non-MD, non-nav, controlled-by-parent child crm_campaign_member is silent, exit 0 [f5-nonmd-nonnav-ungranted] keep. Platform gap, to file
    5 authorization-coverage › the guest set carries no export bit · ownership-model group A security-anchor-high-privilege (objectstack#16110 closed docs-only) ✓ ✗ guest allowExport gives exit 0 [f5-anchor-guest-export]. The rule still keys on isDefault (dist re-read) n/a keep, permanent: the bind-time gate, not lint, owns ADR-0090 D9
    6 none (no standalone alias assertion) security-owd-alias n/a #1973 note confirmed under strict: sharingModel: 'read' gets invalid_value at config load, exit 1, no rule id [f5-owd-alias] the gate to credit is the spec enum nothing to retire
    7 parent-derived-reach (3 assertions) security-controlled-by-parent-no-relation ✓ ✗ master-detail demoted while a required lookup remains gives exit 0 (only a suggestion) [f5-cbp-reqlookup-remains] local red on 3 assertions keep: still the only cover
    8 authorization-coverage rls-predicate-over-budget n/a ✗ still dormant: installed formula 17.7.0 has CEL_PUSHDOWN_LIMITS_MODE = "rc-grace" local is green on it too still blocked: v17 GA limits flip (objectstack#6778)
    9 authorization-coverage security-private-no-readscope n/a ships at severity: "info" in the 17.7.0 dist n/a keep, permanent (ruling)
    10 rls-predicate-unparseable · security-owd-unset · security-cbp-ambiguous-relation · org-axis-* (hook-org-inheritance, ownership-model) as named carried carried carried (#1621/#1637) nothing to retire

    F6 (#1587): write shape

    # test file › assertion rule id G1 G2 G3 (per case) verdict
    1 hook-write-shape › every hook-side derived write reaches the engine in the engine's shape · › no hook-side code may address a write by bare id hook-body-write-unknown-field · hook-body-write-unprovisioned-anchor · hook-body-source-unparseable (blocked on objectstack#16095, now landed) ✓ partial: a lowered handler with a literal ctx.api.object(…) → warning, exit 1 [f6-hook-unknown-field-ctxdirect]. Silent on the repo's const api = ctx.api alias [f6-hook-unknown-field] ✗ the file pins write SHAPE: bare-id update(id, doc) is silent, exit 0, while the local static and runtime cases go red [f6-hook-bare-id] keep. Platform gap, to file (alias, shared with F1 #5)
    2 action-sandbox group A › every script action body executes under QuickJS action-body-source-unparseable ✓ · red ✓ warning, exit 1 [f6-act-unparseable] ✗ free identifier (ReferenceError on every run) is silent [f6-act-free-identifier]. ✗ an empty return is silent [f6-act-returns-nothing] keep. Platform gap, to file (free identifier)
    3 action-sandbox group A action-body-write-unknown-field n/a ✓ warning, exit 1 [f6-act-unknown-field] the group A smoke case stays green on it (the red comes from the business pin "mark_primary calls update with … shape") nothing to retire against group A
    4 action-sandbox · actions-flows-integrity action-record-write-discarded · flow-node-write-unknown-field · flow-runas-unscoped · flow-template-* (objectstack#16111 landed) · flow-multi-write-unfiltered carried carried local green (#1621/#1637) nothing to retire

    Counts (rows in this table)

    family retire keep (partial / pin / permanent) still blocked nothing to retire / mis-credit rows
    F1 2 3 1 6 12
    F2 7 4 0 3 14
    F3 4 0 0 1 5
    F4 4 (incl. gate) 3 0 0 7
    F5 2 (FLS as a branch split) 5 1 2 10
    F6 0 2 0 2 4
    total 19 17 2 14 52

    Platform gaps found by this round ("to file"): (1) the hook write family is blind to a const api = ctx.api alias. (2) An unbound dotted root in a visibility predicate is not reported. (3) security-master-detail-ungranted misses controlled-by-parent children bound by lookup. (4) A free identifier in a script action body is not reported. Low priority: (5) a record:details section with fields: []. (6) The FLS unknown-object branch is by design (#1805 step 5 says file).

    {
      "issue": 1582,
      "status": "done",
      "branch": "claude/issue-1582-family-regrade",
      "pr": null,
      "session": "session_012zh91QzFgePbkmuHnugLN3 (mode:subagent; this is the parent seat's id)",
      "premise_still_valid": true,
      "summary": "This was a read-only re-grade of folded cards F1–F6 (#1582–#1587, plus #1805) at hotcrm c529de2b (@objectstack/* 17.7.0, pnpm lint = objectstack lint --strict). Nothing was deleted, edited or committed, and no PR was opened. The empty branch is pushed as the marker. 52 rows were re-graded under the three-gate, per-case method; the full table is in this comment and at /tmp/claude-0/-home-user/ddab6cfd-cf50-59b8-bff8-60ca43226796/scratchpad/issue-1582/verdict-table.md. Result: 19 retire · 17 keep (partial, convention pin or permanent) · 2 still blocked · 14 nothing to retire or mis-credit. The PM's premise that the upstream gap cards were still open was disproved: all 14 upstream cards are closed and present in 17.7.0. That is why most of the 'blocked upstream' rows re-graded and the retirable set grew. Retire (fold into ONE card): F1 inert config.condition sweep; F1 view-tab-label-inert, crediting the spec 17.5.0 schema tombstone rather than liveness-*; F2 page-field-unknown (highlights/path/details); F2 component-props parse sweep (the 16+1 baseline warnings are now 0); F2 field-group-undeclared, -empty and -shadowed; F2 ai-skill-tool-unresolved; F2 view-references form-field sweep (objectstack#16168); F3 A1, A3, A4 and A8 (objectstack#16105, #16106; chart-dimension-unknown); F4 option-key sweep, apps.*.navigation orphan sweep and section-name-missing sweep; F4 scripts/check-lint-i18n-gate.mjs with its test, its package.json script, its verify step and its two CI workflow steps (fully subsumed: on all 12 reachable i18n/missing-* ids the gate and strict lint both exit 1, and nothing the gate checks escapes strict); F5 FLS assertion, split per #1805 (retire the unqualified and names-a-real-field branches, keep the unknown-object branch); F5 both RLS sweeps (the new rls-predicate-unknown-field and -unknown-user-variable fire; the one silent case, an unknown policy object, stays covered by the kept assertion 'every policy targets an object the same set grants'). F6 has nothing retirable. #1805 probe: grep -c security-fls-unknown-field = 1, control security-fls-unqualified-key = 1, and both injection legs fire at error, exit 1. The #1973 note is confirmed under strict: security-owd-alias is unreachable on defineStack, because sharingModel 'read' is refused at load (invalid_value, exit 1, no rule id), and there is no standalone local alias assertion to retire. Four new platform gaps, each measured against a firing control, are listed in out_of_scope_findings for the seat to file.",
      "tests": "Harness: each probe nests /home/user/objectstack/scripts/ablation-replace.mjs in WRAP mode. The anchor must hit exactly once; the blob change and the anchor/replacement counts are verified on disk. The probe then runs `objectstack lint --strict --json` (raw-decoded; exit captured before any pipe) and `vitest run --maxWorkers=2 FILE --reporter=json`. An outer bash trap on EXIT INT TERM restores every touched absolute path with `git checkout HEAD -- ABS` and proves the restore by git hash-object == HEAD blob, an empty `git diff HEAD` and a clean `git status`. Every heavy run went through `OS_VERIFY_LOCK_SLOT=hotcrm-1582 os-verify-lock.sh` and was read from its VERDICT line (all command-exit 0). Baseline on the clean tree: errors 0 · warnings 0 · suggestions 18 · failing 0 · strict true · hiddenPlatform 3396, exit 0. `pnpm lint` itself: exit 0 on the clean tree, exit 1 with i18n/missing-navigation injected, while the gate also exits 1. Totals: 127 probe runs, of which 114 landed mutations (121 file mutations, since three probes were multi-file). All 127 restores were proven (RESTORED-PROVEN in every log). 13 no-ops are declared, not silently retried: 12 were refused by ablation-replace because the anchor was contained in the replacement (restored), and 1 was an ANCHOR MISS (never written). Each was redone with a unique anchor. Out-of-class or invalid probes, also declared: f4-target-object (key added to objects.service.ts, which the bundle does not spread; redone in en.ts), f4-target-navapp (app translation needs a label; schema-refused), f2-props-invalid-top (maxVisible is an unknown key, not an invalid value; counted as unknown-key and redone as layout: 42), f1-liveness-ctl (rowLevelSecurity label is ledger-live in 17.7.0, so the control premise was stale), f1-liveness-ctl2 (action.shortcut is tombstoned). Final state: HEAD c529de2b, git status clean, git diff HEAD empty, 0 commits. Per-probe raw output is in scratchpad issue-1582/r/LABEL.summary.txt and LABEL.probe.log. Populations for gate 1 were measured by importing the app config: 1496 option keys, 103 nav keys, 69 labelled sections, 21 FLS keys, 4 RLS policies, 54 page components, 18 highlight fields, 3 path statusFields, 85 field groups / 356 grouped fields, 22 skill tool refs, 374 form field refs, 13 report blocks, 24 decision nodes, 44 view predicate envelopes, 0 dashboard header actions.",
      "mcp_calls": "0 — all card and comment reads went through repo-scoped `gh api` REST GETs",
      "api_writes": "1 — the os-dev-report comment on hotcrm#1582 via scripts/pm/post-stamped.mjs. Not counted as a REST write: `git push -u origin claude/issue-1582-family-regrade` (the empty branch marker).",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: `os lint --strict` exits 0 on a lowered hook handler that writes a readonly field (crm_case.is_escalated) or an undeclared field through `const api = ctx.api; api.object('crm_case').update(…)`. The byte-identical statement spelled `ctx.api.object(…)` fires hook-api-update-readonly-field (error, exit 1) and hook-body-write-unknown-field (warning, exit 1). The real producer is hotcrm AGENTS.md, which mandates the alias: 25 alias declarations, 73 call sites, 0 direct. · evidence: probes f6-hook-readonly vs f6-hook-readonly-ctxdirect, f6-hook-unknown-field vs f6-hook-unknown-field-ctxdirect. The dist extractor matches `isCtxDot(…, \"api\")` only. · dedupe words: hook-body-write-unknown-field alias; isCtxDot ctx.api alias; hook-api-update-readonly-field const api; lowered handler ctx.api local",
        "class: a · reach: `os lint --strict` exits 0 on a view form field `visibleOn: has(record.duplicate_of_type) && foo.duplicate_of_type == \"crm_lead\"`. The engine throws 'Unknown variable: foo'. Control: the bare `duplicate_of_type == \"crm_lead\"` fires visibility-bare-identifier (error ×7, exit 1) · evidence: probes f1-unbound-namespace vs f1-bare-ident · dedupe words: visibility predicate unbound namespace root; visibility-bare-identifier dotted; visibleWhen unknown root identifier",
        "class: a · reach: `os lint --strict` exits 0 when crm_campaign_member (controlled_by_parent, bound by a lookup with no master_detail field) is granted in no permission set. The same removal on crm_quote_line_item (master_detail) fires security-master-detail-ungranted (warning, exit 1). The rule keys on firstMasterDetailField, so crm_event_attendee and crm_article_feedback have the same blind spot · evidence: f5-nonmd-nonnav-ungranted vs f5-md-ungranted-ctl · dedupe words: master-detail-ungranted controlled_by_parent lookup; detail object no grant lookup parent; firstMasterDetailField",
        "class: a · reach: `os lint --strict` exits 0 on a script action body (mark_primary) that references an undeclared identifier, which throws ReferenceError in QuickJS on every invocation. Control: an unparseable body fires action-body-source-unparseable (warning, exit 1) · evidence: f6-act-free-identifier vs f6-act-unparseable · dedupe words: action body free identifier; script action undeclared identifier ReferenceError; action-body unbound reference",
        "class: a · reach: `os lint --strict` exits 0 on a record:details section `{ name: 'sla', fields: [] }`, which renders an empty section. Control: `{ group: 'sla_nope' }` fires page-section-group-unknown (warning, exit 1). Low priority · evidence: f2-page-section-empty vs f2-page-section-group-unknown · dedupe words: record:details empty section fields; page section no fields lint",
        "class: b · reach: `os lint --strict` exits 0 on an FLS key naming an undeclared object (`no_such_object.description`). This is deliberate per objectstack PR #16998 ('judged at bind/install time'). #1805 step 5 says this half stays and is filed upstream, so the seat decides whether that still applies given the deliberate design · evidence: f5-fls-unknown-object · dedupe words: FLS key unknown object; security-fls-unknown-field object half; field permission undeclared object",
        "carrier: none (承接者:无) · noted, not filed: flow-decision-unconditional-branch skips a decision with zero out-edges by design (`if (outs.length === 0) continue`), and the local 'every decision node actually decides something' goes red on it [f1-zero-outedges-nested]. translation-target-unknown treats `name` and `space` as implicit on every object, so `fields.name` on crm_case (which declares no name field) is silent [f4-target-field-name, f4-target-field-space]. `_views.web_to_case` (a formView key) is accepted by the rule and red locally [f4-target-views-formview]",
        "carrier: the follow-up retirement card (it edits these files) · noted, not filed: stale prose at c529de2b. test/i18n-references.test.ts (the 'every locale is complete' block comment) says '`pnpm lint` runs with `--skip-i18n`' and 'objectstack lint exits 0 on warnings regardless'; both are false under `lint --strict`. src/sales/profiles/{sales-manager,marketing-user}.profile.ts say rowLevelSecurity label/description are 'liveness: dead in 17.6.0', but the installed 17.7.0 ledger (permission.json) marks them live."
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Verdict on the re-grade report 6053840674 (on #1582) — ACCEPT (a measurement round: nothing deleted, no PR; the empty marker branch claude/issue-1582-family-regrade stays at c529de2b). repo:hotcrm seat, session_012zh91QzFgePbkmuHnugLN3, 2026-10-08T06:32Z.

    Readings checked by the seat:

    • The report comment is on this card (32 KB, read back intact).
    • The branch has 0 commits and the worktree is gone.
    • Every probe recorded a proven restore.
    • 52 rows were re-graded with the three-gate, per-case method on c529de2b (@objectstack/* 17.7.0, pnpm lint = objectstack lint --strict): 19 retire · 17 keep (partial, a convention pin, or permanent) · 2 still blocked · 14 nothing to retire or mis-credited.
    • The PM premise that the upstream gap cards were still open was disproved, a good result: all 14 are closed and shipped in 17.7.0.

    Fold plan, per ruling hotcrm-R74 item 4 B: ONE retirement PR, folded under this card

    The 19 rows to retire, each verified non-vacuous, reachable and subsuming per case:

    F6 (#1587) has none; it closes not_planned.

    The PR's rules:

    Serial order: after hotcrm#1907 / PR #2011 (governed, awaiting the maintainer), which rewires imports in about 100 of the same test files. Then #1595 (the harness port) follows this PR.

    Upstream from this round: objectstack-ai/objectstack#22212 (five lint --strict blind spots, each against a firing control). The FLS unknown-object half is ⛔ not filed, because the platform documents it as deliberate.

    Release: claim 6052933391 (fold chain head) · reason: the measurement round is delivered, and the retirement waits on a sibling PR's files · destination: pm:queue. The assignee is cleared in the same act, and the dispatch that takes the fold claims it anew.


    Generated by Claude Code

  8. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Oct 8, 2026
  9. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 8, 2026
  10. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round R74
    Session: session_012zh91QzFgePbkmuHnugLN3
    Account: os-zhuang (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-1582-family-regrade
    Worktree: hotcrm-issue-1582
    Domain: repo:hotcrm (single-lane repo, no domain:* taxonomy)
    Seat: repo:hotcrm#1
    File surface: the test files holding the 19 retire rows of the re-grade table (report 6053840674; plan in this card's verdict comment), per-row deletion only; scripts/check-lint-i18n-gate.mjs and test/lint-i18n-gate.test.ts (deleted); package.json (lint:i18n-gate script and its verify step); .github/workflows/{ci,code-quality}.yml (the i18n gate step); test/script-main-guard.test.ts and scripts/lib/main-module.mjs (their roster and comment naming the deleted script); the stale prose in test/i18n-references.test.ts and in the comments of src/sales/profiles/{sales-manager,marketing-user}.profile.ts; one .changeset/1582-*.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default (dispatch-gates --tier --repo objectstack-ai/hotcrm answers "no path-derived mandate … the tier stays the PM's per-card judgment call")
    Clause-②: no
    Responsibility: n/a — not a defect card (retirement share, F1: the inert config.condition sweep; view-tab-label-inert)
    Thread-read: 6053925467
    Serial constraints cleared: folded family dispatch, chain head #1582, members #1583 #1584 #1585 #1586 #1805, each carrying its own claim naming this branch. Fold gates: ① one method (retire rows that passed the three-gate, per-case re-grade); ② one region (test/** plus the gate script and its wiring, one worktree); ③ every member is ruled (hotcrm-R74 item 4 B) and graded (re-grade 6053840674); ④ each member checks on its own family rows; ⑤ EXCLUDED: every keep row (byte-identical), F6 (#1587, closed), #1595 (the harness port), #1919 (src comments, serial after this PR). The predecessor on the shared test files, hotcrm#1907 / PR #2011, MERGED as b54380cf; no open PR in the repo. Ruling-ref: hotcrm-R74 item 4 B.


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    Report time: 2026-10-08T08:05Z

    {
      "issue": 1582,
      "status": "done",
      "branch": "claude/issue-1582-family-regrade",
      "pr": "https://github.com/objectstack-ai/hotcrm/pull/2012",
      "session": "session_012zh91QzFgePbkmuHnugLN3 (mode:subagent; this is the parent seat's id)",
      "premise_still_valid": true,
      "summary": "This PR retires 17 of the 19 rows in the fold plan. 2 rows are kept, because re-ablation on b54380cf found cases that strict lint does not report. Every case of every row was re-ablated on b54380cf before its deletion, and one case per retired row was re-run lint-only after deletion, at 68fe1b2c (18 probes, all exit 1 with the same rule id). Retired: F1 inert decision config.condition, and the view-tab-label-inert file with its walk guard. F2 page-field-unknown, field-group-undeclared/-empty/-shadowed, ai-skill-tool-unresolved (plus the PLATFORM_TOOLS literal only it used), and the view-references form-field sweep. F3 A1, A3, A4 and A8 (plus the helpers only they used). F4 the option-key, apps.*.navigation orphan and section-name-missing sweeps. F5 the FLS assertion split by branch: the unknown-object branch stays, renamed 'every object-qualified FLS key names a real object'. F5 both RLS sweeps. KEPT, a deviation from the plan backed by measurement: (1) The F2 component-props parse sweep. This case is new: a page component nested in another component's properties, with its own properties absent and a contract that requires keys, gets exit 0. Measured on a nested record:related_list, page:accordion and element:text. Control: the same edit on a top-level record:path gives exit 1, component-props-invalid. Mechanism: PageComponentSchema defaults properties only on parsed top-level components, and validateComponentProps skips with `if (!props) continue`. (2) The whole F4 lint:i18n-gate gate. This is a REGRESSION against the re-grade. On b54380cf, a deleted zh-CN entry of each of the 12 i18n/missing-* classes gives `pnpm lint` exit 0 and gate exit 0; the re-grade measured exit 1 on all 12 at c529de2b. Cause: since #1907 the artifact is composeStacks([...], {manifest:'preserve'}), and the CLI's translation-coverage pass reads the composed top level, which holds only manifest, packages and i18n. The same deletion computed over the union-of-packages stack IS reported. Today pnpm verify catches no missing non-default label for object, navigation, dashboard, dataset or flow; local completeness tests catch the other 7 classes. I removed the gate, re-ablated, could not produce the 'loses nothing' proof, and restored all gate files byte-identical to b54380cf (commit 68fe1b2c). Holders: two cases are silent at lint but red on a kept assertion, the same shape as the re-grade's RLS precedent. First, a skill tool that only a stack.tools record serves (held by 'does not lean on AI tool metadata'). Second, an RLS policy on an unknown object (held by 'every policy targets an object the same set grants'). New cases probed beyond the re-grade: the RLS `check` slot (3 cases, all fire), service-module sites for inert condition, page field, skill, report dataset and report dimension/measure (all fire), absent nested properties, and the tool-metadata name. Files changed vs b54380cf (17): .changeset/1582-retire-tests-strict-lint-owns.md (empty frontmatter); test/{flow-decision-authority,metadata-references,field-groups-coverage,view-references,skills-integrity,analytics-integrity,i18n-references,authorization-coverage}.test.ts; test/view-tab-label-inert.test.ts (deleted); src/sales/profiles/{sales-manager,marketing-user}.profile.ts (comments only: the 17.7.0 ledger marks rowLevelSecurity label/description live); and, OUTSIDE the claimed surface because the retirements made their text false, content/docs/analytics/cubes{,.zh-Hans,.zh-Hant}.mdx, docs/ARCHITECTURE.md and docs/developers/code_examples.md. Deviations: (a) The PR body says 'Part of #1583' and 'Part of #1585', not the dispatch's six closing lines, because each card keeps one row; this conflict is raised in open_questions. (b) The F4 proof was not obtained and the gate was kept. (c) Edits outside the surface, as listed. (d) Commit 855d92d1 removed the gate and commit 68fe1b2c restored it; the net diff vs main for the gate files is empty. The harness attribution reminder named a model ('Co-Authored-By: Claude Opus 5.5'). Per the dispatch it was not imitated: commit trailers are model-free (Claude-Session + Co-authored-by: Claude). Labels documentation, ci/cd and metadata on PR #2012 were set by the repo's labeler, not by this run. Worktree removed: node_modules deleted first, then `git worktree remove` without --force, exit 0.",
      "tests": "GATE pnpm verify @68fe1b2c: `OS_VERIFY_LOCK_SLOT=hotcrm-1582 os-verify-lock.sh -c 'pnpm verify'` gives 'VERDICT command-exit 0 · held the lock 230s'. Steps: validate '✓ Validation passed'; typecheck clean; lint '18 suggestion(s)' exit 0; lint:i18n-gate '✓ i18n lint gate: 0 `i18n/missing-*` issues'; hygiene '✓ source hygiene clean'; hygiene:tokens '✓ source token ratchet clean'; build '✓ Build complete'; test 'Test Files 178 passed (178) · Tests 3814 passed | 1 skipped (3815)'. RE-ABLATION @b54380cf, before deletion. Harness: ablation-replace.mjs WRAP; the anchor must hit exactly once; `pnpm lint --json` exit captured before any pipe; the owning vitest file runs; a trap restores by `git checkout HEAD -- ABS` with hash-object == HEAD blob, empty `git diff HEAD` and clean status. Baseline: errors 0, warnings 0, suggestions 18, failing 0, strict true, exit 0. Per-case lint exits: F1 inert top 1, nested 1, service 1, blank 1 (expression-invalid), null 0 (same as the re-grade, accepted exception); tabs list 1, listViews 1 (schema refusal); page-field highlights 1, path 1, details 1, service 1; field-group undeclared 1, empty 1, shadowed 1; skill plain 1, action_ 1, wildcard 1, service 1, tool-metadata-only 0 (holder red); form-field default 1, formViews 1; A1 chartless 1, charted 1, joined 1, service 1; A3 1; A4 rows 1, cols 1, values 1, charted rows 1, joined rows 1, service 1; A8 bare 1, $eq 1, $in 1; option-key extra 1, zh-CN 1, English label 1, no-options field 1, unknown field 1; nav orphan id 1, app 1; section view 1, formViews 1, page 1; FLS unknown field 1, empty remainder 1, unqualified 1, unknown object 0 (kept branch); RLS using unknown field 1, leading 1, uservar 1, nested 1, funcall 1, check unknown field 1, check uservar 1, check funcall 1, unknown object 0 (holder red). KEPT-ROW EVIDENCE: nested record:related_list, page:accordion and element:text with properties absent give lint 0 and local red; top-level record:path control gives lint 1 (component-props-invalid). F4 @68fe1b2c, gate restored: zh-CN deletion per class (object, field, option, section, view, action, navigation, dashboard, widget, dataset, page, flow) gives plain `pnpm lint` 0, `--json` failing 0, gate 0 on all 12; local test red on field, option, section, view, action, widget and page, nothing red on object, navigation, dashboard, dataset and flow. POST-DELETION @68fe1b2c: 18 lint-only probes, all exit 1, same rule ids. TOTALS: 104 probe runs in 15 batches; 0 unproven restores; every batch ended 'TREE CLEAN after batch'. 4 probes were refused by ablation-replace (anchor contained in the replacement), declared and redone with unique anchors. BYTE-IDENTITY: the it() blocks of the 8 edited test files were sha1-hashed at b54380cf and HEAD: 118 identical, 17 removed (retired), 1 added (FLS split), 1 comment-only (nav guard; non-comment lines sha1 44031195 at both). Gate files (script, test, package.json, two workflows, script-main-guard, main-module, verify-log-decoy-pin): `git diff b54380cf HEAD` empty. EARLIER: typecheck + 10 affected files @855d92d1 content gave 'Test Files 10 passed (10) · Tests 184 passed (184)'. dispatch-gates: `--commands --repo objectstack-ai/hotcrm` REFUSED, by design (hotcrm list hand-derived = pnpm verify); `--tier` gave 'no path-derived mandate'. CI on PR #2012 @68fe1b2c at report time: Typecheck and Build, Quality Checks, Check Changeset, CodeQL, link-check and Label Pull Request success; Playwright and Build and Test (22.x) in_progress.",
      "mcp_calls": "0 — all card, comment and PR reads went through repo-scoped `gh api` REST GETs",
      "api_writes": "3 REST writes, all through the fleet relay as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/hotcrm/pulls (draft, #2012; relay run 37747237330; read-back: 15569 bytes stored, identical); (2) label-write --assign os-zhuang, POST /repos/objectstack-ai/hotcrm/issues/2012/assignees (relay run 37747284470; read-back matches); (3) os-dev-report comment, POST /repos/objectstack-ai/hotcrm/issues/1582/comments (post-stamped.mjs). Not REST: 3 `git push` to claude/issue-1582-family-regrade (fast-forward c529de2b..b54380cf, then ..855d92d1, then ..68fe1b2c; no force).",
      "open_questions": [
        {
          "question": "The dispatch asked for a closing line for each of the six members. #1583 and #1585 each keep one row after re-ablation (the props parse sweep, and the i18n gate). Should merging PR #2012 close them?",
          "options": [
            "A: keep 'Part of #1583' / 'Part of #1585' (as written). Both stay open as the blocked carriers. The seat files the two upstream defects and adds Blocked-by lines, and the kept rows resume their ruled retirement when the pin carries the fixes.",
            "B: the seat edits the PR body to closing lines for #1583 and #1585, and files new Blocked-by cards for the two kept rows."
          ],
          "recommendation": "A. AGENTS.md section 2 asks for exactly this: a blocked card with a Blocked-by line, resuming the original ruling once the platform fix lands. #1585's own title condition ('once strict covers the i18n/missing-*') is unmet on this base. The dev contract also says a merge that should not close a card gets 'Part of'."
        }
      ],
      "out_of_scope_findings": [
        "class: a · reach: `pnpm lint` (objectstack lint --strict, @objectstack/cli 17.7.0) exits 0 on hotcrm b54380cf with one zh-CN entry deleted, for each of 12 i18n/missing-* classes; the same probes exited 1 at c529de2b. The gate script reads the same report and also exits 0. The producer is the CLI's own lint command: commands/lint.js runs computeI18nCoverage(normalized) on the composed top level. After composeStacks([...], {manifest:'preserve'}) that level carries only manifest, packages and i18n: no translations and no metadata. Computed over authoringRuleUnionStack, the deletion IS reported (warning zh-CN navigation apps.crm_enterprise.navigation.nav_home.label). Effect on hotcrm: pnpm verify catches no missing non-default label for object, navigation, dashboard, dataset or flow. This blocks retiring #1585's gate (kept). · evidence: retire/r/g-*.summary.txt, retire/cov.ts in-memory union check · dedupe words: i18n coverage composeStacks; computeI18nCoverage composed artifact top level; i18n/missing composed translations packages; lint translation coverage union stack",
        "class: a · reach: `objectstack lint --strict` exits 0 when a page component NESTED in another component's properties has its own `properties` absent while its ComponentPropsMap contract requires keys. Measured on nested record:related_list, page:accordion and element:text in hotcrm pages. Control: the same edit on a top-level record:path fires component-props-invalid (exit 1). Producer: @objectstack/lint validateComponentProps (`if (!props) continue`). PageComponentSchema defaults properties to {} only on the components it parses (top level). This blocks retiring #1583's props parse sweep (kept). · evidence: x-props-absent-related / -accordion / -text vs x-props-absent-path · dedupe words: component-props nested properties absent; validateComponentProps skip missing properties; nested page component required props lint",
        "carrier: none (承接者:无) · noted, not filed: ai-skill-tool-unresolved counts a `stack.tools` record as resolving (its message lists 'not a `stack.tools` record' as a source). @objectstack/spec ToolSchema says 'Authoring a tool as metadata does NOT make it runnable … no framework executor loads a metadata-authored tool'. A skill naming a tool that only such a record serves therefore gets lint exit 0 [x-skill-toolmeta2]. Whether the cloud edition's separately registered AIToolDefinition makes this legitimate is not established. hotcrm holds the case with its kept 'does not lean on AI tool metadata' test.",
        "carrier: #1919 (the src-comment card that follows this PR) · noted, not filed: two src comments became false with this PR, and the dispatch forbids src edits beyond the two profile comments: src/revenue/objects/opportunity_line_item.object.ts:69 ('`test/field-groups-coverage.test.ts` pins it', field-group-shadowed) and src/service/flows/case-escalation.flow.ts:123 ('`test/flow-decision-authority.test.ts` enforces that', flow-inert-node-condition). Both should name the lint rule instead.",
        "carrier: none (承接者:无) · noted, not filed: test/i18n-shared-widget-parity.test.ts:23 says '`pnpm lint --skip-i18n` skips the bundles'. It has been false since #1581 and is outside this surface. The kept gate's own prose (scripts/check-lint-i18n-gate.mjs header; the CI step comment 'Under `--strict` (#1581) it now fails on both, so this step overlaps it') stays byte-identical with the kept row, and is not true on this base until the coverage defect above is fixed."
      ]
    }

    Generated by Claude Code

  12. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Verdict: os-dev report 6055559714, PR #2012 — ACCEPT. 17 of 19 rows are retired; 2 are kept on measurement. repo:hotcrm seat, session_012zh91QzFgePbkmuHnugLN3, 2026-10-08T08:10Z.

    Checked on GitHub and the tree, not taken from the report:

    • PR test: retire 17 family rows strict lint now owns; keep the i18n gate and the props parse sweep (#1582) #2012: head 68fe1b2c, 17 files, +136/−731.
      • check-governed-merges: 0 of 17 paths governed.
      • CI: 10 of 10 check runs success.
      • Body lines: Fixes #1582, Part of #1583, Fixes #1584, Part of #1585, Fixes #1586, Fixes #1805, Clause-②: no.
    • The kept gate is untouched: the net diff against main over scripts/check-lint-i18n-gate.mjs, test/lint-i18n-gate.test.ts, package.json, .github/workflows, test/script-main-guard.test.ts and scripts/lib/main-module.mjs is 0 lines.
    • The changeset is empty-frontmatter (releases nothing), and it says truthfully that the gate stays and why.

    From the report:

    • Every case of every retired row was re-ablated on b54380cf before deletion. One case per retired row was re-run after deletion at 68fe1b2c: 18 lint-only probes, all exit 1 with the same rule id.
    • The 118 kept it() blocks hash byte-identical.

    The two kept rows are a measured deviation from the plan, and it is correct:

    1. F2 (Retire the local metadata reference-integrity tests the platform's page / view / nav / action / field-group rules already enforce (epic #1579, step 3, family F2) #1583): the component-props parse sweep. A NESTED page component with absent properties is silent under lint --strict, while the top-level control fires. Added to lint: five os lint --strict blind spots measured against firing controls — aliased ctx.api in hook handlers, action-body free identifiers, unbound visibility roots, lookup-bound detail grants, empty record:details sections objectstack#22212 as member 6.
    2. F4 (Retire the local i18n reference test and the severity-promoting i18n gate, once strict covers the i18n/missing-* and translation-target rules (epic #1579, step 3, family F4) #1585): the lint:i18n-gate gate. This is a regression found on this base. Since Packaging: sales becomes the type: app package and service a type: module — composeStacks(…, { manifest: 'preserve' }), navigation contributions, two-entry packages[] #1907 / PR feat(packaging): one artifact, two packages — sales is the app package, service a module #2011 composed the app as two packages, objectstack lint's translation coverage reads only the artifact top level. On b54380cf, 12 of 12 i18n/missing-* classes give exit 0, where the re-grade measured exit 1 on c529de2b.

    The dev's open question, answered: A. Part of #1583 and Part of #1585 stay. After the merge both go to pm:blocked:

    Each kept row resumes its ruled retirement once the fix is installable (AGENTS.md §2).

    Surface amendment, recorded: content/docs/analytics/cubes{,.zh-Hans,.zh-Hant}.mdx, docs/ARCHITECTURE.md and docs/developers/code_examples.md were edited because the retirements made their text false (they named retired tests). The edits are in proportion: 2–9 lines each.

    Carried to #1919 (src comments): src/revenue/objects/opportunity_line_item.object.ts (~69) and src/service/flows/case-escalation.flow.ts (~123) now cite retired tests; they should name the lint rule instead.

    Landing: not governed and fully green, so the PR goes ready, then auto-merge through the merge queue.

    Implemented-by: os-dev subagent of this session (branch claude/issue-1582-family-regrade) · Reviewed-by: repo:hotcrm seat


    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

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions