NEO.K / MSSP FIELD LAB014-declared-arity
編號014-declared-arity
語言javascript
版本v1.0
日期2026-08-14
行數354
執行node src/main.mjs

014 — When a field can hold more than one value, "read the value" is not a question

candidate. The last assumption this lab had never touched: a request arriving from outside it.

What this program does

It reads one query string four fields at a time, with three readers, and only one of them can come back empty-handed.

node src/main.mjs             # what SCL runs, and what the other readers said
node src/main.mjs --strict    # exit 1 when the configured reader refuses
node src/island_test.mjs      # 23 checks across 6 sections
  q=mssp&page=2&page=3&tag=structure&tag=evidence&sort=date

  field   declared       got   declared-arity                first-wins        last-wins
  q       optional-one   1     "mssp"                        "mssp"            "mssp"
  page    one            2     REFUSED (declared one, got 2) "2"               "3"
  tag     many           2     ["structure","evidence"]      "structure"       "evidence"
  sort    one            1     "date"                        "date"            "date"

The structural decision

Arity is a term of the contract, checked at the read — not something decided by whichever accessor the caller happened to reach for.

page arrived twice. first-wins says "2", last-wins says "3", and neither says a choice was made. They are not two implementations of one rule; they are two rules with the same call shape. Only a reader that knows page was declared one can tell a repeated checkbox from a repeated mistake.

Why now

Six days from the switch to real market applications. Persistence was 011 and 012; this is the other thing every entry so far has assumed away — input arriving from outside the program, where the sender is not the author and repetition is a normal thing for a browser to do.

The island test

Section 2 is what the example exists for:

  PASS  the request carries `page` twice
  PASS  first-wins and last-wins disagree about which one is the value - "2" vs "3"
  PASS  and neither of them reports that it chose
  PASS  they agree on every field that carries exactly one value

The last line matters: the disagreement is about multiplicity, not about parsing. Both readers see the same three values; they differ only in which one they call "the" one.

Section 4 covers absence, which is a different question from multiplicity: optional-one absent is null, many absent is [], and one absent is refused — three different right answers that a single "return null if missing" would have collapsed.

Section 3b is the drill: a reader declaring REFUSES = true that never refuses must be caught, or section 3 is a label agreeing with a label.

live from upstream

Archaeology 014, measured the same day, is where this decision is actually made in production JavaScript:

read keeps
params.get("tag") the first
Object.fromEntries(params).tag the last
params.getAll("tag") all of them

first-wins and last-wins in TMS/readers/ are those two, and they are in the example because they are what the platform hands you, not because I invented a bad option to knock down.

What this example does not solve

Measurable, not measured. How often a repeated key in a real request is accident rather than design, and what refusing costs a caller who was relying on first-wins.

Not measurable here. Whether refusing is the right policy — coercing, clamping and taking the last are all defensible, and choosing one without saying so is the only thing this example takes a position against. And whether a declared arity is correct: someone wrote page: one, and nothing here can tell a wrong declaration from a wrong request.

Source

FMS

