Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,53 @@ counting nothing meaningful. The endpoint had been rewritten between two reading

The same applies to base branches: check whether the PR you are stacking on has merged before
branching from `master`, or you will silently revert it.

---

## 8. Reading and the check each catch what the other cannot. Neither is sufficient

Bought twice in one session, 2026-08-09, on the same claim.

**Reading found what the check could not see.** `check-shared-copy.mjs` matched its `claimPatterns`
with `indexOf` — case-sensitive literals. The heading "No call back to us" on `index.html` sat
outside any tag and the check stayed green, because the pattern was written lower-case. A literal
list also cannot spell one claim two ways, and this site spells it at least five: *call back to us*,
*callback to Observer*, *network call to Observer*, *neither our permission nor our uptime*, *no
runtime dependency on OP*. Each variant shares no wording with the others. Four assertions were
invisible to the enumeration whose entire job is to know where the claim lives — on three pages it
already listed. Only reading the pages found them.

**The check found what reading missed.** After widening the matcher for the fourth variant, it
immediately reported two more instances — one of them on a line of `integrate/index.html` that had
been read end to end minutes earlier. The other was on a page nobody had opened.

**So:** do not treat a passing check as coverage, and do not treat having read the page as
enumeration. A page is covered when a human has read it in a reader's order *and* every claim it
makes is inside a mechanism that will notice the next one. Convert what you find by reading into a
pattern **before** you finish the page, not after — the widened matcher is what caught the instance
the reading had just walked past.

Two corollaries, both bought the same way:

- **Put your own new prose inside the matcher.** A first fix in this session passed its base case for
the wrong reason: the replacement wording tripped no pattern at all, so the control could not see
the sentence just written to fix the claim. The wording you write to correct a claim is a new
instance of that claim.
- **A sweep that found three variants is not evidence there was no fourth.** Keep widening from the
site's own examples. Treat the pattern list as permanently incomplete.

## 9. Confirm a state by fetching the thing, never by reading a write's response

`app.agenticterminal.io` is behind a site-level Netlify password. The Netlify API **echoes
`password: false` back even on a successful write**, so a caller who confirms the lock by reading the
response it got is reading its own request back, not the state.

The general form: **a write's response is a claim about the request, not a measurement of the
result.** Where the state is externally observable, observe it — fetch the site, resolve the DID,
query the row. This is the same shape as [§7](#7-re-measure-immediately-before-writing-a-correction),
one layer down: re-measuring is useless if the instrument is the thing that just wrote.

Also true of a covered/uncovered list. On 2026-08-09 the audit handover's two lists held 33 pages and
`find . -name '*.html'` returned 38 — five pages on neither list, one of them edited by the branch
under review. **Derive the population with a command and diff it against the list.** A partition that
does not cover its domain fails by omission, so nothing ever looks wrong.
14 changes: 12 additions & 2 deletions integrate/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -206,15 +206,25 @@ <h2>Real on-chain proof</h2>
<div class="proof">
<div class="proof-label">Verified x402 Payment - Base Mainnet</div>
<div class="proof-value">
TX: <a href="https://basescan.org/tx/0xd94a6d8bfa9c1634e19b59cac9503e732cb538772b96e02300702bc38fb39b94" target="_blank">0xd94a6d8bfa9c1634...b39b94</a><br>
TX: <a href="https://basescan.org/tx/0xd94a6d8bfa9c1634e19b59cac9503e732cb538772b96e02300702bc38fb39b94" target="_blank">0xd94a6d8bfa9c1634...b39b94</a> &middot; Base mainnet block 45319014<br>
Agent: did:web:observerprotocol.org:agents:d13cdfceaa8f895afe56dc902179d279<br>
Counterparty: did:web:hyperbolic.xyz<br>
Counterparty: did:web:hyperbolic.xyz &mdash; <strong>this DID no longer resolves</strong><br>
Amount: 0.10 USDC<br>
Rail: x402 (USDC on Base)<br>
On-chain verified: true
</div>
</div>

<p style="border-left:3px solid var(--border); padding-left:1rem;">
The counterparty DID is recorded as it stood at the time of the payment, and it no longer
resolves: <code>hyperbolic.xyz</code> redirects to <code>hyperbolic.ai</code>, and
<code>/.well-known/did.json</code> returns 404 there. Checked 9 August 2026. <strong>The
settlement is unaffected and still checkable by anyone</strong> &mdash; it is on Base mainnet at
block 45319014, and the amount, asset and recipient are in the transaction, not in the DID. But a
<code>did:web</code> is only as durable as the domain serving it, and a page about independent
verification is the wrong place to leave that unsaid.
</p>

<div class="proof">
<div class="proof-label">X402PaymentCredential issued</div>
<div class="proof-value">
Expand Down
8 changes: 7 additions & 1 deletion ows/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,13 @@ <h2>OWS Quickstart</h2>
<div class="step-num-wrap"><div class="step-num">2</div><div class="step-line"></div></div>
<div class="step-body">
<div class="step-title">Install OP SDK</div>
<div class="step-desc">Add the Observer Protocol SDK to your project.</div>
<div class="step-desc">Add the Observer Protocol SDK to your project. <strong>Note that this
package is deprecated.</strong> <code>@observer-protocol/sdk</code> was last released in
April 2026, is unmaintained, and carries a deprecation notice on the registry itself. It
still installs and the steps below still run. For <em>verifying</em> a credential use
<code>@observer-protocol/policy-engine</code>; the register-and-attest flow in steps 3 to 5
has no replacement package yet, which is why this page still names the old one. See
<a href="/sdk.html">the SDK page</a> for what replaces which part.</div>
<pre><span class="cmd">npm install @observer-protocol/sdk</span></pre>
</div>
</div>
Expand Down
27 changes: 27 additions & 0 deletions scripts/check-shared-copy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,33 @@ for (const [key, entry] of Object.entries(spec)) {
}
}

