Skip to content

Commit 3fe813e

Browse files
committed
fix(pm): H44 stops reading a session id as a dateless tree tip
`H44_READING_FRAGMENTS`'s `tree` shape is 7-40 hex characters carrying both a letter and a digit, and a session identifier satisfies it: on one live board 6 of 9 H44 rows in a sweep were 「PM session c5c0ce54」/「PM 会话 c5c0ce54」 or the LAST SEGMENT of a `session_<uuid>` line, where the hyphen is a word boundary. The row's remedy — add the time the reading was taken — applied to an identifier asks a seat to stamp a reading nobody took, so the row taught the wrong discipline rather than merely being noisy. The `tree` shape itself is unchanged (pinned by its regex source). What is added is narrowing 4: a per-shape `exclude` predicate, declared on the `tree` entry alone, that drops a CANDIDATE which is lexically part of an identifier — the tail of a longer token behind a flush `-`/`_`, or one introduced immediately by `session` / `会话` / `session_`. The introduction leg reads ADJACENCY rather than 「the sentence mentions a session」 because the loose spelling silenced a real dateless tip reading measured on this board (「this session's checkout HEAD `588475c3`」), which is pinned as CONTROL D. The paragraph is rescanned past an excluded candidate for a shape that declares an exclusion, so a comment that fired can change which fragment it names but a quiet one cannot start firing. Self-test: 4270 -> 4297 cases, H44 105 -> 132. CONTROL A (a dateless real tip) still fires and CONTROL B (the same tip stamped) stays quiet. Claude-Session: https://claude.ai/code/session_01HPfcjvF23QBoBj7P47DDxs Co-authored-by: Claude <noreply@anthropic.com>
1 parent 90b23ab commit 3fe813e

1 file changed

Lines changed: 197 additions & 3 deletions

File tree

‎scripts/pm/check-half-states.mjs‎

Lines changed: 197 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9577,7 +9577,7 @@ export function h43GovernedReviewRequestGap(pr, governed, approvers, reviewed =
95779577
// of these five artefacts a table of bare numbers is a board
95789578
// reading essentially always.
95799579
//
9580-
// ## Three narrowings, each of which can only make the row QUIETER
9580+
// ## Four narrowings, each of which can only make the row QUIETER
95819581
//
95829582
// 1. A paragraph carrying a timestamp is clean, and the timestamp shape is
95839583
// `HH:MM[:SS]Z`. ⚠️ The optional SECONDS field is a deliberate widening of
@@ -9592,6 +9592,35 @@ export function h43GovernedReviewRequestGap(pr, governed, approvers, reviewed =
95929592
// 3. ⛔ No comment-length floor. The fragment requirement is a stronger filter
95939593
// than a character count, and a length constant with no measurement behind it
95949594
// is a number nobody can defend later.
9595+
// 4. ⚠️ A `tree` candidate that is lexically part of an IDENTIFIER is not a tip
9596+
// (#18385). A SESSION ID satisfies the tree shape — measured on a live board,
9597+
// 6 of 9 H44 rows in one sweep were this and nothing else: 「PM session
9598+
// c5c0ce54」 / 「PM 会话 c5c0ce54」 fired on the shorthand itself, and a
9599+
// canonical `Session:` line carrying `session_71836b57-…-c6d9d7dd2cc6` fired
9600+
// on the UUID's LAST SEGMENT, because `-` is a word boundary. ⛔ The cost is
9601+
// not a noisy row: this row's remedy sentence says 「add the time the reading
9602+
// was taken」, and there is no board state behind an id — applied to one it
9603+
// asks a seat to STAMP A READING NOBODY TOOK, and a later reader cannot tell
9604+
// that stamp from a real one. So the row would teach the wrong discipline,
9605+
// which is worse than saying nothing. ⛔ The `tree` SHAPE is not loosened —
9606+
// a real tip is matched exactly as before (`h44TreeCandidateIsIdentifier` is
9607+
// the whole of the change, and it excludes a CANDIDATE, never a spelling):
9608+
// • the UUID leg — the candidate sits immediately behind a `-` or `_` that
9609+
// itself follows a word character, i.e. it is the tail of a longer token.
9610+
// • the introduction leg — the candidate's SENTENCE says `session` /
9611+
// `会话` (`session_` is that word with its separator).
9612+
// ⚠️ SENTENCE, not paragraph, and that boundary is a DECISION pinned in the
9613+
// self-test rather than a detail: every claim comment carries a `Session:`
9614+
// line, so a paragraph-scoped test would silence H44 on the whole claim
9615+
// population — a false negative exactly where CONTROL A lives. A dateless
9616+
// real tip in its OWN sentence still fires with a session id one line above
9617+
// it; the declared false negative is the two in ONE sentence, which is the
9618+
// narrowest shape that covers the measured spellings.
9619+
// ⚠️ The paragraph is RESCANNED past an excluded candidate — only for a shape
9620+
// that declares an `exclude`, which is this one alone — so a comment that
9621+
// fired before can change WHICH fragment it names but a quiet one cannot
9622+
// start firing. Like narrowings 1–3 it admits strictly more paragraphs as
9623+
// clean and cannot manufacture a finding.
95959624
//
95969625
// ## Cost, and the residual this row DECLARES rather than hides
95979626
//
@@ -9711,11 +9740,85 @@ export function h43GovernedReviewRequestGap(pr, governed, approvers, reviewed =
97119740
*/
97129741
export const H44_READING_TIMESTAMP = /(?:\b|(?<=T))\d{2}:\d{2}(?::\d{2})?Z\b/;
97139742

9743+
/**
9744+
* The word that INTRODUCES an identifier rather than a reading — narrowing 4's
9745+
* second leg, anchored at the END of the text that precedes the candidate, so
9746+
* it reads 「the thing right after this word」 and not 「this word occurs
9747+
* somewhere near」. `session_` is `session` with its separator, so one
9748+
* alternative reaches both spellings, and 「会话」 is the same word on a
9749+
* Chinese-language board.
9750+
*
9751+
* ⚠️ ADJACENCY is the whole of its precision, and it was measured rather than
9752+
* chosen: with the leg written as 「the sentence mentions a session」, live
9753+
* comment `5695539587` on this board — 「every harness-loaded path on
9754+
* `origin/main` is in this session's checkout HEAD `588475c3`」 — went QUIET,
9755+
* and that is a real dateless tip reading this row exists to file. A possessive
9756+
* three words away introduces nothing. Only separators may stand between:
9757+
* whitespace, a colon in either width, quotes, backticks, emphasis and bracket
9758+
* characters — the decorations the claim template actually writes.
9759+
*
9760+
* ⛔ An intervening WORD (「session id c5c0ce54」) is therefore not excluded
9761+
* here: it is one measurement away, not a guess to make now.
9762+
*
9763+
* ⛔ Case-insensitive on purpose and safe to be: unlike `H44_VERDICT_MARKER`
9764+
* this word is not a protocol verb whose lower-case twin is ordinary prose —
9765+
* `Session:`, `session` and `SESSION` all name the same thing.
9766+
*/
9767+
export const H44_IDENTIFIER_INTRO = /(?:session|会话)[\s::=·*'"`([\]_-]*$/i;
9768+
9769+
/**
9770+
* The characters that END a sentence for narrowing 4 — the ASCII set and the
9771+
* full-width forms a Chinese-language board writes, plus the newline, which is
9772+
* what keeps a claim's `Session:` LINE from reaching the tip on the line below
9773+
* it (see the banner: that is the whole of the sentence-vs-paragraph decision).
9774+
*/
9775+
export const H44_SENTENCE_BREAKS = '\n。.!?;!?;';
9776+
9777+
/**
9778+
* The part of the candidate's own sentence that PRECEDES it: from the nearest
9779+
* sentence break before `index` up to `index`. ⛔ Never widened to the
9780+
* paragraph, and never read past the candidate — what follows a fragment
9781+
* introduces nothing.
9782+
*/
9783+
export function h44SentenceLead(paragraph, index) {
9784+
const text = String(paragraph ?? '');
9785+
const at = Math.max(0, Math.min(Number(index) || 0, text.length));
9786+
let start = at;
9787+
while (start > 0 && !H44_SENTENCE_BREAKS.includes(text[start - 1])) start--;
9788+
return text.slice(start, at);
9789+
}
9790+
9791+
/**
9792+
* Narrowing 4's predicate: is this `tree` CANDIDATE lexically part of an
9793+
* identifier rather than a tip? Two legs, both measured on live claim comments
9794+
* (#18385), and the banner carries the argument.
9795+
*
9796+
* ⛔ It reads the TEXT AROUND the candidate and never the candidate itself: a
9797+
* session id and a tip are the same 7–40 hex characters, so anything that
9798+
* judged the token alone would have to loosen the shape for real tips, which is
9799+
* the one thing this change must not do.
9800+
*/
9801+
export function h44TreeCandidateIsIdentifier(paragraph, index, _matched = '') {
9802+
const text = String(paragraph ?? '');
9803+
// The UUID leg: `…-c6d9d7dd2cc6` / `…_c6d9d7dd2cc6` — a separator that itself
9804+
// follows a word character, so the candidate is the TAIL of a longer token
9805+
// and not a token a reader could quote as a tip. (A bullet's `- ` or a lone
9806+
// dash is not this: the separator must be flush against both sides.)
9807+
if (/[0-9A-Za-z_][-_]$/.test(text.slice(0, index))) return true;
9808+
// The introduction leg: the candidate's own sentence introduces it as a
9809+
// session, immediately and with only decoration in between.
9810+
return H44_IDENTIFIER_INTRO.test(h44SentenceLead(text, index));
9811+
}
9812+
97149813
/**
97159814
* The reading grammar, as data so the self-test can drive every shape by name
97169815
* and a reader can check the banner against the regexes. ⛔ No `g` flag on any
97179816
* of them: `exec` on a sticky regex carries `lastIndex` between calls, and this
97189817
* row runs the same regex over thousands of paragraphs.
9818+
*
9819+
* An entry may carry an `exclude(paragraph, index, matched)` — narrowing 4's
9820+
* seam, and the `tree` shape is the only holder. A shape without one is read
9821+
* exactly as before: first match wins, no rescan.
97199822
*/
97209823
export const H44_READING_FRAGMENTS = Object.freeze([
97219824
Object.freeze({
@@ -9737,6 +9840,7 @@ export const H44_READING_FRAGMENTS = Object.freeze([
97379840
kind: 'tree',
97389841
what: 'a tree tip',
97399842
re: /\b(?=[0-9a-f]*[a-f])(?=[0-9a-f]*\d)[0-9a-f]{7,40}\b/,
9843+
exclude: h44TreeCandidateIsIdentifier,
97409844
}),
97419845
Object.freeze({
97429846
kind: 'tableCount',
@@ -9847,6 +9951,33 @@ export function h44ArtefactShape(body, onSeatPost = false) {
98479951
return null;
98489952
}
98499953

9954+
/**
9955+
* The first match of one shape in one paragraph that the shape ADMITS, or null.
9956+
*
9957+
* ⛔ The rescan walks the paragraph by SLICING rather than by setting a `g`
9958+
* flag's `lastIndex`: the declared regexes are frozen and shared across every
9959+
* comment in a run, and a sticky one would make this row's answer depend on how
9960+
* many comments preceded it (the grammar pin). A shape with no `exclude` never
9961+
* reaches the loop's second turn, which is what keeps the anchored `tableCount`
9962+
* shape — whose `^`/`$` a slice would misread — on its original reading.
9963+
*
9964+
* @returns {{ matched: string, index: number }|null}
9965+
*/
9966+
export function h44AdmittedMatch(fragment, paragraph) {
9967+
const text = String(paragraph ?? '');
9968+
let offset = 0;
9969+
while (offset <= text.length) {
9970+
const m = fragment.re.exec(text.slice(offset));
9971+
if (!m) return null;
9972+
const index = offset + m.index;
9973+
if (typeof fragment.exclude !== 'function' || !fragment.exclude(text, index, m[0])) {
9974+
return { matched: m[0], index };
9975+
}
9976+
offset = index + Math.max(1, m[0].length);
9977+
}
9978+
return null;
9979+
}
9980+
98509981
/**
98519982
* H44's predicate — null when the comment is clean or out of scope, else the
98529983
* first reading-shaped fragment that carries no time.
@@ -9862,9 +9993,9 @@ export function h44UntimestampedReading(body, onSeatPost = false) {
98629993
if (!paragraph.trim()) continue;
98639994
if (H44_READING_TIMESTAMP.test(paragraph)) continue;
98649995
for (const fragment of H44_READING_FRAGMENTS) {
9865-
const m = fragment.re.exec(paragraph);
9996+
const m = h44AdmittedMatch(fragment, paragraph);
98669997
if (m) {
9867-
return { shape, kind: fragment.kind, what: fragment.what, fragment: m[0].trim().slice(0, H44_FRAGMENT_ECHO_CAP) };
9998+
return { shape, kind: fragment.kind, what: fragment.what, fragment: m.matched.trim().slice(0, H44_FRAGMENT_ECHO_CAP) };
98689999
}
986910000
}
987010001
}
@@ -29238,6 +29369,69 @@ Doubles as the fire's **write self-check** (step 0). \`201\` is not the reading.
2923829369
t('H44 summary: …and that the page is shared rather than bought per row', saidBy('h44Readings', summaryLine({}, 0)).includes('shared with H56, H64 and H65'), true);
2923929370
t('H44: the window count key rides the enumerated forwarding contract too', SWEEP_COUNT_KEYS.includes('readingSeatNewest'), true);
2924029371

29372+
// -- H44's tree shape: an IDENTIFIER is not a tip (#18385) -----------------
29373+
//
29374+
// ⭐ The three firing inputs are the live spellings measured on the cloud
29375+
// board, where 6 of 9 H44 rows in one sweep were this and nothing else. What
29376+
// makes them worse than noise is the row's own remedy: 「add the time the
29377+
// reading was taken」 applied to a session id asks a seat to stamp a reading
29378+
// nobody took. CONTROL A and CONTROL B are the regression pins that say the
29379+
// row still does its job, and CONTROL C is the canonical spelling that was
29380+
// already quiet — the three of them are what make the change a NARROWING
29381+
// rather than a silencing.
29382+
const TIP_18385 = '480080c7a0';
29383+
const CLAIM_SHORTHAND = 'Claim: PM session c5c0ce54 — dispatching this card';
29384+
const CLAIM_SHORTHAND_ZH = 'Claim: PM 会话 c5c0ce54 派发本卡(席位 F)。';
29385+
const CLAIM_UUID = 'Claim: PM loop round 1 (skills seat)\nSession: `session_71836b57-5db6-459d-ae4d-c6d9d7dd2cc6`';
29386+
const CLAIM_TIP_A = `Claim: PM loop round 1\nreviewed head \`${TIP_18385}\` of the PR`;
29387+
const CLAIM_TIP_B = `${CLAIM_TIP_A} at 2026-09-16T06:48Z`;
29388+
const CLAIM_CANONICAL = 'Claim: PM loop round 1\nSession: `session_01TAUTP6Yky8QWoHUAPDKNJQ`';
29389+
t('H44 identifier: ⭐ 「PM session c5c0ce54」 is an id, not a tip — quiet', h44hit(CLAIM_SHORTHAND), null);
29390+
t('H44 identifier: ⭐ …and the 「PM 会话」 spelling of the same claim', h44hit(CLAIM_SHORTHAND_ZH), null);
29391+
t('H44 identifier: ⭐ …and a canonical `Session:` line, which fired on the UUID\'s LAST SEGMENT', h44hit(CLAIM_UUID), null);
29392+
t('H44 identifier: ⭐ CONTROL A — a dateless REAL tip still fires, which is the whole point', h44kind(CLAIM_TIP_A), 'tree');
29393+
t('H44 identifier: ⭐ …echoed as the tip itself, so the remedy still names what to date', h44frag(CLAIM_TIP_A), TIP_18385);
29394+
t('H44 identifier: ⭐ CONTROL B — the same tip WITH its stamp stays quiet', h44hit(CLAIM_TIP_B), null);
29395+
t('H44 identifier: ⭐ CONTROL C — the canonical session spelling was already quiet and still is', h44hit(CLAIM_CANONICAL), null);
29396+
// ⭐ CONTROL D, and it is the reason the introduction leg reads ADJACENCY
29397+
// rather than 「the sentence mentions a session」: this is comment 5695539587
29398+
// on this board, a REAL dateless tip reading, and the loose spelling of the
29399+
// leg silenced it. A possessive three words away introduces nothing.
29400+
const POSSESSIVE_18385 = 'Claim: x\n\n**Harness reading** — every harness-loaded path on `origin/main` is in this session\'s checkout HEAD `588475c3` (the STALE reading that closed the previous shift)';
29401+
t('H44 identifier: ⭐ CONTROL D — 「this session\'s checkout HEAD` + a tip」 is a READING and still fires', h44frag(POSSESSIVE_18385), '588475c3');
29402+
// The sentence-vs-paragraph DECISION, both directions. A claim carries a
29403+
// `Session:` line by template, so a paragraph-scoped test would silence the
29404+
// whole claim population — CONTROL A's own habitat.
29405+
t('H44 identifier: a session line ABOVE a dateless tip does not silence it — the break is the newline', h44frag(`${CLAIM_CANONICAL}\n\`5bc2f2727ae\` is the tip`), '5bc2f2727ae');
29406+
t('H44 identifier: …and the rescan reaches a real tip past an excluded id in ONE sentence', h44frag(`Claim: x\n\nPM session c5c0ce54 reviewed head ${TIP_18385} of the PR`), TIP_18385);
29407+
// ⛔ The two declared residuals: a word between the introduction and the
29408+
// candidate is NOT excluded (one measurement away, not a guess), and no other
29409+
// shape is touched at all.
29410+
t('H44 identifier: ⛔ an intervening word is not an introduction — 「session id …」 still fires', h44kind('Claim: x\n\nPM session id c5c0ce54 dispatching'), 'tree');
29411+
t('H44 identifier: ⛔ a count beside a session id is still a count', h44kind('Claim: PM session c5c0ce54 — 12 open cards on this lane.'), 'count');
29412+
// ⛔ The SHAPE is not loosened for real tips — pinned as the regex source, so
29413+
// a future widening of the token itself cannot ride in under this row.
29414+
const tree18385 = H44_READING_FRAGMENTS.find((f) => f.kind === 'tree');
29415+
t('H44 identifier: ⛔ the `tree` shape itself is byte-for-byte what it was', tree18385.re.source, '\\b(?=[0-9a-f]*[a-f])(?=[0-9a-f]*\\d)[0-9a-f]{7,40}\\b');
29416+
t('H44 identifier: …and the exclusion is declared on exactly one shape', H44_READING_FRAGMENTS.filter((f) => typeof f.exclude === 'function').length, 1);
29417+
t('H44 identifier: …that one being the tree shape', typeof tree18385.exclude, 'function');
29418+
// The two legs, driven directly.
29419+
t('H44 identifier leg: the UUID tail — a separator flush against a word character', h44TreeCandidateIsIdentifier('session_71836b57-5db6-459d-ae4d-c6d9d7dd2cc6', 32), true);
29420+
t('H44 identifier leg: ⛔ …but a bullet dash is not that — the separator must be flush on BOTH sides', h44TreeCandidateIsIdentifier('- 5bc2f2727ae is the tip', 2), false);
29421+
t('H44 identifier leg: the introduction — `session` with only decoration between', h44TreeCandidateIsIdentifier('PM session `c5c0ce54`', 12), true);
29422+
t('H44 identifier leg: …and 「会话」 is the same word', h44TreeCandidateIsIdentifier('PM 会话 c5c0ce54', 6), true);
29423+
t('H44 identifier leg: ⛔ a possessive three words away is not an introduction', h44TreeCandidateIsIdentifier('this session\'s checkout HEAD 588475c3', 29), false);
29424+
t('H44 identifier leg: the lead stops at the sentence break, so a `Session:` LINE cannot reach the line below', h44SentenceLead('Session: `session_01x`\n`5bc2f2727ae` is the tip', 24), '`');
29425+
t('H44 identifier leg: …and a full-width stop breaks it too', h44SentenceLead('会话 c5c0ce54。tip 5bc2f2727ae', 16), 'tip ');
29426+
// The rescan seam. ⛔ A shape with no `exclude` is read exactly as before —
29427+
// first match wins, no slicing, which is what keeps the ANCHORED `tableCount`
29428+
// shape on its original reading.
29429+
t('H44 rescan: the first admissible match is returned with its index', h44AdmittedMatch(tree18385, `session c5c0ce54 then ${TIP_18385}`).matched, TIP_18385);
29430+
t('H44 rescan: …and its index is the position in the WHOLE paragraph, not the slice', h44AdmittedMatch(tree18385, `session c5c0ce54 then ${TIP_18385}`).index, 22);
29431+
t('H44 rescan: a paragraph of nothing but excluded candidates has no match', h44AdmittedMatch(tree18385, 'session c5c0ce54 and 会话 5bc2f2727ae'), null);
29432+
t('H44 rescan: a shape with no exclusion returns its first match untouched', h44AdmittedMatch(H44_READING_FRAGMENTS.find((f) => f.kind === 'count'), 'we hold 12 open cards and 4 PRs').matched, '12 open cards');
29433+
t('H44 rescan: …and the anchored tableCount shape still reads its row', h44AdmittedMatch(H44_READING_FRAGMENTS.find((f) => f.kind === 'tableCount'), '| objectstack | **2** |').matched.includes('**2**'), true);
29434+
2924129435
// -- H45 — reserved and handed over at once (#15667, report-only) ----------
2924229436
// Both directions of a pure label intersection. The neighbour cases pin that
2924329437
// H45 takes the pair H3 cannot see and leaves H3's own pair alone.

0 commit comments

Comments
 (0)