FMS/contract.json
{
  "name": "014-declared-arity",
  "what_it_is": "A query-parameter layer where every field declares how many values it may hold, and a read that disagrees with the declaration refuses instead of picking one.",
  "the_structural_decision": "When a field can hold more than one value, \"read the value\" is not a well-formed operation. Arity is a term of the contract — declared, and checked at the read — not something decided by whichever accessor the caller happened to reach for.",
  "why_this_one": "Six days from the switch to real market applications, and the last assumption this lab has never touched is a request coming from outside it. Archaeology 014 measures what the standard library does here: URLSearchParams.get returns the FIRST value, Object.fromEntries(params) keeps the LAST, and neither mentions that anything was dropped.",
  "status": "candidate",

  "fields": {
    "q":     {"arity": "optional-one", "note": "a search box; absent is normal"},
    "page":  {"arity": "one",          "note": "exactly one, and two is a bug not a preference"},
    "tag":   {"arity": "many",         "note": "checkboxes; three is the point"},
    "sort":  {"arity": "one",          "note": "one ordering, or the result is undefined"}
  },

  "readers": {
    "first-wins":     {"refuses": false, "models": "URLSearchParams.get"},
    "last-wins":      {"refuses": false, "models": "Object.fromEntries(params)"},
    "declared-arity": {"refuses": true,  "models": "nothing upstream — this is the example's addition"}
  },

  "the_finding": "first-wins and last-wins disagree about WHICH value is the value, and neither reports a choice was made. They are not two implementations of one rule; they are two different rules wearing the same call shape. Only a reader that knows what the field was declared to hold can tell a legitimate multi-value from an accident.",

  "sets": {
    "FMS": "this file: the fields and their declared arity, the readers and whether each can refuse, and the units map",
    "SCL": "which reader this deployment uses, and whether a refusal is fatal",
    "SMS": "reader resolution by id, and the read path over a parsed request",
    "TMS": "one file per reader — each declares what it does on multiplicity and whether it can come back empty-handed, and reaches no sibling set",
    "DMS": "what each reader returned for each field, and which of them could not have refused"
  },

  "units": {"TMS/readers": ["declared_arity.mjs", "first_wins.mjs", "last_wins.mjs"]},

  "non_goals": [
    "Being a router or a validation library. Four fields and three readers over one query string.",
    "Saying URLSearchParams is wrong. get() answers the question it is named for; the defect is that callers read it as answering a different one.",
    "Deciding what a system SHOULD do when a request violates the contract. This refuses; whether to refuse, coerce or clamp is a policy question and SCL is where it would live."
  ]
}

SCL

SCL/policy.json
{
  "reader": "declared-arity",
  "a_refusal_is_fatal": true,
  "_note": "island_test.mjs runs every reader over every field regardless of what this says."
}
SCL/policy.mjs
// Which reader this deployment uses, and what it refuses to serve.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const here = path.dirname(fileURLToPath(import.meta.url));
const config = JSON.parse(fs.readFileSync(path.join(here, "policy.json"), "utf8"));

export const reader = () => config.reader;
export const refusalIsFatal = () => Boolean(config.a_refusal_is_fatal);

SMS

SMS/request.mjs
// Reader resolution by id, and the read path over a parsed request.
//
// The parse itself is deliberately the platform's: URLSearchParams already
// keeps every value, and the loss this example is about happens later, at the
// read. Re-implementing the parser would have hidden that.
import * as declaredArity from "../TMS/readers/declared_arity.mjs";
import * as firstWins from "../TMS/readers/first_wins.mjs";
import * as lastWins from "../TMS/readers/last_wins.mjs";

const READERS = Object.fromEntries(
  [firstWins, lastWins, declaredArity].map((module) => [module.READER, module]));

export const readerNames = () => Object.keys(READERS).sort();

export function resolveReader(name) {
  const module = READERS[name];
  if (!module) {
    return { problem: `reader "${name}" has no implementation - fail closed (known: ${readerNames().join(", ")})` };
  }
  return { module };
}

export function parse(queryString) {
  const params = new URLSearchParams(queryString);
  return Object.fromEntries([...new Set(params.keys())].map((key) => [key, params.getAll(key)]));
}

export function readAll(queryString, readerName, fields) {
  const { module, problem } = resolveReader(readerName);
  if (problem) return { problem };
  const parsed = parse(queryString);
  const rows = Object.entries(fields).map(([name, spec]) => {
    const values = parsed[name] ?? [];
    const outcome = module.read(values, spec.arity);
    return { field: name, arity: spec.arity, received: values.length, ...outcome };
  });
  return { reader: readerName, rows, refusals: rows.filter((row) => row.refused) };
}

TMS

TMS/readers/declared_arity.mjs
// Read against what the field was declared to hold.
//
// The only reader here that can come back empty-handed. A field declared `one`
// holding three values is not a value to be picked from — it is a disagreement
// between the request and the contract, and picking silently is what makes it
// invisible.
export const READER = "declared-arity";
export const ON_MULTIPLICITY = "refuses when the count disagrees with the declaration";
export const REFUSES = true;

