clearotron 0.3.1-beta.4 → 0.3.2-beta.0
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/CONTRIBUTING.md +30 -1
- package/README.md +1 -0
- package/build-info.json +2 -2
- package/docs/writing-rules.md +208 -0
- package/docs/writing-standard.md +85 -0
- package/driver/CHANGELOG.md +81 -0
- package/driver/contract-e3-backlog.mjs +3 -3
- package/driver/contract-vocabulary.mjs +15 -14
- package/driver/gateway.mjs +33 -17
- package/driver/package.json +1 -1
- package/driver/publish/render-knockout.mjs +6 -26
- package/driver/publish/render.mjs +25 -5
- package/driver/suite-census.json +30 -0
- package/driver/verify.mjs +22 -3
- package/mcp-server/CHANGELOG.md +19 -0
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/mint-writing-standard-backlog.mjs +82 -0
- package/scripts/release-approve-parked.mjs +151 -0
- package/scripts/writing-standard-check.mjs +122 -0
- package/shared/says-something-new.mjs +62 -0
- package/shared/writing-standard-caveats.json +14 -0
- package/shared/writing-standard-classes.mjs +526 -0
package/driver/gateway.mjs
CHANGED
|
@@ -2596,29 +2596,45 @@ export function correctionHint(lastFail, { gridLedgerName = "common-law-grid.jso
|
|
|
2596
2596
|
const dropped = (lastFail.match(/connotation_query_unrecorded:(.+)$/s) || [])[1] || "";
|
|
2597
2597
|
// ── TWO FAULTS, TWO REMEDIES, and sending the wrong one is what made this permanent ────────────
|
|
2598
2598
|
//
|
|
2599
|
-
// The gate marks each dropped query `[
|
|
2600
|
-
//
|
|
2601
|
-
//
|
|
2602
|
-
//
|
|
2599
|
+
// The gate marks each dropped query `[unmatched; nearest recorded: …]` or `[no recorded query
|
|
2600
|
+
// resembles this one]`, and the difference is what the gate can SEE, not what happened.
|
|
2601
|
+
//
|
|
2602
|
+
// UNMATCHED is knowable: something close is recorded, so the search ran and the wording differs.
|
|
2603
|
+
// Re-running changes nothing, and telling a seat to re-run is how a stage fails four times with the
|
|
2604
|
+
// same string.
|
|
2605
|
+
//
|
|
2606
|
+
// NO RESEMBLANCE IS NOT KNOWABLE, and this hint used to pretend otherwise. It said the query was
|
|
2607
|
+
// missing and to go and run it — but a query recorded under a translation, a transliteration or the
|
|
2608
|
+
// seat's own rewording resembles nothing and has already run, and that seat was then sent round the
|
|
2609
|
+
// same loop the unmatched branch exists to break. The gate cannot tell the two apart; no threshold
|
|
2610
|
+
// can, and a threshold that could would be one that hides a query nobody ran.
|
|
2611
|
+
//
|
|
2612
|
+
// So this branch stops asserting and hands over both repairs. Both are cheap, a seat can tell which
|
|
2613
|
+
// applies by looking at its own ledger, and neither wastes an attempt: if the search did run, fix
|
|
2614
|
+
// the row's wording; if it did not, run it and append the row. Where both labels appear, both
|
|
2615
|
+
// sentences are sent, as before.
|
|
2603
2616
|
const anyUnmatched = /\[unmatched; nearest recorded:/.test(dropped);
|
|
2604
|
-
const
|
|
2605
|
-
hint = anyUnmatched && !
|
|
2617
|
+
const anyUnresembled = /\[no recorded query resembles this one\]/.test(dropped);
|
|
2618
|
+
hint = anyUnmatched && !anyUnresembled
|
|
2606
2619
|
? `these dictated meaning queries ARE recorded in ${gridLedgerName} extras.pr_risk[] under a ` +
|
|
2607
2620
|
`different wording, which is why the driver cannot match them: ${dropped}. Do NOT re-run them — ` +
|
|
2608
2621
|
`the search already ran and its results are already in the ledger. EDIT each row's \`query\` ` +
|
|
2609
2622
|
`field to the query text EXACTLY as the task message dictates it, character for character, and ` +
|
|
2610
2623
|
`leave its results untouched. The driver matches your rows to its list by that text`
|
|
2611
|
-
: `
|
|
2612
|
-
`
|
|
2613
|
-
`
|
|
2614
|
-
`
|
|
2615
|
-
`
|
|
2616
|
-
`
|
|
2617
|
-
`
|
|
2618
|
-
`
|
|
2619
|
-
`
|
|
2620
|
-
`
|
|
2621
|
-
`
|
|
2624
|
+
: `the driver cannot match these dictated meaning queries to any row in ${gridLedgerName} ` +
|
|
2625
|
+
`extras.pr_risk[]: ${dropped}. That means one of two things and the driver cannot tell which, so ` +
|
|
2626
|
+
`check your own ledger and do whichever applies — both are cheap. IF THE SEARCH ALREADY RAN and ` +
|
|
2627
|
+
`you recorded it under different wording — a translation, a reordering, your own phrasing — do ` +
|
|
2628
|
+
`NOT run it again. EDIT that row's \`query\` field to the query text EXACTLY as the task message ` +
|
|
2629
|
+
`dictates it, character for character, and leave its results untouched. IF IT NEVER RAN, run it ` +
|
|
2630
|
+
`now and append a row to extras.pr_risk[]. A query that returned NO results still owes its row: ` +
|
|
2631
|
+
`record it with an empty results array, which is the receipt that the search RAN and came back ` +
|
|
2632
|
+
`clean. On an "offensive meaning" query the empty answer IS the good news, and a missing row is ` +
|
|
2633
|
+
`indistinguishable from a search nobody performed. Either way, record each query's text EXACTLY ` +
|
|
2634
|
+
`as the task message dictates it — the driver matches your rows to its list by that text, so a ` +
|
|
2635
|
+
`reworded query reads as one you never ran. Touch ONLY the listed queries and leave every other ` +
|
|
2636
|
+
`recorded row exactly as it is. Where a query is marked \`[unmatched; nearest recorded: …]\` the ` +
|
|
2637
|
+
`driver has already found its row for you: edit that row's \`query\` and do not search again`;
|
|
2622
2638
|
} else if (/connotation_search_missing/.test(lastFail)) {
|
|
2623
2639
|
// — was "your PR / reputational section claims a clean meaning … but the ledger recorded ZERO
|
|
2624
2640
|
// searches", which under the `ensure` prefix instructed the model to make the unbacked claim.
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.2-beta.0",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
|
|
8
8
|
"engines": {
|
|
@@ -53,6 +53,7 @@ import { demoBannerHtml } from './render.mjs'; // — the SAME banner the clea
|
|
|
53
53
|
// fail-open; re-deriving either in a renderer would be this file starting a second status vocabulary,
|
|
54
54
|
// which is the thing providers/_shared/screen.mjs exists to prevent.
|
|
55
55
|
import { makeClassifyStatus, isAllClass } from '../../providers/_shared/screen.mjs';
|
|
56
|
+
import { saysSomethingNew } from '../../shared/says-something-new.mjs';
|
|
56
57
|
|
|
57
58
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
58
59
|
|
|
@@ -1336,32 +1337,11 @@ const SCOPE_BLOCK_TEXT = 'What this is. A fast screen for obvious blockers to us
|
|
|
1336
1337
|
+ 'goes on to clearance. Every conflict above links to the material we found. The audit workbook holds '
|
|
1337
1338
|
+ 'every search run, every empty result and the working notes. Register data.';
|
|
1338
1339
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
const contentWords = (s) => String(s ?? '').toLowerCase().replace(/[^a-z0-9\s-]/g, ' ')
|
|
1346
|
-
.split(/\s+/).filter((w) => w.length > 2 && !STOPWORDS.has(w));
|
|
1347
|
-
|
|
1348
|
-
/**
|
|
1349
|
-
* Does this line assert anything the reference text does not already assert?
|
|
1350
|
-
*
|
|
1351
|
-
* TRUE unless every content word in the line is already in the reference. An empty line has nothing to
|
|
1352
|
-
* say and returns false; a line with one unfamiliar word is kept. Singular/plural is folded so that
|
|
1353
|
-
* "conclusion" does not read as new beside "conclusions".
|
|
1354
|
-
*/
|
|
1355
|
-
function saysSomethingNew(line, reference) {
|
|
1356
|
-
// The stem must be IDEMPOTENT on the singular, or the fold does nothing: an earlier form stripped
|
|
1357
|
-
// "es" and turned "gives" into "giv" while leaving "give" alone, so the two never matched and every
|
|
1358
|
-
// caveat looked new. Strip one trailing "s" and nothing else.
|
|
1359
|
-
const stem = (w) => w.replace(/ies$/, 'y').replace(/s$/, '');
|
|
1360
|
-
const known = new Set(contentWords(reference).map(stem));
|
|
1361
|
-
const words = contentWords(line);
|
|
1362
|
-
if (!words.length) return false;
|
|
1363
|
-
return words.some((w) => !known.has(stem(w)));
|
|
1364
|
-
}
|
|
1340
|
+
// THE CAVEAT FILTER IS SHARED, NOT LOCAL. `saysSomethingNew` was defined here and is now in
|
|
1341
|
+
// `shared/says-something-new.mjs`, because the writing-standard check asks the identical question of a
|
|
1342
|
+
// page's lede against its title. Two definitions of one rule is one definition and one imitation of it.
|
|
1343
|
+
// The stopword set and the idempotent stem travel with it; the reference text below stays here, because
|
|
1344
|
+
// it is this page's own words and nothing else's.
|
|
1365
1345
|
|
|
1366
1346
|
function readBlock(m, framework) {
|
|
1367
1347
|
const factors = (m.factors ?? []).filter((s) => typeof s === 'string' && s.trim());
|
|
@@ -1661,6 +1661,8 @@ const COV_STATE = {
|
|
|
1661
1661
|
* carries EVERY significant word of the directive it names — a near-match keeps both.
|
|
1662
1662
|
*/
|
|
1663
1663
|
const FOLLOW_UP_PREFIX = 'Follow-up / ';
|
|
1664
|
+
// `<axis> / <what was swept>` — the shape unitLabel composes for a plan-derived coverage unit.
|
|
1665
|
+
const AXIS_LABELLED = /\s\/\s/;
|
|
1664
1666
|
const COV_STOPWORDS = new Set(['the', 'a', 'an', 'as', 'for', 'of', 'in', 'on', 'and', 'or', 'to', 'is',
|
|
1665
1667
|
'was', 'it', 'its', 'this', 'that', 'with', 'by', 'at', 'be', 'been', 'run', 'search', 'searched']);
|
|
1666
1668
|
const covWords = (t) => new Set(String(t || '').toLowerCase().match(/[a-z0-9]+/g)?.filter((w) => !COV_STOPWORDS.has(w)) ?? []);
|
|
@@ -1689,13 +1691,31 @@ function dedupeFollowUps(coverage) {
|
|
|
1689
1691
|
// it. That is exactly the failure this function's header calls the one worth avoiding, and the
|
|
1690
1692
|
// header was right while the code was not.
|
|
1691
1693
|
//
|
|
1692
|
-
//
|
|
1693
|
-
//
|
|
1694
|
-
//
|
|
1695
|
-
//
|
|
1696
|
-
//
|
|
1694
|
+
// THE LIMITATION ABOVE STOPPED BEING HYPOTHETICAL, so it is a rule now rather than a paragraph.
|
|
1695
|
+
//
|
|
1696
|
+
// It read: "it is word containment, so two genuinely different OPEN searches sharing every word of a
|
|
1697
|
+
// short directive would still collapse to one." Measured on a delivered report — 33 coverage entries,
|
|
1698
|
+
// 31 rendered cells. Two open park rows with one- and two-word directives were erased by unrelated
|
|
1699
|
+
// rows that merely mentioned those words. The client read a coverage section that never named two
|
|
1700
|
+
// slices the run had deliberately disclosed, which is the failure this header calls the one worth
|
|
1701
|
+
// avoiding, reached by the route the header predicted.
|
|
1702
|
+
//
|
|
1703
|
+
// THE DISCRIMINATOR IS THE AXIS LABEL, and it is the identity the paragraph above said was missing.
|
|
1704
|
+
// A row whose area carries one — `<axis> / <what was swept>`, the shape `unitLabel` composes — is a
|
|
1705
|
+
// PLAN-DERIVED COVERAGE UNIT. It is not the model restating a deferred slice; it is a different unit
|
|
1706
|
+
// that happens to contain the same word. Only the model's own free-text row can BE a restatement,
|
|
1707
|
+
// and that is exactly the dolphin row this function was built for: "the English word DOLPHIN as a
|
|
1708
|
+
// dedicated exact search", no axis, no separator. So an axis-labelled row may no longer stand in for
|
|
1709
|
+
// a composed follow-up, however many words it shares.
|
|
1710
|
+
//
|
|
1711
|
+
// EVIDENCE, STATED ONE-SIDED BECAUSE IT IS. Every row containing either erased directive was
|
|
1712
|
+
// axis-labelled `register`, so the measured run supports the half that stops suppression. It cannot
|
|
1713
|
+
// support the other half: that run has no open row WITHOUT an axis label, so nothing in it exercises
|
|
1714
|
+
// "a free-text row still suppresses". The witness for that half is the dolphin incident alone, which
|
|
1715
|
+
// is a real delivered page but a single one — and the arm below is what keeps it honest.
|
|
1697
1716
|
return !written.some((w) => {
|
|
1698
1717
|
if (COV_STATE[w?.state]?.cls === 'ok') return false; // a searched-and-clean row reports the opposite
|
|
1718
|
+
if (AXIS_LABELLED.test(String(w?.area || ''))) return false; // a plan unit is not a restatement
|
|
1699
1719
|
const theirs = covWords(w.area);
|
|
1700
1720
|
return [...directive].every((word) => theirs.has(word));
|
|
1701
1721
|
});
|
package/driver/suite-census.json
CHANGED
|
@@ -651,6 +651,12 @@
|
|
|
651
651
|
"skips": 0,
|
|
652
652
|
"todos": 0
|
|
653
653
|
},
|
|
654
|
+
"a-refusal-with-no-near-neighbour-does-not-claim-the-search-never-ran.test.mjs": {
|
|
655
|
+
"tests": 4,
|
|
656
|
+
"asserts": 15,
|
|
657
|
+
"skips": 0,
|
|
658
|
+
"todos": 0
|
|
659
|
+
},
|
|
654
660
|
"a-refused-mcp-call-is-not-a-call-nobody-made.test.mjs": {
|
|
655
661
|
"tests": 7,
|
|
656
662
|
"asserts": 19,
|
|
@@ -765,6 +771,12 @@
|
|
|
765
771
|
"skips": 0,
|
|
766
772
|
"todos": 0
|
|
767
773
|
},
|
|
774
|
+
"a-short-named-open-slice-still-reaches-the-page.test.mjs": {
|
|
775
|
+
"tests": 5,
|
|
776
|
+
"asserts": 6,
|
|
777
|
+
"skips": 0,
|
|
778
|
+
"todos": 0
|
|
779
|
+
},
|
|
768
780
|
"a-signal-immune-fixture-is-reaped-by-its-owner.test.mjs": {
|
|
769
781
|
"tests": 10,
|
|
770
782
|
"asserts": 20,
|
|
@@ -4593,6 +4605,12 @@
|
|
|
4593
4605
|
"skips": 0,
|
|
4594
4606
|
"todos": 0
|
|
4595
4607
|
},
|
|
4608
|
+
"the-cut-approves-its-own-parked-run.test.mjs": {
|
|
4609
|
+
"tests": 4,
|
|
4610
|
+
"asserts": 10,
|
|
4611
|
+
"skips": 0,
|
|
4612
|
+
"todos": 0
|
|
4613
|
+
},
|
|
4596
4614
|
"the-demo-account-shows-a-configured-customer.test.mjs": {
|
|
4597
4615
|
"tests": 4,
|
|
4598
4616
|
"asserts": 24,
|
|
@@ -5133,6 +5151,18 @@
|
|
|
5133
5151
|
"skips": 0,
|
|
5134
5152
|
"todos": 0
|
|
5135
5153
|
},
|
|
5154
|
+
"the-writing-standard-backlog-is-a-floor.test.mjs": {
|
|
5155
|
+
"tests": 3,
|
|
5156
|
+
"asserts": 8,
|
|
5157
|
+
"skips": 2,
|
|
5158
|
+
"todos": 0
|
|
5159
|
+
},
|
|
5160
|
+
"the-writing-standard-check-refuses-five-classes.test.mjs": {
|
|
5161
|
+
"tests": 14,
|
|
5162
|
+
"asserts": 53,
|
|
5163
|
+
"skips": 0,
|
|
5164
|
+
"todos": 0
|
|
5165
|
+
},
|
|
5136
5166
|
"the-xcheck-cap-counts-queries.test.mjs": {
|
|
5137
5167
|
"tests": 9,
|
|
5138
5168
|
"asserts": 25,
|
package/driver/verify.mjs
CHANGED
|
@@ -368,14 +368,21 @@ function commonLawMeaningSeat(p, c) {
|
|
|
368
368
|
const recordedQ = new Set(recordedRaw.map(queryKey));
|
|
369
369
|
const dropped = dictated.filter((q) => !recordedQ.has(queryKey(q)));
|
|
370
370
|
if (dropped.length) {
|
|
371
|
-
// ── THE REFUSAL SAYS
|
|
371
|
+
// ── THE REFUSAL SAYS WHAT IT CAN SEE, AND STOPS SHORT OF WHAT IT CANNOT ───────────────────────
|
|
372
372
|
//
|
|
373
|
-
// ABSENT: no recorded query resembles it, so the search was not run and the seat must run it.
|
|
374
373
|
// UNMATCHED: something close IS recorded, so the search ran and the two spellings disagree beyond
|
|
375
374
|
// what the key folds — a re-ordering, a translation, a truncation, a query the provider chose for
|
|
376
375
|
// itself. Telling the seat to "re-run the missing query" in that case asks for the one thing that
|
|
377
376
|
// cannot help, and that is what turned one attempt into four on a production clearance.
|
|
378
377
|
//
|
|
378
|
+
// NO RESEMBLANCE: nothing recorded looks like it. This used to be reported as ABSENT — "the search
|
|
379
|
+
// was not run and the seat must run it" — and that is a claim the gate has no way to make. A query
|
|
380
|
+
// recorded under a translation, a transliteration, or the seat's own rewording resembles nothing and
|
|
381
|
+
// is not absent; the seat was then told to re-run a search that had already happened, which is the
|
|
382
|
+
// same loop, one wording-distance further out. The two states are genuinely indistinguishable from
|
|
383
|
+
// here and always will be: there is no identity to join on, which is why this comparison exists at
|
|
384
|
+
// all. So the label names the observation and the remedy carries BOTH repairs, cheap either way.
|
|
385
|
+
//
|
|
379
386
|
// NO THRESHOLD DECIDES ANYTHING (owner's ruling). The nearest recorded query is shown so a person or
|
|
380
387
|
// a seat can SEE the difference in one attempt; it never makes the gate pass. A similarity score
|
|
381
388
|
// that could pass this gate would be a score that can hide a skipped query, which is what the gate
|
|
@@ -394,11 +401,23 @@ function commonLawMeaningSeat(p, c) {
|
|
|
394
401
|
// every query. Below half the words in common, say nothing rather than point at a red herring.
|
|
395
402
|
return bestScore >= 0.5 ? best : null;
|
|
396
403
|
};
|
|
404
|
+
// THE SECOND LABEL SAYS WHAT THIS GATE KNOWS, WHICH IS LESS THAN IT USED TO CLAIM.
|
|
405
|
+
//
|
|
406
|
+
// It read `[absent from the ledger]`, and that is an assertion the gate cannot make. No near
|
|
407
|
+
// neighbour means no RECORDED query resembles this one — not that the search never ran. A query that
|
|
408
|
+
// ran and was recorded under a translation, a transliteration, a re-ordering, or the seat's own
|
|
409
|
+
// rewording clears no overlap threshold, and was then told to re-run a search that had already
|
|
410
|
+
// happened. That is the loop this whole gate was filed to break, narrowed but not closed: it needs a
|
|
411
|
+
// large wording difference now rather than a single apostrophe, and it is still reachable.
|
|
412
|
+
//
|
|
413
|
+
// The gate cannot tell the two apart and is not being asked to. There is no identity to join on —
|
|
414
|
+
// that is the entire reason the dictated-versus-recorded comparison exists. So the label states the
|
|
415
|
+
// observation, the remedy carries both cases, and no threshold decides which one a seat is told.
|
|
397
416
|
const parts = dropped.slice(0, 3).map((q) => {
|
|
398
417
|
const n = nearest(q);
|
|
399
418
|
return n
|
|
400
419
|
? `${abbrev(q, 40)} [unmatched; nearest recorded: ${abbrev(n, 40)}]`
|
|
401
|
-
: `${abbrev(q, 40)} [
|
|
420
|
+
: `${abbrev(q, 40)} [no recorded query resembles this one]`;
|
|
402
421
|
});
|
|
403
422
|
return fail(`connotation_query_unrecorded:${parts.join(",")}${dropped.length > 3 ? ` (+${dropped.length - 3} more)` : ""}`);
|
|
404
423
|
}
|
package/mcp-server/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# trademark-artifacts-mcp
|
|
2
2
|
|
|
3
|
+
## 0.3.2-beta.0
|
|
4
|
+
|
|
5
|
+
No changes in this release.
|
|
6
|
+
|
|
7
|
+
## 0.3.1
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- f96c089: Fixed: A clearance for a new client can be ordered through an assistant connector without setting up a company first.
|
|
12
|
+
- 56a760f: Fixed: Removing somebody's access now ends their assistant's connection on its next request. Until now an assistant that had already connected kept the access it started with for up to half an hour.
|
|
13
|
+
- c05e0a7: Fixed: Ask your assistant for a client's recent searches by the client's name and it finds them. Every search in the list now says which client it was for, and which project. A search whose own record cannot be read says so, instead of appearing to belong to nobody.
|
|
14
|
+
- d9798db: Fixed: A clearance that stops now always records that its notice is still owed, so a failure cannot be passed over as already handled.
|
|
15
|
+
- 240673c: Fixed: Sending an access key to the engine's network address now gets a refusal that says so. It reports that the address takes an identity from the sign-in proxy and never a key.
|
|
16
|
+
|
|
17
|
+
Fixed: That refusal also names the local door where a key is accepted. Before, it reported only a missing sign-in assertion, which sent operators to the wrong configuration.
|
|
18
|
+
|
|
19
|
+
For operators: A program on the same machine can now reach the engine's local key door without setting a host name for it. The local door no longer applies a browser protection that only a network address needs.
|
|
20
|
+
- f96c089: New: The free preview of a search lists, for each territory ordered, which registers legally bind it.
|
|
21
|
+
|
|
3
22
|
## 0.3.1-beta.4
|
|
4
23
|
|
|
5
24
|
No changes in this release.
|
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2-beta.0",
|
|
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
package/portal-ui/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "portal-ui",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.2-beta.0",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
|
|
8
8
|
"engines": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-oauth-mcp-bridge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2-beta.0",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
|
+
//
|
|
5
|
+
// RE-MINT THE WRITING-STANDARD BACKLOG — the per-file, per-class count that may only go down.
|
|
6
|
+
//
|
|
7
|
+
// node scripts/mint-writing-standard-backlog.mjs # report the delta, change nothing
|
|
8
|
+
// node scripts/mint-writing-standard-backlog.mjs --check # ... and exit 1 if the file is out of date
|
|
9
|
+
// node scripts/mint-writing-standard-backlog.mjs --apply # write it
|
|
10
|
+
//
|
|
11
|
+
// THE ONLY REASON TO RUN `--apply` IS THAT THE NUMBER WENT DOWN. Nothing here refuses to write a higher
|
|
12
|
+
// one — a table that could not record growth would be unable to describe a tree somebody widened a class
|
|
13
|
+
// over — but the floor arm refuses the growth itself, and it reads the committed file rather than this
|
|
14
|
+
// script's output. So an author who mints upward has recorded the regression rather than absorbed it.
|
|
15
|
+
// The two halves are deliberately not the same program.
|
|
16
|
+
//
|
|
17
|
+
// WHAT THE STANDING POPULATION ACTUALLY IS, so nobody reads the number as noise: nine of it is the
|
|
18
|
+
// knockout's scope block and the clearance renderer's coverage paragraph — the sentences the writing
|
|
19
|
+
// standard quotes as what not to write, still on the page. Two are a reviewer-only marker that reaches
|
|
20
|
+
// the delivered HTML. One is a screen whose only heading is the mark its run was ordered for. One is an
|
|
21
|
+
// environment name passed as an argument, which this check reads as printed text because a string
|
|
22
|
+
// literal is the only site it can see; that one is a false positive and is recorded rather than
|
|
23
|
+
// special-cased, because a special case for it would be a hole the size of every string argument.
|
|
24
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { join, dirname } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { CLASSES, censusOf } from "../shared/writing-standard-classes.mjs";
|
|
28
|
+
import { publishedOf } from "../shared/reference-guard-classes.mjs";
|
|
29
|
+
import { trackedFiles, skipReason } from "../shared/tracked-files.mjs";
|
|
30
|
+
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
31
|
+
|
|
32
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
33
|
+
const FIXTURE = join(ROOT, "driver/test/fixtures/writing-standard-backlog.json");
|
|
34
|
+
const GUARD = "writing-standard-backlog";
|
|
35
|
+
|
|
36
|
+
/** The tree's current census, as the fixture records it. */
|
|
37
|
+
export function mint() {
|
|
38
|
+
const tracked = trackedFiles(GUARD, { root: ROOT });
|
|
39
|
+
if (tracked === null) return null;
|
|
40
|
+
// THE SAME POPULATION THE FLOOR READS, from the same helper. A mint over the index and a floor over
|
|
41
|
+
// HEAD would disagree under the overlay, and `--check` would report a difference that is only the two
|
|
42
|
+
// instruments asking different questions.
|
|
43
|
+
const p = publishedOf(tracked, ROOT);
|
|
44
|
+
if (p.error) { console.error(`mint-writing-standard-backlog: ${p.error}`); process.exit(2); }
|
|
45
|
+
if (p.laid) console.log(`mint-writing-standard-backlog: ${p.laid} tracked path(s) are not in HEAD — laid over this checkout, not published in it, and not counted`);
|
|
46
|
+
const c = censusOf(p.files, (f) => readFileSync(join(ROOT, f), "utf8"));
|
|
47
|
+
return { classes: CLASSES.map((x) => x.id), total: c.total, files: c.files };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Per-class totals, for a reader who wants to know WHICH number moved. */
|
|
51
|
+
export const byClass = (table) =>
|
|
52
|
+
CLASSES.map((c, i) => [c.id, Object.values(table.files).reduce((a, v) => a + v[i], 0)]);
|
|
53
|
+
|
|
54
|
+
function main() {
|
|
55
|
+
const apply = process.argv.includes("--apply");
|
|
56
|
+
const check = process.argv.includes("--check");
|
|
57
|
+
|
|
58
|
+
const now = mint();
|
|
59
|
+
// A COULD-NOT-LOOK EXITS 2, never 0. Outside a checkout there is no corpus, and a mint that wrote an
|
|
60
|
+
// empty table here would replace the whole backlog with nothing and call it a repair.
|
|
61
|
+
if (now === null) { console.error(`mint-writing-standard-backlog: ${skipReason(GUARD)}`); process.exit(2); }
|
|
62
|
+
|
|
63
|
+
console.log(`writing-standard residue: ${now.total} hit(s) across ${Object.keys(now.files).length} file(s)`);
|
|
64
|
+
for (const [id, n] of byClass(now)) console.log(` ${String(n).padStart(5)} ${id}`);
|
|
65
|
+
|
|
66
|
+
let was = null;
|
|
67
|
+
try { was = JSON.parse(readFileSync(FIXTURE, "utf8")); } catch { /* first mint */ }
|
|
68
|
+
|
|
69
|
+
const next = JSON.stringify(now, null, 2) + "\n";
|
|
70
|
+
const same = was && JSON.stringify(was, null, 2) + "\n" === next;
|
|
71
|
+
if (same) { console.log("the backlog is current"); return; }
|
|
72
|
+
|
|
73
|
+
if (was) {
|
|
74
|
+
const delta = now.total - was.total;
|
|
75
|
+
console.log(`\ntotal ${was.total} → ${now.total} (${delta >= 0 ? "+" : ""}${delta})`);
|
|
76
|
+
}
|
|
77
|
+
if (apply) { writeFileSync(FIXTURE, next); console.log("written"); return; }
|
|
78
|
+
console.log("\nre-run with --apply to write it");
|
|
79
|
+
if (check) process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (isEntrypoint(import.meta.url)) main();
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
|
+
//
|
|
5
|
+
// release-approve-parked.mjs — approve the version pull request's parked CI run, so a cut does not wait
|
|
6
|
+
// on a person.
|
|
7
|
+
//
|
|
8
|
+
// WHY A PERSON HAS BEEN CLICKING. The version pull request is authored by the repository's own Actions
|
|
9
|
+
// bot, and a run triggered by a bot-authored pull request arrives `action_required`. Measured across the
|
|
10
|
+
// last forty `pull_request` CI runs: every parked one is on the version branch and no human-authored
|
|
11
|
+
// branch parks, so the thing that distinguishes them is the author, not a repository setting. The
|
|
12
|
+
// approval endpoint works on such a run — proved against a live parked run, which moved from
|
|
13
|
+
// `action_required` to `queued` — even though its documentation names fork pull requests only.
|
|
14
|
+
//
|
|
15
|
+
// AND WHY IT NEEDS A TOKEN RATHER THAN THE BUILT-IN ONE. `GITHUB_TOKEN` cannot approve a workflow run;
|
|
16
|
+
// GitHub blocks self-approval deliberately. So this reads a separate token with Actions read and write.
|
|
17
|
+
//
|
|
18
|
+
// WHAT THAT PERMISSION ACTUALLY BUYS, stated because the next person deciding whether to reuse this
|
|
19
|
+
// secret will read this sentence and not the permission page. Actions write is not "approve runs": it
|
|
20
|
+
// also creates workflow_dispatch events, cancels any run in the repository, and deletes run logs. This
|
|
21
|
+
// workflow carries `workflow_dispatch`, so the token can reach a PUBLISH by the same door a person
|
|
22
|
+
// uses. It cannot push code, open or merge a pull request, or authenticate to the registry — the
|
|
23
|
+
// publish credential is minted per run from this workflow's OIDC token and stored nowhere. So the
|
|
24
|
+
// boundary is real but it is not "it can only do what this script does".
|
|
25
|
+
//
|
|
26
|
+
// IT NEVER FAILS THE CUT. No token, no parked run, an API that refuses — each prints what happened and
|
|
27
|
+
// exits 0, because the cut's own wait still ends the way it always did: a person clicks, or the wait
|
|
28
|
+
// expires. A step that could turn a green cut red in order to save a click would be a worse trade than
|
|
29
|
+
// the click.
|
|
30
|
+
//
|
|
31
|
+
// BY SHA, AND ONLY THE CURRENT ONE. Approving a STALE parked run cancels the live one — CI's concurrency
|
|
32
|
+
// is workflow plus ref with cancel-in-progress on non-main refs — so this resolves the version branch's
|
|
33
|
+
// head at the moment it runs and approves only a run whose `head_sha` equals it.
|
|
34
|
+
//
|
|
35
|
+
// THE INTERVAL THAT MATTERS IS BETWEEN THE READ AND THE POST. Filtering the run list by head_sha and
|
|
36
|
+
// re-checking head_sha per row guards a case the API already guarantees; no row can disagree with the
|
|
37
|
+
// value it was queried by. The way this goes wrong is the HEAD going stale: the version step
|
|
38
|
+
// force-pushes that branch whenever it runs, so a push landing between the list and the approval leaves
|
|
39
|
+
// this approving the run on the superseded head — the exact act that cancels the live cut. The head is
|
|
40
|
+
// therefore re-read immediately before each approval, and a move aborts the whole pass rather than
|
|
41
|
+
// skipping one row, because if the branch moved then every id in the list is stale.
|
|
42
|
+
import { argv, env, exit } from "node:process";
|
|
43
|
+
import { pathToFileURL } from "node:url";
|
|
44
|
+
|
|
45
|
+
const REPO = env.GITHUB_REPOSITORY || "CordilleraSarl/clearotron";
|
|
46
|
+
const BRANCH = arg("--branch") || "changeset-release/main";
|
|
47
|
+
const TOKEN = env.ACTIONS_APPROVE_TOKEN || "";
|
|
48
|
+
|
|
49
|
+
function arg(name) { const i = argv.indexOf(name); return i === -1 ? null : argv[i + 1]; }
|
|
50
|
+
const say = (s) => process.stdout.write(`release-approve-parked: ${s}\n`);
|
|
51
|
+
|
|
52
|
+
async function api(path, init = {}) {
|
|
53
|
+
const r = await fetch(`https://api.github.com${path}`, {
|
|
54
|
+
...init,
|
|
55
|
+
headers: {
|
|
56
|
+
accept: "application/vnd.github+json",
|
|
57
|
+
authorization: `Bearer ${TOKEN}`,
|
|
58
|
+
"x-github-api-version": "2022-11-28",
|
|
59
|
+
...(init.headers ?? {}),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
return r;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* THE DECISION, SEPARATED FROM THE NETWORK so it can be driven against a table rather than matched as
|
|
67
|
+
* source text. Given the runs on a head and that head, which run ids should be approved?
|
|
68
|
+
*
|
|
69
|
+
* `event` AND `conclusion` both: the dispatched CI run on the same head is not the one the pull
|
|
70
|
+
* request's rollup reads, and approving it would do nothing while reading as success. PURE.
|
|
71
|
+
*/
|
|
72
|
+
export function runsToApprove(runs, head) {
|
|
73
|
+
return (Array.isArray(runs) ? runs : [])
|
|
74
|
+
.filter((r) => r?.event === "pull_request" && r?.conclusion === "action_required" && r?.head_sha === head)
|
|
75
|
+
.map((r) => r.id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The run list is PAGED, and a silent truncation here reads as "nothing parked". 100 is the API maximum;
|
|
79
|
+
// the loop stops when a page comes back short, and says so if it ever hits the cap, because a cut that
|
|
80
|
+
// was not approved because the list was cut off should not look like a cut with nothing to approve.
|
|
81
|
+
async function allRunsOn(head) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (let page = 1; page <= 5; page++) {
|
|
84
|
+
const r = await api(`/repos/${REPO}/actions/runs?head_sha=${head}&per_page=100&page=${page}`);
|
|
85
|
+
if (!r.ok) return { ok: false, status: r.status, runs: out };
|
|
86
|
+
const batch = (await r.json())?.workflow_runs ?? [];
|
|
87
|
+
out.push(...batch);
|
|
88
|
+
if (batch.length < 100) return { ok: true, runs: out };
|
|
89
|
+
}
|
|
90
|
+
say(`more than 500 runs on this head — reading the first 500 only.`);
|
|
91
|
+
return { ok: true, runs: out };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// One reading of "where is the version branch now", used twice: once to choose, once immediately before
|
|
95
|
+
// the approval. Null means the question could not be answered, which is never treated as "unchanged".
|
|
96
|
+
async function branchHead() {
|
|
97
|
+
const br = await api(`/repos/${REPO}/branches/${encodeURIComponent(BRANCH)}`);
|
|
98
|
+
if (!br.ok) return null;
|
|
99
|
+
return (await br.json())?.commit?.sha ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function main() {
|
|
103
|
+
// THE ABSENT SECRET IS THE ORDINARY CASE UNTIL THE TOKEN IS MINTED, and it must read as ordinary.
|
|
104
|
+
if (!TOKEN) {
|
|
105
|
+
say("no ACTIONS_APPROVE_TOKEN — nothing approved; the version run waits for a person, as before.");
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const head = await branchHead();
|
|
110
|
+
if (!head) { say(`could not read ${BRANCH} — nothing approved.`); return 0; }
|
|
111
|
+
say(`${BRANCH} is at ${head.slice(0, 7)}`);
|
|
112
|
+
|
|
113
|
+
const rr = await allRunsOn(head);
|
|
114
|
+
if (!rr.ok) { say(`could not list runs for ${head.slice(0, 7)} (${rr.status}) — nothing approved.`); return 0; }
|
|
115
|
+
const ids = runsToApprove(rr.runs, head);
|
|
116
|
+
if (!ids.length) {
|
|
117
|
+
const others = rr.runs.filter((r) => r?.conclusion === "action_required").length;
|
|
118
|
+
say(`no parked pull_request run on ${head.slice(0, 7)}${others ? ` (${others} parked on another event or head, left alone)` : ""} — nothing to approve.`);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const id of ids) {
|
|
123
|
+
// ── THE INTERVAL THAT MATTERS IS BETWEEN THE READ AND THE POST, NOT INSIDE THE LIST ──────────────
|
|
124
|
+
//
|
|
125
|
+
// Filtering the list by head_sha and then re-checking head_sha on each row guards a case the API
|
|
126
|
+
// already guarantees: no row can disagree with the value it was queried by. What can actually go
|
|
127
|
+
// wrong is `head` itself going stale — the version step force-pushes this branch whenever it runs,
|
|
128
|
+
// so a push landing between the list and this POST leaves us approving the run on the SUPERSEDED
|
|
129
|
+
// head. That is precisely the act that cancels the live cut, because CI's concurrency is workflow
|
|
130
|
+
// plus ref with cancel-in-progress on non-main refs.
|
|
131
|
+
//
|
|
132
|
+
// So the head is re-read immediately before each approval, and a move aborts rather than skips: if
|
|
133
|
+
// the branch has moved, every id in this list is stale, not just this one.
|
|
134
|
+
const now = await branchHead();
|
|
135
|
+
if (now == null) { say(`could not re-read ${BRANCH} before approving — nothing approved.`); return 0; }
|
|
136
|
+
if (now !== head) {
|
|
137
|
+
say(`${BRANCH} moved ${head.slice(0, 7)} -> ${now.slice(0, 7)} since the run list was taken — ` +
|
|
138
|
+
`NOTHING APPROVED. Approving a run on the superseded head would cancel the live one; the cut ` +
|
|
139
|
+
`waits for a person, or for the next dispatch.`);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
const a = await api(`/repos/${REPO}/actions/runs/${id}/approve`, { method: "POST" });
|
|
143
|
+
say(a.ok ? `approved run ${id} on ${head.slice(0, 7)}.` : `run ${id} refused approval (${a.status}) — the cut still waits for a person.`);
|
|
144
|
+
}
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// RUN WHEN INVOKED, whatever the file is called. The old guard compared argv[1] to this file's NAME, so
|
|
149
|
+
// a rename made the step print nothing and exit 0 — a script that never ran, wearing the face of a
|
|
150
|
+
// successful no-op. Comparing the resolved URLs asks the real question instead.
|
|
151
|
+
if (import.meta.url === pathToFileURL(argv[1] ?? "").href) main().then((c) => exit(c)).catch((e) => { say(`could not run (${e?.message ?? e}) — nothing approved.`); exit(0); });
|