fix(rules): correct PUT/PATCH/POST semantics in adding-tables-with-schemas (mode: generate flip blocked on documentation#650) - #80
Conversation
…hemas
The endpoint list in this hand-authored (mode: synthesized) rule had drifted
from what Harper's write layer actually does. The PUT entry was actively
dangerous for an agent acting on it.
- PUT /{Table}/{id}: was "Updates an existing record." It is create-or-replace
with upsert semantics — Table.put calls update(..., fullUpdate = true), and
the fullUpdate branch of _writeUpdate replaces the record, so properties
absent from the request body are removed. An agent sending a partial body
would silently destroy every field it omitted.
- PATCH /{Table}/{id}: was "Performs a partial update" with no mention of merge
depth. Table.patch calls update(..., fullUpdate = false) and the merge is
shallow — a nested object in the body replaces the stored one wholesale.
- POST /{Table}/: the trailing slash is required, not stylistic. Resource.post
only creates on a collection target. POST /{Table} (no slash) is rejected in
argument normalization with 404 "A trailing slash is required to POST to the
{Table} collection" (Resource.ts:747-752); POST /{Table}/{id} falls through
to missingMethod and returns 405. Also document that the auto-assigned
primary key comes back in the Location response header as the bare key, not
a URL, alongside a 201.
- DELETE /{Table}/: make the consequence explicit. An unfiltered collection
DELETE is treated as a search target and deletes every record in the table.
- GET /{Table}: describes the resource (table, database, declared attributes),
not "the schema itself"; note that it takes no trailing slash.
- Use "primary key" rather than "ID" on the single-record endpoints, matching
the @PrimaryKey vocabulary the same rule uses earlier.
Reorders DELETE /{Table}/{id} ahead of the collection form so the destructive
endpoint reads last, matching the endpoint table in
HarperFast/documentation#650.
AGENTS.md is a compiled concatenation of rule bodies and moves with the rule.
It was rebuilt with the generator's own assembly step (assembleAgentsMd from
scripts/generation/lib/render.mjs, then oxfmt) — no source resolution, no LLM
call, and no mode: generate rule was touched.
The rule stays mode: synthesized. Migrating it to mode: generate is blocked on
HarperFast/documentation#650, which adds the canonical
"Tables and Their Automatic Endpoints" section it would source from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request updates the documentation in AGENTS.md and adding-tables-with-schemas.md to provide more detailed and accurate descriptions of Harper's automatic REST API endpoints (GET, POST, PUT, PATCH, and DELETE). The review feedback suggests clarifying that for PUT requests, the primary key in the URL overrides any mismatched primary key provided in the request body.
| - `DELETE /{TableName}/{id}`: Deletes a single record by its ID. | ||
| - `GET /{TableName}/{id}`: Retrieves a single record by its primary key. | ||
| - `POST /{TableName}/`: Creates a record and returns `201` with the Harper-assigned primary key in the `Location` response header (the bare key, not a URL). **The trailing slash is required** — `POST /{TableName}` returns `404`, and `POST /{TableName}/{id}` returns `405`. | ||
| - `PUT /{TableName}/{id}`: Creates **or replaces** the record at `{id}` (upsert). The stored record ends up matching the request body exactly — **properties omitted from the body are removed**. Send the complete record; use `PATCH` to change a subset of properties. |
There was a problem hiding this comment.
Since PUT forces the primary key to the URL {id} (as verified in the Harper source code), any mismatched primary key provided in the request body will be overridden by the URL {id} rather than creating a new record or throwing an error. Consider adding this detail explicitly to clarify the exact behavior of the upsert operation:
- `PUT /{TableName}/{id}`: Creates **or replaces** the record at `{id}` (upsert). The stored record ends up matching the request body exactly — **properties omitted from the body are removed** (the primary key in the URL overrides any mismatched primary key in the body). Send the complete record; use `PATCH` to change a subset of properties.There was a problem hiding this comment.
Confirmed and fixed in dc4077e. Table.ts:2484-2487 at c4dd96237:
if (primaryKey && recordUpdate[primaryKey] !== id && (fullUpdate || primaryKey in recordUpdate)) {
// ensure that the primary key is correct, if there is supposed to be one
recordUpdate[primaryKey] = id;
}So the URL id wins and a mismatched body key cannot create a second record. That is now stated in the rule, alongside the two @createdTime / @updatedTime exceptions @heskew raised — the previous "matches the request body exactly" was false in all three ways.
sent with Claude Opus 5
| - `DELETE /{TableName}/{id}`: Deletes a single record by its ID. | ||
| - `GET /{TableName}/{id}`: Retrieves a single record by its primary key. | ||
| - `POST /{TableName}/`: Creates a record and returns `201` with the Harper-assigned primary key in the `Location` response header (the bare key, not a URL). **The trailing slash is required** — `POST /{TableName}` returns `404`, and `POST /{TableName}/{id}` returns `405`. | ||
| - `PUT /{TableName}/{id}`: Creates **or replaces** the record at `{id}` (upsert). The stored record ends up matching the request body exactly — **properties omitted from the body are removed**. Send the complete record; use `PATCH` to change a subset of properties. |
There was a problem hiding this comment.
Since PUT forces the primary key to the URL {id} (as verified in the Harper source code), any mismatched primary key provided in the request body will be overridden by the URL {id} rather than creating a new record or throwing an error. Consider adding this detail explicitly to clarify the exact behavior of the upsert operation:
- `PUT /{TableName}/{id}`: Creates **or replaces** the record at `{id}` (upsert). The stored record ends up matching the request body exactly — **properties omitted from the body are removed** (the primary key in the URL overrides any mismatched primary key in the body). Send the complete record; use `PATCH` to change a subset of properties.
heskew
left a comment
There was a problem hiding this comment.
Requesting changes for one current-head documentation-correctness issue.
Prior-review attribution: I agree with @gemini-code-assist’s existing comments that the URL primary key overrides a mismatched primary key in the request body. Codex also confirmed a second exception: Harper stamps @updatedTime and retains or stamps @createdtime during a full PUT. The new statement that the stored record matches the request body exactly is therefore false in multiple supported schemas. Because this rule is agent-facing and this PR exists to correct destructive-write guidance, please qualify that sentence in the canonical rule and regenerate AGENTS.md before merge.
I independently traced the remaining changed claims: PUT is an upsert/full replacement for ordinary user fields, PATCH is shallow, the POST slash/status/Location behavior is accurate, and unfiltered collection DELETE removes all matching rows. No other blocker found.
Validation at exact head dd0304f: npm ci, npm run validate, and git diff --check passed; GitHub checks are green.
🤖 Posted by Codex on behalf of @heskew
| - `DELETE /{TableName}/{id}`: Deletes a single record by its ID. | ||
| - `GET /{TableName}/{id}`: Retrieves a single record by its primary key. | ||
| - `POST /{TableName}/`: Creates a record and returns `201` with the Harper-assigned primary key in the `Location` response header (the bare key, not a URL). **The trailing slash is required** — `POST /{TableName}` returns `404`, and `POST /{TableName}/{id}` returns `405`. | ||
| - `PUT /{TableName}/{id}`: Creates **or replaces** the record at `{id}` (upsert). The stored record ends up matching the request body exactly — **properties omitted from the body are removed**. Send the complete record; use `PATCH` to change a subset of properties. |
There was a problem hiding this comment.
Codex finding, complementary to @gemini-code-assist’s primary-key comment: ‘matching the request body exactly’ is also false for framework-managed timestamps. A full PUT stamps @updatedTime and preserves or stamps @createdtime, even when those values are absent from or differ in the body. Please describe this as replacement of ordinary user-defined fields, explicitly note that the URL primary key wins, and carve out framework-managed fields; then regenerate the compiled AGENTS.md.
🤖 Posted by Codex on behalf of @heskew
There was a problem hiding this comment.
You are right, and thanks for tracing it — fixed in dc4077e.
I verified all three exceptions against resources/Table.ts at c4dd96237 rather than take them on faith, and all three hold:
@updatedTimeis re-stamped withtxnTimeon every write, full or partial (Table.ts:2460-2467) — unconditional, no reference to the body.- On a full update over an existing entry,
@createdTimeis restored from that entry (Table.ts:2468-2474, comment: "make sure to retain original created time"), so the body value is discarded. Only a genuinely new entry gets a fresh stamp. if (primaryKey && recordUpdate[primaryKey] !== id && (fullUpdate || primaryKey in recordUpdate)) recordUpdate[primaryKey] = id;(Table.ts:2484-2487) — the URL id wins, so a mismatched body key cannot create a second record.
You are also right about the judgment call. The PR body noted the timestamp and primary-key nuances and left them out "to keep the rule terse" — wrong call for this particular sentence, given the rule is agent-facing and the whole point of the PR is correcting destructive-write guidance. An unqualified "exactly" is the same class of false statement I was fixing.
One thing that reinforces it: automatic-apis.md:77 on main already says "PUT replaces the stored record exactly. Three exceptions always apply: a @createdTime attribute keeps the original value, an @updatedTime attribute is re-stamped with the time of the write, and the primary key is forced to match the {id} in the URL." So the old wording did not just overstate — it contradicted a sibling rule. I matched that vocabulary so the two agree.
New wording keeps the hazard in front and the exceptions after:
Creates or replaces the record at
{id}(upsert). Properties omitted from the body are removed — send the complete record, or usePATCHto change a subset. Three exceptions survive the replacement: an@updatedTimeattribute is re-stamped with the time of the write, a@createdTimeattribute keeps its original value, and the primary key is forced to match the{id}in the URL, so a mismatched key in the body cannot create a second record.
AGENTS.md regenerated with the generator's own render step (assembleAgentsMd + oxfmt) — no source resolution, no LLM call. harper-mcp/AGENTS.md came back byte-identical, which is a decent check that the assembly is faithful.
I also merged origin/main, which the branch was 14 commits behind. Worth flagging since it was a latent problem independent of your review: AGENTS.md concatenates every rule body, so assembling it from the stale tree would have reverted rules regenerated since. The merge was conflict-free and the diff against main is now only the intended lines.
sent with Claude Opus 5
kriszyp
left a comment
There was a problem hiding this comment.
generated
🤖 Reviewed with Codex
| - `DELETE /{TableName}/{id}`: Deletes a single record by its ID. | ||
| - `GET /{TableName}/{id}`: Retrieves a single record by its primary key. | ||
| - `POST /{TableName}/`: Creates a record and returns `201` with the Harper-assigned primary key in the `Location` response header (the bare key, not a URL). **The trailing slash is required** — `POST /{TableName}` returns `404`, and `POST /{TableName}/{id}` returns `405`. | ||
| - `PUT /{TableName}/{id}`: Creates **or replaces** the record at `{id}` (upsert). The stored record ends up matching the request body exactly — **properties omitted from the body are removed**. Send the complete record; use `PATCH` to change a subset of properties. |
There was a problem hiding this comment.
Could we qualify “matching the request body exactly”? PUT fully replaces ordinary record properties, but Harper forces the primary key from the URL, preserves the original @createdTime, and re-stamps @updatedTime. For example, a PUT containing only { "name": "new" } will still retain/generate those schema-managed attributes, so omitted properties are not universally removed. Stating these exceptions while retaining the warning about omitted client-controlled fields would make this precise.
kriszyp
left a comment
There was a problem hiding this comment.
Good stuff
🤖 Reviewed with Codex
| - `DELETE /{TableName}/{id}`: Deletes a single record by its ID. | ||
| - `GET /{TableName}/{id}`: Retrieves a single record by its primary key. | ||
| - `POST /{TableName}/`: Creates a record and returns `201` with the Harper-assigned primary key in the `Location` response header (the bare key, not a URL). **The trailing slash is required** — `POST /{TableName}` returns `404`, and `POST /{TableName}/{id}` returns `405`. | ||
| - `PUT /{TableName}/{id}`: Creates **or replaces** the record at `{id}` (upsert). The stored record ends up matching the request body exactly — **properties omitted from the body are removed**. Send the complete record; use `PATCH` to change a subset of properties. |
There was a problem hiding this comment.
Could we qualify “matching the request body exactly”? PUT fully replaces ordinary record properties, but Harper forces the primary key from the URL, preserves the original @createdTime, and re-stamps @updatedTime. For example, a PUT containing only { "name": "new" } will still retain/generate those schema-managed attributes, so omitted properties are not universally removed. Stating these exceptions while retaining the warning about omitted client-controlled fields would make this precise.
Addresses @heskew's requested change and @gemini-code-assist's comment. "The stored record ends up matching the request body exactly" was false in three ways, all confirmed in resources/Table.ts at c4dd96237: - an `@updatedTime` attribute is re-stamped with the write time on every write, full or partial (Table.ts:2460-2467) - on a full update over an existing entry, a `@createdTime` attribute is restored from that entry rather than taken from the body (Table.ts:2468-2474) - the primary key is forced to the URL `{id}` (Table.ts:2484-2487), so a mismatched key in the body cannot create a second record The PR body had noted the timestamp and primary-key nuances and chose to leave them out for terseness. That was the wrong call for this sentence: the rule is agent-facing, the PR exists to correct destructive-write guidance, and an unqualified "exactly" is itself a false statement of the kind being fixed. The wording now matches automatic-apis.md:77 on main, which already documented the same three exceptions — so the two rules agree instead of contradicting each other. The destructive part leads, since that is the hazard; the exceptions follow. AGENTS.md reassembled with the generator's own render step (assembleAgentsMd + oxfmt), no source resolution and no LLM call. harper-mcp/AGENTS.md came back byte-identical, confirming the assembly is faithful. Also merged origin/main, which the branch was 14 commits behind: AGENTS.md concatenates every rule body, so assembling it from a stale tree would have reverted rules regenerated since. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things, cleanly separable
The Job 1 correctness fixes below stand on their own and can merge independently. They fix statements that are wrong in the rule shipping today, and they do not depend on anything.
The
mode: generatemigration is deliberately NOT in this PR — it is blocked on HarperFast/documentation#650 merging. That PR (HarperFast/documentation#650) adds the## Tables and Their Automatic Endpointssection toreference/rest/overview.md, which is the canonical source this rule would generate from. Until it lands,npm run generatecannot resolve the section and fails hard. This PR therefore leavesmode: synthesizedin place and only records what the flip will require, as a checklist for a follow-up PR.Job 1 — correctness fixes (merge-ready)
harper-best-practices/rules/adding-tables-with-schemas.mdismode: synthesized, so its endpoint list was hand-written and had drifted from what the write layer actually does. The most serious one isPUT.PUT /{TableName}/{id}— was actively dangerous{id}(upsert). The stored record ends up matching the request body exactly — properties omitted from the body are removed. Send the complete record; usePATCHto change a subset of properties."Table.putcallsupdate(..., fullUpdate = true), and thefullUpdatebranch of_writeUpdatereplaces the record rather than merging it. An agent reading "updates an existing record" would send a partial body and silently destroy every field it left out.PATCH /{TableName}/{id}— merge depth was unstatedTable.patchcallsupdate(..., fullUpdate = false). The old wording invited an agent to assume a deep merge and lose sibling keys inside a nested object.POST /{TableName}/— trailing slash is load-bearing, and the new key was undocumented201with the Harper-assigned primary key in theLocationresponse header (the bare key, not a URL). The trailing slash is required —POST /{TableName}returns404, andPOST /{TableName}/{id}returns405."Resource.postonly creates on a collection target. The two failure modes have different status codes, which matters for an agent trying to diagnose its own request:POST /{TableName}(no slash) is rejected during argument normalization, before any resource is constructed —Resource.ts:747-752throws404with the message "A trailing slash is required to POST to the {TableName} collection".POST /{TableName}/{id}falls through tomissingMethodand returns405with anAllowheader.The rule also gave the agent no way to learn where the generated primary key comes back. Note that
Locationcarries the bare primary key, not a resolved URL (REST.ts:319-320, corroborated byopenApi.ts:206-208: "primary key of new record"), so it cannot be fetched directly as a href.DELETE /{TableName}/— consequence made explicitTechnically the old wording was not false, but it read as a casual aside for an operation that empties a table. An unfiltered collection
DELETEis treated as a search target and deletes every match.Smaller fixes folded in
GET /{TableName}said "Describes the schema itself" — it describes the resource (table, database, declared attributes). Also noted that it takes no trailing slash, since the slash distinction is what separates it from the collection endpoint directly below it.@primaryKey) the same rule uses three steps earlier.DELETE /{TableName}/{id}now precedes the collection form, so the destructive endpoint reads last and the ordering matches documentation#650's table (less diff churn when this rule is generated from it).Deliberately unchanged, and one line deliberately NOT added
The existing
rest: truewarning is correct and stays.restdoes not appear anywhere instatic/defaultConfig.yaml, andcomponentLoader.ts:610-611uses an app'sconfig.yamlverbatim rather than merging it with defaults — so for any app that has aconfig.yaml(which is the case this rule addresses), omittingrest: truegenuinely means no REST endpoints.I had intended to also add a line pinning the table's HTTP method set to exactly GET/PUT/POST/DELETE/PATCH. Verification against the harper source refuted that, so it is not in this PR — see "Verification" below. Asserting it would have introduced a new false statement while fixing four old ones.
harper-best-practices/AGENTS.mdis a compiled concatenation of all rule bodies, so it moves with the rule body. It was reproduced with the generator's own assembly step (assembleAgentsMdfromscripts/generation/lib/render.mjs, then oxfmt) — no source resolution, no LLM call, and nomode: generaterule was touched.npm run validatepasses, including the AGENTS.md round-trip check.Job 2 — what the
mode: generateflip will require (follow-up PR, after documentation#650)Do not start this until documentation#650 is merged to
mainin the docs repo.1.
harper-best-practices/rules.manifest.yaml— the only file that needs an editEverything else is derived. The
adding-tables-with-schemasentry (currently the first rule in the file) changes from:to:
Notes on each field, because the exact values matter:
sources[].pathis a docs build path, not a repo path. documentation#650 editsreference/rest/overview.md, but sources resolve against<docs-path>/build/, where current-version docs live underreference/v5/. So the path isreference/v5/rest/overview.md. Verified:build/reference/v5/rest/overview.mdexists and its heading levels match the source file 1:1.sources[].sectionmust match documentation#650's heading text exactly.sliceSectioninscripts/generation/lib/sources.mjsnormalizes whitespace and lowercases, but does not fuzzy-match. If the heading is renamed during review of #650, this string must be updated in lockstep orgenerate-rules.mjsexits 1 withSection heading "..." not found. Re-read the merged heading before writing this value.sourcesis required and must be non-empty formode: generate(validate-generated.mjsLayer 1), and conversely must be omitted while the mode issynthesized— which is why it cannot be added ahead of the flip.must_coveris only valid onmode: generate(Layer 1 rejects it on any other mode), and each string must appear verbatim in the generated body (Layer 4). Keep the list short and pin the facts this PR just fixed —Locationandshalloware the two that regressed silently before.cross_linksis already legal in any mode and each entry must be a known rule slug. The current hand-written body links todefining-relationships,extending-tables,automatic-apis, andquerying-rest-apis; declare them so the generator can reproduce those links instead of dropping them.2. Scope decision to make before flipping — this is the real work, not the manifest edit
The
## Tables and Their Automatic Endpointssection from #650 covers only step 4 of this rule (the endpoint list and therest: truerequirement). It does not cover:.graphqlfiles per table andgraphqlSchema.fileswildcardsnode_modules/harper/schema.graphqlas the directive reference@relationship(delegated todefining-relationships)@exportif you intend to extend the table" (delegated toextending-tables)A flip that sources only the REST section will silently lose steps 1, 2, and 5. Steps 1–2 partly overlap
schema-design-tooling(order 2), which already sourcesschema.md#Overview/#Type Directives/#Field Directives. So pick one, explicitly:schema.md#Loading Schemassource shown above and accept some overlap withschema-design-tooling; orschema-design-tooling, and updatedescriptionaccordingly — note that changingdescriptionalso moves the generated index block inSKILL.md; ormode: synthesized. This is a legitimate outcome.synthesizedexists for rules with no single canonical source, and this rule is a navigation hub across four other rules, not a 1:1 restatement of one docs section. #650 makes (a)/(b) possible; it does not make them correct.The
node_modules/harper/schema.graphqlpointer in step 2 has no docs source at all and will be dropped by the generator's "do not invent" rule (scripts/generation/templates/system-prompt.md) under any of (a) or (b). Decide whether that is an acceptable loss.3. Frontmatter fields the generated rule will carry that this one lacks
Written by
buildFrontmatterinscripts/generation/lib/render.mjs— do not hand-write these, the generator owns them:metadata.sourcespath#sectionstringsmetadata.sourceCommitmetadata.inputHashmetadata.modeflipssynthesized→generate.nameanddescriptionare already present and are written from the manifest. Layer 3 of the validator asserts all three new fields are absent while the mode issynthesizedand thatmetadata.sourcesmatches the manifest exactly once it isgenerate— so the manifest edit and the regeneration must land in the same commit.4. No other file needs an entry
Checked explicitly, so the follow-up does not go hunting:
scripts/generation/lib/manifest.mjs—SKILLSis keyed per skill directory, not per rule. No entry needed..github/workflows/generate.yaml— iterates every non-synthesized manifest rule automatically. No entry needed.harper-best-practices/AGENTS.md— regenerated by the generator (pipeline step 7). Never hand-edited.harper-best-practices/SKILL.md— only the block between the<!-- BEGIN GENERATED INDEX -->/<!-- END GENERATED INDEX -->sentinels is generated, and it derives fromrule/description/category/priority/orderonly. It changes only if option (b) above changesdescription.rules.manifest.yaml, the# Phase 2 … Phase 4 …block) is a running log of which rules were flipped in which phase and currently ends "All others remain synthesized." Add a Phase 5 line so the log stays accurate.5. Commands for the follow-up, in order
The docs checkout must be built at
mainHEAD, or the rule is "born stale": its recordedsourceCommit/inputHashpoint at an old commit and the next auto-sync PR looks like an unrelated docs change regenerated this rule.--ruleskips the AGENTS.md rebuild, which is why the full run follows it.ANTHROPIC_API_KEYmust be set in.env—mode: generatemakes a live LLM call.Then review the generated body against this PR's diff: confirm the
PUTupsert warning, thePATCHshallow-merge caveat, thePOSTtrailing-slash404/405distinction, theLocationheader, and the unfiltered-DELETEwarning all survived the rewrite. Those five facts are the reason this PR exists; a generated body that drops any of them is a regression, andmust_coveronly guards the substrings listed above.Be aware that documentation#650's endpoint table does not currently carry the
404-vs-405distinction forPOST, so a generated body sourced only from that section will lose it. Either land that detail in #650 first, or accept the loss knowingly.Verification
Every statement was re-checked against the harper source at
c4dd96237(HarperFast/harpermain) rather than against the docs, so this rule and documentation#650 are grounded in the same behavior. Confirmed as written: thePUTupsert/replace path (Table.ts:2263-2266→Table.ts:1900-1905→_writeUpdateatTable.ts:2932-2941, where a full update setsrecordToStore = recordUpdatewith no reference toexistingRecord); the shallowPATCHmerge (Table.ts:2356-2359withfullUpdate = false, merging via the one-level spread intracked.ts:421-443); and the unfiltered collectionDELETE(Table.ts:3125-3151, whereisSearchTargetis a bareisCollectiontest with no minimum-condition guard).Three places where the source did not match the assumptions I started from — all three changed what shipped in this PR:
POST /{Table}without a trailing slash returns404, not405. My first draft of this fix said 405 for both slash-less forms.Resource.ts:747-752throws a purpose-built404for the no-slash case during argument normalization; onlyPOST /{Table}/{id}reachesmissingMethodand returns405. Corrected before commit.QUERYgenuinely works on a plain exported table —Resource.ts:347-351routesstatic querytoTable.prototype.search(Table.ts:3244) with the request body as the search target.HEADalso works, handled generically in the REST layer (REST.ts:240-242plus the body strip atREST.ts:352). OnlyCOPYandMOVEactually405. Separately,allowedMethods()does not mean what it looks like: the OPTIONS handler calls it on the class (REST.ts:251-258), and it inspects statics (Resource.ts:1003-1009), soOPTIONSadvertisesGET, PUT, POST, DELETE, PATCH, QUERY, MOVE, COPY— over-reporting MOVE/COPY and omitting HEAD. It is not a reliable description of a table's surface.restis never on by default" needs a qualifier. True ofstatic/defaultConfig.yaml(norestkey at all), butcomponents/DEFAULT_CONFIG.ts:1-4setsrest: trueas the component fallback used when a component directory has no config file whatsoever (componentLoader.ts:612-613). SincecomponentLoader.ts:610-611uses an existingconfig.yamlverbatim rather than merging it, the rule's existing wording — which is scoped toconfig.yaml— stays correct and unchanged.Two smaller nuances, noted rather than folded in to keep the rule terse:
@createdTime/@updatedTimeattributes survive aPUT(Table.ts:2461-2473), so "properties omitted are removed" holds for user-defined attributes but not framework timestamps; andPUTforces the primary key to the URL id (Table.ts:2484-2487), so a mismatchedidin the body cannot create a second record.Worth feeding back to documentation#650
Two sentences in that PR's new section look imprecise against the source, and it would be better to fix them there than to inherit them when this rule is generated from it:
OPTIONSreports the resource's supported methods in anAllowheader" — it reports the class's statics, which over-reportsMOVE/COPYand omitsHEAD(finding 2 above).restis not enabled by default" — needs the no-config-file carve-out (finding 3 above).The section's
POSTrow says only "Requires the trailing slash" without a status code, so it is not wrong, but adding the404-vs-405distinction there would make it more useful.Gate
npm run validatepasses (format check, build,validate-skills.mjs,validate-generated.mjs), as does the fullernode scripts/generation/validate-generated.mjs --docs-path ../documentationincluding the source-exists and byte-identical checks.🤖 Generated with Claude Code