@descryy/core 0.1.0
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/LICENSE +21 -0
- package/dist/capabilities/index.d.ts +3 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +2 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/preconditions.d.ts +99 -0
- package/dist/capabilities/preconditions.d.ts.map +1 -0
- package/dist/capabilities/preconditions.js +94 -0
- package/dist/capabilities/preconditions.js.map +1 -0
- package/dist/contracts/engine.d.ts +90 -0
- package/dist/contracts/engine.d.ts.map +1 -0
- package/dist/contracts/engine.js +291 -0
- package/dist/contracts/engine.js.map +1 -0
- package/dist/contracts/index.d.ts +9 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +5 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/orphans.d.ts +96 -0
- package/dist/contracts/orphans.d.ts.map +1 -0
- package/dist/contracts/orphans.js +241 -0
- package/dist/contracts/orphans.js.map +1 -0
- package/dist/contracts/paths.d.ts +132 -0
- package/dist/contracts/paths.d.ts.map +1 -0
- package/dist/contracts/paths.js +245 -0
- package/dist/contracts/paths.js.map +1 -0
- package/dist/contracts/shapes.d.ts +99 -0
- package/dist/contracts/shapes.d.ts.map +1 -0
- package/dist/contracts/shapes.js +196 -0
- package/dist/contracts/shapes.js.map +1 -0
- package/dist/governance/budget.d.ts +89 -0
- package/dist/governance/budget.d.ts.map +1 -0
- package/dist/governance/budget.js +101 -0
- package/dist/governance/budget.js.map +1 -0
- package/dist/governance/candidate-boundary.d.ts +49 -0
- package/dist/governance/candidate-boundary.d.ts.map +1 -0
- package/dist/governance/candidate-boundary.js +51 -0
- package/dist/governance/candidate-boundary.js.map +1 -0
- package/dist/governance/escalation.d.ts +63 -0
- package/dist/governance/escalation.d.ts.map +1 -0
- package/dist/governance/escalation.js +72 -0
- package/dist/governance/escalation.js.map +1 -0
- package/dist/governance/fact-boundary.d.ts +77 -0
- package/dist/governance/fact-boundary.d.ts.map +1 -0
- package/dist/governance/fact-boundary.js +91 -0
- package/dist/governance/fact-boundary.js.map +1 -0
- package/dist/governance/finding-funnel.d.ts +109 -0
- package/dist/governance/finding-funnel.d.ts.map +1 -0
- package/dist/governance/finding-funnel.js +137 -0
- package/dist/governance/finding-funnel.js.map +1 -0
- package/dist/governance/index.d.ts +11 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/index.js +6 -0
- package/dist/governance/index.js.map +1 -0
- package/dist/graph/build.d.ts +127 -0
- package/dist/graph/build.d.ts.map +1 -0
- package/dist/graph/build.js +0 -0
- package/dist/graph/build.js.map +1 -0
- package/dist/graph/cross-language.d.ts +70 -0
- package/dist/graph/cross-language.d.ts.map +1 -0
- package/dist/graph/cross-language.js +104 -0
- package/dist/graph/cross-language.js.map +1 -0
- package/dist/graph/index.d.ts +11 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +6 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/merge.d.ts +95 -0
- package/dist/graph/merge.d.ts.map +1 -0
- package/dist/graph/merge.js +131 -0
- package/dist/graph/merge.js.map +1 -0
- package/dist/graph/persist.d.ts +42 -0
- package/dist/graph/persist.d.ts.map +1 -0
- package/dist/graph/persist.js +52 -0
- package/dist/graph/persist.js.map +1 -0
- package/dist/graph/runtime-confirmation.d.ts +169 -0
- package/dist/graph/runtime-confirmation.d.ts.map +1 -0
- package/dist/graph/runtime-confirmation.js +318 -0
- package/dist/graph/runtime-confirmation.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger/index.d.ts +5 -0
- package/dist/ledger/index.d.ts.map +1 -0
- package/dist/ledger/index.js +3 -0
- package/dist/ledger/index.js.map +1 -0
- package/dist/ledger/mint.d.ts +81 -0
- package/dist/ledger/mint.d.ts.map +1 -0
- package/dist/ledger/mint.js +138 -0
- package/dist/ledger/mint.js.map +1 -0
- package/dist/ledger/resolve.d.ts +124 -0
- package/dist/ledger/resolve.d.ts.map +1 -0
- package/dist/ledger/resolve.js +290 -0
- package/dist/ledger/resolve.js.map +1 -0
- package/dist/multipr/fingerprint.d.ts +56 -0
- package/dist/multipr/fingerprint.d.ts.map +1 -0
- package/dist/multipr/fingerprint.js +169 -0
- package/dist/multipr/fingerprint.js.map +1 -0
- package/dist/multipr/hidden-dependency.d.ts +120 -0
- package/dist/multipr/hidden-dependency.d.ts.map +1 -0
- package/dist/multipr/hidden-dependency.js +185 -0
- package/dist/multipr/hidden-dependency.js.map +1 -0
- package/dist/multipr/index.d.ts +14 -0
- package/dist/multipr/index.d.ts.map +1 -0
- package/dist/multipr/index.js +8 -0
- package/dist/multipr/index.js.map +1 -0
- package/dist/multipr/mechanical.d.ts +239 -0
- package/dist/multipr/mechanical.d.ts.map +1 -0
- package/dist/multipr/mechanical.js +333 -0
- package/dist/multipr/mechanical.js.map +1 -0
- package/dist/multipr/migration-heads.d.ts +160 -0
- package/dist/multipr/migration-heads.d.ts.map +1 -0
- package/dist/multipr/migration-heads.js +212 -0
- package/dist/multipr/migration-heads.js.map +1 -0
- package/dist/multipr/overlap.d.ts +186 -0
- package/dist/multipr/overlap.d.ts.map +1 -0
- package/dist/multipr/overlap.js +254 -0
- package/dist/multipr/overlap.js.map +1 -0
- package/dist/multipr/scope-store.d.ts +105 -0
- package/dist/multipr/scope-store.d.ts.map +1 -0
- package/dist/multipr/scope-store.js +182 -0
- package/dist/multipr/scope-store.js.map +1 -0
- package/dist/multipr/superseded.d.ts +133 -0
- package/dist/multipr/superseded.d.ts.map +1 -0
- package/dist/multipr/superseded.js +128 -0
- package/dist/multipr/superseded.js.map +1 -0
- package/dist/query/confirmed-facts.d.ts +56 -0
- package/dist/query/confirmed-facts.d.ts.map +1 -0
- package/dist/query/confirmed-facts.js +64 -0
- package/dist/query/confirmed-facts.js.map +1 -0
- package/dist/query/declared-value-closure.d.ts +126 -0
- package/dist/query/declared-value-closure.d.ts.map +1 -0
- package/dist/query/declared-value-closure.js +156 -0
- package/dist/query/declared-value-closure.js.map +1 -0
- package/dist/query/index.d.ts +28 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +15 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/memory.d.ts +20 -0
- package/dist/query/memory.d.ts.map +1 -0
- package/dist/query/memory.js +55 -0
- package/dist/query/memory.js.map +1 -0
- package/dist/query/provider.d.ts +127 -0
- package/dist/query/provider.d.ts.map +1 -0
- package/dist/query/provider.js +54 -0
- package/dist/query/provider.js.map +1 -0
- package/dist/query/queries.d.ts +119 -0
- package/dist/query/queries.d.ts.map +1 -0
- package/dist/query/queries.js +267 -0
- package/dist/query/queries.js.map +1 -0
- package/dist/query/refusal-fetch.d.ts +114 -0
- package/dist/query/refusal-fetch.d.ts.map +1 -0
- package/dist/query/refusal-fetch.js +200 -0
- package/dist/query/refusal-fetch.js.map +1 -0
- package/dist/query/refusal-questions.d.ts +123 -0
- package/dist/query/refusal-questions.d.ts.map +1 -0
- package/dist/query/refusal-questions.js +122 -0
- package/dist/query/refusal-questions.js.map +1 -0
- package/dist/query/root-cause-score.d.ts +218 -0
- package/dist/query/root-cause-score.d.ts.map +1 -0
- package/dist/query/root-cause-score.js +251 -0
- package/dist/query/root-cause-score.js.map +1 -0
- package/dist/query/row-closure-picture.d.ts +108 -0
- package/dist/query/row-closure-picture.d.ts.map +1 -0
- package/dist/query/row-closure-picture.js +131 -0
- package/dist/query/row-closure-picture.js.map +1 -0
- package/dist/query/similar-incidents.d.ts +76 -0
- package/dist/query/similar-incidents.d.ts.map +1 -0
- package/dist/query/similar-incidents.js +169 -0
- package/dist/query/similar-incidents.js.map +1 -0
- package/dist/query/sqlite.d.ts +13 -0
- package/dist/query/sqlite.d.ts.map +1 -0
- package/dist/query/sqlite.js +54 -0
- package/dist/query/sqlite.js.map +1 -0
- package/dist/query/traverse.d.ts +110 -0
- package/dist/query/traverse.d.ts.map +1 -0
- package/dist/query/traverse.js +0 -0
- package/dist/query/traverse.js.map +1 -0
- package/dist/query/unresolved.d.ts +195 -0
- package/dist/query/unresolved.d.ts.map +1 -0
- package/dist/query/unresolved.js +225 -0
- package/dist/query/unresolved.js.map +1 -0
- package/dist/query/verification-status.d.ts +134 -0
- package/dist/query/verification-status.d.ts.map +1 -0
- package/dist/query/verification-status.js +163 -0
- package/dist/query/verification-status.js.map +1 -0
- package/dist/recording/index.d.ts +26 -0
- package/dist/recording/index.d.ts.map +1 -0
- package/dist/recording/index.js +30 -0
- package/dist/recording/index.js.map +1 -0
- package/dist/recording/migrate.d.ts +22 -0
- package/dist/recording/migrate.d.ts.map +1 -0
- package/dist/recording/migrate.js +51 -0
- package/dist/recording/migrate.js.map +1 -0
- package/dist/recording/reader.d.ts +27 -0
- package/dist/recording/reader.d.ts.map +1 -0
- package/dist/recording/reader.js +55 -0
- package/dist/recording/reader.js.map +1 -0
- package/dist/recording/redact.d.ts +63 -0
- package/dist/recording/redact.d.ts.map +1 -0
- package/dist/recording/redact.js +98 -0
- package/dist/recording/redact.js.map +1 -0
- package/dist/recording/schema.d.ts +41 -0
- package/dist/recording/schema.d.ts.map +1 -0
- package/dist/recording/schema.js +104 -0
- package/dist/recording/schema.js.map +1 -0
- package/dist/recording/writer.d.ts +120 -0
- package/dist/recording/writer.d.ts.map +1 -0
- package/dist/recording/writer.js +81 -0
- package/dist/recording/writer.js.map +1 -0
- package/dist/scoping/fanout.d.ts +86 -0
- package/dist/scoping/fanout.d.ts.map +1 -0
- package/dist/scoping/fanout.js +95 -0
- package/dist/scoping/fanout.js.map +1 -0
- package/dist/scoping/index.d.ts +10 -0
- package/dist/scoping/index.d.ts.map +1 -0
- package/dist/scoping/index.js +6 -0
- package/dist/scoping/index.js.map +1 -0
- package/dist/scoping/score.d.ts +83 -0
- package/dist/scoping/score.d.ts.map +1 -0
- package/dist/scoping/score.js +125 -0
- package/dist/scoping/score.js.map +1 -0
- package/dist/scoping/tiers.d.ts +32 -0
- package/dist/scoping/tiers.d.ts.map +1 -0
- package/dist/scoping/tiers.js +35 -0
- package/dist/scoping/tiers.js.map +1 -0
- package/dist/scoping/traverse.d.ts +60 -0
- package/dist/scoping/traverse.d.ts.map +1 -0
- package/dist/scoping/traverse.js +172 -0
- package/dist/scoping/traverse.js.map +1 -0
- package/dist/scoping/weights.d.ts +97 -0
- package/dist/scoping/weights.d.ts.map +1 -0
- package/dist/scoping/weights.js +167 -0
- package/dist/scoping/weights.js.map +1 -0
- package/dist/sources/git/diff.d.ts +53 -0
- package/dist/sources/git/diff.d.ts.map +1 -0
- package/dist/sources/git/diff.js +140 -0
- package/dist/sources/git/diff.js.map +1 -0
- package/dist/sources/git/env.d.ts +2 -0
- package/dist/sources/git/env.d.ts.map +1 -0
- package/dist/sources/git/env.js +21 -0
- package/dist/sources/git/env.js.map +1 -0
- package/dist/sources/git/history.d.ts +140 -0
- package/dist/sources/git/history.d.ts.map +1 -0
- package/dist/sources/git/history.js +314 -0
- package/dist/sources/git/history.js.map +1 -0
- package/dist/sources/git/index.d.ts +7 -0
- package/dist/sources/git/index.d.ts.map +1 -0
- package/dist/sources/git/index.js +4 -0
- package/dist/sources/git/index.js.map +1 -0
- package/dist/sources/git/source.d.ts +95 -0
- package/dist/sources/git/source.d.ts.map +1 -0
- package/dist/sources/git/source.js +0 -0
- package/dist/sources/git/source.js.map +1 -0
- package/dist/sources/migrations/dialects.d.ts +59 -0
- package/dist/sources/migrations/dialects.d.ts.map +1 -0
- package/dist/sources/migrations/dialects.js +84 -0
- package/dist/sources/migrations/dialects.js.map +1 -0
- package/dist/sources/migrations/index.d.ts +3 -0
- package/dist/sources/migrations/index.d.ts.map +1 -0
- package/dist/sources/migrations/index.js +2 -0
- package/dist/sources/migrations/index.js.map +1 -0
- package/dist/sources/migrations/read.d.ts +113 -0
- package/dist/sources/migrations/read.d.ts.map +1 -0
- package/dist/sources/migrations/read.js +270 -0
- package/dist/sources/migrations/read.js.map +1 -0
- package/dist/sources/workspace/index.d.ts +3 -0
- package/dist/sources/workspace/index.d.ts.map +1 -0
- package/dist/sources/workspace/index.js +2 -0
- package/dist/sources/workspace/index.js.map +1 -0
- package/dist/sources/workspace/workspace.d.ts +64 -0
- package/dist/sources/workspace/workspace.d.ts.map +1 -0
- package/dist/sources/workspace/workspace.js +294 -0
- package/dist/sources/workspace/workspace.js.map +1 -0
- package/dist/store/driver/driver.d.ts +82 -0
- package/dist/store/driver/driver.d.ts.map +1 -0
- package/dist/store/driver/driver.js +31 -0
- package/dist/store/driver/driver.js.map +1 -0
- package/dist/store/driver/node-sqlite.d.ts +28 -0
- package/dist/store/driver/node-sqlite.d.ts.map +1 -0
- package/dist/store/driver/node-sqlite.js +188 -0
- package/dist/store/driver/node-sqlite.js.map +1 -0
- package/dist/store/index.d.ts +33 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +46 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/migrate.d.ts +68 -0
- package/dist/store/migrate.d.ts.map +1 -0
- package/dist/store/migrate.js +140 -0
- package/dist/store/migrate.js.map +1 -0
- package/dist/store/reader.d.ts +127 -0
- package/dist/store/reader.d.ts.map +1 -0
- package/dist/store/reader.js +269 -0
- package/dist/store/reader.js.map +1 -0
- package/dist/store/schema.d.ts +87 -0
- package/dist/store/schema.d.ts.map +1 -0
- package/dist/store/schema.js +322 -0
- package/dist/store/schema.js.map +1 -0
- package/dist/store/writer.d.ts +116 -0
- package/dist/store/writer.d.ts.map +1 -0
- package/dist/store/writer.js +279 -0
- package/dist/store/writer.js.map +1 -0
- package/dist/tiers/certify.d.ts +375 -0
- package/dist/tiers/certify.d.ts.map +1 -0
- package/dist/tiers/certify.js +434 -0
- package/dist/tiers/certify.js.map +1 -0
- package/dist/tiers/index.d.ts +5 -0
- package/dist/tiers/index.d.ts.map +1 -0
- package/dist/tiers/index.js +3 -0
- package/dist/tiers/index.js.map +1 -0
- package/dist/tiers/ladder.d.ts +73 -0
- package/dist/tiers/ladder.d.ts.map +1 -0
- package/dist/tiers/ladder.js +197 -0
- package/dist/tiers/ladder.js.map +1 -0
- package/dist/validation/config-graph.d.ts +104 -0
- package/dist/validation/config-graph.d.ts.map +1 -0
- package/dist/validation/config-graph.js +205 -0
- package/dist/validation/config-graph.js.map +1 -0
- package/dist/validation/env.d.ts +243 -0
- package/dist/validation/env.d.ts.map +1 -0
- package/dist/validation/env.js +447 -0
- package/dist/validation/env.js.map +1 -0
- package/dist/validation/index.d.ts +16 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +9 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/join-substitution.d.ts +186 -0
- package/dist/validation/join-substitution.d.ts.map +1 -0
- package/dist/validation/join-substitution.js +191 -0
- package/dist/validation/join-substitution.js.map +1 -0
- package/dist/validation/migrations.d.ts +133 -0
- package/dist/validation/migrations.d.ts.map +1 -0
- package/dist/validation/migrations.js +315 -0
- package/dist/validation/migrations.js.map +1 -0
- package/dist/validation/nodes.d.ts +32 -0
- package/dist/validation/nodes.d.ts.map +1 -0
- package/dist/validation/nodes.js +45 -0
- package/dist/validation/nodes.js.map +1 -0
- package/dist/validation/rollback.d.ts +124 -0
- package/dist/validation/rollback.d.ts.map +1 -0
- package/dist/validation/rollback.js +302 -0
- package/dist/validation/rollback.js.map +1 -0
- package/dist/validation/route-drift.d.ts +108 -0
- package/dist/validation/route-drift.d.ts.map +1 -0
- package/dist/validation/route-drift.js +103 -0
- package/dist/validation/route-drift.js.map +1 -0
- package/dist/validation/sources.d.ts +153 -0
- package/dist/validation/sources.d.ts.map +1 -0
- package/dist/validation/sources.js +409 -0
- package/dist/validation/sources.js.map +1 -0
- package/dist/worker/index.d.ts +5 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +3 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/pool.d.ts +73 -0
- package/dist/worker/pool.d.ts.map +1 -0
- package/dist/worker/pool.js +203 -0
- package/dist/worker/pool.js.map +1 -0
- package/dist/worker/protocol.d.ts +91 -0
- package/dist/worker/protocol.d.ts.map +1 -0
- package/dist/worker/protocol.js +19 -0
- package/dist/worker/protocol.js.map +1 -0
- package/dist/worker/traversal.worker.d.ts +25 -0
- package/dist/worker/traversal.worker.d.ts.map +1 -0
- package/dist/worker/traversal.worker.js +124 -0
- package/dist/worker/traversal.worker.js.map +1 -0
- package/package.json +29 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparing two endpoint paths, and knowing when not to.
|
|
3
|
+
*
|
|
4
|
+
* This file is small and is the most dangerous part of the contract engine, so
|
|
5
|
+
* it is separate and tested on its own.
|
|
6
|
+
*
|
|
7
|
+
* ## The problem it exists to solve
|
|
8
|
+
*
|
|
9
|
+
* A caller reaching an endpoint no route serves is *sometimes* a bug and
|
|
10
|
+
* *usually* not. Measured on two real repositories: 117 callers, 24 routes, 16
|
|
11
|
+
* joined. The other 101 are overwhelmingly calls to services that are simply not
|
|
12
|
+
* in this graph — a third-party API, a backend nobody has analysed. Reporting
|
|
13
|
+
* all 101 as "no route serves this" would be 101 confident wrong statements, and
|
|
14
|
+
* would bury the handful that are real.
|
|
15
|
+
*
|
|
16
|
+
* So an unmatched caller is only a **finding** when a served route exists that
|
|
17
|
+
* it nearly matches. Nothing similar on the provider side means the provider was
|
|
18
|
+
* probably never analysed, which belongs in §20.2's *not analysable* category
|
|
19
|
+
* and is disclosed as a count rather than enumerated as defects.
|
|
20
|
+
*
|
|
21
|
+
* ## The near-miss rules are deliberately narrow
|
|
22
|
+
*
|
|
23
|
+
* Each one describes a specific, common, *mechanical* mistake. None of them is a
|
|
24
|
+
* similarity score, because a threshold on a score is a knob that turns "is this
|
|
25
|
+
* a bug" into "how high did we set it".
|
|
26
|
+
*
|
|
27
|
+
* | rule | example | why it is a real mistake |
|
|
28
|
+
* | --- | --- | --- |
|
|
29
|
+
* | method differs, path identical | `POST /orders` vs `GET /orders` | someone changed the verb on one side |
|
|
30
|
+
* | exactly one **literal** segment differs, **and the two differ by a typo's worth of characters** | `/order/{param}/items` vs `/orders/{param}/items` | a typo or a rename applied on one side |
|
|
31
|
+
*
|
|
32
|
+
* ## Rule 2's similarity floor, and the measurement that set it
|
|
33
|
+
*
|
|
34
|
+
* That second clause was missing for the whole life of this file, and its
|
|
35
|
+
* absence was not a small thing. "Exactly one segment differs" says nothing at
|
|
36
|
+
* all about *how* it differs: `/api/hmac` and `/api/novu-agent` differ in
|
|
37
|
+
* exactly one segment, share a method and share arity. Precision came only
|
|
38
|
+
* incidentally, from `{param}` segments happening to sit where the paths
|
|
39
|
+
* diverge.
|
|
40
|
+
*
|
|
41
|
+
* `DEC-317` found this from the GraphQL side — every operation path is arity-2
|
|
42
|
+
* with no `{param}` and a first segment drawn from a set of size three, so the
|
|
43
|
+
* whole GraphQL surface of a repository collapsed into three buckets in which
|
|
44
|
+
* every member near-missed every other — and gated it rather than fixing it,
|
|
45
|
+
* because a threshold chosen carelessly costs the near-typo detections this
|
|
46
|
+
* rule exists for, and costs them invisibly.
|
|
47
|
+
*
|
|
48
|
+
* So it was measured, against 3,524 routes from 27 real repositories, by
|
|
49
|
+
* `bench/near-miss-similarity.mjs` — which pre-registers the metric, the
|
|
50
|
+
* candidate thresholds and the decision rule in its header, and was committed
|
|
51
|
+
* before it was run. What it found:
|
|
52
|
+
*
|
|
53
|
+
* - **The rule was not a precision rule.** Treating each real route as an
|
|
54
|
+
* unmatched caller against the rest of its own application's table, it
|
|
55
|
+
* reported a `ONE_SEGMENT` near miss for **1,796 of 3,524 routes — 51%.**
|
|
56
|
+
* Every one is a false positive by construction: both sides are real,
|
|
57
|
+
* deliberately distinct routes of the same service.
|
|
58
|
+
* - **On the genuinely reachable population it was worse.** Across every
|
|
59
|
+
* unmatched `USES_API` caller in the corpus, the rule emitted **18 findings,
|
|
60
|
+
* and all 18 were nonsense** — 15 of them naming the same `/api/novu-agent`
|
|
61
|
+
* route from callers like `/api/hmac` and `/api/send-email`. There were no
|
|
62
|
+
* true positives to lose.
|
|
63
|
+
*
|
|
64
|
+
* The floor is `1 - distance / max(len) >= 0.4`, with any distance of 1 or less
|
|
65
|
+
* admitted outright so that a one-character mistake in a short segment (`/v1`
|
|
66
|
+
* against `/v2`) is not scored away. It rejects **87.4%** of those 1,796 and
|
|
67
|
+
* retains **100%** of a seeded near-typo population of 49,405 — real route
|
|
68
|
+
* segments mutated by transposition, deletion, insertion, substitution,
|
|
69
|
+
* singular/plural and separator variance. Not one seeded typo is lost, in any
|
|
70
|
+
* class.
|
|
71
|
+
*
|
|
72
|
+
* **The distance counts an adjacent transposition as one edit, not two, and
|
|
73
|
+
* that is load-bearing.** With plain Levenshtein a swap costs two, so `api` →
|
|
74
|
+
* `aip` scores `1 - 2/3 = 0.33` and every candidate threshold silently deleted
|
|
75
|
+
* the entire transposition class — 2,916 real detections, all on segments of
|
|
76
|
+
* three characters. The first measurement pass rejected every threshold for
|
|
77
|
+
* exactly this reason, and the reason was a defect in the metric rather than a
|
|
78
|
+
* fact about the corpus.
|
|
79
|
+
*
|
|
80
|
+
* **What the floor is not.** It is a similarity test, not an oracle. `activate`
|
|
81
|
+
* and `deactivate` are two real routes in one API and score 0.80, so they are
|
|
82
|
+
* still reported. Nothing here can distinguish a two-character prefix from a
|
|
83
|
+
* typo, and this does not claim to — it buys precision against *unrelated*
|
|
84
|
+
* words, which is what the corpus says the problem was.
|
|
85
|
+
*
|
|
86
|
+
* **A trailing-slash rule was written and then deleted, and the reason is worth
|
|
87
|
+
* keeping.** `/orders/` and `/orders` look like the classic contract mismatch,
|
|
88
|
+
* but `normaliseEndpointPath` strips the trailing slash *before hashing*, so the
|
|
89
|
+
* two sides mint the **same `API_ENDPOINT` id** and join correctly. The rule
|
|
90
|
+
* could never fire. It was caught by a test that asserted it would.
|
|
91
|
+
*
|
|
92
|
+
* That unification is the right default — the two paths mean the same endpoint
|
|
93
|
+
* to everyone who writes them — but it does make one real class of bug
|
|
94
|
+
* invisible here: a framework configured for strict routing genuinely serves
|
|
95
|
+
* only one of them. Whether it does depends on router configuration this graph
|
|
96
|
+
* does not read, so the limit is stated rather than papered over with a rule
|
|
97
|
+
* that fires on nothing.
|
|
98
|
+
*
|
|
99
|
+
* **Deliberately not a rule: a parameter where the other side has a literal.**
|
|
100
|
+
* `/orders/{param}` and `/orders/recent` legitimately coexist in every routing
|
|
101
|
+
* framework on earth — the literal is matched first and the parameter catches
|
|
102
|
+
* the rest. Treating that as a near miss would report the single most common
|
|
103
|
+
* correct pattern in REST routing as a defect.
|
|
104
|
+
*
|
|
105
|
+
* Segment counts must be equal for the segment rule. `/orders` and
|
|
106
|
+
* `/orders/{param}` are two different endpoints, not a near miss, and a rule
|
|
107
|
+
* that ignored length would pair every collection route with its item route.
|
|
108
|
+
*/
|
|
109
|
+
/** The normalised placeholder every adapter writes. See `normaliseEndpointPath`. */
|
|
110
|
+
const PARAM = "{param}";
|
|
111
|
+
/**
|
|
112
|
+
* How alike rule 2's two differing segments must be, as a fraction of the
|
|
113
|
+
* longer one's length. **Measured, not chosen** — see the module header, and
|
|
114
|
+
* `bench/near-miss-similarity.mjs` for the pre-registered decision rule that
|
|
115
|
+
* selected it: the lowest candidate that keeps 100% of a 49,405-case seeded
|
|
116
|
+
* near-typo population, which it does while rejecting 87.4% of the false
|
|
117
|
+
* positives the unguarded rule produced on 3,524 real routes.
|
|
118
|
+
*/
|
|
119
|
+
const SEGMENT_SIMILARITY_FLOOR = 0.4;
|
|
120
|
+
/**
|
|
121
|
+
* Damerau-Levenshtein distance, optimal string alignment.
|
|
122
|
+
*
|
|
123
|
+
* **An adjacent transposition costs one edit, not two**, and that is the whole
|
|
124
|
+
* reason this is not the three-line Levenshtein. A swapped pair of characters
|
|
125
|
+
* is a single keystroke mistake and the single most common way a path is
|
|
126
|
+
* typo'd; charging it two edits puts `api`/`aip` at 0.33 and deletes the entire
|
|
127
|
+
* class from what rule 2 can report.
|
|
128
|
+
*/
|
|
129
|
+
function editDistance(a, b) {
|
|
130
|
+
if (a === b)
|
|
131
|
+
return 0;
|
|
132
|
+
if (a.length === 0)
|
|
133
|
+
return b.length;
|
|
134
|
+
if (b.length === 0)
|
|
135
|
+
return a.length;
|
|
136
|
+
const rows = [];
|
|
137
|
+
for (let i = 0; i <= a.length; i += 1)
|
|
138
|
+
rows.push(new Array(b.length + 1).fill(0));
|
|
139
|
+
for (let i = 0; i <= a.length; i += 1)
|
|
140
|
+
rows[i][0] = i;
|
|
141
|
+
for (let j = 0; j <= b.length; j += 1)
|
|
142
|
+
rows[0][j] = j;
|
|
143
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
144
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
145
|
+
const substitution = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
146
|
+
let best = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + substitution);
|
|
147
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
148
|
+
best = Math.min(best, rows[i - 2][j - 2] + 1);
|
|
149
|
+
}
|
|
150
|
+
rows[i][j] = best;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return rows[a.length][b.length];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Are these two segments close enough that one could be a mistyping of the
|
|
157
|
+
* other, rather than simply a different word?
|
|
158
|
+
*
|
|
159
|
+
* A distance of one is admitted whatever the lengths, because on a short
|
|
160
|
+
* segment a single edit is a large fraction of the string and normalising it
|
|
161
|
+
* away would reject `/v1` against `/v2` — the most mechanical mismatch there
|
|
162
|
+
* is.
|
|
163
|
+
*/
|
|
164
|
+
function nearlyTheSameWord(a, b) {
|
|
165
|
+
const distance = editDistance(a, b);
|
|
166
|
+
if (distance <= 1)
|
|
167
|
+
return true;
|
|
168
|
+
return 1 - distance / Math.max(a.length, b.length) >= SEGMENT_SIMILARITY_FLOOR;
|
|
169
|
+
}
|
|
170
|
+
function segments(path) {
|
|
171
|
+
return path.split("/").filter((s) => s !== "");
|
|
172
|
+
}
|
|
173
|
+
/** Trailing slash removed, so `/orders/` and `/orders` compare as one shape. */
|
|
174
|
+
function trimmed(path) {
|
|
175
|
+
return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
176
|
+
}
|
|
177
|
+
export function sameEndpoint(a, b) {
|
|
178
|
+
return a.method === b.method && a.pathTemplate === b.pathTemplate;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The closest served endpoint to `wanted`, or null if nothing is close enough.
|
|
182
|
+
*
|
|
183
|
+
* Rules are tried in order of how certain they are, and the first hit wins —
|
|
184
|
+
* a method mismatch on an otherwise identical path is a better explanation than
|
|
185
|
+
* a one-segment difference somewhere else, and reporting both would be reporting
|
|
186
|
+
* one mistake twice.
|
|
187
|
+
*/
|
|
188
|
+
export function nearestMiss(wanted, served) {
|
|
189
|
+
const wantedPath = trimmed(wanted.pathTemplate);
|
|
190
|
+
const wantedSegments = segments(wantedPath);
|
|
191
|
+
// 1. Same path, different method.
|
|
192
|
+
for (const candidate of served) {
|
|
193
|
+
if (trimmed(candidate.pathTemplate) === wantedPath && candidate.method !== wanted.method) {
|
|
194
|
+
return {
|
|
195
|
+
kind: "METHOD",
|
|
196
|
+
against: candidate,
|
|
197
|
+
detail: `the path is served, but as ${candidate.method} rather than ${wanted.method}. ` +
|
|
198
|
+
"One side changed the verb.",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// 2. Same method, same segment count, exactly one segment differs, and both
|
|
203
|
+
// sides of that segment are literals.
|
|
204
|
+
for (const candidate of served) {
|
|
205
|
+
if (candidate.method !== wanted.method)
|
|
206
|
+
continue;
|
|
207
|
+
const candidateSegments = segments(trimmed(candidate.pathTemplate));
|
|
208
|
+
if (candidateSegments.length !== wantedSegments.length)
|
|
209
|
+
continue;
|
|
210
|
+
let differing = -1;
|
|
211
|
+
let count = 0;
|
|
212
|
+
for (let i = 0; i < wantedSegments.length; i += 1) {
|
|
213
|
+
if (wantedSegments[i] !== candidateSegments[i]) {
|
|
214
|
+
differing = i;
|
|
215
|
+
count += 1;
|
|
216
|
+
if (count > 1)
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (count !== 1)
|
|
221
|
+
continue;
|
|
222
|
+
const mine = wantedSegments[differing];
|
|
223
|
+
const theirs = candidateSegments[differing];
|
|
224
|
+
// A parameter opposite a literal is the correct, universal REST pattern —
|
|
225
|
+
// the literal is matched first and the parameter catches the rest. Calling
|
|
226
|
+
// it a near miss would report `/orders/{param}` and `/orders/recent` as a
|
|
227
|
+
// defect in every codebase.
|
|
228
|
+
if (mine === PARAM || theirs === PARAM)
|
|
229
|
+
continue;
|
|
230
|
+
// Two different words are not a near miss, and without this the rule fired
|
|
231
|
+
// on 51% of every real route table it was measured against. `continue`, not
|
|
232
|
+
// `return null`: a dissimilar candidate must not hide a similar one later
|
|
233
|
+
// in the list, which is how the rule used to name the wrong route.
|
|
234
|
+
if (!nearlyTheSameWord(mine, theirs))
|
|
235
|
+
continue;
|
|
236
|
+
return {
|
|
237
|
+
kind: "ONE_SEGMENT",
|
|
238
|
+
against: candidate,
|
|
239
|
+
detail: `a route differs from this call in exactly one segment: "${theirs}" where the call says ` +
|
|
240
|
+
`"${mine}". Everything else about the path and the method matches.`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/contracts/paths.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparing two declared shapes — the R3 half of §13.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a pure function over `attrs`, and that is the design rather
|
|
5
|
+
* than an accident. §13.2's mechanism is **shape comparison, not identifier
|
|
6
|
+
* matching**, and a shape reaches this file as a list of fields with optional
|
|
7
|
+
* per-field detail. No type here names a language, and nothing branches on one:
|
|
8
|
+
* a Pydantic model, an OpenAPI schema and a TypeScript interface arrive
|
|
9
|
+
* identically or they do not arrive at all.
|
|
10
|
+
*
|
|
11
|
+
* ## Direction is not symmetric, and getting that wrong produces noise
|
|
12
|
+
*
|
|
13
|
+
* A consumer that ignores a field the provider sends is **fine** — that is how
|
|
14
|
+
* every API client on earth works. A consumer that reads a field the provider
|
|
15
|
+
* does not send is **broken**. So the comparison takes an *expected* side and a
|
|
16
|
+
* *provided* side, and a field present only in `provided` is not a finding.
|
|
17
|
+
*
|
|
18
|
+
* A symmetric diff would report every unused response field in every codebase,
|
|
19
|
+
* which is both enormous and wrong.
|
|
20
|
+
*
|
|
21
|
+
* ## Detail is compared only when both sides have it
|
|
22
|
+
*
|
|
23
|
+
* A shape read from `attrs.fields` is a list of names — R2-grade evidence, good
|
|
24
|
+
* enough to say *this field is missing* and nothing else. A shape read from
|
|
25
|
+
* `attrs.fieldDetail` carries type, nullability and requiredness, which is what
|
|
26
|
+
* a type checker buys.
|
|
27
|
+
*
|
|
28
|
+
* When either side lacks detail, `comparedDetail` comes back false and **no type
|
|
29
|
+
* or nullability differences are emitted**. That flag is load-bearing: an empty
|
|
30
|
+
* difference list is otherwise indistinguishable from "the types agree", and
|
|
31
|
+
* reporting "no type mismatches" after comparing two lists of names is exactly
|
|
32
|
+
* the overstatement principle 3 exists to stop.
|
|
33
|
+
*/
|
|
34
|
+
import type { IRNode } from "@descryy/ir";
|
|
35
|
+
export interface ShapeField {
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly type?: string | undefined;
|
|
38
|
+
readonly nullable?: boolean | undefined;
|
|
39
|
+
readonly required?: boolean | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The field holds *many* of `type`, not one.
|
|
42
|
+
*
|
|
43
|
+
* **A separate flag rather than part of the type string, and that is rule 1.**
|
|
44
|
+
* The same field is written `Foo[]` by one producer, `List[Foo]` by another and
|
|
45
|
+
* `array` with an `items` schema by a third. Comparing those as strings is an
|
|
46
|
+
* engine above the IR deciding what a language's array syntax looks like, which
|
|
47
|
+
* is the leak the boundary exists to stop — and it fails in both directions at
|
|
48
|
+
* once: `Foo[]` vs `List[Foo]` reports a mismatch that is not there, while
|
|
49
|
+
* `Foo` vs `Foo` with one side an array reports nothing when the consumer is
|
|
50
|
+
* about to iterate an object.
|
|
51
|
+
*
|
|
52
|
+
* Producers put the **element** type in `type` and the cardinality here.
|
|
53
|
+
*/
|
|
54
|
+
readonly list?: boolean | undefined;
|
|
55
|
+
}
|
|
56
|
+
export interface Shape {
|
|
57
|
+
/** The node this was read from, so a finding can point at a file and a line. */
|
|
58
|
+
readonly source: IRNode;
|
|
59
|
+
readonly fields: readonly ShapeField[];
|
|
60
|
+
/** True when per-field type/nullability/requiredness is present — R3-grade. */
|
|
61
|
+
readonly detailed: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read a shape out of a node's `attrs`, or return null if it declares none.
|
|
65
|
+
*
|
|
66
|
+
* Two forms, in order of richness. `fieldDetail` is what a producer emits when a
|
|
67
|
+
* type checker or a schema gave it per-field facts; `fields` is the name-level
|
|
68
|
+
* fallback. Anything else is not a shape and is not guessed at.
|
|
69
|
+
*/
|
|
70
|
+
export declare function readShape(node: IRNode): Shape | null;
|
|
71
|
+
export type ShapeDifferenceKind = "FIELD_MISSING" | "TYPE_DIFFERS" | "NULLABILITY_DIFFERS" | "REQUIREDNESS_DIFFERS" | "LIST_DIFFERS";
|
|
72
|
+
export interface ShapeDifference {
|
|
73
|
+
readonly kind: ShapeDifferenceKind;
|
|
74
|
+
readonly field: string;
|
|
75
|
+
/** What the consuming side expects. */
|
|
76
|
+
readonly expected: string;
|
|
77
|
+
/** What the providing side declares. */
|
|
78
|
+
readonly provided: string;
|
|
79
|
+
readonly detail: string;
|
|
80
|
+
}
|
|
81
|
+
export interface ShapeComparison {
|
|
82
|
+
readonly differences: readonly ShapeDifference[];
|
|
83
|
+
/**
|
|
84
|
+
* True only when **both** sides carried per-field detail.
|
|
85
|
+
*
|
|
86
|
+
* When false, an empty `differences` means "no field is missing", not "the
|
|
87
|
+
* types agree" — and every consumer of this result has to say so.
|
|
88
|
+
*/
|
|
89
|
+
readonly comparedDetail: boolean;
|
|
90
|
+
readonly fieldsCompared: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* What the consuming side asks for that the providing side does not deliver.
|
|
94
|
+
*
|
|
95
|
+
* @param expected the shape the consumer reads
|
|
96
|
+
* @param provided the shape the producer declares
|
|
97
|
+
*/
|
|
98
|
+
export declare function compareShapes(expected: Shape, provided: Shape): ShapeComparison;
|
|
99
|
+
//# sourceMappingURL=shapes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shapes.d.ts","sourceRoot":"","sources":["../../src/contracts/shapes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACxC;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,KAAK;IACpB,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC;IACvC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAUpD;AAwDD,MAAM,MAAM,mBAAmB,GAC3B,eAAe,GACf,cAAc,GACd,qBAAqB,GACrB,sBAAsB,GACtB,cAAc,CAAC;AAEnB,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,wCAAwC;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAC;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,GAAG,eAAe,CAoG/E"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparing two declared shapes — the R3 half of §13.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a pure function over `attrs`, and that is the design rather
|
|
5
|
+
* than an accident. §13.2's mechanism is **shape comparison, not identifier
|
|
6
|
+
* matching**, and a shape reaches this file as a list of fields with optional
|
|
7
|
+
* per-field detail. No type here names a language, and nothing branches on one:
|
|
8
|
+
* a Pydantic model, an OpenAPI schema and a TypeScript interface arrive
|
|
9
|
+
* identically or they do not arrive at all.
|
|
10
|
+
*
|
|
11
|
+
* ## Direction is not symmetric, and getting that wrong produces noise
|
|
12
|
+
*
|
|
13
|
+
* A consumer that ignores a field the provider sends is **fine** — that is how
|
|
14
|
+
* every API client on earth works. A consumer that reads a field the provider
|
|
15
|
+
* does not send is **broken**. So the comparison takes an *expected* side and a
|
|
16
|
+
* *provided* side, and a field present only in `provided` is not a finding.
|
|
17
|
+
*
|
|
18
|
+
* A symmetric diff would report every unused response field in every codebase,
|
|
19
|
+
* which is both enormous and wrong.
|
|
20
|
+
*
|
|
21
|
+
* ## Detail is compared only when both sides have it
|
|
22
|
+
*
|
|
23
|
+
* A shape read from `attrs.fields` is a list of names — R2-grade evidence, good
|
|
24
|
+
* enough to say *this field is missing* and nothing else. A shape read from
|
|
25
|
+
* `attrs.fieldDetail` carries type, nullability and requiredness, which is what
|
|
26
|
+
* a type checker buys.
|
|
27
|
+
*
|
|
28
|
+
* When either side lacks detail, `comparedDetail` comes back false and **no type
|
|
29
|
+
* or nullability differences are emitted**. That flag is load-bearing: an empty
|
|
30
|
+
* difference list is otherwise indistinguishable from "the types agree", and
|
|
31
|
+
* reporting "no type mismatches" after comparing two lists of names is exactly
|
|
32
|
+
* the overstatement principle 3 exists to stop.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Read a shape out of a node's `attrs`, or return null if it declares none.
|
|
36
|
+
*
|
|
37
|
+
* Two forms, in order of richness. `fieldDetail` is what a producer emits when a
|
|
38
|
+
* type checker or a schema gave it per-field facts; `fields` is the name-level
|
|
39
|
+
* fallback. Anything else is not a shape and is not guessed at.
|
|
40
|
+
*/
|
|
41
|
+
export function readShape(node) {
|
|
42
|
+
const detail = readEntries(node.attrs["fieldDetail"]);
|
|
43
|
+
if (detail !== null && detail.fields.length > 0) {
|
|
44
|
+
return { source: node, fields: detail.fields, detailed: true };
|
|
45
|
+
}
|
|
46
|
+
const fields = readEntries(node.attrs["fields"]);
|
|
47
|
+
if (fields !== null && fields.fields.length > 0) {
|
|
48
|
+
return { source: node, fields: fields.fields, detailed: fields.detailed };
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read a field list in either spelling a producer actually uses.
|
|
54
|
+
*
|
|
55
|
+
* **Measured, not anticipated.** Two shipped adapters both write
|
|
56
|
+
* `attrs.fields` and disagree on what it holds: the contract reader writes a
|
|
57
|
+
* list of **names** with per-field detail alongside in `fieldDetail`, and the
|
|
58
|
+
* TypeScript adapter writes a list of **objects** (`{ name, nullable }`) under
|
|
59
|
+
* the same key. On real repositories, **77 of 77** shape pointers resolved to a
|
|
60
|
+
* node that plainly declared its fields, and every one read as *no shape at
|
|
61
|
+
* all*, because the reader filtered for strings and found none.
|
|
62
|
+
*
|
|
63
|
+
* The silence is the part worth fixing: a reader that cannot read a node which
|
|
64
|
+
* visibly declares fields, and says nothing, answers "no" where it should
|
|
65
|
+
* answer "I could not tell".
|
|
66
|
+
*
|
|
67
|
+
* Reading both beats forcing one spelling on two adapters. `attrs` is where
|
|
68
|
+
* language-specific detail lives and nothing above the IR may demand a single
|
|
69
|
+
* form of it. Neither adapter changed.
|
|
70
|
+
*
|
|
71
|
+
* `detailed` is derived rather than declared: an entry carrying anything beyond
|
|
72
|
+
* a name is per-field evidence. A producer emitting `{ name, nullable }` gets
|
|
73
|
+
* nullability compared and nothing else — exactly what it supplied.
|
|
74
|
+
*/
|
|
75
|
+
function readEntries(raw) {
|
|
76
|
+
if (!Array.isArray(raw))
|
|
77
|
+
return null;
|
|
78
|
+
const fields = [];
|
|
79
|
+
let detailed = false;
|
|
80
|
+
for (const entry of raw) {
|
|
81
|
+
if (typeof entry === "string") {
|
|
82
|
+
fields.push({ name: entry });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (typeof entry !== "object" || entry === null)
|
|
86
|
+
continue;
|
|
87
|
+
const record = entry;
|
|
88
|
+
const name = record["name"];
|
|
89
|
+
if (typeof name !== "string")
|
|
90
|
+
continue;
|
|
91
|
+
const field = {
|
|
92
|
+
name,
|
|
93
|
+
...(typeof record["type"] === "string" ? { type: record["type"] } : {}),
|
|
94
|
+
...(typeof record["nullable"] === "boolean" ? { nullable: record["nullable"] } : {}),
|
|
95
|
+
...(typeof record["required"] === "boolean" ? { required: record["required"] } : {}),
|
|
96
|
+
...(typeof record["list"] === "boolean" ? { list: record["list"] } : {}),
|
|
97
|
+
};
|
|
98
|
+
if (Object.keys(field).length > 1)
|
|
99
|
+
detailed = true;
|
|
100
|
+
fields.push(field);
|
|
101
|
+
}
|
|
102
|
+
return { fields, detailed };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* What the consuming side asks for that the providing side does not deliver.
|
|
106
|
+
*
|
|
107
|
+
* @param expected the shape the consumer reads
|
|
108
|
+
* @param provided the shape the producer declares
|
|
109
|
+
*/
|
|
110
|
+
export function compareShapes(expected, provided) {
|
|
111
|
+
const byName = new Map(provided.fields.map((f) => [f.name, f]));
|
|
112
|
+
const comparedDetail = expected.detailed && provided.detailed;
|
|
113
|
+
const differences = [];
|
|
114
|
+
for (const want of expected.fields) {
|
|
115
|
+
const have = byName.get(want.name);
|
|
116
|
+
if (have === undefined) {
|
|
117
|
+
differences.push({
|
|
118
|
+
kind: "FIELD_MISSING",
|
|
119
|
+
field: want.name,
|
|
120
|
+
expected: "present",
|
|
121
|
+
provided: "absent",
|
|
122
|
+
detail: `"${want.name}" is read from ${expected.source.name} and is not declared by ` +
|
|
123
|
+
`${provided.source.name}.`,
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Below here every claim is about a *shape*, which no amount of name
|
|
128
|
+
// resolution establishes. Skipped entirely rather than compared against
|
|
129
|
+
// undefined — `undefined !== "string"` would report a type mismatch on every
|
|
130
|
+
// field of every name-level shape.
|
|
131
|
+
if (!comparedDetail)
|
|
132
|
+
continue;
|
|
133
|
+
if (want.type !== undefined && have.type !== undefined && want.type !== have.type) {
|
|
134
|
+
differences.push({
|
|
135
|
+
kind: "TYPE_DIFFERS",
|
|
136
|
+
field: want.name,
|
|
137
|
+
expected: want.type,
|
|
138
|
+
provided: have.type,
|
|
139
|
+
detail: `"${want.name}" is read as ${want.type} and declared as ${have.type}.`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// Cardinality, and **unlike nullability this one is symmetric**. A consumer
|
|
143
|
+
// that iterates what the provider sends as a single object breaks, and a
|
|
144
|
+
// consumer that reads fields off what the provider sends as an array breaks
|
|
145
|
+
// too. Neither direction is the safe one, so both are reported.
|
|
146
|
+
//
|
|
147
|
+
// Compared only when **both sides say**. An absent `list` is *unknown*, not
|
|
148
|
+
// `false`. A producer that never emits the flag would otherwise read as
|
|
149
|
+
// declaring every field singular, and every array field on the other side
|
|
150
|
+
// would come back as a finding — the 77-of-77 conflation running the other
|
|
151
|
+
// way. There the reader saw no shape and answered "no fields"; here it
|
|
152
|
+
// would see no flag and answer "not a list". Absence of evidence is allowed
|
|
153
|
+
// to stay absence of evidence.
|
|
154
|
+
if (want.list !== undefined && have.list !== undefined && want.list !== have.list) {
|
|
155
|
+
const asRead = want.list ? "a list" : "a single value";
|
|
156
|
+
const asDeclared = have.list ? "a list" : "a single value";
|
|
157
|
+
differences.push({
|
|
158
|
+
kind: "LIST_DIFFERS",
|
|
159
|
+
field: want.name,
|
|
160
|
+
expected: asRead,
|
|
161
|
+
provided: asDeclared,
|
|
162
|
+
detail: `"${want.name}" is read from ${expected.source.name} as ${asRead} and declared by ` +
|
|
163
|
+
`${provided.source.name} as ${asDeclared}.`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// A field the provider may return as null and the consumer does not expect
|
|
167
|
+
// to be null. The reverse — consumer tolerates null, provider never sends it
|
|
168
|
+
// — is safe and is not reported.
|
|
169
|
+
if (want.nullable === false && have.nullable === true) {
|
|
170
|
+
differences.push({
|
|
171
|
+
kind: "NULLABILITY_DIFFERS",
|
|
172
|
+
field: want.name,
|
|
173
|
+
expected: "not null",
|
|
174
|
+
provided: "nullable",
|
|
175
|
+
detail: `"${want.name}" may be null according to ${provided.source.name}, and ` +
|
|
176
|
+
`${expected.source.name} reads it as though it never is.`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// A field the consumer requires and the provider may omit. `required` and
|
|
180
|
+
// `nullable` assert different things — a field absent from `required` may be
|
|
181
|
+
// *omitted*, not set to null — so they are checked apart.
|
|
182
|
+
if (want.required === true && have.required === false) {
|
|
183
|
+
differences.push({
|
|
184
|
+
kind: "REQUIREDNESS_DIFFERS",
|
|
185
|
+
field: want.name,
|
|
186
|
+
expected: "always present",
|
|
187
|
+
provided: "optional",
|
|
188
|
+
detail: `"${want.name}" is required by ${expected.source.name} and optional in ` +
|
|
189
|
+
`${provided.source.name}, so it may be absent entirely.`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
differences.sort((a, b) => a.field.localeCompare(b.field) || a.kind.localeCompare(b.kind));
|
|
194
|
+
return { differences, comparedDetail, fieldsCompared: expected.fields.length };
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=shapes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shapes.js","sourceRoot":"","sources":["../../src/contracts/shapes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hard per-run budget — §41 rule 16, `ai-layer-plan.md` §11 (Phase 4,
|
|
3
|
+
* P17/P18). `ai-governance-spec.md` §8 names this explicitly as *not* its
|
|
4
|
+
* document's job: *"a property of the harness (Phase 4, P17/P18), not of a
|
|
5
|
+
* per-finding gate... Full spec: Phase 4 (P17), not this document."* This
|
|
6
|
+
* module is that property.
|
|
7
|
+
*
|
|
8
|
+
* *"Enforced by the runtime harness, not by the AI's own judgment of when to
|
|
9
|
+
* stop"* (`descry-ai-layer.md`, "Execution budget"). A cap the model can
|
|
10
|
+
* reason its way past is not a cap (§41 rule 16's own words) — so, same as
|
|
11
|
+
* G1–G4, this is a pure function the pipeline calls, never a prompt
|
|
12
|
+
* instruction.
|
|
13
|
+
*
|
|
14
|
+
* ## Four caps, two scopes
|
|
15
|
+
*
|
|
16
|
+
* `ai-layer-plan.md` §11 names four: *"max turns per investigation · max
|
|
17
|
+
* cost per run · max concurrent open hypotheses · max tool calls per
|
|
18
|
+
* hypothesis."* Two are per-run (cost, concurrency); two are per-hypothesis
|
|
19
|
+
* (turns, tool calls) and are checked against every currently-open
|
|
20
|
+
* hypothesis independently — one hypothesis running long does not budget-fail
|
|
21
|
+
* a sibling that has spent nothing.
|
|
22
|
+
*
|
|
23
|
+
* ## The default values
|
|
24
|
+
*
|
|
25
|
+
* `documents/architecture/investigation-budget-spec.md` (P17) is where these
|
|
26
|
+
* are ruled and justified; this module imports them from there in spirit —
|
|
27
|
+
* they are declared once, here, because the spec document is prose and this
|
|
28
|
+
* is the value the harness actually reads. A caller may always supply its
|
|
29
|
+
* own {@link InvestigationBudget}; the exported default is a starting point,
|
|
30
|
+
* tuned from data, the same convention `CLAUDE.md` already applies to node
|
|
31
|
+
* budgets.
|
|
32
|
+
*/
|
|
33
|
+
/** The four caps, `ai-layer-plan.md` §11's own names. */
|
|
34
|
+
export interface InvestigationBudget {
|
|
35
|
+
/** Loop iterations spent investigating one hypothesis before it is forced to a terminal state. */
|
|
36
|
+
readonly maxTurnsPerInvestigation: number;
|
|
37
|
+
/** Tool calls issued investigating one hypothesis. */
|
|
38
|
+
readonly maxToolCallsPerHypothesis: number;
|
|
39
|
+
/** Hypotheses allowed in `investigating` or `supported` at once, run-wide. */
|
|
40
|
+
readonly maxConcurrentOpenHypotheses: number;
|
|
41
|
+
/** Total spend for the whole run, in USD. */
|
|
42
|
+
readonly costCeilingPerRunUsd: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* P17's ruled starting values — `documents/architecture/investigation-budget-spec.md`.
|
|
46
|
+
* Not a measurement; a starting point to tune from real runs, same as the
|
|
47
|
+
* node budgets `CLAUDE.md` already treats this way.
|
|
48
|
+
*/
|
|
49
|
+
export declare const DEFAULT_INVESTIGATION_BUDGET: InvestigationBudget;
|
|
50
|
+
/** Usage accumulated for one open hypothesis. */
|
|
51
|
+
export interface HypothesisBudgetUsage {
|
|
52
|
+
readonly hypothesisId: string;
|
|
53
|
+
readonly turnsSpent: number;
|
|
54
|
+
readonly toolCallsSpent: number;
|
|
55
|
+
}
|
|
56
|
+
/** Usage accumulated for the whole run, as of the moment this is checked. */
|
|
57
|
+
export interface RunBudgetUsage {
|
|
58
|
+
readonly costSpentUsd: number;
|
|
59
|
+
/** Every hypothesis currently `investigating` or `supported` — not `candidate`, and not a terminal state. */
|
|
60
|
+
readonly openHypotheses: readonly HypothesisBudgetUsage[];
|
|
61
|
+
}
|
|
62
|
+
export declare const BUDGET_CAP_NAMES: readonly ["maxTurnsPerInvestigation", "maxToolCallsPerHypothesis", "maxConcurrentOpenHypotheses", "costCeilingPerRunUsd"];
|
|
63
|
+
export type BudgetCapName = (typeof BUDGET_CAP_NAMES)[number];
|
|
64
|
+
export interface BudgetCapExceeded {
|
|
65
|
+
readonly cap: BudgetCapName;
|
|
66
|
+
readonly limit: number;
|
|
67
|
+
readonly actual: number;
|
|
68
|
+
/** Which hypothesis this violation belongs to — absent for the two run-level caps. */
|
|
69
|
+
readonly hypothesisId?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface BudgetCheckResult {
|
|
72
|
+
readonly withinBudget: boolean;
|
|
73
|
+
/** Every cap currently exceeded — empty when `withinBudget`. More than one may fire at once. */
|
|
74
|
+
readonly exceeded: readonly BudgetCapExceeded[];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Check `usage` against `budget`. Never throws — this is a check, not an
|
|
78
|
+
* execution of the thing it is checking, same posture as G1–G4.
|
|
79
|
+
*/
|
|
80
|
+
export declare function checkRunBudget(budget: InvestigationBudget, usage: RunBudgetUsage): BudgetCheckResult;
|
|
81
|
+
/**
|
|
82
|
+
* Whether a specific hypothesis has reached its per-hypothesis round limit —
|
|
83
|
+
* the input `../../ir/src/hypothesis.ts`'s `deriveHypothesisState` needs for
|
|
84
|
+
* its `roundLimitReached` field. A thin, named convenience over
|
|
85
|
+
* {@link checkRunBudget} so a caller does not have to re-derive "does this
|
|
86
|
+
* one hypothesis's own cap show up in `exceeded`" by hand.
|
|
87
|
+
*/
|
|
88
|
+
export declare function hypothesisRoundLimitReached(budget: InvestigationBudget, usage: HypothesisBudgetUsage): boolean;
|
|
89
|
+
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../../src/governance/budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IAClC,kGAAkG;IAClG,QAAQ,CAAC,wBAAwB,EAAE,MAAM,CAAC;IAC1C,sDAAsD;IACtD,QAAQ,CAAC,yBAAyB,EAAE,MAAM,CAAC;IAC3C,8EAA8E;IAC9E,QAAQ,CAAC,2BAA2B,EAAE,MAAM,CAAC;IAC7C,6CAA6C;IAC7C,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;CACvC;AAED;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,EAAE,mBAK1C,CAAC;AAEF,iDAAiD;AACjD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,6GAA6G;IAC7G,QAAQ,CAAC,cAAc,EAAE,SAAS,qBAAqB,EAAE,CAAC;CAC3D;AAED,eAAO,MAAM,gBAAgB,2HAKnB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,gGAAgG;IAChG,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACjD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,iBAAiB,CAuCpG;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAKT"}
|