* test(#3514): add failing-first denylist and integrity suites * fix(#3514): deny internal fetch hosts; disclose unverified integrity * docs(#3514): trust-model, glossary, and changeset entries * fix(#3514): scope v6 checks to literals; exact pin kinds in prompt * chore(#3514): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"count": 263,
|
||||
"count": 261,
|
||||
"classes": {
|
||||
"ARCH": 1,
|
||||
"CI": 2,
|
||||
@@ -17,7 +17,7 @@
|
||||
"PROC": 14,
|
||||
"PROHIB": 10,
|
||||
"RELEASE-NOTES": 31,
|
||||
"RULESET": 60,
|
||||
"RULESET": 58,
|
||||
"SESSION": 9,
|
||||
"WAVE": 5,
|
||||
"WORKSTREAM": 5,
|
||||
@@ -979,16 +979,6 @@
|
||||
"klass": "RULESET",
|
||||
"value": "requires confirmed-bug before implementation (legacy 'confirmed' label is back-compat only for duplicate-sweep exemption, not a valid implementation gate)"
|
||||
},
|
||||
{
|
||||
"id": "RULESET.CONTRIB.GAP-TRACKING",
|
||||
"klass": "RULESET",
|
||||
"value": "a known gap is tracked only by an issue — a code comment, TODO, or changeset note is attached to code and dies with the refactor that deletes it, while the gap stays real (worked example: archived changeset clever-yaks-cheer.md recorded the shortFormToId backfill as 'tracked as a follow-up parity gap' + a // KNOWN GAP: comment marked the call site; the SDK retirement deleted both, no issue was opened, and it surfaced a year later as #3427). If you write 'tracked as a follow-up' anywhere, open the issue first and cite its number. Prose twin of #3473 criterion B6 (a guard needs a retirement condition)"
|
||||
},
|
||||
{
|
||||
"id": "RULESET.CONTRIB.GATE.DELETED-MODULE-DISPOSITION",
|
||||
"klass": "RULESET",
|
||||
"value": "a PR deleting a module/package/lineage with a surviving counterpart carries a symbol-level disposition table for the deleted side — every declared name (exported or local) marked migrated (location named) | renamed → X (conventions normalized: verifyX → cmdVerifyX) | dropped because Y; SCREAMING_CASE constants ranked first (policy lives there; both confirmed ADR-0174 losses were bounds); the surviving tests passing is NOT evidence — the surviving tests belong to the surviving side (#3484, ADR-0174 behavior-carry-forward amendment; losses: #3427 shortFormToId tier + unresolved-deps warning, #3477 regexForKeyLinkPattern 512-char cap + nested-quantifier screen, epic #3473 B5 MAX_JSON_SEARCH_DEPTH=48). Method with mandatory positive control: docs/how-to/audit-a-retiring-lineage.md"
|
||||
},
|
||||
{
|
||||
"id": "RULESET.CONTRIB.GATE.ORDER",
|
||||
"klass": "RULESET",
|
||||
|
||||
@@ -24,7 +24,6 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md)
|
||||
- [Resolve edge-coverage findings](how-to/resolve-edge-coverage-findings.md) — turn the spec phase's surfaced domain-boundary edges into covered, dismissed, or backstopped spec decisions
|
||||
- [Resolve prohibition findings](how-to/resolve-prohibition-findings.md) — turn the spec phase's surfaced must-NOT constraints into resolved, dismissed, or deferred spec decisions
|
||||
- [Resolve an ESLint glob-coverage finding](how-to/resolve-eslint-coverage-findings.md) — bring a source file that matches no lint rule under coverage, or record a reasoned exemption
|
||||
- [Audit a retiring lineage](how-to/audit-a-retiring-lineage.md) — produce the symbol-level disposition table a module-deleting consolidation PR must carry, with a mandatory positive control
|
||||
- [Plan a phase](how-to/plan-a-phase.md) — run research, decompose work, and verify plan quality
|
||||
- [Execute a phase](how-to/execute-a-phase.md) — run plans in parallel waves with fresh-context subagents
|
||||
- [Verify and ship](how-to/verify-and-ship.md) — walk through completed work, diagnose failures, and create the PR
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ADR-0174: Retire @opengsd/gsd-sdk package boundary — single-runtime collapse
|
||||
|
||||
- **Status:** Accepted (2026-05-23); amended #1642 (2026-06-23) — §5 reconciled to as-built Result type + `exitReason?` field added on `InvalidArgs`; amended #3484 (2026-08-14) — behavior-carry-forward amendment appended
|
||||
- **Status:** Accepted (2026-05-23); amended #1642 (2026-06-23) — §5 reconciled to as-built Result type + `exitReason?` field added on `InvalidArgs`
|
||||
- **Date:** 2026-05-23
|
||||
- **Tracking issue:** [#174](https://github.com/open-gsd/get-shit-done-redux/issues/174) — sub-issues #175–#197
|
||||
|
||||
@@ -161,46 +161,3 @@ Seven phases, ~15–18 PRs total. Each phase is a coherent slice that leaves the
|
||||
| 7 — Land this ADR's PR | The PR for this ADR closes the umbrella tracking issue. | 1 (this PR) |
|
||||
|
||||
Implementation is tracked in [#174 — sub-issues #175–#197](https://github.com/open-gsd/get-shit-done-redux/issues/174).
|
||||
|
||||
## Amendment (2026-08-14): Behavior carry-forward for lineage-retiring consolidations (#3484)
|
||||
|
||||
This amendment was appended after the migration completed. It changes nothing about the
|
||||
collapse decision above — one runtime still cannot drift from itself, and ADR-3524's
|
||||
parity apparatus is still correctly gone. What this ADR lacked was a clause about the
|
||||
*instant of collapse*: the two lineages were not equivalent at the moment `sdk/` was
|
||||
deleted (drift had been accumulating for months — ADR-3524 itself names nine prior
|
||||
one-sided fixes), and the collapse took the CJS lineage as canonical. Everything the SDK
|
||||
side had and the CJS side did not was deleted with the package — and the parity tests
|
||||
that would have caught exactly that were deleted by Migration Plan Phase 3 in the same
|
||||
motion. The surviving tests were the CJS side's tests, and they passed throughout.
|
||||
|
||||
**The requirement.** A consolidation that retires a lineage must enumerate the retiring
|
||||
side's behavior and record, per item, whether it was **`migrated`**, **`renamed → X`**,
|
||||
or **`dropped because Y`** — before the deletion merges. "The tests pass" is explicitly
|
||||
not sufficient evidence, because the surviving tests belong to the surviving side and
|
||||
cannot observe behavior only the deleted side had.
|
||||
|
||||
**The evidence that produced this amendment — three confirmed losses through the gap:**
|
||||
|
||||
| Lost in the collapse | Surviving site | Consequence |
|
||||
|---|---|---|
|
||||
| `shortFormToId` resolution tier + the `unresolved depends_on reference` warning | `src/phase.cts` | [#3427](https://github.com/open-gsd/gsd-core/issues/3427) — short-form `depends_on` edges silently dropped; dependency-ordered plans execute in parallel |
|
||||
| `regexForKeyLinkPattern`'s 512-char cap + nested-quantifier screen | `src/verify.cts` | [#3477](https://github.com/open-gsd/gsd-core/issues/3477) — ReDoS; `verify-phase` hangs on an untrusted plan pattern |
|
||||
| `MAX_JSON_SEARCH_DEPTH = 48` | `src/intel.cts` | unbounded recursion in `matchesInValue`; stack overflow on deeply nested intel JSON (carried by epic #3473, B5) |
|
||||
|
||||
None was noticed for months; #3427 surfaced only when a user hit it in production. The
|
||||
`shortFormToId` loss is the sharpest: the gap was *known* — recorded as "tracked as a
|
||||
follow-up parity gap" in an archived changeset (`.changeset/archived/clever-yaks-cheer.md`)
|
||||
and marked with a `// KNOWN GAP:` comment — then the SDK was retired, the comment died
|
||||
with the lineage it annotated, and no issue was ever opened. A comment is attached to
|
||||
code; it is not a tracking mechanism. A gap that matters gets an issue.
|
||||
|
||||
A fourth-loss audit run on 2026-08-14 (method recorded in [#3484](https://github.com/open-gsd/gsd-core/issues/3484)
|
||||
and in the contributor how-to) found no further losses — which calibrates the audit
|
||||
method, not a claim that the class is closed.
|
||||
|
||||
**Where the rule lives now.** The general form of this requirement — not scoped to this
|
||||
ADR — is a merge gate in `CONTRIBUTING.md` ("Deleting a module that has a surviving
|
||||
counterpart"): the deleting PR carries a symbol-level disposition table, SCREAMING_CASE
|
||||
constants ranked first, with a mandatory positive control. Future lineage-retiring
|
||||
consolidations cite this amendment and satisfy that gate.
|
||||
|
||||
@@ -288,6 +288,37 @@ An `integrity` field in `capability.json` carries a `sha512-<base64>` digest
|
||||
of the capability bundle. When present, GSD verifies this digest before
|
||||
extracting any files. A mismatch aborts the install.
|
||||
|
||||
When NO pin is supplied, the consent prompt says so plainly: a
|
||||
`content: NO PINNED HASH — staged unverified` line distinguishes an install
|
||||
whose bytes were verified against a commitment from one that was not
|
||||
([#3514](https://github.com/open-gsd/gsd-core/issues/3514)). A computed
|
||||
sha512 of what was actually fetched is still recorded in the ledger at
|
||||
install, so a later `trust` inspection shows exactly which bytes landed.
|
||||
Prompt claims are exact per kind: a sha512 `--integrity` pin renders as
|
||||
*supplied and verified*, a git source checked out at a `#sha:<commit>` ref
|
||||
renders as *pinned to a git commit* (never as a sha512 pin — none was
|
||||
supplied), and a mutable `#sha:<branch>` ref is not a pin at all.
|
||||
|
||||
### Fetch-host denylist
|
||||
|
||||
The URL importer's fetch transport refuses, before any bytes leave
|
||||
([#3514](https://github.com/open-gsd/gsd-core/issues/3514)):
|
||||
|
||||
- **loopback, link-local, and unspecified hosts** — `127.0.0.0/8`,
|
||||
`169.254.0.0/16` (which contains the cloud metadata addresses), `0.0.0.0/8`,
|
||||
`::1`, `fe80::/10`, `::`, their IPv4-mapped IPv6 spellings, and
|
||||
`localhost`/`*.localhost` names. No legitimate capability install fetches
|
||||
these.
|
||||
- **plaintext `http://` URLs** — the transport is `https`-only; an `http://`
|
||||
tarball spec still *classifies* (so an internal-mirror workflow fails with a
|
||||
clear, named reason instead of a raw protocol error) but never fetches.
|
||||
|
||||
Deliberate limits: RFC1918 private ranges (`10/8`, `172.16/12`,
|
||||
`192.168/16`) are **not** denied — an internal https mirror is a legitimate
|
||||
install source, and the denylist is not an allowlist. The check is on the
|
||||
URL's host literal; a public hostname that *resolves* via DNS to a denied
|
||||
range (rebinding) is out of scope.
|
||||
|
||||
What integrity pinning defends against: a capability hosted at a URL or in a
|
||||
registry that is later replaced with a different bundle (whether by an attacker
|
||||
who has compromised the hosting, or by an author publishing a silent breaking
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
# Audit a retiring lineage (behavior carry-forward)
|
||||
|
||||
**You need this if** your PR deletes a module, package, or runtime lineage — and any part
|
||||
of the deleted side has a surviving counterpart in the tree. The merge gate
|
||||
([CONTRIBUTING.md → "Deleting a module that has a surviving counterpart"](../../CONTRIBUTING.md))
|
||||
requires the PR to carry a symbol-level disposition table. This page is the calibrated
|
||||
method for producing that table. It was first run against the ADR-0174 SDK retirement
|
||||
and found three confirmed losses after the fact (#3484); run it *before* the deletion
|
||||
merges and those losses become review findings instead of production bugs.
|
||||
|
||||
## Why "the tests pass" proves nothing here
|
||||
|
||||
The surviving tests belong to the surviving side. They were written against the
|
||||
surviving implementation and pass before, during, and after the deletion — including
|
||||
when the deleted side carried an invariant the survivor never had. ADR-3524 existed
|
||||
precisely because the two sides had already drifted in nine known places; deleting one
|
||||
side without enumerating it is choosing the survivor's omissions by default.
|
||||
|
||||
## The audit, step by step
|
||||
|
||||
All commands run against `<parent>` — the last commit **before** the retirement (for the
|
||||
SDK retirement this was `04b3be683`, the parent of the retiring commit).
|
||||
|
||||
### 1. List the deleted source files
|
||||
|
||||
```bash
|
||||
git ls-tree -r --name-only <parent> -- sdk/src
|
||||
```
|
||||
|
||||
172 files for the SDK run. Everything under the deleted root is a candidate; test files
|
||||
are excluded from pairing (they are the deleted side's claims, not its behavior).
|
||||
|
||||
### 2. Pair each file with a surviving counterpart
|
||||
|
||||
Pair by **basename**: `sdk/src/query/phase.ts` ↔ `src/phase.cts`. For the SDK run this
|
||||
yielded 22 paired modules. Files with **no** counterpart are whole surfaces deleted on
|
||||
purpose — list them in the disposition as one `dropped because …` line each (or grouped
|
||||
by surface), but they are out of scope for symbol diffing.
|
||||
|
||||
### 3. Diff *declared* names, not raw tokens
|
||||
|
||||
Strip comments from both sides, then diff the declared names (functions, constants,
|
||||
types — exported or local). **Do not tokenize the raw text**: raw token diffing matches
|
||||
prose in comments and produced ~500 false positives on the first SDK run.
|
||||
|
||||
### 4. Normalize rename conventions before reporting
|
||||
|
||||
The two lineages used different naming conventions. Normalize before diffing:
|
||||
|
||||
| Deleted-side name | Surviving-side convention |
|
||||
|---|---|
|
||||
| `verifyX` | `cmdVerifyX` |
|
||||
| `preserveExistingProgress` | `shouldPreserveExistingProgress` |
|
||||
|
||||
Skipping this step is what makes the raw diff unreadable — every convention-renamed
|
||||
symbol reads as a loss.
|
||||
|
||||
### 5. Rank what remains by shape
|
||||
|
||||
**SCREAMING_CASE constants are the high-signal class.** That is where policy lives
|
||||
(bounds, limits, tiers, thresholds), and both real losses the SDK audit confirmed were
|
||||
bounds (`regexForKeyLinkPattern`'s 512-char cap; `MAX_JSON_SEARCH_DEPTH = 48`). Local
|
||||
variable renames are noise — summarize them.
|
||||
|
||||
### 6. Positive control — mandatory
|
||||
|
||||
Before trusting the audit, verify it **re-detects a known loss**. For the SDK lineage the
|
||||
control is `shortFormToId` — a *local* `const` inside a surviving function at
|
||||
`sdk/src/query/phase.ts:609`, not an export. An export-only scan misses it entirely and
|
||||
reports a clean bill of health; a calibrated run finds both halves of #3427 (the tier
|
||||
`shortFormToId` and the diagnostic `unresolvedDeps`). If your audit method cannot find
|
||||
the control, it is not calibrated — fix the method, do not ship the table.
|
||||
|
||||
### 7. Write the disposition table into the PR
|
||||
|
||||
One row per name that survived steps 3–5: `migrated` (name the location), `renamed → X`,
|
||||
or `dropped because Y`. Unpaired whole surfaces get their own grouped `dropped because`
|
||||
rows. The table goes in the PR body where the reviewer of the deletion will see it.
|
||||
|
||||
## Reading the result honestly
|
||||
|
||||
- **No losses found** means the audit found none — with the method calibrated by the
|
||||
positive control. It is evidence, not proof; say which control you ran.
|
||||
- **A "migrated" claim you cannot point at** is a loss wearing a disposition. Every
|
||||
`migrated` row names a file or symbol in the surviving tree.
|
||||
- The audit is run **once per retirement**, against that retirement's parent commit —
|
||||
it is not a standing CI job. Its value is the moment before the deletion merges.
|
||||
|
||||
## Reason codes at a glance
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| `migrated` | Behavior exists at a named location in the surviving tree |
|
||||
| `renamed → X` | Same behavior under the surviving convention's name |
|
||||
| `dropped because Y` | Deliberate deletion, reason stated |
|
||||
| *(nothing — no issue)* | Not tracked. A gap that matters gets an issue; a comment or changeset note dies with the code it annotates (`clever-yaks-cheer.md` → #3427) |
|
||||
Reference in New Issue
Block a user