From 7e674aa0fa7dc6603dcabbedaf5f0856c06f24b4 Mon Sep 17 00:00:00 2001 From: Forge Date: Tue, 22 Sep 2026 13:08:13 +0000 Subject: [PATCH] feat: pin reviewed Drupal 0.1.1 while retaining immutable 0.1.0 --- marketplace/cloudflare/official-policy.json | 47 +++++++++++++------ marketplace/cloudflare/official-test.mjs | 3 +- .../validation/cloudflare-official-drupal.cjs | 5 +- scripts/validation/drupal-review-sample.cjs | 4 +- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/marketplace/cloudflare/official-policy.json b/marketplace/cloudflare/official-policy.json index e680e30d7..bdf283225 100644 --- a/marketplace/cloudflare/official-policy.json +++ b/marketplace/cloudflare/official-policy.json @@ -2,19 +2,36 @@ "format": "codegraph-official-policy-1", "publisherId": "codegraph", "publisher": "CodeGraph", - "releases": [{ - "id": "drupal", - "version": "0.1.0", - "packageName": "@colbymchenry/codegraph-drupal", - "sha256": "e5015747d02fe50d6af607b6c44f1db9cf7969a0070866ed03c7643377a459c8", - "bytes": 289167, - "repository": "https://github.com/colbymchenry/codegraph", - "sourceRevision": "37d4dd837120c1d56316c3cc65b6587b743a9112", - "sourcePath": "extensions/drupal/index.cjs", - "sourceSha256": "eb7688957a14edd36efd8c109283a7355ceb7dd8199987fe3264c3f37489e841", - "entrySha256": "0e6d117bab017fd4b0a6f09fe2289bf42c4b597548228be244b5729f7a022dd8", - "name": "Drupal", - "description": "Follow Drupal routes, services, hooks, plugins and events through your codebase.", - "readme": "Official CodeGraph Drupal extension preview. Routes and forms connect to their handlers; services connect to implementations and explicit injections; literal hooks and events connect to declared implementations.\n\nInstalling replaces the built-in Drupal resolver for this project. Requires CodeGraph >=1.6.0 <2 with extension support.\n\nComputed identifiers, missing external dependencies, ambiguous classes and unknown entity handlers remain unresolved. Broader Drupal coverage and performance review remain open." - }] + "releases": [ + { + "id": "drupal", + "version": "0.1.0", + "packageName": "@colbymchenry/codegraph-drupal", + "sha256": "e5015747d02fe50d6af607b6c44f1db9cf7969a0070866ed03c7643377a459c8", + "bytes": 289167, + "repository": "https://github.com/colbymchenry/codegraph", + "sourceRevision": "37d4dd837120c1d56316c3cc65b6587b743a9112", + "sourcePath": "extensions/drupal/index.cjs", + "sourceSha256": "eb7688957a14edd36efd8c109283a7355ceb7dd8199987fe3264c3f37489e841", + "entrySha256": "0e6d117bab017fd4b0a6f09fe2289bf42c4b597548228be244b5729f7a022dd8", + "name": "Drupal", + "description": "Follow Drupal routes, services, hooks, plugins and events through your codebase.", + "readme": "Official CodeGraph Drupal extension preview. Routes and forms connect to their handlers; services connect to implementations and explicit injections; literal hooks and events connect to declared implementations.\n\nInstalling replaces the built-in Drupal resolver for this project. Requires CodeGraph >=1.6.0 <2 with extension support.\n\nComputed identifiers, missing external dependencies, ambiguous classes and unknown entity handlers remain unresolved. Broader Drupal coverage and performance review remain open." + }, + { + "id": "drupal", + "version": "0.1.1", + "packageName": "@colbymchenry/codegraph-drupal", + "sha256": "e5ed73bee51524585e4f4da1d5017fdee3037a3fa71d8f08edab6c318fedc59a", + "bytes": 291889, + "repository": "https://github.com/colbymchenry/codegraph", + "sourceRevision": "b27bf875c95d3b82a0df411f68d0d8497993667b", + "sourcePath": "extensions/drupal/index.cjs", + "sourceSha256": "e9a5f13baaf0ba53ab17f0e348edd73ba91c80a11034746372c808eda8e3714d", + "entrySha256": "17aa1ea7feed33cb42e010d9eae15f4a07035987ced3b9cd0f6f39a2ee6adc91", + "name": "Drupal", + "description": "Follow Drupal routes, services, hooks, plugins and events through your codebase.", + "readme": "Official CodeGraph Drupal extension 0.1.1 preview. Rejects commented/quoted declarations and handles explicit legacy and modern Symfony event argument order. Routes and forms connect to their handlers; services connect to implementations and explicit injections; literal hooks and events connect to declared implementations.\n\nInstalling replaces the built-in Drupal resolver for this project. Requires CodeGraph >=1.6.0 <2 with extension support.\n\nComputed identifiers, missing external dependencies, ambiguous classes and unknown entity handlers remain unresolved. Broader Drupal coverage and performance review remain open." + } + ] } diff --git a/marketplace/cloudflare/official-test.mjs b/marketplace/cloudflare/official-test.mjs index c3f16103e..c633620b0 100644 --- a/marketplace/cloudflare/official-test.mjs +++ b/marketplace/cloudflare/official-test.mjs @@ -25,6 +25,7 @@ try{ assert.throws(()=>makePlan(Buffer.concat([bytes,Buffer.from(' ')]),s.target,artifact),/integrity/);pass('tampered but valid JSON package rejected by reviewed provenance pin'); await assert.rejects(()=>importOfficial(s.db,s.bucket,{...plan,listing:{...plan.listing,name:'forged'}},bytes),/plan changed/);pass('edited publication plan rejected'); const wrong=makePlan(bytes,{...s.target,registryId:'0'.repeat(32)},artifact);await assert.rejects(()=>importOfficial(s.db,s.bucket,wrong,bytes),/identity/);pass('wrong registry identity rejected'); + const previousBytes=fs.readFileSync(path.join(root,'dist/extensions/drupal-0.1.0.cgext'));const previousPlan=makePlan(previousBytes,s.target,path.join(root,'dist/extensions/drupal-0.1.0.cgext'));assert.equal((await importOfficial(s.db,s.bucket,previousPlan,previousBytes)).status,'imported');assert.equal((await importOfficial(s.db,s.bucket,previousPlan,previousBytes)).status,'unchanged');pass('historical 0.1.0 remains immutable and importable alongside the reviewed current version'); const result=await importOfficial(s.db,s.bucket,plan,bytes);assert.equal(result.status,'imported');assert.equal(result.listing.publisherId,'codegraph');assert.equal(result.listing.official,true);assert.deepEqual(Buffer.from(await(await s.bucket.get('packages/sha256/'+plan.listing.integrity)).arrayBuffer()),bytes);pass('official identity provenance and exact bytes published through real D1/R2'); assert.equal((await importOfficial(s.db,s.bucket,makePlan(bytes,s.target,artifact),bytes)).status,'unchanged');pass('safe repeated plan is idempotent and retains original listing'); const other=JSON.parse(bytes);other.package.codegraph.id='unofficial';other.package.name='unofficial';const community=Buffer.from(JSON.stringify(other)); @@ -32,7 +33,7 @@ try{ const normal=await submit(s,signed(community),201);assert.equal(normal.official,false);assert.notEqual(normal.publisherId,'codegraph');pass('public API rejects official identity spoof while preserving community publication'); await submit(s,signed(bytes),400);pass('community cannot replace reserved official Drupal ownership'); const owner=await servicesStart('owner-conflict');await submit(owner,signed(bytes),201);await assert.rejects(()=>importOfficial(owner.db,owner.bucket,makePlan(bytes,owner.target,artifact),bytes),/ownership conflict/);pass('prior community ownership is not stolen by operator import');await owner.close(); - const version=await servicesStart('version-conflict'),vp=makePlan(bytes,version.target,artifact);await version.db.prepare('INSERT INTO releases VALUES(?,?,?,?,?,?,?)').bind('drupal','0.1.0','codegraph',JSON.stringify({...vp.listing,name:'existing immutable listing'}),'packages/sha256/'+vp.listing.integrity,vp.listing.integrity,bytes.length).run();await assert.rejects(()=>importOfficial(version.db,version.bucket,vp,bytes),/version conflict/);pass('existing immutable version metadata conflict is not overwritten');await version.close(); + const version=await servicesStart('version-conflict'),vp=makePlan(bytes,version.target,artifact);await version.db.prepare('INSERT INTO releases VALUES(?,?,?,?,?,?,?)').bind('drupal',vp.listing.version,'codegraph',JSON.stringify({...vp.listing,name:'existing immutable listing'}),'packages/sha256/'+vp.listing.integrity,vp.listing.integrity,bytes.length).run();await assert.rejects(()=>importOfficial(version.db,version.bucket,vp,bytes),/version conflict/);pass('existing immutable version metadata conflict is not overwritten');await version.close(); const race=await servicesStart('race'),rp=makePlan(bytes,race.target,artifact);const results=await Promise.all([importOfficial(race.db,race.bucket,rp,bytes),importOfficial(race.db,race.bucket,rp,bytes)]);assert.deepEqual(results.map(r=>r.status).sort(),['imported','unchanged']);pass('concurrent identical official imports converge to one release');await race.close(); const failure=await servicesStart('failure'),fp=makePlan(bytes,failure.target,artifact);await assert.rejects(()=>importOfficial(failure.db,{put:async()=>{throw Error('R2 failure');}},fp,bytes),/R2 failure/);assert.equal(await failure.db.prepare('SELECT count(*) n FROM extensions').first('n'),0); await failure.db.exec("CREATE TRIGGER reject_official AFTER INSERT ON releases BEGIN SELECT RAISE(ABORT,'fixture D1 failure'); END;");await assert.rejects(()=>importOfficial(failure.db,failure.bucket,fp,bytes));assert.equal(await failure.db.prepare('SELECT count(*) n FROM extensions').first('n'),0);assert.equal(await failure.db.prepare('SELECT count(*) n FROM releases').first('n'),0);await failure.db.exec('DROP TRIGGER reject_official');assert.equal((await importOfficial(failure.db,failure.bucket,fp,bytes)).status,'imported');pass('R2 and D1 failures expose no release or orphan owner; exact retry succeeds');await failure.close(); diff --git a/scripts/validation/cloudflare-official-drupal.cjs b/scripts/validation/cloudflare-official-drupal.cjs index 93d3b1d01..4e08705db 100644 --- a/scripts/validation/cloudflare-official-drupal.cjs +++ b/scripts/validation/cloudflare-official-drupal.cjs @@ -11,6 +11,7 @@ const lab=fs.mkdtempSync(path.join(os.tmpdir(),'cg-official-browser-')),corpus=p const project=path.join(lab,'pathauto'),other=path.join(lab,'untouched'),state=path.join(lab,'registry'),artifact=path.join(root,'dist/extensions/drupal.cgext'),bytes=fs.readFileSync(artifact),checks=[],commands=[]; fs.mkdirSync(project);fs.mkdirSync(other);fs.writeFileSync(path.join(other,'example.php'),'crypto.createHash('sha256').update(b).digest('hex'); function snapshot(){const db=new DatabaseSync(path.join(project,'.codegraph/codegraph.db'),{readOnly:true});try{const nodes=db.prepare('SELECT * FROM nodes ORDER BY id').all().map(({updated_at,...n})=>n),edges=db.prepare('SELECT source,target,kind,metadata,line,col,provenance FROM edges ORDER BY source,target,kind,line,col,metadata').all();return {nodes,edges,pluginEdges:edges.filter(e=>JSON.parse(e.metadata||'{}').synthesizedBy==='drupal'),hash:hash(JSON.stringify({nodes,edges})),config:fs.existsSync(path.join(project,'codegraph.json'))?fs.readFileSync(path.join(project,'codegraph.json'),'utf8'):null};}finally{db.close();}} function verify(data){const definitions=[['/admin/config/search/path/settings','pathauto.routing.yml','buildForm','src/Form/PathautoSettingsForm.php','Drupal route handler'],['/admin/config/search/path/update_bulk','pathauto.routing.yml','buildForm','src/Form/PathautoBulkUpdateForm.php','Drupal route handler'],['getPunctuationCharacters','src/AliasCleaner.php','pathautoPunctuationCharsAlter','tests/modules/pathauto_custom_punctuation_test/src/Hook/PathautoCustomPunctuationTestHooks.php','Drupal hook pathauto_punctuation_chars_alter'],['PathautoGenerator','src/PathautoGenerator.php','AliasCleaner','src/AliasCleaner.php','Drupal service injection']];for(const [from,file,to,target,label]of definitions){const sources=new Set(data.nodes.filter(n=>n.name===from&&n.file_path===file).map(n=>n.id)),targets=new Set(data.nodes.filter(n=>n.name===to&&n.file_path===target).map(n=>n.id));assert.ok(data.pluginEdges.some(e=>sources.has(e.source)&&targets.has(e.target)&&JSON.parse(e.metadata).label===label),'Missing '+label+': '+from);}return definitions;} @@ -24,9 +25,9 @@ let server,bridge,browser,page; const g=await CodeGraph.init(project);try{const r=await g.indexAll();assert.equal(r.filesErrored,0);}finally{g.close();}assert.equal(snapshot().pluginEdges.length,0); bridge=await startExtensionBridge([other,project],origin);browser=await chromium.launch({executablePath:process.env.CHROME_PATH||'/opt/google/chrome/chrome',headless:true,args:['--no-sandbox']});page=await browser.newPage({viewport:{width:1440,height:1000}});page.setDefaultTimeout(60000);let popups=0;page.on('popup',()=>popups++);const pageErrors=[];page.on('pageerror',e=>pageErrors.push(e.message)); await page.goto(bridge.connectionUrl);await page.getByRole('button',{name:'Official',exact:true}).click();await page.locator('.extension-card .official').waitFor();assert.equal(await page.locator('.extension-card').count(),1);assert.match(await page.locator('.extension-card .byline').innerText(),/CodeGraph.*OFFICIAL/s);pass('official catalog filter displays trusted CodeGraph Drupal identity'); - const popupReady=page.waitForEvent('popup');await page.locator('#connect').click();const popup=await popupReady;await popup.getByRole('button',{name:'Allow connection'}).click();await page.getByText('CodeGraph connected',{exact:true}).waitFor();await page.locator('#project').selectOption('1');await page.getByRole('link',{name:'Drupal',exact:true}).click();await page.locator('.compatibility').filter({hasText:'Selected 0.1.0'}).waitFor();assert.equal(await page.locator('.metadata dd.integrity').first().innerText(),'codegraph');await page.getByRole('button',{name:'Install extension'}).click();await page.waitForFunction(()=>state.snapshot?.jobId===1&&!state.snapshot.busy);assert.equal(await page.evaluate(()=>state.snapshot.error),undefined);assert.equal(popups,1);const installed=snapshot(),flows=verify(installed);assert.ok(!CodeGraph.isInitialized(other));pass('one connection and one Install refresh only selected pinned Drupal project; four real route hook and DI edges verified'); + const popupReady=page.waitForEvent('popup');await page.locator('#connect').click();const popup=await popupReady;await popup.getByRole('button',{name:'Allow connection'}).click();await page.getByText('CodeGraph connected',{exact:true}).waitFor();await page.locator('#project').selectOption('1');await page.getByRole('link',{name:'Drupal',exact:true}).click();await page.locator('.compatibility').filter({hasText:'Selected '+extensionVersion}).waitFor();assert.equal(await page.locator('.metadata dd.integrity').first().innerText(),'codegraph');await page.getByRole('button',{name:'Install extension'}).click();await page.waitForFunction(()=>state.snapshot?.jobId===1&&!state.snapshot.busy);assert.equal(await page.evaluate(()=>state.snapshot.error),undefined);assert.equal(popups,1);const installed=snapshot(),flows=verify(installed);assert.ok(!CodeGraph.isInitialized(other));pass('one connection and one Install refresh only selected pinned Drupal project; four real route hook and DI edges verified'); fs.writeFileSync(path.join(out,'graph-proof.json'),JSON.stringify({corpus:'pathauto',revision:pin,artifactSHA:hash(bytes),nodes:installed.nodes.length,edges:installed.edges.length,pluginEdges:installed.pluginEdges.length,hash:installed.hash,expected:flows},null,2));await page.screenshot({path:path.join(out,'desktop-installed.png'),fullPage:true}); - const manager=new ExtensionManager(project),key='packages/sha256/'+hash(bytes);await server.bucket.put(key,Buffer.from('tampered'));await assert.rejects(()=>manager.installFromRegistry({registry:origin,id:'drupal',version:'0.1.0',replaces:['drupal']}));assert.equal(snapshot().hash,installed.hash);assert.equal(snapshot().config,installed.config);await server.bucket.put(key,bytes);pass('tampered registry artifact fails installation without changing existing graph or config'); + const manager=new ExtensionManager(project),key='packages/sha256/'+hash(bytes);await server.bucket.put(key,Buffer.from('tampered'));await assert.rejects(()=>manager.installFromRegistry({registry:origin,id:'drupal',version:extensionVersion,replaces:['drupal']}));assert.equal(snapshot().hash,installed.hash);assert.equal(snapshot().config,installed.config);await server.bucket.put(key,bytes);pass('tampered registry artifact fails installation without changing existing graph or config'); const incompatible=JSON.parse(bytes);incompatible.package.codegraph.engines='>=9';await assert.rejects(()=>manager.install({bytes:Buffer.from(JSON.stringify(incompatible)),replaces:['drupal']}),/requires/);await assert.rejects(()=>manager.install({bytes:Buffer.from('{}'),replaces:['drupal']}));assert.equal(snapshot().hash,installed.hash);assert.equal(snapshot().config,installed.config);pass('incompatible and malformed installs preserve previously working Drupal graph'); await page.getByRole('button',{name:'Disable',exact:true}).click();await page.getByRole('button',{name:'Enable',exact:true}).waitFor();assert.equal(snapshot().pluginEdges.length,0);pass('browser disable removes actual extension contributions'); await page.getByRole('button',{name:'Enable',exact:true}).click();await page.getByRole('button',{name:'Disable',exact:true}).waitFor();verify(snapshot());await page.setViewportSize({width:390,height:844});await page.screenshot({path:path.join(out,'mobile-official.png'),fullPage:true});pass('browser enable restores expected graph and mobile official details'); diff --git a/scripts/validation/drupal-review-sample.cjs b/scripts/validation/drupal-review-sample.cjs index 4ae66eb49..2d15e66b5 100644 --- a/scripts/validation/drupal-review-sample.cjs +++ b/scripts/validation/drupal-review-sample.cjs @@ -5,6 +5,8 @@ const {CodeGraph}=require('../../dist'); const {ExtensionManager}=require('../../dist/plugins/manager'); Object.assign(process.env,{CODEGRAPH_TELEMETRY:'0',CODEGRAPH_PARSE_WORKERS:'2',CODEGRAPH_RESOLVE_WORKERS:'0',CODEGRAPH_SYNTH_TIMINGS:'all'}); const [project,mode,operation,output,artifact='dist/extensions/drupal.cgext']=process.argv.slice(2); +const sourceRevision=cp.execFileSync('git',['rev-parse','HEAD'],{encoding:'utf8'}).trim(); +const sourceDirty=!!cp.execFileSync('git',['status','--porcelain'],{encoding:'utf8'}).trim(); const hash=b=>crypto.createHash('sha256').update(b).digest('hex'); function graphSnapshot(){const db=new DatabaseSync(path.join(project,'.codegraph/codegraph.db'),{readOnly:true});try{ const nodes=db.prepare('SELECT * FROM nodes ORDER BY id').all().map(({updated_at,...n})=>n); @@ -18,6 +20,6 @@ function graphSnapshot(){const db=new DatabaseSync(path.join(project,'.codegraph else {const graph=CodeGraph.isInitialized(project)?await CodeGraph.open(project):await CodeGraph.init(project);try{result=operation==='rebuild'?await graph.refreshPluginIndex():operation==='sync'?await graph.sync():await graph.indexAll();}finally{graph.close();}} assert.ok(!result?.filesErrored && result?.success!==false,JSON.stringify(result)); const measured={wallMs:performance.now()-started,cpu:process.cpuUsage(cpu),resources:process.resourceUsage()}; - const record={revision:cp.execFileSync('git',['rev-parse','HEAD'],{encoding:'utf8'}).trim(),dirty:!!cp.execFileSync('git',['status','--porcelain'],{encoding:'utf8'}).trim(),project,mode,operation,artifactSHA:mode==='external'?hash(fs.readFileSync(artifact)):null,node:process.version,platform:process.platform,...measured,graph:graphSnapshot()}; + const record={revision:sourceRevision,dirty:sourceDirty,project,mode,operation,artifactSHA:mode==='external'?hash(fs.readFileSync(artifact)):null,node:process.version,platform:process.platform,...measured,graph:graphSnapshot()}; fs.mkdirSync(path.dirname(output),{recursive:true});fs.writeFileSync(output,JSON.stringify(record,null,2));console.log(JSON.stringify(record)); })().catch(e=>{console.error(e);process.exitCode=1;});