export function read(values, arity) {
  if (arity === "many") return { value: values, refused: false };
  if (arity === "optional-one" && values.length === 0) return { value: null, refused: false };
  if (values.length === 1) return { value: values[0], refused: false };
  return {
    value: null,
    refused: true,
    because: `declared ${arity}, received ${values.length}`,
  };
}
TMS/readers/first_wins.mjs
// Return the first value and say nothing about the rest.
//
// This is URLSearchParams.get, and it is the most reached-for accessor in every
// web request path there is. It is not wrong; it answers a question. The
// question it answers is "what is the first value", and callers read it as
// "what is the value".
export const READER = "first-wins";
export const ON_MULTIPLICITY = "returns the first, discards the rest, reports nothing";
export const REFUSES = false;

export function read(values) {
  return { value: values.length ? values[0] : null, refused: false };
}
TMS/readers/last_wins.mjs
// Return the last value and say nothing about the rest.
//
// This is what Object.fromEntries(params) does, and it is the other most
// reached-for shape. It disagrees with first-wins about WHICH value is "the"
// value, and neither of them mentions that there was a choice.
export const READER = "last-wins";
export const ON_MULTIPLICITY = "returns the last, discards the rest, reports nothing";
export const REFUSES = false;

export function read(values) {
  return { value: values.length ? values[values.length - 1] : null, refused: false };
}

DMS

DMS/report.mjs
// What each reader returned, and which of them could not have refused.

export function table(results, fields, out) {
  const readers = results.map((r) => r.reader);
  out(`\n  ${"field".padEnd(7)} ${"declared".padEnd(14)} ${"got".padEnd(5)} `
    + readers.map((r) => r.padEnd(30)).join(""));
  for (const [index, name] of Object.keys(fields).entries()) {
    const first = results[0].rows[index];
    const cells = results.map((result) => {
      const row = result.rows[index];
      return (row.refused ? `REFUSED (${row.because})` : JSON.stringify(row.value)).padEnd(30);
    });
    out(`  ${name.padEnd(7)} ${first.arity.padEnd(14)} ${String(first.received).padEnd(5)} ${cells.join("")}`);
  }
}

export function whoCouldRefuse(results, modules, out) {
  out("\n  which readers can come back empty-handed at all:");
  for (const result of results) {
    const module = modules[result.reader];
    out(`    ${result.reader.padEnd(16)} refuses=${String(module.REFUSES).padEnd(6)} ${module.ON_MULTIPLICITY}`);
  }
}

export function gaps(out) {
  out("\n  measurable, not measured here:");
  out("    - how often real requests carry a repeated key by accident rather than design");
  out("    - what refusing costs a caller who was relying on first-wins");
  out("\n  not measurable by this program at all:");
  out("    - whether refusing is the right policy. Coercing, clamping and taking the");
  out("      last are all defensible; what is not defensible is choosing one without");
  out("      saying so, which is the only thing this example takes a position on.");
  out("    - whether a declared arity is correct. Someone wrote `page: one`, and");
  out("      nothing here can tell a wrong declaration from a wrong request.");
}

root

island_test.mjs
// The island test.
//
//   node src/island_test.mjs
//
// Section 2 is what the example exists for: the same request, read by two
// idiomatic readers, produces two different answers for the same field, and
// neither reader reports that it chose.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import * as request from "./SMS/request.mjs";
import * as policy from "./SCL/policy.mjs";
import { QUERY } from "./main.mjs";

