diff --git a/CHANGELOG.md b/CHANGELOG.md index 064f03285..273c2d195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Extension preview +- Prepare immutable Drupal extension 0.1.1: ignore unrelated static receivers and commented/quoted declarations, and resolve both explicit Symfony event argument orders. Preserve the reviewed 0.1.0 artifact and policy. +- Keep installed and selected marketplace versions distinct while compatibility metadata refreshes; pending requests no longer display an incompatible catalog version as installed. + - Add project-scoped framework and semantic extensions, managed package installation, worker loading, and graph refresh on install/update/enable/disable/remove. Failed activation preserves the previous project configuration and graph. - Add an external Drupal extension for declared routes/forms, services, hooks, plugins and event subscribers. Literal route paths can be explore endpoints; extension edges retain their labels and registration sites. Comments and quoted examples do not count as PHP invocations. - Add a local marketplace preview with signed publisher submissions, immutable releases, publisher ownership checks and a companion connection for project selection. Mobile detail pages retain version, compatibility and source information. Hosted deployment remains follow-up work; this is not a release announcement. diff --git a/docs/design/framework-coverage.md b/docs/design/framework-coverage.md index 57043ce44..0b42d8e00 100644 --- a/docs/design/framework-coverage.md +++ b/docs/design/framework-coverage.md @@ -4,11 +4,26 @@ queries in [Checking this file is still true](#checking-this-file-is-still-true) before trusting a row; this is a snapshot, not a live view. -**Extension preview, 2026-09-22:** the optional external Drupal package adds -declared route/form handlers, service injection, literal hook/event dispatch -and plugin implementation links. It does not add Drupal screen navigation or -PHP branch guards. Scope and reproducible validation are recorded in -[the extension recovery report](../validation/extensions-recovery-20260922.md). +**Extension preview, 2026-09-22:** Drupal 0.1.1 supports explicit `_controller` +and `_form` route defaults, unique YAML service classes/aliases and literal +constructor arguments, literal service lookups, attached hook documentation or +`Hook` attributes, explicit plugin IDs from attached annotations/attributes, +and named event dispatch/subscriber links. Both Symfony `dispatch(name, event)` +and `dispatch(event, name)` forms require an explicit literal/constant name. +Commented calls, quoted declaration examples, unrelated `NotDrupal::service`, +unknown targets, cyclic service aliases, computed IDs and duplicate plugin IDs +are covered by negative checks. + +The review samples 28 exact links across pinned Pathauto, Commerce and Drupal +core, plus four unresolved corpus cases and focused synthetic positives/negatives. +It is not an exhaustive precision/recall measurement. Dynamic entity forms, +container factories/decorators, autowiring, computed dispatch names, single-argument +object event inference, grouped imports and multiple namespaces per file are not +certified. Receiver matching for hook/event/plugin APIs remains heuristic, not +PHP runtime type analysis. No Drupal screen navigation or PHP branch guards are +added. Immutable 0.1.0 is retained alongside 0.1.1. See the +[Drupal review report](../validation/extensions-drupal-review-20260922.md) for +exact source, measurements, exclusions and remaining limits. This file exists to be read cold. It says, for every framework and language the README claims, **which of the three pictures it can draw today** and what is diff --git a/docs/extensions/official-drupal-import.md b/docs/extensions/official-drupal-import.md index 363507015..1c6a9b2e7 100644 --- a/docs/extensions/official-drupal-import.md +++ b/docs/extensions/official-drupal-import.md @@ -4,9 +4,16 @@ The Cloudflare registry's public `/api/publish` endpoint cannot assign official ## Trust and immutable publication -`marketplace/cloudflare/official-policy.json` is a reviewed allowlist, committed alongside the operator. It pins Drupal `0.1.0`, package name, exact 289,167-byte artifact SHA-256, bundled entry hash, source repository/revision/path/hash and official metadata. The trusted identity is `publisherId: codegraph`, display name `CodeGraph`; it is distinct from the 64-hex SPKI identities assigned by community signatures. The policy is an operator trust decision anchored in reviewed source, **not a claim of a new signed upstream release**. Changing the policy requires review and a new immutable version when bytes change. +`marketplace/cloudflare/official-policy.json` is a reviewed allowlist, committed alongside the operator. It retains Drupal `0.1.0` unchanged and adds the reviewed `0.1.1` accuracy revision. Each entry pins package name, version, byte length, artifact SHA-256, bundled entry hash, source repository/revision/path/hash and official metadata. The trusted identity is `publisherId: codegraph`, display name `CodeGraph`; it is distinct from the 64-hex SPKI identities assigned by community signatures. The policy is an operator trust decision anchored in reviewed source, **not a claim of a new signed upstream release**. Changing the policy requires review and a new immutable version when bytes change. -Artifact provenance: `dist/extensions/drupal.cgext` SHA-256 `e5015747d02fe50d6af607b6c44f1db9cf7969a0070866ed03c7643377a459c8`; source `extensions/drupal/index.cjs` at `37d4dd837120c1d56316c3cc65b6587b743a9112`. The existing `node scripts/build-extensions.mjs` build is reproducible with repository-locked dependencies. The command validates the complete artifact, supported extension API and the target engine version before any write, then enforces the pinned identity/integrity/entry provenance. +Artifact provenance: + +| Version | Artifact | SHA-256 | Reviewed source | +|---|---|---|---| +| 0.1.0 | `dist/extensions/drupal-0.1.0.cgext` (289,167 bytes) | `e5015747d02fe50d6af607b6c44f1db9cf7969a0070866ed03c7643377a459c8` | `37d4dd837120c1d56316c3cc65b6587b743a9112` | +| 0.1.1 | `dist/extensions/drupal-0.1.1.cgext` (291,889 bytes); also current `drupal.cgext` | `e5ed73bee51524585e4f4da1d5017fdee3037a3fa71d8f08edab6c318fedc59a` | `b27bf875c95d3b82a0df411f68d0d8497993667b` | + +The build preserves the archived 0.1.0 package under `extensions/drupal/releases/` and copies it byte-for-byte. `node scripts/build-extensions.mjs` builds 0.1.1 with repository-locked dependencies. The command validates the complete artifact, supported extension API and target engine before any write, then enforces pinned identity/integrity/entry provenance. Publish 0.1.1 as a new version; never replace 0.1.0. Both can coexist and repeated identical imports are idempotent. Each plan includes an explicit registry identity and destination. `apply` requires the SHA-256 of the exact reviewed plan and validates its fields against the policy again. A different registry, unsupported engine, malformed artifact, changed plan, unexpected package or digest mismatch fails without publication. Plans and receipts contain public metadata and paths; credentials are never placed in either. @@ -80,4 +87,4 @@ PLAYWRIGHT_MODULE=/absolute/path/to/playwright PATHAUTO_CORPUS=/absolute/pinned/ The browser test exports clean Pathauto source at `b97aadf47a37cff25f7d6105c3dade6778fccb47` into a disposable project. It uses the real CLI and local workerd/D1/R2, connects the browser once, clicks Install once, and verifies two route handlers, one hook and one service injection in actual SQLite. It proves malformed/incompatible/tampered failure preservation, disable/enable/remove cleanup, official catalog/detail/filter, and desktop/mobile layout. It does not modify the source corpus or rerun the larger corpus benchmark. -[Hosting/cost limits](cloudflare-hosting.md) remain unchanged: no durable Cloudflare URL, provider persistence, remote backup proof or Free-plan CPU fit is established. `marketplace.getcodegraph.com` remains proposed. Broader Drupal accuracy/performance review and current integrated full regression remain later milestones; historical measured overhead is 22–30%. +[Hosting/cost limits](cloudflare-hosting.md) remain unchanged: no durable Cloudflare URL, provider persistence, remote backup proof or Free-plan CPU fit is established. `marketplace.getcodegraph.com` remains proposed. The Drupal review evidence is recorded separately; historical measured rebuild overhead was 22–30%, and local rebuild timings do not establish Workers Free CPU suitability. diff --git a/scripts/validation/drupal-pass-profile.cjs b/scripts/validation/drupal-pass-profile.cjs new file mode 100644 index 000000000..a7a567951 --- /dev/null +++ b/scripts/validation/drupal-pass-profile.cjs @@ -0,0 +1,22 @@ +// Isolate the two versioned wiring callbacks over one identical, read-only +// node/source snapshot. This is not a whole-index or graph-equivalence test. +const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict'),crypto=require('node:crypto'); +const {DatabaseSync}=require('node:sqlite');const {QueryBuilder}=require('../../dist/db/queries'); +const [project,output]=process.argv.slice(2),scratch=fs.mkdtempSync(path.join(os.tmpdir(),'drupal-pass-profile-')); +const hash=b=>crypto.createHash('sha256').update(b).digest('hex'); +(async()=>{const db=new DatabaseSync(path.join(project,'.codegraph/codegraph.db'),{readOnly:true}); + try{ + const q=new QueryBuilder(db),all=q.getAllNodes(),byFile=new Map(),byName=new Map(); + for(const n of all){for(const [map,key]of [[byFile,n.filePath],[byName,n.name]]){if(!map.has(key))map.set(key,[]);map.get(key).push(n);}} + const files=db.prepare('SELECT path FROM files ORDER BY path').all().map(f=>f.path),sources=new Map(files.map(f=>[f,fs.readFileSync(path.join(project,f),'utf8')])); + const ctx={getAllFiles:()=>files,getNodesInFile:f=>byFile.get(f)||[],getNodesByName:n=>byName.get(n)||[],readFile:f=>sources.get(f)||null}; + const passes={},artifacts={};for(const v of ['0.1.0','0.1.1']){const bytes=fs.readFileSync(`dist/extensions/drupal-${v}.cgext`),p=JSON.parse(bytes),file=path.join(scratch,v+'.cjs');fs.writeFileSync(file,p.files[p.package.main]);passes[v]=require(file)().synthPasses[0];artifacts[v]=hash(bytes);} + const record={project,files:files.length,nodes:all.length,artifacts,description:'Warm in-memory same snapshot; callback only; changed versions have intentionally different results',samples:[]}; + for(let iteration=1;iteration<=3;iteration++)for(const version of (iteration%2?['0.1.0','0.1.1']:['0.1.1','0.1.0'])){ + const cpu=process.cpuUsage(),start=performance.now();const edges=await passes[version].run(ctx,()=>new Promise(resolve=>setImmediate(resolve))); + const sample={iteration,version,wallMs:performance.now()-start,cpu:process.cpuUsage(cpu),edges:edges.length,hash:hash(JSON.stringify(edges))};record.samples.push(sample);console.log(sample); + const prior=record.samples.find(s=>s.version===version&&s!==sample);if(prior)assert.equal(sample.hash,prior.hash); + fs.mkdirSync(path.dirname(output),{recursive:true});fs.writeFileSync(output,JSON.stringify(record,null,2)); + } + }finally{db.close();fs.rmSync(scratch,{recursive:true,force:true});} +})().catch(e=>{console.error(e);process.exitCode=1;}); diff --git a/scripts/validation/drupal-review-incremental.cjs b/scripts/validation/drupal-review-incremental.cjs index 6c8b32524..e8b890eca 100644 --- a/scripts/validation/drupal-review-incremental.cjs +++ b/scripts/validation/drupal-review-incremental.cjs @@ -15,7 +15,7 @@ function snapshot(){const db=new DatabaseSync(path.join(project,'.codegraph/code for(const file of files)assert.ok(!fs.existsSync(path.join(project,file))); fs.writeFileSync(path.join(project,files[0]),` 'onReview']; } public function onReview($event) {} } +class ReviewSubscriber implements \\Symfony\\Component\\EventDispatcher\\EventSubscriberInterface { public static function getSubscribedEvents() { return ['review.convergence' => 'onReview']; } public function onReview($event) {} } class ReviewDispatch { public function send($dispatcher,$event) { $dispatcher->dispatch('review.convergence', $event); } } `); fs.writeFileSync(path.join(project,files[1]),"review.route:\n path: '/__review_unknown'\n defaults:\n _controller: '\\Missing\\Unknown::missing'\n");