// REQUIRES DISCLOSURE. Not a forbidden stem: naming the package is allowed, naming it
// silently is not. A page may legitimately instruct a deprecated package when nothing
// replaces the flow — what it may not do is let a reader adopt it without knowing.
const requires = spec['$requiresDisclosure'] ?? {};
for (const [concept, rule] of Object.entries(requires)) {
const stems = rule.stems ?? [];
const musts = (rule.mustAlsoContain ?? []).map((x) => x.toLowerCase());
if (stems.length === 0 || musts.length === 0) continue;

for (const file of allHtml()) {
const src = readFileSync(file, 'utf8');
const rel = file.slice(root.length + 1);
const low = src.toLowerCase();
const hit = stems.find((stem) => low.includes(stem.toLowerCase()));
if (!hit) continue;
const missing = musts.filter((m) => !low.includes(m));
if (missing.length === 0) continue;
const line = src.slice(0, low.indexOf(hit.toLowerCase())).split('\n').length;
failures.push(
`${rel}:${line}: names "${hit}" but the page never says ${missing.map((m) => `"${m}"`).join(' or ')}.\n` +
` Concept: ${concept}. Instructing a deprecated package is allowed where nothing\n` +
` replaces the flow. Instructing it silently is not — a reader adopts it without\n` +
` knowing. Add the disclosure to this page, or stop naming the package.`
);
}
}

// FORBIDDEN STEMS. A concept ruled off the site cannot be policed by searching for
// the phrasing it had when it was ruled off — that is how reputation survived three
// clearances. Forbid the stem; allowlist each surviving occurrence with a reason.
Expand Down
14 changes: 14 additions & 0 deletions scripts/shared-copy.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@
"claimPatternsAreRegex": "Case-insensitive regexes, matched by check-shared-copy.mjs. Written from the site's own examples rather than from one remembered sentence: the claim is spelled 'call back to us' on index.html and 'callback to Observer' on docs.html, and a literal lower-case list saw neither the capitalised heading nor the Observer variant. Four assertions were invisible to this block on 2026-08-09 for exactly that reason. When you add a home for the claim, add its WORDING here, not just its file.",
"claimPatternNote": "Any occurrence of these in an HTML file MUST sit inside an element tagged data-shared-copy=\"offline-scope\". That is what makes a tenth instance a build failure rather than a discovery. Add the tag, or add the file to mustAppearIn if it is a new home for the claim. Some tagged instances REFER to the claim rather than asserting it (docs.html's resolution-hosting rationale is one). They are tagged anyway: the enumeration's job is to know everywhere the phrase appears, and a human scoping the claim decides per instance."
},
"$requiresDisclosure": {
"deprecated-sdk": {
"why": "A page may name a deprecated package — there is no replacement for the register/attest flows, so instructing it is sometimes correct. What is never correct is instructing it SILENTLY. On 2026-08-09 four pages disclosed the deprecation and four did not, and the four that did not included the integration guide end to end, under the heading 'Ship chargeback prevention today'. This is not a $forbidden stem: the package name is allowed, the omission is not. Verified at the registry, not from our own copy: npm carries the deprecation notice on the package, and PyPI's newest observer-protocol is 0.2.0 from 2026-04-30.",
"stems": [
"@observer-protocol/sdk",
"@observerprotocol/sdk",
"pip install observer-protocol"
],
"mustAlsoContain": [
"deprecat"
],
"note": "mustAlsoContain is matched case-insensitively anywhere in the same FILE, not the same element: a page-level notice covers every mention on that page, which is how the four disclosing pages already do it. Stem, not phrase — 'deprecat' catches deprecated/deprecation/deprecates, because the last three vocabulary misses on this site were all a matcher that knew one spelling of its subject."
}
},
"$forbidden": {
"reputation-model": {
"why": "Reputation was ruled off this site entirely. It was cleared three times and was wrong three times, because each search was shaped like the previous fix: the phrase, then the subject, then the subject as previously written. 'trust scoring' does not contain 'trust score' \u2014 the stem diverges at scor|e versus scor|ing. This forbids the STEM, so a gerund, a bare noun, or a number with a label cannot reappear unnoticed.",
Expand Down
Loading