clearotron 0.2.4 → 0.3.0-beta.1
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 +13 -2
- package/CONTRIBUTING.md +1 -1
- package/INSTALL.md +62 -38
- package/bin/brandowner.mjs +18 -169
- package/bin/clearotron.mjs +3 -1
- package/bin/connect.mjs +28 -19
- package/bin/disconnect.mjs +3 -3
- package/bin/example.mjs +7 -7
- package/bin/framework-preflight.mjs +49 -0
- package/bin/grant.mjs +151 -93
- package/bin/onboard.mjs +220 -138
- package/bin/start.mjs +146 -137
- package/bin/stop.mjs +2 -2
- package/bin/update.mjs +1 -1
- package/build-info.json +2 -2
- package/docs/CLIENT-MCP.md +2 -2
- package/docs/E2E.md +12 -2
- package/docs/ONBOARDING.md +1 -1
- package/docs/PORTAL.md +14 -13
- package/docs/SECURITY.md +23 -24
- package/docs/architecture/04-configuration-reference.md +12 -5
- package/docs/architecture/05-config-governance.md +7 -7
- package/docs/architecture/07-quality-and-audit.md +1 -1
- package/docs/architecture/08-development-guide.md +5 -0
- package/docs/configuration.md +118 -0
- package/docs/decisions/0004-documentation-structure.md +2 -2
- package/docs/decisions/0006-what-the-public-repository-carries.md +2 -2
- package/driver/CHANGELOG.md +70 -0
- package/driver/ask-ledger.mjs +2 -2
- package/driver/cancel.mjs +27 -0
- package/driver/case-law-sources.mjs +3 -3
- package/driver/company-bundle.mjs +261 -0
- package/driver/compare.mjs +1 -1
- package/driver/compose-read.mjs +2 -2
- package/driver/config-inventory.mjs +2 -2
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/contract-vocabulary.mjs +1 -1
- package/driver/declination-call.mjs +1 -1
- package/driver/deliver-trigger.sh +3 -3
- package/driver/dev-portal.mjs +3 -1
- package/driver/digest-queue.mjs +1 -1
- package/driver/disposition-tool.mjs +1 -1
- package/driver/doc-constants.mjs +1 -1
- package/driver/drain-posture.mjs +2 -2
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +44 -9
- package/driver/effective-scope.mjs +30 -1
- package/driver/effort-model.mjs +6 -6
- package/driver/engine/CONTRACT.md +2 -2
- package/driver/engine/anthropic-agent.mjs +11 -11
- package/driver/engine/jx-turn.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +29 -4
- package/driver/engine/mcp/recording-server.mjs +73 -1
- package/driver/engine/openai-agent.mjs +1 -1
- package/driver/engine/probe.mjs +28 -4
- package/driver/enqueue-schema.mjs +23 -3
- package/driver/findings-model.mjs +2 -2
- package/driver/flag-snapshot.mjs +2 -2
- package/driver/floor-duty.mjs +2 -2
- package/driver/frame-diff-model.mjs +1 -1
- package/driver/framework-preflight.mjs +143 -0
- package/driver/gateway.mjs +9 -1
- package/driver/hit-list.mjs +1 -1
- package/driver/jx-lanes.mjs +1 -1
- package/driver/jx.mjs +1 -1
- package/driver/knockout-assess-record.mjs +1 -1
- package/driver/knockout-review-record.mjs +435 -0
- package/driver/order-probe.mjs +1 -1
- package/driver/outbox-backoff.mjs +2 -2
- package/driver/owner-use-check.mjs +2 -2
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +105 -8
- package/driver/pipeline.mjs +81 -30
- package/driver/plain-register.mjs +77 -3
- package/driver/portal-access.mjs +141 -74
- package/driver/portal-config-view.mjs +59 -70
- package/driver/portal-report.mjs +4 -4
- package/driver/portal-service.mjs +348 -141
- package/driver/portal-upstream.mjs +105 -17
- package/driver/predelivery-lint.mjs +43 -18
- package/driver/product-rows.mjs +1 -1
- package/driver/products.mjs +1 -1
- package/driver/profile-page.html +30 -5
- package/driver/profile-service.mjs +197 -26
- package/driver/profiles.mjs +48 -1
- package/driver/publish/index.mjs +31 -13
- package/driver/publish/knockout.mjs +9 -5
- package/driver/publish/office-record-links.mjs +189 -0
- package/driver/publish/parse.mjs +3 -3
- package/driver/publish/publish-inputs.mjs +26 -0
- package/driver/publish/render-knockout.mjs +42 -42
- package/driver/publish/render.mjs +29 -4
- package/driver/publish/report-data.mjs +2 -2
- package/driver/publish/seed-pool.mjs +1 -1
- package/driver/publish/templates/report.css +8 -8
- package/driver/publish/xlsx.mjs +49 -7
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/recipe-service.mjs +1 -1
- package/driver/record-carry.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/reference-strip-signatures.mjs +1 -1
- package/driver/register-availability.mjs +4 -3
- package/driver/register-count.mjs +3 -3
- package/driver/register-records.mjs +1 -1
- package/driver/repair-composers.mjs +1 -1
- package/driver/repairs.mjs +3 -3
- package/driver/replay-archive.mjs +1 -1
- package/driver/report-card-record.mjs +1 -1
- package/driver/result-noun-fields.mjs +5 -0
- package/driver/roster-verdict.mjs +48 -5
- package/driver/run-activity.mjs +1 -1
- package/driver/run-requirements.mjs +18 -5
- package/driver/runner.mjs +24 -15
- package/driver/search-policy.mjs +8 -8
- package/driver/senior-rights.mjs +1 -1
- package/driver/skills/prelim-search/delivery-contract.md +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.md +10 -7
- package/driver/stages-knockout.mjs +72 -6
- package/driver/stages.mjs +10 -10
- package/driver/suite-census.json +238 -70
- package/driver/synthesis-record.mjs +2 -2
- package/driver/systemd/clearotron-client-mcp.service +3 -3
- package/driver/systemd/clearotron-deploy.service +2 -2
- package/driver/systemd/clearotron-mcp-face.service +1 -1
- package/driver/systemd/clearotron-portal.service +3 -3
- package/driver/systemd/clearotron-worker.service +5 -5
- package/driver/systemd/install-census.mjs +1 -1
- package/driver/systemd/render-units.mjs +9 -9
- package/driver/terminal-clamp.mjs +1 -1
- package/driver/trigger-cap.mjs +18 -2
- package/driver/unit-inventory.mjs +8 -8
- package/driver/usage-ledger.mjs +5 -3
- package/driver/verify-knockout.mjs +7 -7
- package/driver/verify.mjs +5 -5
- package/driver/whatif-memo-run.mjs +1 -1
- package/driver/whatif-queue.mjs +3 -3
- package/driver/whatif-worker.mjs +2 -2
- package/examples/README.md +1 -1
- package/examples/grants.example.json +25 -24
- package/mcp-server/CHANGELOG.md +10 -0
- package/mcp-server/http-server.mjs +3 -3
- package/mcp-server/key-socket.mjs +1 -1
- package/mcp-server/lib/audit-view.mjs +3 -3
- package/mcp-server/lib/brief.mjs +3 -3
- package/mcp-server/lib/driver.mjs +1 -1
- package/mcp-server/lib/events.mjs +1 -1
- package/mcp-server/lib/http-handler.mjs +2 -2
- package/mcp-server/lib/instructions.mjs +2 -2
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/lib/ops.mjs +6 -3
- package/mcp-server/lib/options.mjs +15 -4
- package/mcp-server/lib/plan.mjs +7 -6
- package/mcp-server/lib/runs.mjs +10 -0
- package/mcp-server/lib/whatif.mjs +5 -5
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/README.md +1 -1
- package/mcp-server/remote/client-mcp-apikey.service +1 -1
- package/mcp-server/remote/client-mcp.service +2 -2
- package/mcp-server/remote/trademark-artifacts-http.service +1 -1
- package/mcp-server/server.mjs +38 -24
- package/package.json +2 -2
- package/portal-ui/dist/assets/{index-KFAHMgdT.js → index-CWTHP0sH.js} +3471 -1901
- package/portal-ui/dist/assets/{index-1ziUJX1E.css → index-KpytsmNH.css} +79 -26
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/lane-probe.mjs +9 -3
- package/providers/jx-subclass/lookup.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
- package/scripts/citation-drift-report.mjs +1 -1
- package/scripts/citation-line-check.mjs +2 -2
- package/scripts/drive-env-check.mjs +1 -1
- package/scripts/e2e.mjs +5 -5
- package/scripts/env-audit.mjs +13 -1
- package/scripts/headless-page.mjs +5 -5
- package/scripts/live-surface-check.mjs +26 -2
- package/scripts/mint-names-in-force.mjs +19 -5
- package/scripts/mint-reference-strip-backlog.mjs +1 -1
- package/scripts/mint-suite-census.mjs +37 -10
- package/scripts/pack-publishable.mjs +1 -1
- package/scripts/preinstall-node-check.mjs +1 -1
- package/scripts/release-await-cut.mjs +3 -3
- package/scripts/release-cut-decision.mjs +1 -1
- package/scripts/release-dist-tag.mjs +1 -1
- package/scripts/release-install-check.mjs +1 -1
- package/scripts/release-notes-lint.mjs +1 -1
- package/scripts/release-publish-guard.mjs +1 -1
- package/scripts/release-version-pr-checks.mjs +2 -2
- package/scripts/release-version.mjs +61 -5
- package/scripts/render-brand-banner.mjs +1 -1
- package/scripts/render-check.mjs +2 -2
- package/scripts/repo-writes.mjs +1 -1
- package/scripts/report-frame-check.mjs +1 -1
- package/scripts/report-screenshot.mjs +2 -2
- package/scripts/retire-bare-refs.mjs +1 -1
- package/scripts/revisit-render-check.mjs +1 -1
- package/scripts/score.mjs +1 -1
- package/scripts/strip-titles-and-attributions.mjs +389 -0
- package/scripts/strip-tracker-citations.mjs +122 -5
- package/scripts/test-run.mjs +4 -4
- package/scripts/third-party-notices.mjs +1 -1
- package/scripts/verify-publishable.mjs +1 -1
- package/shared/access-audience.mjs +2 -2
- package/shared/anon-overlay.mjs +1 -1
- package/shared/brand.mjs +15 -1
- package/shared/bundle-freshness.mjs +1 -1
- package/shared/bundle-rebuild.mjs +1 -1
- package/shared/checkout-move.mjs +2 -2
- package/shared/client-door.mjs +8 -8
- package/shared/connect-clients.mjs +7 -7
- package/shared/connector-signin-probe.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/env-local.mjs +5 -5
- package/shared/grants-edit.mjs +76 -0
- package/shared/install-auth.mjs +1 -1
- package/shared/listen.mjs +3 -3
- package/shared/mcp-challenge.mjs +1 -1
- package/shared/names-in-force.mjs +2 -1
- package/shared/onboarding-store.mjs +19 -2
- package/shared/reference-guard-classes.mjs +44 -2
- package/shared/register-selection.mjs +1 -1
- package/shared/scope.mjs +223 -56
- package/shared/secret-file.mjs +1 -1
- package/shared/server-units.mjs +1 -1
- package/shared/staff-domain.mjs +45 -78
- package/shared/summary-blocks.mjs +2 -2
- package/shared/systemd-failure.mjs +3 -3
- package/shared/tracked-files.mjs +1 -1
- package/shared/trigger-lane.mjs +1 -1
- package/shared/tty-style.mjs +1 -1
- package/shared/usage-block.mjs +1 -1
- package/shared/vacuous-pass.mjs +1 -1
- package/shared/verb-shim.mjs +1 -1
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
//
|
|
22
22
|
// Three rules follow, and each is enforced here rather than trusted to a caller:
|
|
23
23
|
//
|
|
24
|
-
// 1. THE ACCOUNT COMES FROM THE PRINCIPAL, NEVER FROM THE PATH OR THE BODY. A
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// 1. THE ACCOUNT COMES FROM THE PRINCIPAL, NEVER FROM THE PATH OR THE BODY. A named account must be
|
|
25
|
+
// inside the person's access, and a person with one company has it substituted. A person who sees
|
|
26
|
+
// everything must name one, and whether it EXISTS is answered downstream: the profile service 404s
|
|
27
|
+
// an unknown key and this wall passes that 404 through unchanged. There is no code path where a
|
|
28
|
+
// body field or a URL segment picks the tenant.
|
|
27
29
|
//
|
|
28
30
|
// 2. A FOREIGN ACCOUNT IS 404, byte-identical to a nonexistent one. Never 403. A 403 confirms the
|
|
29
31
|
// account exists, which is exactly the fact a competitor would be probing for.
|
|
@@ -40,7 +42,7 @@
|
|
|
40
42
|
// the archived ones. Tenancy is untouched: it lives in resolveAccount and rule 2, as it always did.
|
|
41
43
|
|
|
42
44
|
import { KNOWN_PROFILE_KEYS, PROJECT_KEYS } from "./profiles.mjs";
|
|
43
|
-
import { assertPrincipal, PortalDeny } from "./portal-access.mjs";
|
|
45
|
+
import { assertPrincipal, PortalDeny, seesEverything, mayManage } from "./portal-access.mjs";
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
48
|
* The five fields the UI must never write.
|
|
@@ -172,11 +174,15 @@ export function frameworkView(framework, { staff = false } = {}) {
|
|
|
172
174
|
* account — the same shape portal-service already turns into a 404 body — so a caller cannot forget to
|
|
173
175
|
* check a return value and proceed with an unresolved account.
|
|
174
176
|
*/
|
|
175
|
-
export function resolveAccount(principal, requested) {
|
|
176
|
-
// assertPrincipal is the existing chokepoint: it
|
|
177
|
-
// foreign
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
export function resolveAccount(principal, requested, gates = {}) {
|
|
178
|
+
// assertPrincipal is the existing chokepoint: it holds a person to their own access and 404s a
|
|
179
|
+
// foreign account. Reusing it means tenancy has ONE implementation, not two that must agree. `gates`
|
|
180
|
+
// carries the permission a write needs — `manage` for settings, `run` for saved searches.
|
|
181
|
+
const account = assertPrincipal(principal, { account: requested ?? null, ...gates });
|
|
182
|
+
if (!account) throw new PortalDeny(400, "name an account (?account=)");
|
|
183
|
+
// Generic's settings are one file for the whole install, whichever organisation's Generic is open, so
|
|
184
|
+
// changing them is a change at the top of the tree.
|
|
185
|
+
if (account === "generic" && gates.manage && !seesEverything(principal)) throw new PortalDeny(404, "not found");
|
|
180
186
|
return account;
|
|
181
187
|
}
|
|
182
188
|
|
|
@@ -193,7 +199,7 @@ export function resolveAccount(principal, requested) {
|
|
|
193
199
|
* there, and this is the channel that respects it. What must never happen is the reverse: a body field
|
|
194
200
|
* that looks like an author and is quietly trusted by some future reader.
|
|
195
201
|
*/
|
|
196
|
-
export function makeUpstream({ callUpstream, callRecipes = null, roster = async () => [] }) {
|
|
202
|
+
export function makeUpstream({ callUpstream, callRecipes = null, recipesOff = null, roster = async () => [], fileCompany = null }) {
|
|
197
203
|
const call = async (method, path, body, identity) => {
|
|
198
204
|
const r = await callUpstream(method, path, body, identity);
|
|
199
205
|
// Upstream 404s (unknown profile) and ours (not yours) are deliberately the same answer.
|
|
@@ -204,7 +210,20 @@ export function makeUpstream({ callUpstream, callRecipes = null, roster = async
|
|
|
204
210
|
// owns a different store and a different validator. Absent ⇒ every saved-search route answers 404,
|
|
205
211
|
// which is what a deployment with no recipe store configured should say: the feature is not here.
|
|
206
212
|
const callRec = async (method, path, body, identity) => {
|
|
207
|
-
|
|
213
|
+
// A CONFIGURED-OFF FEATURE IS NOT A MISSING PAGE. Both answer 404 — the route genuinely is not
|
|
214
|
+
// there — but a bare `not_found` is what let the screen say "try again shortly" about a permanent
|
|
215
|
+
// state the portal had already diagnosed at boot. The code names it and the detail carries the
|
|
216
|
+
// diagnosis, so the surface can say what must change instead of advising a retry that cannot work.
|
|
217
|
+
//
|
|
218
|
+
// THE DETAIL IS FOR A PERSON WITH ACCESS TO EVERYTHING, as a company's file paths are. It names
|
|
219
|
+
// environment variables and filesystem paths on the server, and this screen is company-scoped — a
|
|
220
|
+
// person holding one company can reach it. The CODE travels to everyone, because "this installation
|
|
221
|
+
// does not have saved searches" is true and useful for them and gives away nothing; only the
|
|
222
|
+
// sentence saying which variable to change is held back.
|
|
223
|
+
if (!callRecipes) {
|
|
224
|
+
return { status: 404, json: { error: recipesOff?.code ?? "not_found",
|
|
225
|
+
...(seesEverything(identity) && recipesOff?.detail ? { detail: recipesOff.detail } : {}) } };
|
|
226
|
+
}
|
|
208
227
|
const r = await callRecipes(method, path, body, identity);
|
|
209
228
|
if (r.status === 404) return { status: 404, json: { error: "not_found" } };
|
|
210
229
|
return r;
|
|
@@ -223,7 +242,7 @@ export function makeUpstream({ callUpstream, callRecipes = null, roster = async
|
|
|
223
242
|
const r = await call("GET", `/profiles/${encodeURIComponent(account)}`, undefined, principal);
|
|
224
243
|
if (r.status !== 200) return r;
|
|
225
244
|
const p = r.json?.profile ?? r.json ?? {};
|
|
226
|
-
const staff = principal
|
|
245
|
+
const staff = seesEverything(principal);
|
|
227
246
|
return { status: 200, json: {
|
|
228
247
|
account,
|
|
229
248
|
profile: serializeProfile(p),
|
|
@@ -242,7 +261,8 @@ export function makeUpstream({ callUpstream, callRecipes = null, roster = async
|
|
|
242
261
|
*/
|
|
243
262
|
async writeProfile(principal, requested, action, body) {
|
|
244
263
|
if (action !== "validate" && action !== "save") return { status: 404, json: { error: "not_found" } };
|
|
245
|
-
|
|
264
|
+
// A company's settings are Manage's to change.
|
|
265
|
+
const account = resolveAccount(principal, requested, { manage: true });
|
|
246
266
|
const profile = stripCodeOwned(body?.profile);
|
|
247
267
|
if (!profile || typeof profile !== "object" || Array.isArray(profile))
|
|
248
268
|
return { status: 400, json: { error: "a profile object is required" } };
|
|
@@ -284,7 +304,8 @@ export function makeUpstream({ callUpstream, callRecipes = null, roster = async
|
|
|
284
304
|
|
|
285
305
|
async writeProject(principal, requested, project, action, body) {
|
|
286
306
|
if (action !== "validate" && action !== "save") return { status: 404, json: { error: "not_found" } };
|
|
287
|
-
|
|
307
|
+
// A project sits in the tree under its company, so adding or changing one is Manage's.
|
|
308
|
+
const account = resolveAccount(principal, requested, { manage: true });
|
|
288
309
|
if (!isSlug(project)) return { status: 404, json: { error: "not_found" } };
|
|
289
310
|
// A project overlay may only carry PROJECT_KEYS. Customer-only keys are rejected upstream with a
|
|
290
311
|
// real 400, but the same stripping discipline applies one level down: identity and rating
|
|
@@ -335,7 +356,8 @@ export function makeUpstream({ callUpstream, callRecipes = null, roster = async
|
|
|
335
356
|
*/
|
|
336
357
|
async writeSearch(principal, requested, slug, action, body) {
|
|
337
358
|
if (action !== "validate" && action !== "save") return { status: 404, json: { error: "not_found" } };
|
|
338
|
-
|
|
359
|
+
// A saved search is how a clearance is ordered again, so it is Run's.
|
|
360
|
+
const account = resolveAccount(principal, requested, { run: true });
|
|
339
361
|
if (!isSlug(slug)) return { status: 404, json: { error: "not_found" } };
|
|
340
362
|
const recipe = body?.recipe;
|
|
341
363
|
if (!recipe || typeof recipe !== "object" || Array.isArray(recipe))
|
|
@@ -346,14 +368,80 @@ export function makeUpstream({ callUpstream, callRecipes = null, roster = async
|
|
|
346
368
|
}, principal);
|
|
347
369
|
},
|
|
348
370
|
|
|
349
|
-
/**
|
|
371
|
+
/** Every company on the install, for the switcher of a person who sees everything. Nobody else reaches this. */
|
|
350
372
|
async listRoster(principal) {
|
|
351
|
-
if (principal
|
|
373
|
+
if (!seesEverything(principal)) return { status: 404, json: { error: "not_found" } };
|
|
352
374
|
return { status: 200, json: { customers: await roster() } };
|
|
353
375
|
},
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Create a company. The ONE method here that does not resolve an account, because the account is
|
|
379
|
+
* what it is making.
|
|
380
|
+
*
|
|
381
|
+
* THE RULE THIS LOOKS LIKE IT BREAKS. Rule 1 at the top of this file says the account comes from
|
|
382
|
+
* the principal and never from the path or the body — and a create's key IS a body field. The rule
|
|
383
|
+
* is about REACHING an account: substituting the caller's own so nobody can name someone else's. A
|
|
384
|
+
* create names no existing account, so there is nothing to substitute and nothing to reach. What
|
|
385
|
+
* takes its place is the permission test on the line below, and the roster check further upstream,
|
|
386
|
+
* which refuses a key that already exists rather than opening it.
|
|
387
|
+
*
|
|
388
|
+
* That makes this the second account-less method, after the roster, and it is gated the same way
|
|
389
|
+
* and for the same reason. The browser's own Manage check decides whether the control is drawn; it
|
|
390
|
+
* is not a boundary and cannot be one. This line is the boundary.
|
|
391
|
+
*
|
|
392
|
+
* 404 rather than 403, per rule 2: a refusal that distinguishes "you may not" from "there is no
|
|
393
|
+
* such endpoint" tells a stranger which endpoints exist.
|
|
394
|
+
*
|
|
395
|
+
* THE FIELDS ARE NAMED, not forwarded. Two reasons, and the second is the load-bearing one. A body
|
|
396
|
+
* passed through whole hands the upstream route whatever a future caller invents. And
|
|
397
|
+
* `frameworkPath` is a real input on that route — so forwarding the body would put a framework
|
|
398
|
+
* chooser one fetch away from any browser, which the design rejected outright: no browser path sets
|
|
399
|
+
* a framework, the create resolves the house default and the receipt names it. Leaving the field
|
|
400
|
+
* out here is what makes that true by construction rather than by the screen's good manners.
|
|
401
|
+
*/
|
|
402
|
+
async createCompany(principal, body) {
|
|
403
|
+
if (!mayManage(principal)) return { status: 404, json: { error: "not_found" } };
|
|
404
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
405
|
+
return { status: 400, json: { error: "a company is required" } };
|
|
406
|
+
// WHICH ORGANISATION IT BELONGS TO. A company belongs to exactly one, and the person creating it
|
|
407
|
+
// must hold that organisation whole: Manage on one company cannot add a sibling beside it. Named by
|
|
408
|
+
// `tenant`, or implied when the person holds exactly one organisation. A person who sees everything
|
|
409
|
+
// on an install with no organisation yet creates it under none, and sees it because they see
|
|
410
|
+
// everything.
|
|
411
|
+
const orgs = principal.genericOrgs ?? [];
|
|
412
|
+
const named = typeof body.tenant === "string" && body.tenant.trim() ? body.tenant.trim() : null;
|
|
413
|
+
let org = null;
|
|
414
|
+
if (named != null) {
|
|
415
|
+
if (!orgs.includes(named)) return { status: 404, json: { error: "not_found" } };
|
|
416
|
+
org = named;
|
|
417
|
+
} else if (orgs.length === 1) org = orgs[0];
|
|
418
|
+
else if (orgs.length > 1) return { status: 400, json: { error: "name the organisation this company belongs to (tenant)" } };
|
|
419
|
+
else if (!seesEverything(principal)) return { status: 404, json: { error: "not_found" } };
|
|
420
|
+
const draft = {};
|
|
421
|
+
for (const k of CREATABLE_FIELDS) if (body[k] !== undefined) draft[k] = body[k];
|
|
422
|
+
const r = await call("POST", "/profiles", draft, principal);
|
|
423
|
+
if (r.status !== 201 || org == null || !fileCompany) return r;
|
|
424
|
+
const key = r.json?.key ?? r.json?.profile?.key ?? draft.key;
|
|
425
|
+
try { await fileCompany({ tenant: org, account: key }); }
|
|
426
|
+
catch (e) {
|
|
427
|
+
return { status: 500, json: { error: `The company was created, but it could not be filed under its organisation (${String(e?.message ?? e).slice(0, 200)}). A person with access to everything can see it and file it.`, key } };
|
|
428
|
+
}
|
|
429
|
+
return { ...r, json: { ...r.json, tenant: org } };
|
|
430
|
+
},
|
|
354
431
|
};
|
|
355
432
|
}
|
|
356
433
|
|
|
434
|
+
/**
|
|
435
|
+
* What a browser may state when creating a company.
|
|
436
|
+
*
|
|
437
|
+
* Everything else the profile carries has a default the create path resolves, or is code-owned and set
|
|
438
|
+
* there. `frameworkPath` is deliberately absent — see `createCompany`.
|
|
439
|
+
*/
|
|
440
|
+
export const CREATABLE_FIELDS = Object.freeze([
|
|
441
|
+
"name", "key", "industry", "matchDomains", "platforms",
|
|
442
|
+
"selfExclusionOwners", "defaultClasses", "defaultJurisdictions",
|
|
443
|
+
]);
|
|
444
|
+
|
|
357
445
|
/**
|
|
358
446
|
* A project key must be a plain slug.
|
|
359
447
|
*
|
|
@@ -33,6 +33,7 @@ import { findRegistryArithmeticIssues, findRegistryViolations, splitBlocks } fro
|
|
|
33
33
|
import { CLIENT_TIER_BY_COMPOSITE, joinFindingToBlock, parseBlockOrd, worstLiveBand, NO_RATED_CONFLICTS, deriveActionConditions, isUnconditionalProceed, verdictStance, joinAskToAnswer, projectAssessmentField, POSITION_REQUIRED_DISPOSITIONS, OFF_FIELD_GROUNDS, FINDINGS_SCHEMA_VERSION, netChainMarkers, STATEMENT_CLAUSE_MAX } from "./findings-model.mjs";
|
|
34
34
|
import { normalizeBand } from "./framework.mjs";
|
|
35
35
|
import { knockoutNoteView, REQUEST_NOTE_WORDS, REQUEST_SUBJECT_WORDS } from "./findings-model.mjs"; // one reader for where a note prints
|
|
36
|
+
import { isEngineAppendedCaveat } from "./verify-knockout.mjs"; // ONE derivation for "the engine appended this caveat, not a seat"
|
|
36
37
|
|
|
37
38
|
// V4-3: diacritics FOLD (NFD strip) instead of being deleted — "Televisión" must normalize to
|
|
38
39
|
// "television" (deletion made it "televisin", so a diacritic mention never matched its introduction).
|
|
@@ -700,7 +701,7 @@ export function competitorClaimChecks({ text, ownerScreen, recordsByUri, markVoc
|
|
|
700
701
|
return failures.length ? failures : [check("competitor-claim-verification", "registry", surface, true, "")];
|
|
701
702
|
}
|
|
702
703
|
|
|
703
|
-
// ── COVERAGE CLAIMS IN PROSE vs WHAT THE RUN ACTUALLY SEARCHED
|
|
704
|
+
// ── COVERAGE CLAIMS IN PROSE vs WHAT THE RUN ACTUALLY SEARCHED ──────────────────────────────────────
|
|
704
705
|
//
|
|
705
706
|
// THE DEFECT. `coverage_line:` is code-stamped from scope-facts.json; the narrative is model-written
|
|
706
707
|
// prose. Nothing bound them to one searched-territory set. On one recorded run the masthead read
|
|
@@ -2354,7 +2355,7 @@ const knockoutSurfaces = (findings) => {
|
|
|
2354
2355
|
return { report: report.filter(Boolean).join("\n\n"), working: working.filter(Boolean).join("\n\n") };
|
|
2355
2356
|
};
|
|
2356
2357
|
|
|
2357
|
-
// ── PLAIN LANGUAGE ON WHAT A READER SEES FIRST
|
|
2358
|
+
// ── PLAIN LANGUAGE ON WHAT A READER SEES FIRST ─────────────────────────────────────────────────────
|
|
2358
2359
|
//
|
|
2359
2360
|
// The report goes to a lawyer who layers advice on top, and that lawyer's client reads the same page.
|
|
2360
2361
|
// The band, the summary, the basis line and the one-liners are the whole product for the second reader,
|
|
@@ -2362,7 +2363,7 @@ const knockoutSurfaces = (findings) => {
|
|
|
2362
2363
|
// comparison", "subsisting European rights", "the confusion comparison meets on every limb".
|
|
2363
2364
|
//
|
|
2364
2365
|
// THE PARTITION IS DEFAULT-VISIBLE vs FOLDED, WHICH IS NOT THE report/working SPLIT ABOVE. Since the
|
|
2365
|
-
// page folds each card's argument
|
|
2366
|
+
// page folds each card's argument, a finding's `basis` is now behind a click,
|
|
2366
2367
|
// and 333 rule 2 allows the lawyer's vocabulary there where a plain word would lose precision. The same
|
|
2367
2368
|
// is true of the long `assessment`. So this check reads exactly the fields the renderer DRAWS without
|
|
2368
2369
|
// a click, and reads nothing else — a check whose population is "the report" would flag the very
|
|
@@ -2399,22 +2400,39 @@ const knockoutSurfaces = (findings) => {
|
|
|
2399
2400
|
/**
|
|
2400
2401
|
* The fields a reader of the knockout meets before opening anything, named one by one rather than
|
|
2401
2402
|
* derived, so adding a field to the page is a deliberate addition here too.
|
|
2403
|
+
*
|
|
2404
|
+
* EVERY ENTRY CARRIES A TYPED ADDRESS as well as its human label, and the reviewing pass binds its
|
|
2405
|
+
* rewrites to that address rather than to the label. The label is prose for a person; two marks whose
|
|
2406
|
+
* names differ only in case produce two different labels for one field, and a rewrite matched on prose
|
|
2407
|
+
* would land on whichever the seat quoted. `at` is {field, mark?, index?, ordinal?} and is what the
|
|
2408
|
+
* merge joins on — the knockout has no correction cycle to inherit a join key from, so this lane
|
|
2409
|
+
* defines its own rather than resolving prose after the fact.
|
|
2410
|
+
*
|
|
2411
|
+
* `at.engineOwned` marks the sentences the ENGINE appends, not a seat: the survivor-boundary note and
|
|
2412
|
+
* the skipped-capability note. They stay in the walk because the internal lint should still read
|
|
2413
|
+
* them — an over-long engine caveat is a real defect, for whoever edits the code that emits it. They
|
|
2414
|
+
* are not offered to the reviewing pass: a rewrite there would either break `SURVIVOR_BOUNDARY_RE` at
|
|
2415
|
+
* delivery or append a second copy of the note, and both are silent. `isEngineAppendedCaveat` is the
|
|
2416
|
+
* derivation that already tells them apart; this does not write a second one.
|
|
2402
2417
|
*/
|
|
2403
|
-
function knockoutVisibleProse(findings) {
|
|
2418
|
+
export function knockoutVisibleProse(findings) {
|
|
2404
2419
|
const out = [];
|
|
2405
|
-
const add = (where, v) => { const t = String(v ?? "").trim(); if (t) out.push({ where, text: t }); };
|
|
2406
|
-
add("the batch summary", findings?.batch?.executiveSummary);
|
|
2407
|
-
|
|
2420
|
+
const add = (where, v, at) => { const t = String(v ?? "").trim(); if (t) out.push({ where, text: t, at }); };
|
|
2421
|
+
add("the batch summary", findings?.batch?.executiveSummary, { field: "batch.executiveSummary" });
|
|
2422
|
+
(findings?.batch?.standardCaveats ?? []).forEach((c, i) =>
|
|
2423
|
+
add("a standing caveat", c, { field: "batch.standardCaveats", index: i, engineOwned: isEngineAppendedCaveat(c) }));
|
|
2408
2424
|
for (const m of findings?.marks ?? []) {
|
|
2409
2425
|
const n = String(m?.name ?? "a mark");
|
|
2410
|
-
add(`${n}'s basis line`, m?.basis);
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
add(`${n}'s mitigation line`, m?.mitigation);
|
|
2414
|
-
|
|
2415
|
-
|
|
2426
|
+
add(`${n}'s basis line`, m?.basis, { field: "basis", mark: n });
|
|
2427
|
+
(m?.factors ?? []).forEach((f, i) => add(`${n}'s "why this band" list`, f, { field: "factors", mark: n, index: i }));
|
|
2428
|
+
(m?.counterFactors ?? []).forEach((f, i) => add(`${n}'s "why not the next band" list`, f, { field: "counterFactors", mark: n, index: i }));
|
|
2429
|
+
add(`${n}'s mitigation line`, m?.mitigation, { field: "mitigation", mark: n });
|
|
2430
|
+
(m?.purpleNotes ?? []).forEach((p, i) => add(`${n}'s note to the reviewing lawyer`, p?.text ?? p, { field: "purpleNotes", mark: n, index: i }));
|
|
2431
|
+
(m?.registerReads ?? []).forEach((r, i) => add(`${n}'s read of a filing`, r?.read, { field: "registerReads", mark: n, index: i }));
|
|
2416
2432
|
// The finding's ONE sentence. Its `basis` is folded and is deliberately not read here.
|
|
2417
|
-
|
|
2433
|
+
// BOUND BY ORDINAL, NOT BY POSITION: the merged gate re-ranks and renumbers findings on the band,
|
|
2434
|
+
// so an index into this array is a different row after normalisation and the ordinal is not.
|
|
2435
|
+
for (const f of m?.findings ?? []) add(`${n} conflict ${f?.ordinal ?? ""}`.trim(), f?.net, { field: "findings.net", mark: n, ordinal: f?.ordinal });
|
|
2418
2436
|
}
|
|
2419
2437
|
return out;
|
|
2420
2438
|
}
|
|
@@ -2453,7 +2471,14 @@ export function plainLanguageChecks({ findings } = {}) {
|
|
|
2453
2471
|
const vocab = [];
|
|
2454
2472
|
const longSentences = [];
|
|
2455
2473
|
for (const { where, text } of fields) {
|
|
2456
|
-
|
|
2474
|
+
// THE REPLACEMENT TRAVELS WITH THE FLAG. Naming the term and pointing at a document for the swap is
|
|
2475
|
+
// the half the doctrine calls load-bearing, left out — and it is the half a pass that REWRITES the
|
|
2476
|
+
// line needs in hand. The pinned source already carries a worked plain form for every term; there
|
|
2477
|
+
// was no reason for this flag to name only the fault.
|
|
2478
|
+
for (const [term, plain] of PLAIN_FORMS) {
|
|
2479
|
+
if (!termMatcher(term).test(text)) continue;
|
|
2480
|
+
vocab.push(plain ? `${where}: "${term}" → "${plain}"` : `${where}: "${term}" (an engine word — cut it)`);
|
|
2481
|
+
}
|
|
2457
2482
|
for (const sentence of text.split(/(?<=[.!?])\s+|\n+/)) {
|
|
2458
2483
|
const n = sentence.trim().split(/\s+/).filter(Boolean).length;
|
|
2459
2484
|
if (n > SENTENCE_WORD_LIMIT) longSentences.push(`${where}: ${n} words`);
|
|
@@ -2487,7 +2512,7 @@ export function plainLanguageChecks({ findings } = {}) {
|
|
|
2487
2512
|
check("reviewer-note-subject", "voice", surface, misfiled.length === 0,
|
|
2488
2513
|
misfiled.length ? `a note about what was asked that never names the request — it will print under this name's conflicts rather than at the top of the page, where a question about the request belongs. Name the request in the note: ${say(misfiled)}` : ""),
|
|
2489
2514
|
check("plain-language-vocabulary", "voice", surface, vocab.length === 0,
|
|
2490
|
-
vocab.length ? `the lawyer's vocabulary on lines a reader meets before opening anything — rewrite the
|
|
2515
|
+
vocab.length ? `the lawyer's vocabulary on lines a reader meets before opening anything — rewrite the sentence in the words the reader already owns, do not swap the word: ${say(vocab)}` : ""),
|
|
2491
2516
|
check("plain-language-sentence-length", "voice", surface, longSentences.length === 0,
|
|
2492
2517
|
longSentences.length ? `a default-visible sentence carrying more than one idea (over ${SENTENCE_WORD_LIMIT} words) — split it, conclusion first: ${say(longSentences)}` : ""),
|
|
2493
2518
|
];
|
|
@@ -2569,7 +2594,7 @@ export function runKnockoutLint({ findings }) {
|
|
|
2569
2594
|
// (publish/report-registry.mjs re-renders archived findings without re-running the merged validator)
|
|
2570
2595
|
// this scan is the only permission-prose coverage there is.
|
|
2571
2596
|
if (working.trim()) checks.push(...permissionProseChecks({ text: working, surface: "findings", idSuffix: ":knockout-working", structural: true, cards: false }));
|
|
2572
|
-
//
|
|
2597
|
+
// the plain-language reviewer, over the fields a reader meets before opening a
|
|
2573
2598
|
// fold. Internal by surface, so a hit reaches whoever is fixing the run and never a delivery surface.
|
|
2574
2599
|
checks.push(...plainLanguageChecks({ findings }));
|
|
2575
2600
|
const failures = checks.filter((c) => !c.pass);
|
|
@@ -2714,7 +2739,7 @@ export function deliveryFlagLines(failures) {
|
|
|
2714
2739
|
const id = String(f?.id ?? "");
|
|
2715
2740
|
const base = id.split(":")[0];
|
|
2716
2741
|
const family = String(f?.family ?? "");
|
|
2717
|
-
// A CHECK WHOSE DISTINCTION LIVES IN ITS SUFFIX COULD NOT BE SAID HERE
|
|
2742
|
+
// A CHECK WHOSE DISTINCTION LIVES IN ITS SUFFIX COULD NOT BE SAID HERE.
|
|
2718
2743
|
//
|
|
2719
2744
|
// Grouping on `base` alone is right for the common case — a word-cap violation on nine write-ups is
|
|
2720
2745
|
// one delivery line, not nine. But it also collapsed `narrative-write-ups:could-not-read` into the
|
package/driver/product-rows.mjs
CHANGED
|
@@ -54,7 +54,7 @@ import { leversFromResolved, turnaround, turnaroundHours } from "./effort-model.
|
|
|
54
54
|
* multi-country-focus-search 2.5h 1.5–2.5 hours
|
|
55
55
|
* full-country-search 2.5h 1.5–2.5 hours
|
|
56
56
|
*
|
|
57
|
-
* ALL THREE CLEARANCES ARE IDENTICAL HERE and that is the
|
|
57
|
+
* ALL THREE CLEARANCES ARE IDENTICAL HERE and that is the ruling, not a flattening to fix.
|
|
58
58
|
* The table above used to separate them with lane adders (+0.5 case law, +0.5 native, +0.5 single
|
|
59
59
|
* territory). Eight delivered runs refuted that: the full-country run — the only one carrying every lane,
|
|
60
60
|
* quoted 3.0h — came in at 2.33h, SHORTER than five of the other seven, which carried fewer lanes and were
|
package/driver/products.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
3
|
// products.mjs — THE OFFERING. Which of the four products a request is, and what that product carries.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Ruling, 2026-08-06. There are four products and a client buys one of them:
|
|
6
6
|
//
|
|
7
7
|
// Knockout search worldwide or a chosen set; up to 8 names; no case law; no native language
|
|
8
8
|
// Global preliminary search WORLDWIDE, nothing else; no case law; no native language
|
package/driver/profile-page.html
CHANGED
|
@@ -261,7 +261,7 @@ function collect() {
|
|
|
261
261
|
// (set outside this page) must survive a save round-trip instead of being silently rewritten
|
|
262
262
|
const delivery = { ...((current.profile && current.profile.delivery) || {}), email: $("f_email").value };
|
|
263
263
|
// THE MARKING IS THREE-STATE ON THE WIRE and this control has two live options, so the default one
|
|
264
|
-
// writes ABSENCE and never `false
|
|
264
|
+
// writes ABSENCE and never `false`. It was a checkbox, which rendered absent and
|
|
265
265
|
// `false` identically and wrote a boolean on every save — so a customer who had given no instruction
|
|
266
266
|
// was recorded as "strip the line" the first time anybody pressed Save on an unrelated field, and
|
|
267
267
|
// their next report shipped with no confidentiality line. That is the incident this door has already had.
|
|
@@ -379,18 +379,43 @@ async function submit(write) {
|
|
|
379
379
|
}
|
|
380
380
|
showMsg("", write ? "Saving…" : "Checking…");
|
|
381
381
|
const action = write ? "save" : "validate";
|
|
382
|
-
|
|
382
|
+
// CREATING IS ITS OWN ROUTE, and this is the whole of the defect it fixes. `save` is an upsert: its
|
|
383
|
+
// preserve step takes the on-disk value of every code-owned field and DELETES the field when there is
|
|
384
|
+
// nothing on disk. A customer being created has nothing on disk, so creating through `save` wrote no
|
|
385
|
+
// risk framework at all, silently, and every matter for that customer was rated under the general
|
|
386
|
+
// default from then on with nothing on any screen saying so.
|
|
387
|
+
//
|
|
388
|
+
// The create route asks the same questions the command line asks, always sets the framework, and
|
|
389
|
+
// refuses a colliding email domain before writing rather than leaving the next start unable to
|
|
390
|
+
// resolve ANY customer. Its body is FLAT; `save` takes a nested profile.
|
|
391
|
+
const r = (write && current.isNew)
|
|
392
|
+
? await jpost(API.replace(/\/$/, ""), {
|
|
393
|
+
name: profile.name, key,
|
|
394
|
+
industry: profile.industry,
|
|
395
|
+
matchDomains: profile.matchDomains,
|
|
396
|
+
platforms: profile.platforms,
|
|
397
|
+
})
|
|
398
|
+
: await jpost(`${API}${encodeURIComponent(key)}/${action}`, { profile, contextPack });
|
|
383
399
|
if (!write) {
|
|
384
400
|
if (r.json.ok) showMsg("ok", r.json.isNew ? "Looks good — ready to create this customer." : "Looks good — ready to save.");
|
|
385
401
|
else showServerErrors("Almost there — please fix:", r.json.errors);
|
|
386
402
|
return;
|
|
387
403
|
}
|
|
388
|
-
if (r.status === 200 && r.json.written) {
|
|
389
|
-
|
|
404
|
+
if ((r.status === 200 || r.status === 201) && r.json.written) {
|
|
405
|
+
// 201 from the create route, 200 from a save. Testing only for 200 would report a successful
|
|
406
|
+
// creation as a failure, which is the one answer that sends somebody to make the customer twice.
|
|
407
|
+
const fw = r.json.framework
|
|
408
|
+
? ` · ${r.json.framework.defaulted ? "general default rating" : "its own rating framework"}`
|
|
409
|
+
: "";
|
|
410
|
+
showMsg("ok", `Saved — ${r.json.created?"created":"updated"} ${key}${fw} · commit ${String(r.json.commit||"").slice(0,9)}`);
|
|
390
411
|
await loadRoster(key);
|
|
391
412
|
if (current.isNew) openCustomer(key);
|
|
392
413
|
} else if (r.json.error === "validation_failed") {
|
|
393
414
|
showServerErrors("Couldn’t save — please fix:", r.json.errors);
|
|
415
|
+
} else if (r.json.message) {
|
|
416
|
+
// The create route names its refusals in a sentence and puts a machine token beside it. Showing
|
|
417
|
+
// the token is showing engine vocabulary to a lawyer.
|
|
418
|
+
showMsg("err", r.json.message);
|
|
394
419
|
} else {
|
|
395
420
|
showMsg("err", `Couldn’t save (${r.status}). ${r.json.error||"Please try again."}`);
|
|
396
421
|
}
|
|
@@ -492,7 +517,7 @@ function collectProject() {
|
|
|
492
517
|
// delivery is merged as a WHOLE field (project → customer): overriding it here intentionally replaces the
|
|
493
518
|
// customer's delivery, so a project's delivery carries only email + privileged (style/template are the
|
|
494
519
|
// customer's presentation defaults; a project that needs its own sets them via a code change, like the framework).
|
|
495
|
-
// Same three-state rule as the customer form
|
|
520
|
+
// Same three-state rule as the customer form: the default option writes NO key, so
|
|
496
521
|
// an overlay never invents "strip the marking" for a project that gave no instruction.
|
|
497
522
|
const email = $("p_email").value; if (email) { o.delivery = { email }; if ($("p_priv").value === "no") o.delivery.privileged = false; }
|
|
498
523
|
const app = ($("p_appetite").value||"").trim(); if (app) o.riskAppetite = app;
|