const here = path.dirname(fileURLToPath(import.meta.url));
const CONTRACT = JSON.parse(fs.readFileSync(path.join(here, "FMS", "contract.json"), "utf8"));
const failures = [];
const check = (label, ok, detail = "") => {
  process.stdout.write(`  ${ok ? "PASS" : "FAIL"}  ${label}${detail ? ` - ${detail}` : ""}\n`);
  if (!ok) failures.push(label);
};
const say = (line = "") => process.stdout.write(`${line}\n`);
const readWith = (name) => request.readAll(QUERY, name, CONTRACT.fields);
const cell = (result, field) => result.rows.find((row) => row.field === field);

say("\n== 1. every reader is an island, declares itself, and FMS matches the tree");
const dir = path.join(here, "TMS", "readers");
const files = fs.readdirSync(dir).filter((n) => n.endsWith(".mjs")).sort();
check("there are three reader files", files.length === 3, files.join(", "));
for (const file of files) {
  const source = fs.readFileSync(path.join(dir, file), "utf8");
  const reaches = [...source.matchAll(/^\s*import[^"']*["']([^"']+)["']/gm)].map((m) => m[1]);
  check(`${file} reaches no sibling set`, reaches.length === 0,
    reaches.join(", ") || "no imports at all");
}
for (const [where, expected] of Object.entries(CONTRACT.units)) {
  const onDisk = fs.readdirSync(path.join(here, ...where.split("/")))
    .filter((n) => n.endsWith(".mjs")).sort();
  check(`${where}: FMS declares ${expected.length}, on disk ${onDisk.length}`,
    JSON.stringify(onDisk) === JSON.stringify([...expected].sort()), onDisk.join(", "));
}
for (const name of request.readerNames()) {
  const { module } = request.resolveReader(name);
  check(`${name} declares what it does on multiplicity`, Boolean(module.ON_MULTIPLICITY),
    module.ON_MULTIPLICITY);
}

say("\n== 2. two idiomatic readers, one field, two different answers");
const first = readWith("first-wins");
const last = readWith("last-wins");
check("the request carries `page` twice", cell(first, "page").received === 2);
check("first-wins and last-wins disagree about which one is the value",
  cell(first, "page").value !== cell(last, "page").value,
  `${JSON.stringify(cell(first, "page").value)} vs ${JSON.stringify(cell(last, "page").value)}`);
check("and neither of them reports that it chose",
  !cell(first, "page").refused && !cell(last, "page").refused,
  "both return a value and no complaint");
check("they agree on every field that carries exactly one value",
  ["q", "sort"].every((f) => cell(first, f).value === cell(last, f).value),
  "so the disagreement is about multiplicity, not about parsing");
say("        These are not two implementations of one rule. They are two rules");
say("        with the same call shape, and archaeology 014 measures both of them");
say("        in the standard library: get() keeps the first, Object.fromEntries");
say("        keeps the last.");

say("\n== 3. only a reader that knows the declaration can refuse");
const declared = readWith("declared-arity");
check("declared-arity refuses `page`", cell(declared, "page").refused,
  cell(declared, "page").because);
check("and does NOT refuse `tag`, which was declared many",
  !cell(declared, "tag").refused && Array.isArray(cell(declared, "tag").value),
  JSON.stringify(cell(declared, "tag").value));
check("nor `q`, which was declared optional and arrived once",
  !cell(declared, "q").refused, JSON.stringify(cell(declared, "q").value));
check("the other two readers refuse nothing, ever",
  first.refusals.length === 0 && last.refusals.length === 0);

say("\n== 3b. the drill: a reader that claims it can refuse and never does");
const liar = { READER: "claims-to-refuse", REFUSES: true,
  ON_MULTIPLICITY: "says it refuses", read: (values) => ({ value: values[0], refused: false }) };
const measured = ["q", "page", "tag", "sort"].map((f) =>
  liar.read(request.parse(QUERY)[f] ?? [], CONTRACT.fields[f].arity).refused);
check("a reader declaring REFUSES=true that never refuses is caught",
  liar.REFUSES === true && measured.every((r) => r === false),
  "declared it can refuse, refused nothing across four fields");

say("\n== 4. an empty field, and a field nobody sent");
const sparse = request.readAll("page=1", "declared-arity", CONTRACT.fields);
check("`q` declared optional-one and absent is accepted as null",
  !cell(sparse, "q").refused && cell(sparse, "q").value === null);
check("`sort` declared one and absent is refused",
  cell(sparse, "sort").refused, cell(sparse, "sort").because);
check("`tag` declared many and absent is an empty list, not a refusal",
  !cell(sparse, "tag").refused && JSON.stringify(cell(sparse, "tag").value) === "[]");

say("\n== 5. fail closed");
check("an unresolvable reader stops the run",
  Boolean(request.readAll(QUERY, "whatever-you-like", CONTRACT.fields).problem),
  request.readAll(QUERY, "whatever-you-like", CONTRACT.fields).problem);
check("SCL names a reader that exists", request.readerNames().includes(policy.reader()),
  policy.reader());
check("and it names one that can refuse",
  request.resolveReader(policy.reader()).module.REFUSES, policy.reader());

say("\n== 6. what this example does not solve");
say("        MEASURABLE, NOT MEASURED");
say("          - how often a repeated key in a real request is accident rather than design");
say("          - what refusing costs a caller who was relying on first-wins");
say("        NOT MEASURABLE HERE");
say("          - whether refusing is the right policy. Coercing, clamping and taking");
say("            the last are all defensible; choosing one without saying so is not,");
say("            and that is the only thing this example takes a position on.");
say("          - whether a declared arity is correct. Nothing here can tell a wrong");
say("            declaration from a wrong request.");

say(failures.length ? `\n${failures.length} failure(s)` : "\nall checks passed");
for (const f of failures) say(`  - ${f}`);
process.exit(failures.length ? 1 : 0);
main.mjs
// One query string, three readers, four fields with declared arity.
//
//   node src/main.mjs             what SCL runs, and what the other readers said
//   node src/main.mjs --strict    exit 1 when the configured reader refuses
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import * as report from "./DMS/report.mjs";
import * as policy from "./SCL/policy.mjs";
import * as request from "./SMS/request.mjs";
import * as declaredArity from "./TMS/readers/declared_arity.mjs";
import * as firstWins from "./TMS/readers/first_wins.mjs";
import * as lastWins from "./TMS/readers/last_wins.mjs";

const here = path.dirname(fileURLToPath(import.meta.url));
const CONTRACT = JSON.parse(fs.readFileSync(path.join(here, "FMS", "contract.json"), "utf8"));
const MODULES = Object.fromEntries(
  [firstWins, lastWins, declaredArity].map((m) => [m.READER, m]));

// A request a browser can produce without anything going wrong: two checkboxes
// ticked, and `page` repeated because a stale form field was submitted twice.
export const QUERY = "q=mssp&page=2&page=3&tag=structure&tag=evidence&sort=date";

function main(argv) {
  const out = (line = "") => process.stdout.write(`${line}\n`);
  out(`\n== one request, read three ways\n\n  ${QUERY}`);

  const results = request.readerNames().map((name) =>
    request.readAll(QUERY, name, CONTRACT.fields));
  const bad = results.find((r) => r.problem);
  if (bad) { out(`  !! ${bad.problem}`); return 1; }

  report.table(results, CONTRACT.fields, out);
  report.whoCouldRefuse(results, MODULES, out);

  out("\n  `page` was sent twice. first-wins says 2, last-wins says 3, and neither");
  out("  says a choice was made. Only the reader that knows `page` was declared");
  out("  `one` can tell a repeated checkbox from a repeated mistake.");

  const configured = results.find((r) => r.reader === policy.reader());
  out(`\n== this deployment reads with ${policy.reader()}`);
  for (const row of configured.refusals) {
    out(`    REFUSED  ${row.field}: ${row.because}`);
  }
  if (!configured.refusals.length) out("    nothing refused");

  report.gaps(out);

  if (argv.includes("--strict") && configured.refusals.length && policy.refusalIsFatal()) return 1;
  return 0;
}

process.exitCode = main(process.argv.slice(2));