clearotron 0.3.2-beta.11 → 0.3.2-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -1
- package/bin/clearotron.mjs +14 -0
- package/bin/connect.mjs +68 -3
- package/bin/key.mjs +6 -1
- package/bin/onboard.mjs +7 -1
- package/bin/passphrase.mjs +4 -2
- package/bin/start.mjs +12 -3
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +16 -0
- package/driver/contract-vocabulary.mjs +5 -5
- package/driver/engine/mcp/recording-server.mjs +1 -1
- package/driver/engine/mcp/supplemental.mjs +22 -5
- package/driver/knockout-next-step.mjs +72 -0
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +26 -0
- package/driver/portal-report.mjs +21 -2
- package/driver/portal-service.mjs +8 -8
- package/driver/publish/render-knockout.mjs +14 -9
- package/driver/register-availability.mjs +1 -1
- package/driver/register-plan.mjs +81 -11
- package/driver/result-noun-fields.mjs +3 -1
- package/driver/stages-knockout.mjs +1 -1
- package/driver/suite-census.json +47 -17
- package/driver/verify-knockout.mjs +0 -27
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-CtvwLCti.css → index-7Lq-dXDV.css} +12 -9
- package/portal-ui/dist/assets/{index-DXSRxPV_.js → index-w8GFZftk.js} +110 -69
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/ask-ai-render-check.mjs +26 -1
- package/scripts/dead-names.mjs +17 -16
- package/scripts/env-audit.mjs +6 -0
- package/scripts/repo-writes.mjs +42 -0
- package/scripts/report-sections-render-check.mjs +245 -0
- package/scripts/report-theme-render-check.mjs +31 -3
- package/scripts/settings-render-check.mjs +10 -7
- package/scripts/test-run.mjs +63 -4
- package/shared/client-door.mjs +20 -0
- package/shared/parent-watch.mjs +33 -0
- package/shared/running-start.mjs +14 -3
- package/shared/scope.mjs +22 -3
|
@@ -3931,7 +3931,9 @@ const escHtml = (t) => String(t).replace(/[&<>"']/g, (c) => ({ "&": "&", "<"
|
|
|
3931
3931
|
// and brand pack §01 fixes dark at #0f0e0c near-black + #f0e8d8 parchment. The pack wins.
|
|
3932
3932
|
// ── THE PLAIN LINE LEADS; THE ADMINISTRATOR'S TWO LINES STEP BACK INTO A FOLD ────────────────────────
|
|
3933
3933
|
//
|
|
3934
|
-
// A person arriving here needs the field, the button, and one fact: this install
|
|
3934
|
+
// A person arriving here needs the field, the button, and one fact: this install has one user, and which
|
|
3935
|
+
// address that is. The owner's words, 2026-09-19: "Clearotron portal, as <address>." read as "you are
|
|
3936
|
+
// <address>", and a person who found `key issue` first minted a key and could not get in.
|
|
3935
3937
|
// The reset command and the way to add people are an administrator's business, so they sit in a closed
|
|
3936
3938
|
// "Administrator help" fold, word for word as they were, with the same link People gives to putting a
|
|
3937
3939
|
// login system in front. A `<details>` needs no script, which this door must render without.
|
|
@@ -3976,9 +3978,7 @@ ${DOOR_THEME_INIT}
|
|
|
3976
3978
|
.hint { margin-top:18px; font-size:13px; }
|
|
3977
3979
|
code { font-family:var(--mono); font-size:12.5px; background:var(--code-bg);
|
|
3978
3980
|
padding:1px 5px; border-radius:4px; overflow-wrap:anywhere; }
|
|
3979
|
-
.
|
|
3980
|
-
.lead b { font-weight:600; }
|
|
3981
|
-
.fold { margin-top:10px; }
|
|
3981
|
+
.fold { margin-top:18px; padding-top:14px; border-top:1px solid var(--line); }
|
|
3982
3982
|
.fold > summary { display:inline-flex; align-items:center; gap:6px; list-style:none; cursor:pointer;
|
|
3983
3983
|
font-size:13px; color:var(--link); }
|
|
3984
3984
|
.fold > summary::-webkit-details-marker { display:none; }
|
|
@@ -3993,7 +3993,7 @@ ${signedIn
|
|
|
3993
3993
|
? `<p>You are signed in as <span class="who">${escHtml(email)}</span>.</p>
|
|
3994
3994
|
<div><a href="/portal">Go to the portal</a></div>
|
|
3995
3995
|
<form method="post" action="/portal/logout"><button type="submit">Sign out</button></form>`
|
|
3996
|
-
: `<p
|
|
3996
|
+
: `<p>This ${escHtml(BRAND.name)} has one user: <b class="who">${escHtml(email)}</b>. Enter its passphrase.</p>
|
|
3997
3997
|
${discarded ? `<p class="hint">A session this portal did not start, from another ${escHtml(BRAND.name)} on this address or an expired one, was set aside. Sign in below.</p>` : ""}
|
|
3998
3998
|
${error ? `<p class="err">${escHtml(error)}</p>` : ""}
|
|
3999
3999
|
<form method="post" action="/portal/login">
|
|
@@ -4001,10 +4001,10 @@ ${error ? `<p class="err">${escHtml(error)}</p>` : ""}
|
|
|
4001
4001
|
<input id="passphrase" name="passphrase" type="password" autocomplete="new-password" autofocus>
|
|
4002
4002
|
<button type="submit">Sign in</button>
|
|
4003
4003
|
</form>
|
|
4004
|
-
<p class="lead"><b>This ${escHtml(BRAND.name)} signs in one person: you.</b></p>
|
|
4005
4004
|
<details class="fold"><summary><span>Administrator help</span><svg class="chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m9 18 6-6-6-6"/></svg></summary>
|
|
4006
|
-
<p class="hint">
|
|
4007
|
-
running this portal. It
|
|
4005
|
+
<p class="hint">The passphrase was printed once when this ${escHtml(BRAND.name)} first started. Lost it? Run
|
|
4006
|
+
<code>${escHtml(reset)}</code> on the machine running this portal. It prints a new one, once, for the same user.</p>
|
|
4007
|
+
<p class="hint">A key from <code>${escHtml(bareInvocation("key"))} issue</code> is for an AI assistant, not for this page.</p>
|
|
4008
4008
|
<p class="hint">To add people, put it behind a login system such as your company single sign-on. <a href="${escHtml(LOGIN_IN_FRONT_DOC)}" target="_blank" rel="noreferrer">How to set that up</a></p>
|
|
4009
4009
|
</details>`}
|
|
4010
4010
|
</div></body></html>`;
|
|
@@ -62,6 +62,7 @@ import { EXPORT_TOGGLE, exportPopover, EXPORT_MENU_JS } from './report-topbar.mj
|
|
|
62
62
|
// which is the thing providers/_shared/screen.mjs exists to prevent.
|
|
63
63
|
import { makeClassifyStatus, isAllClass } from '../../providers/_shared/screen.mjs';
|
|
64
64
|
import { saysSomethingNew } from '../../shared/says-something-new.mjs';
|
|
65
|
+
import { isNextStepHeading } from '../knockout-next-step.mjs';
|
|
65
66
|
|
|
66
67
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
67
68
|
|
|
@@ -1513,21 +1514,25 @@ function koScale(framework, band) {
|
|
|
1513
1514
|
// It MOVES, it is not copied: `splitOutcome` returns the assessment without it, and the fold renders
|
|
1514
1515
|
// that remainder, so the words appear once.
|
|
1515
1516
|
//
|
|
1517
|
+
// A RECORD WRITTEN SINCE 2026-09-19 NEVER REACHES THIS WITH ONE: the pipeline removes a next-step section
|
|
1518
|
+
// before it writes the record (`knockout-next-step.mjs`), and the heading test below is that module's, so
|
|
1519
|
+
// the two cannot come to disagree about what a next step is. What still draws the section is a record
|
|
1520
|
+
// written before then, re-rendered.
|
|
1521
|
+
//
|
|
1516
1522
|
// THE HEADING IS THE MODEL'S OWN, so this hoists only the LAST block of the assessment. The assessing
|
|
1517
|
-
// skill fixes no heading vocabulary — it asks for sub-headers "where the content divides" and
|
|
1518
|
-
//
|
|
1519
|
-
//
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
const OUTCOME_HEADING_RE = /^#{1,6}\s*(?:what to do with it|what happens next|next steps?|recommendations?)\b\s*$/i;
|
|
1523
|
+
// skill fixes no heading vocabulary — it asks for sub-headers "where the content divides", and the
|
|
1524
|
+
// wording is written fresh each run. Matching a list of phrases anywhere in the document would let a
|
|
1525
|
+
// mark whose read happens to argue under "Recommendation" in the MIDDLE have that middle lifted to the
|
|
1526
|
+
// bottom, handing the client the engine's argument in an order the engine did not write, with nothing
|
|
1527
|
+
// going red. A trailing block is the one position where a move cannot reorder what is left. So the
|
|
1528
|
+
// phrase list only decides whether the last block is the outcome; anything earlier stays where it was
|
|
1529
|
+
// written, and the page renders the assessment whole.
|
|
1525
1530
|
export function splitOutcome(assessment) {
|
|
1526
1531
|
const whole = String(assessment ?? '').trim();
|
|
1527
1532
|
const lines = whole.split('\n');
|
|
1528
1533
|
const heads = lines.map((l, i) => [l.trim(), i]).filter(([l]) => /^#{1,6}\s/.test(l));
|
|
1529
1534
|
const last = heads[heads.length - 1];
|
|
1530
|
-
if (!last || !
|
|
1535
|
+
if (!last || !isNextStepHeading(last[0])) return { body: whole, outcome: '' };
|
|
1531
1536
|
const outcome = lines.slice(last[1] + 1).join('\n').trim();
|
|
1532
1537
|
if (!outcome) return { body: whole, outcome: '' };
|
|
1533
1538
|
return { body: lines.slice(0, last[1]).join('\n').trim(), outcome };
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
// CORRECTED 2026-08-11 — THIS PARAGRAPH USED TO SAY "one qid per office … which is the shape
|
|
42
42
|
// joinPlanToBands already has", AND THAT WAS FALSE ABOUT THE COMPILER IT DOCUMENTS. There is no
|
|
43
43
|
// per-office qid. compileRegisterPlan narrows ONE shared `regions` array and hands it to every entry
|
|
44
|
-
// (register-plan.mjs:
|
|
44
|
+
// (register-plan.mjs:805 compileRegisterPlan); the unreachable office produces no entry, so no qid, so no band block, so
|
|
45
45
|
// nothing ever reaches joinPlanToBands' deferred bucket — whose only source is a block stamped
|
|
46
46
|
// `error:true && deferred:true` (register-plan.mjs:1317 extendRegisterPlan).
|
|
47
47
|
//
|
package/driver/register-plan.mjs
CHANGED
|
@@ -705,12 +705,7 @@ export function excludeHouseElement(manifest, house) {
|
|
|
705
705
|
|
|
706
706
|
const lcEl = element.toLowerCase();
|
|
707
707
|
const lcWords = words.map((w) => w.toLowerCase());
|
|
708
|
-
|
|
709
|
-
// remainder word "on" inside an unrelated mutation and keep a query this exists to drop.
|
|
710
|
-
const namesRemainder = (v) => {
|
|
711
|
-
const t = String(v ?? "").toLowerCase();
|
|
712
|
-
return lcWords.some((w) => new RegExp(`(^|[^\\p{L}\\p{N}])${w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^\\p{L}\\p{N}]|$)`, "u").test(t));
|
|
713
|
-
};
|
|
708
|
+
const namesRemainder = (v) => namesAnyWord(v, lcWords);
|
|
714
709
|
|
|
715
710
|
// THE DOMINANT WORD OF THE REMAINDER: longest, ties broken by the LAST of them. The lawyer's own
|
|
716
711
|
// phrasing was "the last word alone", and on the matter that produced this the two agree; longest is
|
|
@@ -738,11 +733,75 @@ export function excludeHouseElement(manifest, house) {
|
|
|
738
733
|
// REQUIREMENT 3: the element is still checked ONCE, as a confirmation of the client's own live
|
|
739
734
|
// registrations rather than as a conflict search — so the exclusion is evidenced on the report by a
|
|
740
735
|
// query that ran, not by a sentence saying one would have.
|
|
736
|
+
// The remainder rides on it so a writer AFTER the compile can apply the same rule (`houseElementOf`).
|
|
741
737
|
const confirmation = { axis: "incumbent-class", predicate: "owner", term: element,
|
|
742
|
-
expected_kind: "enumerate", provenance: "mark", house_element_confirmation: true
|
|
738
|
+
expected_kind: "enumerate", provenance: "mark", house_element_confirmation: true,
|
|
739
|
+
house_element_remainder: remainder };
|
|
743
740
|
return { manifest: next, confirmation, refused: null };
|
|
744
741
|
}
|
|
745
742
|
|
|
743
|
+
/**
|
|
744
|
+
* Does `text` carry any of `words` (lower-case) as a word of its own? By word boundary rather than by
|
|
745
|
+
* containment: `includes` would count the remainder word "on" inside an unrelated mutation.
|
|
746
|
+
*/
|
|
747
|
+
function namesAnyWord(text, words) {
|
|
748
|
+
const t = String(text ?? "").toLowerCase();
|
|
749
|
+
return words.some((w) => new RegExp(`(^|[^\\p{L}\\p{N}])${w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^\\p{L}\\p{N}]|$)`, "u").test(t));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* The house element a plan was compiled without, read off the plan itself, or null.
|
|
754
|
+
*
|
|
755
|
+
* WHY THE PLAN CARRIES IT. The compile is not the only writer. After it, the common-law cross-check,
|
|
756
|
+
* the recall lane, the frame reopen and the jurisdiction candidates fold entries in, and the register
|
|
757
|
+
* session proposes its own. On the first live run that proposed an exclusion (2026-09-19) the compile
|
|
758
|
+
* dropped the element and two later writers put it straight back: a contains query from the cross-check
|
|
759
|
+
* and an exact query the register session proposed, both on the bare element. Those writers hold the
|
|
760
|
+
* plan and nothing else, so the plan is where the exclusion has to be readable. Its confirmation row
|
|
761
|
+
* names the element, and the remainder rides beside it because the rule needs both.
|
|
762
|
+
*/
|
|
763
|
+
export function houseElementOf(plan) {
|
|
764
|
+
const c = (plan?.entries ?? []).find((e) => e?.house_element_confirmation === true);
|
|
765
|
+
const element = String(c?.term ?? "").trim();
|
|
766
|
+
if (!element) return null;
|
|
767
|
+
const remainder = String(c?.house_element_remainder ?? "").trim().toLowerCase();
|
|
768
|
+
return { element, remainderWords: remainder.split(/\s+/).filter(Boolean) };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Is this term a search on the excluded house element? The compile's own rule (`excludeHouseElement`
|
|
773
|
+
* keeps a variant only when it names a word of the remainder), applied to a term that arrives after
|
|
774
|
+
* the compile: a term carrying the element and no word of the remainder is a search on the element,
|
|
775
|
+
* whether it is the bare element, the element with a suffix, or the element beside other words. A term
|
|
776
|
+
* carrying a word of the remainder, like the whole mark, is kept, as the compile keeps it.
|
|
777
|
+
*
|
|
778
|
+
* A plan frozen before the remainder was recorded knows the element only, and then only the bare
|
|
779
|
+
* element counts: "carries no word of the remainder" cannot be decided without the remainder.
|
|
780
|
+
*/
|
|
781
|
+
export function isHouseElementTerm(term, house) {
|
|
782
|
+
if (!house) return false;
|
|
783
|
+
const t = String(term ?? "").trim().toLowerCase();
|
|
784
|
+
const el = house.element.toLowerCase();
|
|
785
|
+
if (t === el || (norm(el) !== "" && norm(t) === norm(el) && formKey(t) === formKey(el))) return true;
|
|
786
|
+
if (!house.remainderWords.length) return false;
|
|
787
|
+
const carries = t.includes(el) || (norm(el) !== "" && norm(t).includes(norm(el)));
|
|
788
|
+
return carries && !namesAnyWord(t, house.remainderWords);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* The entry without its house-element terms, or null when none is left. An owner sweep is not mark
|
|
793
|
+
* text and comes back untouched: the one owner query on the element is the confirmation itself. PURE.
|
|
794
|
+
*/
|
|
795
|
+
export function withoutHouseElementTerms(entry, house) {
|
|
796
|
+
if (!house || !entry || entry.predicate === "owner") return entry;
|
|
797
|
+
if (Array.isArray(entry.terms)) {
|
|
798
|
+
const kept = entry.terms.filter((t) => !isHouseElementTerm(t, house));
|
|
799
|
+
if (!kept.length) return null;
|
|
800
|
+
return kept.length === entry.terms.length ? entry : { ...entry, terms: kept };
|
|
801
|
+
}
|
|
802
|
+
return isHouseElementTerm(entry.term, house) ? null : entry;
|
|
803
|
+
}
|
|
804
|
+
|
|
746
805
|
export function compileRegisterPlan({ manifest, job, form = null, skillVersion = "", capabilities = null, unavailableOffices = [], houseElement = null }) {
|
|
747
806
|
// ── AN EXCLUDED ELEMENT'S FORM BAND MUST BE UNREACHABLE, NOT MERELY UNASKED-FOR ─────────────────
|
|
748
807
|
//
|
|
@@ -1495,10 +1554,13 @@ export function entryQuestionKey(entry, plan) {
|
|
|
1495
1554
|
* an absence, and an absence reported as success is the failure this whole issue is about. Every
|
|
1496
1555
|
* caller writes them somewhere a reader will meet them.
|
|
1497
1556
|
*
|
|
1498
|
-
* Returns { plan, added: [qid], refused: [{qid, term, issue}] }
|
|
1557
|
+
* Returns { plan, added: [qid], refused: [{qid, term, issue}], excluded: [qid] } — `excluded` are rows on
|
|
1558
|
+
* the house element the compile set aside, left out quietly (see houseElementOf).
|
|
1499
1559
|
*/
|
|
1500
1560
|
export function foldSupplementalEntries(plan, entries) {
|
|
1501
1561
|
const have = new Set(plan.entries.map((e) => e.qid));
|
|
1562
|
+
const house = houseElementOf(plan);
|
|
1563
|
+
const excluded = [];
|
|
1502
1564
|
// — first qid wins, so the refusal names the row already IN the plan rather than a later
|
|
1503
1565
|
// arrival. The compiler's own rows are in here too: a supplemental re-proposal of a compiler slice
|
|
1504
1566
|
// is the case that actually fired on the round in evidence.
|
|
@@ -1530,8 +1592,9 @@ export function foldSupplementalEntries(plan, entries) {
|
|
|
1530
1592
|
// one identity, which is the fault. Keyed on the qid alone this refused the harmless case too, and
|
|
1531
1593
|
// the suite's own arm said so.
|
|
1532
1594
|
const inBatch = new Map(); // qid -> the term that claimed it
|
|
1533
|
-
for (const
|
|
1534
|
-
if (!
|
|
1595
|
+
for (const offered of entries ?? []) {
|
|
1596
|
+
if (!offered || typeof offered.qid !== "string" || !offered.qid) continue;
|
|
1597
|
+
let e = offered;
|
|
1535
1598
|
const eTerm = String(e.term ?? e.terms?.[0] ?? "");
|
|
1536
1599
|
if (inBatch.has(e.qid) && inBatch.get(e.qid) === eTerm) continue; // the same row twice — one refusal
|
|
1537
1600
|
if (inBatch.has(e.qid)) {
|
|
@@ -1546,6 +1609,12 @@ export function foldSupplementalEntries(plan, entries) {
|
|
|
1546
1609
|
inBatch.set(e.qid, eTerm);
|
|
1547
1610
|
if (have.has(e.qid)) continue; // already in the PLAN — a re-proposal, expected and quiet
|
|
1548
1611
|
have.add(e.qid); // a refused qid is claimed too — the same row twice is one refusal
|
|
1612
|
+
// THE CLIENT'S OWN ELEMENT, SET ASIDE BY THE COMPILE, IS NOT FOLDED BACK IN (see houseElementOf).
|
|
1613
|
+
// QUIET, NOT REFUSED: a refused row renders to the client as an OPEN ask, and this is no unanswered
|
|
1614
|
+
// question. The plan's confirmation row answers it and the report states the exclusion.
|
|
1615
|
+
const kept = withoutHouseElementTerms(e, house);
|
|
1616
|
+
if (!kept) { excluded.push(e.qid); continue; }
|
|
1617
|
+
e = kept;
|
|
1549
1618
|
const issues = entryTermIssues(e);
|
|
1550
1619
|
if (issues.length) {
|
|
1551
1620
|
refused.push({ qid: e.qid, term: String(e.term ?? e.terms?.[0] ?? ""),
|
|
@@ -1570,11 +1639,12 @@ export function foldSupplementalEntries(plan, entries) {
|
|
|
1570
1639
|
if (key && e.unsupported !== true) asked.set(key, e.qid); // dedupes the batch too, on the question
|
|
1571
1640
|
added.push(e);
|
|
1572
1641
|
}
|
|
1573
|
-
if (!added.length) return { plan, added: [], refused };
|
|
1642
|
+
if (!added.length) return { plan, added: [], refused, excluded };
|
|
1574
1643
|
return {
|
|
1575
1644
|
plan: { ...plan, plan_version: (plan.plan_version ?? 1) + 1, entries: [...plan.entries, ...added] },
|
|
1576
1645
|
added: added.map((e) => e.qid),
|
|
1577
1646
|
refused,
|
|
1647
|
+
excluded,
|
|
1578
1648
|
};
|
|
1579
1649
|
}
|
|
1580
1650
|
|
|
@@ -95,7 +95,9 @@ export const RESULT_NOUN_FIELDS = Object.freeze([
|
|
|
95
95
|
// writing site like the other two — it is a returned CLASSIFICATION of a checkout, not a field named
|
|
96
96
|
// for a result whose value is what a call returned, so the verdict is unchanged.
|
|
97
97
|
{ file: "driver/engine-build.mjs", noun: "outcome", sites: 3, atWriteSite: 0, verdict: "out-of-scope" },
|
|
98
|
-
|
|
98
|
+
// Read at the writing site: the third is the reply when every proposal was the house element the
|
|
99
|
+
// compile set aside — `executed: false` because nothing ran, the same statement as the error reply.
|
|
100
|
+
{ file: "driver/engine/mcp/supplemental.mjs", noun: "executed", sites: 3, atWriteSite: 0, verdict: "out-of-scope" },
|
|
99
101
|
{ file: "driver/outbox-backoff.mjs", noun: "outcome", sites: 5, atWriteSite: 0, verdict: "out-of-scope" },
|
|
100
102
|
{ file: "driver/portal-service.mjs", noun: "outcome", sites: 2, atWriteSite: 0, verdict: "out-of-scope" },
|
|
101
103
|
{ file: "driver/remedy-accounting.mjs", noun: "executed", sites: 1, atWriteSite: 0, verdict: "out-of-scope" },
|
|
@@ -418,7 +418,7 @@ export const KO_STAGES = {
|
|
|
418
418
|
// The depth rule is not stylistic. `# ` opens a SECTION of the delivered report — parseSections
|
|
419
419
|
// splits on /^# /m and batchSummaryOf terminates on it — so a single hash written here ends the
|
|
420
420
|
// client's summary at that line and drops everything after it, silently, on the entry point.
|
|
421
|
-
`EVERY mark ALSO carries "assessment": the opening read a reader of THIS MARK'S OWN report meets first, before any table. Write it for a client who ordered this one name and nothing else: what the name is, what the landscape around it looks like, what drives the rating
|
|
421
|
+
`EVERY mark ALSO carries "assessment": the opening read a reader of THIS MARK'S OWN report meets first, before any table. Write it for a client who ordered this one name and nothing else: what the name is, what the landscape around it looks like, what drives the rating.`,
|
|
422
422
|
`GIVE IT STRUCTURE A READER CAN SCAN — that is what changed. Sub-headers where the content divides, "- " bullets for the load-bearing points, a blank line between blocks. A sub-header is "## " or "### ". NEVER "# ": one hash opens a section of the report itself, and a summary that opens a section ends there and takes the rest of itself off the client's page. A single sub-header over one unbroken block is not structure — the blocks are what make the length readable, and unstructured length is the complaint this replaces.`,
|
|
423
423
|
`KEEP THE LENGTH. It is still the most useful part of a triage and it is still not rationed. What it now has to be is CONSISTENT: this mark's opening read is the scale of a single-mark report's opening read, and the marks of one batch sit within reach of each other. One assessment several times the size of its siblings is out of family. No word count and none is coming — judge it against the other marks in front of you.`,
|
|
424
424
|
`SYNTHESISE THE SOURCES, DO NOT SHELVE THEM SEPARATELY. Where filings were handed to you for this mark, weigh them in the SAME passage as the marketplace and common-law reading — what the screened field shows, what those filings do to that reading, and what the two together mean for the band. A reader must not be handed one block per source and left to reconcile them.`,
|
package/driver/suite-census.json
CHANGED
|
@@ -279,6 +279,12 @@
|
|
|
279
279
|
"skips": 0,
|
|
280
280
|
"todos": 0
|
|
281
281
|
},
|
|
282
|
+
"a-demo-stops-when-whatever-started-it-is-gone.test.mjs": {
|
|
283
|
+
"tests": 3,
|
|
284
|
+
"asserts": 11,
|
|
285
|
+
"skips": 0,
|
|
286
|
+
"todos": 0
|
|
287
|
+
},
|
|
282
288
|
"a-depth-check-that-read-nothing-is-on-the-investigate-list.test.mjs": {
|
|
283
289
|
"tests": 3,
|
|
284
290
|
"asserts": 8,
|
|
@@ -555,6 +561,12 @@
|
|
|
555
561
|
"skips": 1,
|
|
556
562
|
"todos": 0
|
|
557
563
|
},
|
|
564
|
+
"a-next-step-the-model-writes-comes-off-at-publish.test.mjs": {
|
|
565
|
+
"tests": 12,
|
|
566
|
+
"asserts": 28,
|
|
567
|
+
"skips": 0,
|
|
568
|
+
"todos": 0
|
|
569
|
+
},
|
|
558
570
|
"a-note-the-release-refuses-is-refused-on-its-own-pull-request.test.mjs": {
|
|
559
571
|
"tests": 2,
|
|
560
572
|
"asserts": 3,
|
|
@@ -837,6 +849,12 @@
|
|
|
837
849
|
"skips": 0,
|
|
838
850
|
"todos": 0
|
|
839
851
|
},
|
|
852
|
+
"a-running-demo-connects-by-its-folder.test.mjs": {
|
|
853
|
+
"tests": 4,
|
|
854
|
+
"asserts": 18,
|
|
855
|
+
"skips": 0,
|
|
856
|
+
"todos": 0
|
|
857
|
+
},
|
|
840
858
|
"a-save-that-cannot-commit-says-so.test.mjs": {
|
|
841
859
|
"tests": 4,
|
|
842
860
|
"asserts": 19,
|
|
@@ -1827,6 +1845,12 @@
|
|
|
1827
1845
|
"skips": 0,
|
|
1828
1846
|
"todos": 0
|
|
1829
1847
|
},
|
|
1848
|
+
"dead-names.test.mjs": {
|
|
1849
|
+
"tests": 5,
|
|
1850
|
+
"asserts": 8,
|
|
1851
|
+
"skips": 2,
|
|
1852
|
+
"todos": 0
|
|
1853
|
+
},
|
|
1830
1854
|
"declared-park-excuses-proof-not-judgment.test.mjs": {
|
|
1831
1855
|
"tests": 9,
|
|
1832
1856
|
"asserts": 34,
|
|
@@ -2062,8 +2086,8 @@
|
|
|
2062
2086
|
"todos": 0
|
|
2063
2087
|
},
|
|
2064
2088
|
"doctor-refuses-what-cannot-run.test.mjs": {
|
|
2065
|
-
"tests":
|
|
2066
|
-
"asserts":
|
|
2089
|
+
"tests": 9,
|
|
2090
|
+
"asserts": 22,
|
|
2067
2091
|
"skips": 0,
|
|
2068
2092
|
"todos": 0
|
|
2069
2093
|
},
|
|
@@ -2812,8 +2836,8 @@
|
|
|
2812
2836
|
"todos": 0
|
|
2813
2837
|
},
|
|
2814
2838
|
"knockout-structured-read.test.mjs": {
|
|
2815
|
-
"tests":
|
|
2816
|
-
"asserts":
|
|
2839
|
+
"tests": 12,
|
|
2840
|
+
"asserts": 57,
|
|
2817
2841
|
"skips": 0,
|
|
2818
2842
|
"todos": 0
|
|
2819
2843
|
},
|
|
@@ -3004,8 +3028,8 @@
|
|
|
3004
3028
|
"todos": 0
|
|
3005
3029
|
},
|
|
3006
3030
|
"no-test-writes-inside-the-checkout.test.mjs": {
|
|
3007
|
-
"tests":
|
|
3008
|
-
"asserts":
|
|
3031
|
+
"tests": 20,
|
|
3032
|
+
"asserts": 52,
|
|
3009
3033
|
"skips": 0,
|
|
3010
3034
|
"todos": 0
|
|
3011
3035
|
},
|
|
@@ -3305,7 +3329,7 @@
|
|
|
3305
3329
|
},
|
|
3306
3330
|
"portal-local-login.test.mjs": {
|
|
3307
3331
|
"tests": 28,
|
|
3308
|
-
"asserts":
|
|
3332
|
+
"asserts": 113,
|
|
3309
3333
|
"skips": 0,
|
|
3310
3334
|
"todos": 0
|
|
3311
3335
|
},
|
|
@@ -3862,8 +3886,8 @@
|
|
|
3862
3886
|
"todos": 0
|
|
3863
3887
|
},
|
|
3864
3888
|
"render.test.mjs": {
|
|
3865
|
-
"tests":
|
|
3866
|
-
"asserts":
|
|
3889
|
+
"tests": 162,
|
|
3890
|
+
"asserts": 675,
|
|
3867
3891
|
"skips": 0,
|
|
3868
3892
|
"todos": 0
|
|
3869
3893
|
},
|
|
@@ -4486,8 +4510,8 @@
|
|
|
4486
4510
|
"todos": 0
|
|
4487
4511
|
},
|
|
4488
4512
|
"status-and-stop-see-a-foreground-start.test.mjs": {
|
|
4489
|
-
"tests":
|
|
4490
|
-
"asserts":
|
|
4513
|
+
"tests": 7,
|
|
4514
|
+
"asserts": 30,
|
|
4491
4515
|
"skips": 0,
|
|
4492
4516
|
"todos": 0
|
|
4493
4517
|
},
|
|
@@ -4959,6 +4983,12 @@
|
|
|
4959
4983
|
"skips": 0,
|
|
4960
4984
|
"todos": 0
|
|
4961
4985
|
},
|
|
4986
|
+
"the-house-element-stays-out-of-what-folds-in-later.test.mjs": {
|
|
4987
|
+
"tests": 6,
|
|
4988
|
+
"asserts": 22,
|
|
4989
|
+
"skips": 0,
|
|
4990
|
+
"todos": 0
|
|
4991
|
+
},
|
|
4962
4992
|
"the-identifier-sweep-fires-on-its-sentinels.test.mjs": {
|
|
4963
4993
|
"tests": 10,
|
|
4964
4994
|
"asserts": 36,
|
|
@@ -5327,7 +5357,7 @@
|
|
|
5327
5357
|
},
|
|
5328
5358
|
"the-sign-in-door-folds-its-administrator-help.test.mjs": {
|
|
5329
5359
|
"tests": 5,
|
|
5330
|
-
"asserts":
|
|
5360
|
+
"asserts": 23,
|
|
5331
5361
|
"skips": 0,
|
|
5332
5362
|
"todos": 0
|
|
5333
5363
|
},
|
|
@@ -5344,8 +5374,8 @@
|
|
|
5344
5374
|
"todos": 0
|
|
5345
5375
|
},
|
|
5346
5376
|
"the-summary-carries-structure.test.mjs": {
|
|
5347
|
-
"tests":
|
|
5348
|
-
"asserts":
|
|
5377
|
+
"tests": 14,
|
|
5378
|
+
"asserts": 37,
|
|
5349
5379
|
"skips": 0,
|
|
5350
5380
|
"todos": 0
|
|
5351
5381
|
},
|
|
@@ -6034,7 +6064,7 @@
|
|
|
6034
6064
|
},
|
|
6035
6065
|
"root-mcp-json-connects.test.mjs": {
|
|
6036
6066
|
"tests": 3,
|
|
6037
|
-
"asserts":
|
|
6067
|
+
"asserts": 14,
|
|
6038
6068
|
"skips": 0,
|
|
6039
6069
|
"todos": 0
|
|
6040
6070
|
},
|
|
@@ -6446,8 +6476,8 @@
|
|
|
6446
6476
|
"todos": 0
|
|
6447
6477
|
},
|
|
6448
6478
|
"reportSections.test.ts": {
|
|
6449
|
-
"tests":
|
|
6450
|
-
"asserts":
|
|
6479
|
+
"tests": 13,
|
|
6480
|
+
"asserts": 41,
|
|
6451
6481
|
"skips": 0,
|
|
6452
6482
|
"todos": 0
|
|
6453
6483
|
},
|
|
@@ -29,25 +29,6 @@ const nameKey = (s) => norm(s).toLowerCase().replace(/\s+/g, " ");
|
|
|
29
29
|
|
|
30
30
|
// ── Tone guards (the skill's rules, mechanical subset) ───────────────────────────────────────────────
|
|
31
31
|
export const BANNED_TONE_RE = /\b(extremely difficult|most dangerous|massive|enormous)\b/i;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* A RECOMMENDATION IS A SHAPE, AND THIS SCREEN MAY NOT WRITE ONE (owner, 2026-09-18).
|
|
35
|
-
*
|
|
36
|
-
* The screen states findings and a rating; what to do with the name is the reading lawyer's. Two
|
|
37
|
-
* instructions that asked for a conclusion were cut first, and a re-run then wrote one anyway, under a
|
|
38
|
-
* heading of its own: "Practical next step — <NAME> is not knocked out at this screening depth. Advance
|
|
39
|
-
* it to clearance…". Cutting an instruction stops the engine asking; only a refusal stops the offer.
|
|
40
|
-
*
|
|
41
|
-
* TWO SHAPES, because both reached a reader: a block the writer heads itself, and a closing line that
|
|
42
|
-
* moves the name on. WHAT THIS CANNOT SEE: the same conclusion as ordinary prose — "the remaining work
|
|
43
|
-
* here is a full search" — and no pattern can. It is a floor under the doctrine, never a substitute.
|
|
44
|
-
*
|
|
45
|
-
* The false positives were measured rather than assumed: the degraded note this lane REQUIRES reads
|
|
46
|
-
* "Manual verification recommended" and matches neither, and a register record "proceeding to
|
|
47
|
-
* registration" matches neither — only proceeding *to clearance* does.
|
|
48
|
-
*/
|
|
49
|
-
export const RECOMMENDATION_RE = /^[ \t]*(?:[*_]{0,2}|#{1,6}\s*)(?:practical\s+|immediate\s+|suggested\s+)?(?:next steps?|what to do\b|what happens next|recommendations?|our recommendation)/im;
|
|
50
|
-
export const ADVANCE_RE = /\b(?:advance|move|take|send)\s+(?:it|this|the name|[A-Z][A-Z0-9'’-]{2,})\s+(?:on\s+)?(?:to|into|through)\s+(?:a\s+)?(?:full\s+)?clearance\b|\bproceeds?\s+to\s+(?:a\s+)?(?:full\s+)?clearance\b|\bshould\s+(?:proceed|advance|be advanced|move)\b|\bwe\s+recommend\b/i;
|
|
51
32
|
export const QUANT_CLAIM_RE = /(\b\d[\d,.]*\s*(?:M|million|billion|k|thousand)?\s*(?:streams|downloads|copies sold|units sold)\b)|(\$\s?\d[\d,.]*\s*(?:M|million|billion|k)?\s*(?:in\s+)?(?:annual\s+)?(?:revenue|sales))/i;
|
|
52
33
|
|
|
53
34
|
// ── URL normalization for the receipts gate ──────────────────────────────────────────────────────────
|
|
@@ -505,14 +486,6 @@ export const validators = {
|
|
|
505
486
|
const claim = [prose, c.chunkSummary].filter(Boolean).join("\n").match(REGISTER_CLAIM_RE);
|
|
506
487
|
if (claim)
|
|
507
488
|
return { ok: false, reason: `knockout_register_claim:${m.name}: "${claim[0].trim()}" — this turn cannot see the register lane and must not describe it. The report states register coverage in code, from the run's own count sidecar; a summary that says the registers were not run, above a table of counts that were, is the contradiction this rule closes. Delete the clause` };
|
|
508
|
-
// THE PER-NAME READ IS SWEPT TOO, and until now it was not: the checks below read the finding
|
|
509
|
-
// fields, while `assessment` was validated for length and structure alone — which is exactly the
|
|
510
|
-
// field the conclusion arrived in. Refused HERE so the stage re-asks with the line quoted, rather
|
|
511
|
-
// than a reader meeting it.
|
|
512
|
-
const read = [m.assessment, prose].filter(Boolean).join("\n");
|
|
513
|
-
const recommended = read.match(RECOMMENDATION_RE) ?? read.match(ADVANCE_RE);
|
|
514
|
-
if (recommended)
|
|
515
|
-
return { ok: false, reason: `mark "${m.name}": "${recommended[0].trim()}" — this screen states findings and a rating; what to do with the name, and whether it advances, is the reading lawyer's. Delete the sentence: the report's own closing caveat already says what the screen is` };
|
|
516
489
|
const banned = prose.match(BANNED_TONE_RE);
|
|
517
490
|
if (banned) return { ok: false, reason: `mark "${m.name}": banned tone "${banned[0]}" — measured tone only (the band colour carries urgency)` };
|
|
518
491
|
const quant = prose.match(QUANT_CLAIM_RE);
|
package/mcp-server/CHANGELOG.md
CHANGED
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.3.2-beta.
|
|
3
|
+
"version": "0.3.2-beta.12",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
|
package/package.json
CHANGED
|
@@ -254,19 +254,19 @@ html.anon-on [data-anon='mark']:not(.anon-clear) {
|
|
|
254
254
|
border-bottom: 1px solid var(--border-hairline);
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
/* THE HEADER'S
|
|
257
|
+
/* THE HEADER'S LOWER ROWS: which read you are in, then where you are inside it.
|
|
258
258
|
The Reads strip used to sit BELOW this box and the report's own section breadcrumb inside the frame,
|
|
259
259
|
so neither stayed on screen — the frame is sized to its content, which leaves a `position:sticky` bar
|
|
260
260
|
in there pinned to nothing. Both are rows of the pinned header now (owner, 2026-09-18).
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
.
|
|
264
|
-
|
|
261
|
+
THE SECTIONS HAVE A ROW OF THEIR OWN, UNDER THE READS (owner, 2026-09-19). Side by side, sixteen reads
|
|
262
|
+
wrapped into two ragged columns while the strip floated beside them at mid-height. The reads wrap; the
|
|
263
|
+
sections scroll sideways rather than wrapping, so the strip stays one line however narrow the screen. */
|
|
264
|
+
.report-nav { display: flex; flex-direction: column; align-items: stretch; gap: 8px; margin-top: 9px; }
|
|
265
|
+
.report-sections {
|
|
266
|
+
display: flex; align-items: center; gap: 2px;
|
|
265
267
|
overflow-x: auto; scrollbar-width: none;
|
|
266
268
|
}
|
|
267
|
-
.report-
|
|
268
|
-
.report-nav-sep { flex: none; width: 1px; align-self: stretch; background: var(--border-hairline); }
|
|
269
|
-
.report-sections { display: flex; align-items: center; gap: 2px; }
|
|
269
|
+
.report-sections::-webkit-scrollbar { display: none; }
|
|
270
270
|
.report-section {
|
|
271
271
|
flex: none; display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
|
|
272
272
|
font: inherit; font-size: 12.5px; font-weight: 600; letter-spacing: .01em;
|
|
@@ -277,8 +277,11 @@ html.anon-on [data-anon='mark']:not(.anon-clear) {
|
|
|
277
277
|
.report-section-dot {
|
|
278
278
|
width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid currentColor; box-sizing: border-box;
|
|
279
279
|
}
|
|
280
|
+
/* PROGRESS: every section reached has a filled dot, the rest stay hollow; the one in view also sits on
|
|
281
|
+
the sunken pill, so it can be told from the ones behind it. */
|
|
282
|
+
.report-section[data-reached='true'] { color: var(--text-strong); }
|
|
283
|
+
.report-section[data-reached='true'] .report-section-dot { background: var(--accent); border-color: var(--accent); }
|
|
280
284
|
.report-section[aria-current='true'] { color: var(--text-strong); background: var(--surface-sunken); }
|
|
281
|
-
.report-section[aria-current='true'] .report-section-dot { border-color: var(--accent); border-width: 3px; }
|
|
282
285
|
|
|
283
286
|
/* The breadcrumb above it: one step back as a quiet button, then where the reader is. Compound selectors
|
|
284
287
|
throughout this block, because the button and row rules they adjust are declared later in the file. */
|