Repository navigation
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
Description
Blocked-by: #1581
Sub-issue of the epic #1579; pm:epic. Filed by the director seat (objectstack#12708, summon #15).
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:
- 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-uncontainedto 0) #1611: two walkers passed green while inspecting nothing, 61 cases → 47.) - Reachable — the platform rule fires on this repo at all.
- Subsuming — for EVERY CASE the assertion covers, ⛔ not for the assertion as a whole and ⛔ not for "a defect of the rule's class".
⇒ 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 — |
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
os lint --strictfirst, then retire the local re-implementations by family #1579objectstack-fleet commentedon Oct 8, 2026
Ruling: batch hotcrm-R74 item 4 · letter B · maintainer 「同意」 2026-10-08T03:15Z
repo:hotcrmseat,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:blockedon theBlocked-by: #1581line already in this body ·pm:epicis 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 closesnot_plannedif 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
objectstack-fleet commentedon Oct 8, 2026
Claim: PM loop round R74
Session:
session_012zh91QzFgePbkmuHnugLN3Account:
os-zhuang(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-1582-family-regradeWorktree:
hotcrm-issue-1582Domain:
repo:hotcrm(single-lane repo, nodomain:*taxonomy)Seat:
repo:hotcrm#1File surface: none committed: a READ-ONLY measurement round over the
test/**andscripts/**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/hotcrmanswers "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
objectstack-fleet commentedon Oct 8, 2026
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. Branchclaude/issue-1582-family-regradeis 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 --strictHow 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 inscratchpad/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
config.conditioncondition: ''→ expression-invalid (error) [f1-inert-empty]. Silent only oncondition: 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.if (outs.length === 0) continueconditions[]-routability inspects an empty set (0 decisions declareconditions[]). Mechanism tests are live-enginetabs(+ walk guard)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 policylabelis ledger-livein 17.7.0 [f1-liveness-ctl], andaction.shortcutis also tombstoned [f1-liveness-ctl2]ctx.api.object(…)(error, exit 1) [f6-hook-readonly-ctxdirect]. Silent on this repo's mandatedconst api = ctx.apialias [f6-hook-readonly]: 25 alias declarations, 73api.object(call sites, 0 directhas(x) && x == …now fires, error ×7 [f1-guarded-bare-ident]. ✓ unparseable → visibility-predicate-syntax, error ×7 [f1-unparseable-predicate]foo.duplicate_of_typeis silent: exit 0. Local red, and the engine throws "Unknown variable: foo" [f1-unbound-namespace]record.ratingin a flow condition gives exit 0 [f1-flow-unguarded]. Flow conditions are excluded by #4953 clause 3, per #15936's answerF2 (#1583): reference integrity
page:header[f2-props-unknown-slot3]. ✓ invalid value at top level [f2-props-invalid-top2] and nested [f2-props-invalid-nested]action_prefix [f2-skill-action] · wildcard [f2-skill-wildcard2]form[f2-form-field-default] ·formViewsobject entry [f2-form-field-formviews]{ name, fields: [] }is silent: exit 0, local red [f2-page-section-empty]allowReadonly (#1637)actionUrlF3 (#1584): analytics
$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)F4 (#1585): i18n and the severity-promoting gate
fields.nameandfields.spaceon crm_case, which declares neither, are silent: exit 0, local red [f4-target-field-name, f4-target-field-space]. (organization_idis also silent, but it is an injected column, so that red is a local false positive [f4-target-field-orgid])name/spaceare blanket-implicit in the rule)_viewsentry for a view the stack does not ship_views.web_to_case(a shipped formView key) is silent and the local goes red [f4-target-views-formview]. Not established whether that key rendersapps.*.navigationentry the app does not declareform[f4-section-view] ·formViews[f4-section-formviews2] · page record:details nested in tabs [f4-section-page]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]scripts/check-lint-i18n-gate.mjs+test/lint-i18n-gate.test.ts+lint:i18n-gateinverifyand in both CI workflowspnpm lintitself exits 1 on i18n/missing-navigation [pnpm-lint-ctl]foldCoverageIssuesemits onlyerrororwarning, neversuggestion, and the gate andpnpm lintshare 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.F5 (#1586): security, sharing and org axis (with #1805)
grep -c= 1, control unqualified-key = 1)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]no_such_object.descriptionis silent, exit 0, local red [f5-fls-unknown-object]. This is deliberate per objectstack PR #16998usingpredicate existscurrent_user.nope[f5-rls-unknown-uservar] · nested path [f5-rls-nested-ctl] · function call [f5-rls-funcall]objectcase is silent at lint, but the kept, uncredited "every policy targets an object the same set grants" holds it (red) [f5-rls-unknown-object]public_read_write→ error ×2, exit 1 [f5-share-prw-ctl]public_readis still silent [f5-share-read-on-publicread]crm_campaign_memberis silent, exit 0 [f5-nonmd-nonnav-ungranted]allowExportgives exit 0 [f5-anchor-guest-export]. The rule still keys onisDefault(dist re-read)sharingModel: 'read'getsinvalid_valueat config load, exit 1, no rule id [f5-owd-alias]suggestion) [f5-cbp-reqlookup-remains]CEL_PUSHDOWN_LIMITS_MODE = "rc-grace"severity: "info"in the 17.7.0 distF6 (#1587): write shape
ctx.api.object(…)→ warning, exit 1 [f6-hook-unknown-field-ctxdirect]. Silent on the repo'sconst api = ctx.apialias [f6-hook-unknown-field]update(id, doc)is silent, exit 0, while the local static and runtime cases go red [f6-hook-bare-id]Counts (rows in this table)
Platform gaps found by this round ("to file"): (1) the hook write family is blind to a
const api = ctx.apialias. (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 withfields: []. (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
objectstack-fleet commentedon Oct 8, 2026
Verdict on the re-grade report
6053840674(on #1582) — ACCEPT (a measurement round: nothing deleted, no PR; the empty marker branchclaude/issue-1582-family-regradestays atc529de2b).repo:hotcrmseat,session_012zh91QzFgePbkmuHnugLN3, 2026-10-08T06:32Z.Readings checked by the seat:
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.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:
config.conditionsweep (flow-inert-node-condition);view-tab-label-inert(credit the spec 17.5.0 schema tombstone, notliveness-*).page-field-unknown(highlights, path and details); the component-props parse sweep;field-group-undeclared/-empty/-shadowed;ai-skill-tool-unresolved; theview-referencesform-field sweep (form-field-unknown).chart-dataset-unknown, A3chart-dimension-unknown, A4 dimension and measure, A8filter-preset-comparand.i18n/missing-*and translation-target rules (epic #1579, step 3, family F4) #1585 (4): the option-key sweep; theapps.*.navigationorphan sweep; the section-name-missing sweep; and the wholescripts/check-lint-i18n-gate.mjsgate (its test, itspackage.jsonscript, itsverifystep and its two CI workflow steps). Strict lint subsumes it on all 12 reachablei18n/missing-*ids, and nothing it checks escapes strict.test/authorization-coverage.test.ts's qualified-FLS-key half once@objectstack/lintshipssecurity-fls-unknown-field(objectstack#16108 → objectstack PR #16998, merged, not yet released) #1805 (2): the FLS assertion split by branch (retire the unqualified-key and names-a-real-field branches; KEEP the unknown-object branch, which the platform defers to bind/install time by design in objectstack PR #16998); both RLS sweeps (rls-predicate-unknown-field/-unknown-user-variable), keeping "every policy targets an object the same set grants".F6 (#1587) has none; it closes
not_planned.The PR's rules:
Fixes #Nline per member: Retire the local tests that re-implement the platform's flow / predicate / readonly-write lint rules, onceos lint --strictguards them (epic #1579, step 3, family F1) #1582, 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, Retire the local analytics / dataset integrity tests the platform's chart / widget / dataset rules already enforce (epic #1579, step 3, family F3) #1584, Retire the local i18n reference test and the severity-promoting i18n gate, once strict covers thei18n/missing-*and translation-target rules (epic #1579, step 3, family F4) #1585, Retire the local security / sharing / org-axis coverage tests the platform's security-* and sharing-rule-* rules already enforce (epic #1579, step 3, family F5) #1586, Re-run the three-gate test ontest/authorization-coverage.test.ts's qualified-FLS-key half once@objectstack/lintshipssecurity-fls-unknown-field(objectstack#16108 → objectstack PR #16998, merged, not yet released) #1805.test/i18n-references.test.tssayspnpm lintruns with--skip-i18n, and that lint exits 0 on warnings;src/sales/profiles/{sales-manager,marketing-user}.profile.tssay therowLevelSecuritylabel and description are "liveness: dead in 17.6.0", but the 17.7.0 ledger marks them live.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 --strictblind 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
objectstack-fleet commentedon Oct 8, 2026
Claim: PM loop round R74
Session:
session_012zh91QzFgePbkmuHnugLN3Account:
os-zhuang(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-1582-family-regradeWorktree:
hotcrm-issue-1582Domain:
repo:hotcrm(single-lane repo, nodomain:*taxonomy)Seat:
repo:hotcrm#1File 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.mjsandtest/lint-i18n-gate.test.ts(deleted);package.json(lint:i18n-gatescript and itsverifystep);.github/workflows/{ci,code-quality}.yml(the i18n gate step);test/script-main-guard.test.tsandscripts/lib/main-module.mjs(their roster and comment naming the deleted script); the stale prose intest/i18n-references.test.tsand in the comments ofsrc/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/hotcrmanswers "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-grade6053840674); ④ 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 asb54380cf; no open PR in the repo. Ruling-ref: hotcrm-R74 item 4 B.Generated by Claude Code
test: retire the F1/F2 rows strict lint now owns (#1582, #1583)
objectstack-fleet commentedon Oct 8, 2026
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
objectstack-fleet commentedon Oct 8, 2026
Verdict: os-dev report
6055559714, PR #2012 — ACCEPT. 17 of 19 rows are retired; 2 are kept on measurement.repo:hotcrmseat,session_012zh91QzFgePbkmuHnugLN3, 2026-10-08T08:10Z.Checked on GitHub and the tree, not taken from the report:
68fe1b2c, 17 files, +136/−731.check-governed-merges: 0 of 17 paths governed.success.Fixes #1582,Part of #1583,Fixes #1584,Part of #1585,Fixes #1586,Fixes #1805,Clause-②: no.mainoverscripts/check-lint-i18n-gate.mjs,test/lint-i18n-gate.test.ts,package.json,.github/workflows,test/script-main-guard.test.tsandscripts/lib/main-module.mjsis 0 lines.From the report:
b54380cfbefore deletion. One case per retired row was re-run after deletion at68fe1b2c: 18 lint-only probes, all exit 1 with the same rule id.it()blocks hash byte-identical.The two kept rows are a measured deviation from the plan, and it is correct:
propertiesis silent underlint --strict, while the top-level control fires. Added to lint: fiveos lint --strictblind spots measured against firing controls — aliasedctx.apiin hook handlers, action-body free identifiers, unbound visibility roots, lookup-bound detail grants, empty record:details sections objectstack#22212 as member 6.i18n/missing-*and translation-target rules (epic #1579, step 3, family F4) #1585): thelint:i18n-gategate. This is a regression found on this base. Since Packaging: sales becomes thetype: apppackage and service atype: module—composeStacks(…, { manifest: 'preserve' }), navigation contributions, two-entrypackages[]#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. Onb54380cf, 12 of 12i18n/missing-*classes give exit 0, where the re-grade measured exit 1 onc529de2b.pnpm verifyis blind today to a missing non-default label for objects, navigation, dashboards, datasets and flows. Local tests still hold the other 7 classes.os linttranslation coverage reads only a multi-package artifacts top level — everyi18n/missing-*` goes silent once an app ships as packages (missed reader of the ADR-0130 2026-09-22 addendum; family of #22189) objectstack#22238, the family card for "CLI readers of a multi-package artifact's top level", with #22189 as its second member.The dev's open question, answered: A.
Part of #1583andPart of #1585stay. After the merge both go topm:blocked:Blocked-by: objectstack-ai/objectstack#22212;i18n/missing-*and translation-target rules (epic #1579, step 3, family F4) #1585 withBlocked-by: objectstack-ai/objectstack#22238.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.mdanddocs/developers/code_examples.mdwere 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) andsrc/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:hotcrmseatGenerated by Claude Code