diff --git a/.github/workflows/deploy-public-preview.yml b/.github/workflows/deploy-public-preview.yml new file mode 100644 index 0000000..d8193c4 --- /dev/null +++ b/.github/workflows/deploy-public-preview.yml @@ -0,0 +1,34 @@ +name: Deploy Atlas public preview + +on: + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build-and-deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Rebuild and verify public preview + run: npm run compendium:build && npm run compendium:check && npm run test:compendium && npm run pages:build && npm run test:pages + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: dist/pages + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/package-navigator-preview.yml b/.github/workflows/package-navigator-preview.yml new file mode 100644 index 0000000..7fbfdd6 --- /dev/null +++ b/.github/workflows/package-navigator-preview.yml @@ -0,0 +1,39 @@ +name: Package Navigator public preview + +on: + workflow_dispatch: + pull_request: + paths: + - "navigator-preview/**" + - "config/navigator-preview-allowlist.json" + - "scripts/build-navigator-preview.mjs" + - "content/nodes/**" + - "tests/navigator-preview.spec.cjs" + - "package.json" + +permissions: + contents: read + +jobs: + package: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Test public preview core + run: npm run test:navigator-preview + - name: Build checksum-listed VSIX + run: npm run navigator:build + - name: Test and package demo workspace + run: npm run test:navigator-demo && npm run navigator:demo:package + - uses: actions/upload-artifact@v4 + with: + name: atlas-navigator-public-preview + path: | + dist/navigator-preview/* + dist/navigator-demo/* + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/public-boundary.yml b/.github/workflows/public-boundary.yml index 213e81e..f243c27 100644 --- a/.github/workflows/public-boundary.yml +++ b/.github/workflows/public-boundary.yml @@ -19,5 +19,6 @@ jobs: node-version: 22 cache: npm - run: npm ci + - run: npm run verify:public-preview - run: npx playwright install --with-deps chromium - - run: npm test + - run: npm run test:browser diff --git a/.gitignore b/.gitignore index 7295a0f..29a6826 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ test-results/ Thumbs.db *.zip *.vsix + +dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8951dd8..449b2ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Rebuilt Control Lite as a complete Prepare → Instruct → Inspect → Evidence → Decide → Export workflow with autosave, editable sessions, scope comparison, evidence states, and decision-alignment guidance. +- Added a dependency-free bounded-change exercise with Accept, Revise, and Reject paths, runnable behavior and safety verification, honest evidence records, and maintained Playwright lifecycle coverage. +- Added a limited public Navigator VSIX, two synthetic Laravel demo workspaces, strict knowledge allowlisting, checksum-listed packages, and a manual packaging workflow without copying the private Navigator engine. +- Added a clean-room six-node Compendium preview generated only from public Markdown and evidence sidecars, plus an integrated manual GitHub Pages deployment containing Control Lite and Compendium. +- Added a reproducible release gate and release-review checklist that keep generated artifacts outside the canonical public-tree manifest. - Restored public node integrity with valid frontmatter, canonical and public-content hashes, and one-to-one evidence validation. - Added a canonical public-tree manifest so public-only drift fails validation. - Added a pull-request control workflow and documented required branch protections. diff --git a/README.md b/README.md index b5a8414..e318664 100644 --- a/README.md +++ b/README.md @@ -5,190 +5,123 @@ [![Status: public preview](https://img.shields.io/badge/status-public_preview-7257d8.svg)](docs/product/ROADMAP.md) [![Privacy: local first](https://img.shields.io/badge/privacy-local--first-167d6d.svg)](docs/public/privacy-and-safety.md) -**Human-controlled AI development for Laravel and Vue: define scope, understand changes, review -evidence, and make the final decision.** +## Keep control of AI-generated code changes + +**Define what may change, inspect what actually changed, record real evidence, and make the final human decision.** ![Atlas Navigator showing a Laravel request flow](screenshots/navigator-flow.png) -### [Build a local Change Contract with Control Lite →](control-lite/README.md) +### Start with one controlled change -**[Try the guided 15-minute Control walkthrough](docs/getting-started/TRY_ATLAS.md)**
-**[See current Navigator preview access](https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/docs/testing/START_TESTING.md)** -**[Browse all public documentation](docs/README.md)** +1. **[Open Control Lite](control-lite/index.html)** and define a bounded change. +2. Generate a scoped AI instruction and review record. +3. Compare the supplied or real patch with the allowed scope. +4. Record checks as **Passed**, **Failed**, or **Not run**. +5. Decide to **Accept**, **Revise**, or **Reject**. +6. Open the related public Compendium node when a concept or boundary needs explanation. -> **Current status:** public preview and internal alpha. The software and control lifecycle are real -> and extensively tested. Broad usability, retention, independent review, and paid demand remain -> unproven. +[Try the guided Control walkthrough →](docs/getting-started/TRY_ATLAS.md) -## Keep AI-assisted work understandable and bounded +> **Current status:** public preview and internal alpha. The technical lifecycle is real and tested, +> while broad usability, repeat use, independent review, and willingness to pay remain unproven. -AI can produce code faster than a person can understand, review, and remember it. Atlas is being -built around one explicit loop: +## The Atlas control loop ```mermaid flowchart LR - Goal["Human goal"] --> Instruction["Scoped AI instruction"] - Instruction --> Change["Traceable change"] - Change --> Understand["Understand impact"] - Understand --> Verify["Review evidence"] - Verify --> Decision["Accept, revise, or reject"] - Decision --> Memory["Remember the decision"] - Memory --> Goal + Goal["Human goal"] --> Scope["Bound the change"] + Scope --> Instruction["Scoped AI instruction"] + Instruction --> Change["Inspect the patch"] + Change --> Evidence["Record observed evidence"] + Evidence --> Decision["Accept, revise, or reject"] + Decision --> Memory["Preserve the decision"] ``` -### Keep the AI in scope - -Declare allowed files, protected behavior, acceptance criteria, and parked ideas before a patch -begins. Risky expansion remains a human confirmation boundary. - -### Understand what changed - -Navigator connects routes, controllers, views, dependencies, and learning context so the reviewer can -reason about the change instead of accepting a diff on trust. - -### Verify before accepting - -Atlas separates scope conformance from observed checks. A missing test result remains “not run”; the -AI cannot promote its own claim into evidence or make the final decision. - -## One concrete example - -The public exercise starts with this request: - -> Improve the status page so a reviewer knows the application is ready for a human check. - -The baseline prompt contains no file boundary or proof requirement. The Atlas-controlled version -allows two Laravel files, protects routes and authentication, defines visible acceptance criteria, -and requires honest test reporting. +Atlas does not ask an AI to certify its own work. Missing evidence remains **Not run**, scope expansion +remains visible, and the accountable human keeps the final decision. -The supplied patch stays in scope, but the completed decision is **revise** because this static -repository cannot run the real Laravel test command. That is the product thesis in practice: preserve -useful work without pretending missing evidence is a pass. +## Three connected product surfaces -[Open the complete worked example](control/examples/laravel-status-label/README.md). - -## Control, Navigator, and Compendium - -Near the top, Atlas has three jobs: +| Surface | Job | Public experience | +| --- | --- | --- | +| **Control** | Define scope, evidence requirements, and the final decision | [Control Lite](control-lite/README.md) and the [15-minute walkthrough](docs/getting-started/TRY_ATLAS.md) | +| **Navigator** | Show a bounded Laravel route → controller → Blade flow in VS Code | [Build the limited public VSIX](navigator-preview/README.md) | +| **Compendium** | Explain unfamiliar concepts with version scope, evidence, and practical boundaries | [Open the six-node public preview](compendium-preview/index.html) | -- **Control** defines the change, records supplied evidence, and preserves the human decision. -- **Navigator** brings the lifecycle into VS Code beside code flow and impact. -- **Compendium** supplies primary-source-mapped context when the reviewer needs to understand a - concept or boundary. +The learning missions and packs support onboarding, but they are not the primary product. -Control Lite now provides Prepare → Instruct → Evidence → Decide inside Navigator. It writes only to -the local project, does not launch an AI, does not run the declared verification command, and does not -infer acceptance. +## What the public preview demonstrates -### See code flow +### Keep the AI in scope -![Atlas Navigator flow view](screenshots/navigator-flow.png) +Declare allowed files, protected behavior, acceptance criteria, required checks, and parked ideas before +a patch begins. Suggestions outside that boundary remain visible without silently entering the change. -Navigator traces a Laravel request from route to controller, Blade view, and included partial without -executing arbitrary project commands. +### Understand what changed -### Review impact +Navigator connects Laravel routes, controllers, Blade views, components, and dependencies so a reviewer +can reason about a patch instead of accepting a diff on trust. ![Atlas Navigator impact view](screenshots/navigator-impact.png) -Impact and Git comparison views expose incoming users, outgoing dependencies, and flow changes while -project content remains local. +### Separate claims from evidence -### Learn in context +A test command written in an AI response is not evidence that the command ran. Atlas records each +required check as Passed, Failed, or Not run and keeps known limitations beside the decision. -![Atlas Navigator contextual learning cards](screenshots/navigator-learning.png) +### Learn without leaving the review -Compact cards explain unfamiliar code and link to Compendium nodes whose AI-assisted and independent -review states remain visible. +Contextual learning cards connect unfamiliar code to Compendium nodes whose source mapping, +AI-assistance state, version scope, limitations, and independent-review state remain visible. -### Explore the Compendium preview +![Atlas Navigator contextual learning cards](screenshots/navigator-learning.png) -![Atlas Compendium home](screenshots/compendium-home.png) +## One honest worked example -The Compendium connects a large programming Lexicon with practical mistakes, version scope, -verification design, and evidence. Public hosting is still being prepared; screenshots and selected -node samples are available here now. +The current Laravel exercise asks for a clearer review-ready status page. The proposed patch stays +inside its two-file boundary, but the repository cannot execute the required Laravel test. The sample +decision is therefore **Revise**, not an invented pass. -More images and their review status are listed in [`screenshots/README.md`](screenshots/README.md). +[Review the complete Laravel example](control/examples/laravel-status-label/README.md), then try the [runnable remaining-count exercise](control/exercises/browser-list-count/README.md). -## Current availability +## Availability -| Experience | Available here | Limitation | +| Experience | Available | Current boundary | | --- | --- | --- | -| Local Control Lite generator | Yes | Runs in the browser; does not execute AI or verification commands | -| Guided Control walkthrough | Yes | Static Laravel example; no real Laravel runtime | -| Safe browser example | Yes | Learning example, not the full product | -| Selected missions and nodes | Yes | Curated preview; independent review pending | -| Navigator VSIX | Tester access only | General signed release and clean-profile proof pending | -| Hosted Compendium | Not yet | Static hosting review in progress | - -The manual packaging workflow can prepare a checksum-listed exercise archive for maintainer review. -See [`DOWNLOAD.md`](docs/getting-started/DOWNLOAD.md); no public release is implied until a human publishes one. - -## Evidence and limitations +| Browser-based Control Lite | Yes | Local templates and review workflow; no AI or command execution | +| Guided Laravel Control exercise | Yes | Static example; the real Laravel test remains Not run | +| Safe browser teaching example | Yes | Learning fixture, not the full product | +| Selected missions and nodes | Yes | Curated preview; independent review is incomplete | +| Limited Navigator VSIX | Buildable public preview | Conventional Laravel controller-array routes only; unsigned and not the private product | +| Compendium preview | Yes, local and deployable | Six allowlisted public nodes; no private Compendium source or full Lexicon | -The private development repository currently validates: +## Evidence, privacy, and limitations -- 54 maintained Knowledge Nodes and 664 Lexicon entries; -- 752 generated Compendium pages; -- Control, Continuity, Navigator, Compendium, policy, security, packaging, and lifecycle checks; -- founder dogfooding and a five-person embedded design cohort; -- one preserved external-alpha round whose same-tester retest remains pending. +The private development repository currently validates 54 maintained Knowledge Nodes, 664 Lexicon +entries, 752 generated Compendium pages, and the Control, Continuity, Navigator, Compendium, policy, +security, packaging, and lifecycle checks. Founder dogfooding, a five-person embedded design cohort, +and one external-alpha round support formative usefulness—not product-market fit. -Every exported node now shows its evidence date, source count, AI assistance, automated check state, -version scope, and pending independent human review. “Gold” describes an internal teaching-depth -target; it does **not** mean final certification. +The public preview is local-first, contains no telemetry, requires no account, performs no silent AI +action, and does not execute or infer verification results. Public exports pass through an allowlist, +secret and path scanning, local-link validation, integrity hashes, and human review. -This supports technical credibility and formative usefulness. It does not prove product-market fit, -retention, willingness to pay, universal correctness, or production safety. Read -[`testing-status.md`](docs/public/testing-status.md), -[`known-limitations.md`](docs/public/known-limitations.md), and the public [`FAQ`](docs/product/FAQ.md). +Read the [testing status](docs/public/testing-status.md), [known limitations](docs/public/known-limitations.md), +[privacy and safety notes](docs/public/privacy-and-safety.md), and [security policy](.github/SECURITY.md). -## Supporting work +## Explore and contribute -The learning material helps people build the understanding needed to use the control loop; it is not -the primary product demonstration. - -| Area | Role | Start | -| --- | --- | --- | -| First Expedition and beginner missions | Confidence-building onboarding | [Mission index](content/missions/README.md) | -| AI Collaboration Pack | Prompt and review habits | [Pack entry](packs/ai-collaboration/README.md) | -| Public node samples | Just-in-time concept reference | [Node index](content/nodes/README.md) | -| Continuity research | Local readiness, health, and recovery | Tracked as parallel alpha work in the roadmap | -| Testing and research | Protocols, facilitator questions, evidence status | [Testing status](docs/public/testing-status.md) | - -## Privacy and trust - -- Local-first by default. -- No telemetry in the current Navigator preview. -- No account or payment requirement in the current free preview. -- No silent AI actions, verification execution, or acceptance decisions. -- Remote Compendium links require HTTPS; localhost is allowed for development. -- Public material passes through a one-way allowlist, secret/path scanning, local-link validation, and - manual review. -- Runnable browser teaching code is tested against HTML-like user input and may not assign user data - through `innerHTML`. - -Read [`SECURITY.md`](.github/SECURITY.md) and -[`privacy-and-safety.md`](docs/public/privacy-and-safety.md). - -## Roadmap and feedback - -The immediate public focus is to validate Control Lite in a clean profile, repeat the external-alpha -workflow, publish a safe preview artifact, and measure whether the control loop helps real reviewers. -The public [`ROADMAP.md`](docs/product/ROADMAP.md) keeps supporting research separate. - -Precise feedback is welcome for reproducible bugs, unclear first steps, accessibility barriers, -privacy concerns, or workflows that would help people retain control. See -[`CONTRIBUTING.md`](.github/CONTRIBUTING.md) and the public -[`FEEDBACK.md`](https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/docs/testing/FEEDBACK.md). +- [Public documentation](docs/README.md) +- [Product roadmap](docs/product/ROADMAP.md) +- [Mission index](content/missions/README.md) +- [AI Collaboration Pack](packs/ai-collaboration/README.md) +- [Feedback instructions](docs/testing/FEEDBACK.md) +- [Contribution guide](.github/CONTRIBUTING.md) ## License and provenance -The files committed to this public preview are available under the [MIT License](LICENSE). The -private Developer Atlas monorepo and unreleased product source are separate and are not licensed by -this repository. - -Development is materially AI-assisted and human-directed. Automated verification is kept separate -from independent human review and final acceptance. See [`PROVENANCE.md`](docs/governance/PROVENANCE.md) and the -outcome-focused [`CHANGELOG.md`](CHANGELOG.md). +The files in this public preview are available under the [MIT License](LICENSE). The private Developer +Atlas monorepo and unreleased product source are separate and are not licensed by this repository. +Development is materially AI-assisted and human-directed; automated checks remain separate from +independent human review and final acceptance. See [PROVENANCE.md](docs/governance/PROVENANCE.md) and +the outcome-focused [CHANGELOG.md](CHANGELOG.md). diff --git a/compendium-preview/README.md b/compendium-preview/README.md new file mode 100644 index 0000000..96c186b --- /dev/null +++ b/compendium-preview/README.md @@ -0,0 +1,21 @@ +# Atlas Compendium — clean-room public preview + +This static preview is built only from exact public node and evidence paths in [`config/compendium-preview-allowlist.json`](../config/compendium-preview-allowlist.json). It does not import or copy the private Compendium application, full Canon index, complete Lexicon, internal review packets, or private knowledge source. + +## Use locally + +Open `compendium-preview/index.html` in a modern browser. The generated data is embedded in `data.js`, so search and node reading work without a server or network connection. External primary-source links require internet access. + +## Rebuild and verify + +```bash +npm run compendium:build +npm run compendium:check +npm run test:compendium +``` + +The builder escapes public Markdown before generating article HTML and fingerprints every allowlisted source and evidence file. + +## Publish manually + +The `Deploy Atlas public preview` GitHub Actions workflow is manual. It rebuilds and validates the allowlisted data, creates an integrated Pages bundle containing Control Lite and this Compendium preview, tests the cross-surface links, and deploys `dist/pages`. Enable and run it only after reviewing the pull request and repository Pages settings. diff --git a/compendium-preview/app.js b/compendium-preview/app.js new file mode 100644 index 0000000..a17e9b6 --- /dev/null +++ b/compendium-preview/app.js @@ -0,0 +1,132 @@ +'use strict' + +const data = window.ATLAS_COMPENDIUM_DATA +const catalogView = document.querySelector('#catalog-view') +const nodeView = document.querySelector('#node-view') +const nodeGrid = document.querySelector('#node-grid') +const search = document.querySelector('#search') +const resultCount = document.querySelector('#result-count') + +if (!data || !Array.isArray(data.nodes)) { + resultCount.textContent = 'The public Compendium data bundle is unavailable.' +} else { + search.addEventListener('input', renderCatalog) + const requestedNode = new URLSearchParams(window.location.search).get('node') + const selected = data.nodes.find((node) => node.id === requestedNode) + if (selected) renderNode(selected) + else renderCatalog() +} + +function renderCatalog() { + catalogView.hidden = false + nodeView.hidden = true + const query = search.value.trim().toLowerCase() + const matches = data.nodes.filter((node) => searchable(node).includes(query)) + nodeGrid.textContent = '' + for (const node of matches) nodeGrid.appendChild(nodeCard(node)) + resultCount.textContent = `${matches.length} of ${data.nodes.length} public nodes shown.` +} + +function nodeCard(node) { + const article = document.createElement('article') + article.className = 'node-card' + const tags = document.createElement('p') + tags.className = 'tags' + tags.textContent = node.tags.join(' · ') + const title = document.createElement('h2') + title.textContent = node.title + const summary = document.createElement('p') + summary.textContent = node.summary + const facts = document.createElement('dl') + addFact(facts, 'Version', node.versionScope.join('; ') || 'Not recorded') + addFact(facts, 'Sources', String(node.sourceCount)) + addFact(facts, 'Human review', node.independentReview) + const link = document.createElement('a') + link.href = `?node=${encodeURIComponent(node.id)}` + link.textContent = 'Read this node →' + article.append(tags, title, summary, facts, link) + return article +} + +function renderNode(node) { + catalogView.hidden = true + nodeView.hidden = false + const meta = document.querySelector('#node-meta') + const content = document.querySelector('#node-content') + const summary = document.querySelector('#evidence-summary') + const sourceList = document.querySelector('#source-list') + meta.textContent = '' + content.textContent = '' + summary.textContent = '' + sourceList.textContent = '' + + const eyebrow = document.createElement('p') + eyebrow.className = 'eyebrow' + eyebrow.textContent = node.id + const title = document.createElement('h1') + title.textContent = node.title + const intro = document.createElement('p') + intro.className = 'lead' + intro.textContent = node.summary + const grid = document.createElement('div') + grid.className = 'meta-grid' + addMeta(grid, 'Version scope', node.versionScope.join('; ') || 'Not recorded') + addMeta(grid, 'Primary sources', String(node.sourceCount)) + addMeta(grid, 'Verification designs', String(node.verificationCount)) + addMeta(grid, 'Independent review', node.independentReview) + meta.append(eyebrow, title, intro, grid) + + // node.html is generated at build time from allowlisted public Markdown after HTML escaping. + content.innerHTML = node.html + + const status = document.createElement('p') + status.className = node.independentReview === 'complete' ? 'status-complete' : 'status-pending' + status.textContent = `Independent human review: ${node.independentReview}.` + const reviewed = document.createElement('p') + reviewed.textContent = `Primary-source mapping reviewed: ${node.reviewedAt || 'date not recorded'}.` + const links = document.createElement('p') + const sourceLink = externalLink(node.sourceUrl, 'Open source Markdown') + const evidenceLink = externalLink(node.evidenceUrl, 'Open evidence sidecar') + links.append(sourceLink, document.createTextNode(' · '), evidenceLink) + summary.append(status, reviewed, links) + + for (const source of node.sources) { + const item = document.createElement('li') + const link = externalLink(source.url, source.title) + item.append(link, document.createTextNode(` — ${source.publisher}; checked ${source.checkedAt || 'date not recorded'}`)) + sourceList.appendChild(item) + } + document.title = `${node.title} — Atlas Compendium Preview` +} + +function addFact(list, term, definition) { + const dt = document.createElement('dt') + dt.textContent = term + const dd = document.createElement('dd') + dd.textContent = definition + list.append(dt, dd) +} + +function addMeta(grid, label, value) { + const item = document.createElement('div') + item.className = 'meta-item' + const heading = document.createElement('span') + heading.textContent = label + const body = document.createElement('strong') + body.textContent = value + item.append(heading, body) + grid.appendChild(item) +} + +function externalLink(url, label) { + const link = document.createElement('a') + link.href = url + link.target = '_blank' + link.rel = 'noreferrer' + link.textContent = label + return link +} + +function searchable(node) { + return [node.id, node.title, node.summary, ...node.tags, ...node.versionScope].join(' ').toLowerCase() +} diff --git a/compendium-preview/data-manifest.json b/compendium-preview/data-manifest.json new file mode 100644 index 0000000..3f6d158 --- /dev/null +++ b/compendium-preview/data-manifest.json @@ -0,0 +1,63 @@ +{ + "schemaVersion": "1.0.0", + "allowlist": "config/compendium-preview-allowlist.json", + "nodeIds": [ + "CANON-BLADE-VIEW-001", + "CANON-CONTROLLER-001", + "CANON-DOM-001", + "CANON-LARAVEL-REQUEST-LIFECYCLE-001", + "CANON-LOCALSTORAGE-001", + "CANON-ROUTE-001" + ], + "sources": [ + { + "path": "content/nodes/evidence/CANON-BLADE-VIEW-001.evidence.json", + "sha256": "87d5727720286e42b74a1baada8d0485ddf128a64eb77c8fbcf286a82c2134d2" + }, + { + "path": "content/nodes/evidence/CANON-CONTROLLER-001.evidence.json", + "sha256": "caac97797224263cb0d2a257119baa05c04af60b4c279d6b2338ab89f35b1be8" + }, + { + "path": "content/nodes/evidence/CANON-DOM-001.evidence.json", + "sha256": "1ff55cc92ff718667d25bf4833d5c54b9d180646d052c5cb94026dfe80358c84" + }, + { + "path": "content/nodes/evidence/CANON-LARAVEL-REQUEST-LIFECYCLE-001.evidence.json", + "sha256": "4975cc1001b542028cbcd8f0e70be35d9609dbfbf90dfb70f90d896227f1063c" + }, + { + "path": "content/nodes/evidence/CANON-LOCALSTORAGE-001.evidence.json", + "sha256": "81ccfb8b540cfbe82e41ed9fe691b528683aa486570fb10e83abc006a8e05b8f" + }, + { + "path": "content/nodes/evidence/CANON-ROUTE-001.evidence.json", + "sha256": "ec9928f32ad646c1b4d5de8580c0954fd83d08c8ea92ff28e1534f45e7a9878a" + }, + { + "path": "content/nodes/gold/CANON-BLADE-VIEW-001.md", + "sha256": "676e1d9e222c2fac07d0443031b3f05653c6ebb5312019cca248756867bb957a" + }, + { + "path": "content/nodes/gold/CANON-CONTROLLER-001.md", + "sha256": "66146df21386862e5a162d6c3707ba828abd20bc1726da595f39b97f9129ff03" + }, + { + "path": "content/nodes/gold/CANON-DOM-001.md", + "sha256": "e90b6d6432d9ce1aed1c553b8bd80d35a28979501d74df0596d289de4b9fb28c" + }, + { + "path": "content/nodes/gold/CANON-LARAVEL-REQUEST-LIFECYCLE-001.md", + "sha256": "106053ec2a64f5c8d567f1dfc297facac0304a4ed79056d067ec740d02021df4" + }, + { + "path": "content/nodes/gold/CANON-LOCALSTORAGE-001.md", + "sha256": "52a63ca661d004903690eca5e313cd6bb64a26f41d73a59f5b2c875842eb5a6c" + }, + { + "path": "content/nodes/gold/CANON-ROUTE-001.md", + "sha256": "4192d9230394fa3ccbe60f0b7bbd3bb49e1f2ed37fb902ee0328efab9128282e" + } + ], + "dataSha256": "c4ade5ef8c3986694b123619210e0c7d3b23f27f5e8c8874820be144b1858311" +} diff --git a/compendium-preview/data.js b/compendium-preview/data.js new file mode 100644 index 0000000..e8eb2c9 --- /dev/null +++ b/compendium-preview/data.js @@ -0,0 +1,300 @@ +window.ATLAS_COMPENDIUM_DATA = { + "schemaVersion": "1.0.0", + "generatedFrom": "allowlisted public node and evidence files only", + "nodeCount": 6, + "nodes": [ + { + "id": "CANON-BLADE-VIEW-001", + "title": "Blade View", + "summary": "A Blade view is a Laravel template, normally stored under resources/views, that compiles to PHP and renders presentation output from supplied data.", + "tags": [ + "Laravel", + "Blade", + "view" + ], + "versionScope": [ + "Laravel 13.x" + ], + "reviewedAt": "2026-07-12", + "sourceCount": 2, + "verificationCount": 3, + "independentReview": "pending", + "sources": [ + { + "title": "Blade Templates", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/blade", + "checkedAt": "2026-07-12" + }, + { + "title": "Views", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/views", + "checkedAt": "2026-07-12" + } + ], + "limitations": [ + "Escaped {{ }} output is safer for untrusted text than raw {!! !!} output.", + "A Blade view is one response representation; other response types do not require Blade.", + "Blade permits PHP, but request handling and business rules should remain outside presentation templates.", + "Independent human review remains required before promotion to gold." + ], + "sourcePath": "content/nodes/gold/CANON-BLADE-VIEW-001.md", + "sourceUrl": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/content/nodes/gold/CANON-BLADE-VIEW-001.md", + "evidenceUrl": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/content/nodes/evidence/CANON-BLADE-VIEW-001.evidence.json", + "html": "\n

Quick answer

\n

A Blade view is a Laravel template, normally stored under resources/views, that compiles to PHP and renders presentation output from supplied data.

\n

Where it fits in the flow

\n

Blade is one possible representation near the end of the request path. A route or controller returns a view with data; Blade renders HTML; Laravel places that HTML in the outgoing response.

\n

controller view data -> Blade template -> rendered HTML -> response

\n

Mental model

\n

Think of Blade as a document template. HTML supplies the structure, Blade directives control simple presentation branches and loops, and escaped echo syntax fills values into the page. The template presents decisions made elsewhere rather than owning request or business logic.

\n

Working example

\n
@extends('layouts.app')\n\n@section('content')\n    <h1>Items</h1>\n\n    <ul>\n        @forelse ($items as $item)\n            <li>{{ $item->name }}</li>\n        @empty\n            <li>No items yet.</li>\n        @endforelse\n    </ul>\n@endsection
\n

The controller must pass an items value. {{ $item->name }} escapes the displayed text by default.

\n

What you can safely change

\n\n

Common failure and recovery

\n\n

How to verify it

\n\n

Boundaries and version notes

\n\n

AI steering rule

\n

AI should inspect the controller's view name and data contract plus the existing layout before editing Blade, preserve escaped output by default, and avoid moving request handling or business logic into the template.

\n

Human readability rule

\n

Keep HTML structure visible, indent Blade directives consistently, give empty and error states explicit markup, and name variables after the data the user sees.

\n

Related nodes

\n\n

Evidence status

\n

Primary-source reviewed on 2026-07-12. Independent human review and promotion are pending. See `../evidence/CANON-BLADE-VIEW-001.evidence.json` for claim mappings, source locations, limitations, and verification contracts.

" + }, + { + "id": "CANON-CONTROLLER-001", + "title": "Controller", + "summary": "A Laravel controller groups related request-handling methods. When a controller route matches, Laravel invokes the configured public action, which coordinates the work needed to return a response.", + "tags": [ + "Laravel", + "controller", + "action" + ], + "versionScope": [ + "Laravel 13.x" + ], + "reviewedAt": "2026-07-12", + "sourceCount": 3, + "verificationCount": 3, + "independentReview": "pending", + "sources": [ + { + "title": "Controllers", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/controllers", + "checkedAt": "2026-07-12" + }, + { + "title": "Routing", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/routing", + "checkedAt": "2026-07-12" + }, + { + "title": "Validation", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/validation", + "checkedAt": "2026-07-12" + } + ], + "limitations": [ + "Controllers are optional; routes may use closures or invokable classes.", + "A controller does not automatically validate, authorize, or persist data.", + "Controllers do not have to extend Laravel's base controller class.", + "Independent human review remains required before promotion to gold." + ], + "sourcePath": "content/nodes/gold/CANON-CONTROLLER-001.md", + "sourceUrl": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/content/nodes/gold/CANON-CONTROLLER-001.md", + "evidenceUrl": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/content/nodes/evidence/CANON-CONTROLLER-001.evidence.json", + "html": "\n

Quick answer

\n

A Laravel controller groups related request-handling methods. When a controller route matches, Laravel invokes the configured public action, which coordinates the work needed to return a response.

\n

Where it fits in the flow

\n

The controller comes after route matching and route middleware. It receives request context, may validate or authorize input and call models or services, then returns a view, redirect, JSON response, or another supported response.

\n

matched route -> controller action -> application work -> response

\n

Mental model

\n

Think of a controller action as a coordinator at a service desk. It accepts a specific request, sends work to the right place, and returns the result. It should make the sequence visible without becoming the home for every business rule.

\n

Working example

\n
namespace App\\Http\\Controllers;\n\nuse App\\Models\\Item;\nuse Illuminate\\View\\View;\n\nclass ItemController extends Controller\n{\n    public function index(): View\n    {\n        $items = Item::query()->latest()->get();\n\n        return view('items.index', ['items' => $items]);\n    }\n}
\n

The route selects index(). The action obtains the items and passes them to resources/views/items/index.blade.php under the name $items.

\n

What you can safely change

\n\n

Common failure and recovery

\n\n

How to verify it

\n\n

Boundaries and version notes

\n\n

AI steering rule

\n

AI should confirm the mapped route, action contract, input, and expected response before editing a controller, keep the change inside the focused action, and avoid adding service or repository layers unless the responsibility or user request requires them.

\n

Human readability rule

\n

Keep controller actions short enough to scan and organize them as visible input, validation or authorization, data preparation, operation, and response blocks.

\n

Related nodes

\n\n

Evidence status

\n

Primary-source reviewed on 2026-07-12. Independent human review and promotion are pending. See `../evidence/CANON-CONTROLLER-001.evidence.json` for claim mappings, source locations, limitations, and verification contracts.

" + }, + { + "id": "CANON-DOM-001", + "title": "DOM", + "summary": "The DOM represents a document as a node tree and provides APIs that code can use to inspect and change that in-memory document.", + "tags": [ + "JavaScript", + "DOM", + "browser" + ], + "versionScope": [ + "Current DOM standard", + "Evergreen browsers" + ], + "reviewedAt": "2026-07-12", + "sourceCount": 1, + "verificationCount": 3, + "independentReview": "pending", + "sources": [ + { + "title": "Document Object Model (DOM)", + "publisher": "Mozilla MDN", + "url": "https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model", + "checkedAt": "2026-07-12" + } + ], + "limitations": [ + "The DOM is the browser's current document representation, not an automatic edit to the source HTML file.", + "Selector methods can return no match; code must handle that path instead of assuming an element exists.", + "DOM availability and timing depend on the host environment and document lifecycle.", + "Independent human review remains required before promotion to gold." + ], + "sourcePath": "content/nodes/gold/CANON-DOM-001.md", + "sourceUrl": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/content/nodes/gold/CANON-DOM-001.md", + "evidenceUrl": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/content/nodes/evidence/CANON-DOM-001.evidence.json", + "html": "\n

Hover summary

\n

The DOM is the browser's live version of the HTML page.

\n

One-sentence truth

\n

The DOM represents a document as a node tree and provides APIs that code can use to inspect and change that in-memory document.

\n

Mental model

\n

HTML is the written recipe. The DOM is the meal the browser builds from that recipe.

\n

Beginner explanation

\n

When the browser opens your HTML, it creates a live page structure. JavaScript can find parts of this structure and change them.

\n

Technical explanation

\n

DOM stands for Document Object Model. Browser JavaScript uses DOM methods like querySelector to select elements.

\n
const button = document.querySelector('.change-message-button')
\n

When to use it

\n

Use DOM methods when JavaScript needs to find or change something on the page.

\n

Common mistakes

\n\n

AI steering rule

\n

AI should explain the connection between HTML class names and JavaScript selectors before patching DOM bugs.

\n

Human readability rule

\n

DOM-related variable names should name the element clearly, like submitButton or messageText.

\n

Related nodes

\n\n

Verification

\n\n

Applicability and boundaries

\n\n

Evidence status

\n

Primary-source reviewed on 2026-07-12. Independent human review and promotion are pending. See `../evidence/CANON-DOM-001.evidence.json` for claim mappings, source locations, limitations, and verification contracts.

" + }, + { + "id": "CANON-LARAVEL-REQUEST-LIFECYCLE-001", + "title": "Laravel Request Lifecycle", + "summary": "A Laravel HTTP request enters through public/index.php, passes through application bootstrap and middleware, reaches a matching route action, and returns a response outward through middleware to the client.", + "tags": [ + "Laravel", + "request flow", + "verification" + ], + "versionScope": [ + "Laravel 13.x", + "PHP 8.3+", + "Standard Laravel HTTP request lifecycle" + ], + "reviewedAt": "2026-07-12", + "sourceCount": 8, + "verificationCount": 5, + "independentReview": "pending", + "sources": [ + { + "title": "Request Lifecycle", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/lifecycle", + "checkedAt": "2026-07-12" + }, + { + "title": "Routing", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/routing", + "checkedAt": "2026-07-12" + }, + { + "title": "Controllers", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/controllers", + "checkedAt": "2026-07-12" + }, + { + "title": "Middleware", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/middleware", + "checkedAt": "2026-07-12" + }, + { + "title": "HTTP Responses", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/responses", + "checkedAt": "2026-07-12" + }, + { + "title": "Views", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/views", + "checkedAt": "2026-07-12" + }, + { + "title": "HTTP Tests", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/http-tests", + "checkedAt": "2026-07-12" + }, + { + "title": "Release Notes", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/releases", + "checkedAt": "2026-07-12" + } + ], + "limitations": [ + "The review covers Laravel 13.x documentation and must be repeated for a future major version.", + "The node intentionally omits framework-internal class-by-class execution details that are unnecessary for its beginner mental model.", + "Long-running servers such as Octane may add worker lifecycle concerns that are outside this node's request-level scope.", + "Application middleware, exception handling, service providers, and response preparation can add project-specific behavior.", + "Independent human review remains required before the node can be marked independently_reviewed or gold." + ], + "sourcePath": "content/nodes/gold/CANON-LARAVEL-REQUEST-LIFECYCLE-001.md", + "sourceUrl": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/content/nodes/gold/CANON-LARAVEL-REQUEST-LIFECYCLE-001.md", + "evidenceUrl": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/content/nodes/evidence/CANON-LARAVEL-REQUEST-LIFECYCLE-001.evidence.json", + "html": "\n

Quick answer

\n

A Laravel HTTP request enters through public/index.php, passes through application bootstrap and middleware, reaches a matching route action, and returns a response outward through middleware to the client.

\n

Where it fits in the flow

\n

This is the map for the full server-side journey. Use it before changing code when you do not yet know whether a failure belongs to the request, middleware, route, action, view, or response.

\n

client -> public/index.php -> bootstrap -> global middleware -> router -> route middleware -> action -> response -> middleware -> client

\n

Mental model

\n

Think of the request as a parcel moving through checkpoints. public/index.php receives it, bootstrap prepares the application, middleware may inspect or stop it, the router chooses a destination, the action creates a response, and middleware handles that response on its way out.

\n

The controller and Blade view are common stops, not mandatory ones. A route closure can produce a response directly, and a controller can return JSON, a redirect, a string, or a response object instead of a view.

\n

Working example

\n
// routes/web.php\nuse App\\Http\\Controllers\\GroceryController;\nuse Illuminate\\Support\\Facades\\Route;\n\nRoute::get('/groceries', [GroceryController::class, 'index'])\n    ->name('groceries.index');
\n
// app/Http/Controllers/GroceryController.php\nuse Illuminate\\View\\View;\n\npublic function index(): View\n{\n    return view('groceries.index');\n}
\n

For GET /groceries, Laravel boots, runs middleware, matches the route, calls GroceryController::index(), renders the view, and sends the resulting response back through middleware.

\n

What you can safely change

\n\n

Common failure and recovery

\n\n

How to verify it

\n\n

Boundaries and version notes

\n\n

AI steering rule

\n

Before changing a Laravel request-flow bug, AI should identify the observed method and URI, matching route, applicable middleware, route action, and returned response; it should not assume the controller ran or introduce new architecture before locating the first broken boundary.

\n

Human readability rule

\n

Present the method, URI, middleware, action, response type, and verification result as one traceable request-to-response chain.

\n

Related nodes

\n\n

Evidence status

\n

Primary-source reviewed on 2026-07-12 for Laravel 13.x. Independent human review is pending. See `../evidence/CANON-LARAVEL-REQUEST-LIFECYCLE-001.evidence.json` for claim mappings, source locations, limitations, and verification contracts.

" + }, + { + "id": "CANON-LOCALSTORAGE-001", + "title": "localStorage", + "summary": "localStorage stores string key/value pairs for an origin and normally persists them across browser sessions.", + "tags": [ + "JavaScript", + "browser", + "storage" + ], + "versionScope": [ + "Current Web Storage standard", + "Evergreen browsers" + ], + "reviewedAt": "2026-07-12", + "sourceCount": 3, + "verificationCount": 3, + "independentReview": "pending", + "sources": [ + { + "title": "Window.localStorage", + "publisher": "Mozilla MDN", + "url": "https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage", + "checkedAt": "2026-07-12" + }, + { + "title": "JSON.stringify()", + "publisher": "Mozilla MDN", + "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify", + "checkedAt": "2026-07-12" + }, + { + "title": "JSON.parse()", + "publisher": "Mozilla MDN", + "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse", + "checkedAt": "2026-07-12" + } + ], + "limitations": [ + "Storage is scoped by origin and behavior for file URLs is not a reliable contract.", + "Keys and values are strings; structured values require an explicit serialization format such as JSON.", + "Storage access can fail, is synchronous, has browser-dependent limits, and should not hold secrets or server-authoritative data.", + "Independent human review remains required before promotion to gold." + ], + "sourcePath": "content/nodes/gold/CANON-LOCALSTORAGE-001.md", + "sourceUrl": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/content/nodes/gold/CANON-LOCALSTORAGE-001.md", + "evidenceUrl": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/content/nodes/evidence/CANON-LOCALSTORAGE-001.evidence.json", + "html": "\n

Hover summary

\n

localStorage saves small browser data after refresh.

\n

One-sentence truth

\n

localStorage stores string key/value pairs for an origin and normally persists them across browser sessions.

\n

Mental model

\n

It is a tiny notebook in the browser.

\n

Beginner explanation

\n

Without saving, a page forgets changes after refresh. localStorage lets the browser remember simple data.

\n

Technical explanation

\n

localStorage stores string key-value pairs.

\n
localStorage.setItem('name', 'Jefry')\n\nconst savedName = localStorage.getItem('name')
\n

For arrays or objects, use JSON:

\n
localStorage.setItem('items', JSON.stringify(items))\n\nconst savedItems = JSON.parse(localStorage.getItem('items'))
\n

When to use it

\n

Use it for small local data in beginner projects.

\n

When not to use it

\n

Do not use it for passwords, private data, or real multi-user database needs.

\n

Common mistakes

\n\n

AI steering rule

\n

AI should warn when localStorage is being used for data that should be private or server-side.

\n

Human readability rule

\n

Use clear key names and keep save/load logic grouped.

\n

Related nodes

\n\n

Verification

\n\n

Applicability and boundaries

\n\n

Evidence status

\n

Primary-source reviewed on 2026-07-12. Independent human review and promotion are pending. See `../evidence/CANON-LOCALSTORAGE-001.evidence.json` for claim mappings, source locations, limitations, and verification contracts.

" + }, + { + "id": "CANON-ROUTE-001", + "title": "Route", + "summary": "A Laravel route matches an HTTP method and URI, then dispatches the request to a configured closure, controller action, or other supported route action.", + "tags": [ + "Laravel", + "route", + "HTTP" + ], + "versionScope": [ + "Laravel 13.x" + ], + "reviewedAt": "2026-07-12", + "sourceCount": 2, + "verificationCount": 3, + "independentReview": "pending", + "sources": [ + { + "title": "Routing", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/routing", + "checkedAt": "2026-07-12" + }, + { + "title": "URL Generation", + "publisher": "Laravel", + "url": "https://laravel.com/docs/13.x/urls", + "checkedAt": "2026-07-12" + } + ], + "limitations": [ + "HTTP method and URI are both part of route matching.", + "Named routes generate URLs by name but do not change the dispatched action.", + "Middleware, model binding, and authorization can affect the request around the action.", + "Independent human review remains required before promotion to gold." + ], + "sourcePath": "content/nodes/gold/CANON-ROUTE-001.md", + "sourceUrl": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/content/nodes/gold/CANON-ROUTE-001.md", + "evidenceUrl": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/content/nodes/evidence/CANON-ROUTE-001.evidence.json", + "html": "\n

Quick answer

\n

A Laravel route matches an HTTP method and URI, then dispatches the request to a configured closure, controller action, or other supported route action.

\n

Where it fits in the flow

\n

The route is the decision point after Laravel boots and applicable global middleware runs. It connects an incoming method and URI to the next action and may add route middleware, parameters, and a name used for URL generation.

\n

request method + URI -> route match -> route middleware -> action

\n

Mental model

\n

Think of a route as a signpost with two required coordinates: method and path. The route name is a reusable label for generating a URL; it is not the action itself.

\n

Working example

\n
use App\\Http\\Controllers\\ItemController;\nuse Illuminate\\Support\\Facades\\Route;\n\nRoute::get('/items', [ItemController::class, 'index'])\n    ->name('items.index');\n\nRoute::post('/items', [ItemController::class, 'store'])\n    ->name('items.store');
\n

GET /items dispatches index; POST /items dispatches store. Blade can generate the first URL with route('items.index') without hard-coding /items.

\n

What you can safely change

\n\n

Common failure and recovery

\n\n

How to verify it

\n\n

Boundaries and version notes

\n\n

AI steering rule

\n

AI should inspect the current request method and URI plus route:list before editing a route, and should change only the mismatched route property and its direct callers unless broader routing work is requested.

\n

Human readability rule

\n

Group related routes, use consistent resource-oriented names, and keep method, URI, action, middleware, and route name easy to scan on one definition.

\n

Related nodes

\n\n

Evidence status

\n

Primary-source reviewed on 2026-07-12. Independent human review and promotion are pending. See `../evidence/CANON-ROUTE-001.evidence.json` for claim mappings, source locations, limitations, and verification contracts.

" + } + ] +}; diff --git a/compendium-preview/index.html b/compendium-preview/index.html new file mode 100644 index 0000000..986a373 --- /dev/null +++ b/compendium-preview/index.html @@ -0,0 +1,46 @@ + + + + + + + Atlas Compendium — Public Preview + + + + + +
+
+

ALLOWLISTED PUBLIC KNOWLEDGE

+

Understand the code before accepting the change.

+

Search a small evidence-mapped preview covering the Laravel request flow and the browser-list verification exercise. Every node keeps its version scope, source count, limitations, AI-assistance disclosure, and independent-review state visible.

+ + +

+
+
+ + +
+ + + + + diff --git a/compendium-preview/style.css b/compendium-preview/style.css new file mode 100644 index 0000000..58da2a8 --- /dev/null +++ b/compendium-preview/style.css @@ -0,0 +1,59 @@ +:root { + color-scheme: light dark; + --bg: #0b1020; + --surface: #141b31; + --surface-soft: #1b2440; + --border: #344064; + --text: #f3f5ff; + --muted: #aeb7d5; + --accent: #aa97ff; + --accent-dark: #241c4c; + --success: #72ddb0; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} +* { box-sizing: border-box; } +body { margin: 0; background: radial-gradient(circle at 10% 0, #202a55 0, var(--bg) 32rem); color: var(--text); line-height: 1.65; } +a { color: var(--accent); } +.skip-link { position: absolute; top: -5rem; left: 1rem; background: white; color: black; padding: .7rem 1rem; z-index: 10; } +.skip-link:focus { top: 1rem; } +.site-header { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; padding: 1rem max(1rem, calc((100% - 1120px) / 2)); border-bottom: 1px solid var(--border); background: rgba(11, 16, 32, .86); backdrop-filter: blur(12px); } +.site-header .brand { color: var(--text); font-weight: 850; text-decoration: none; } +.site-header span { color: var(--muted); margin-right: auto; } +.shell { width: min(1120px, calc(100% - 2rem)); margin: 0 auto; padding: clamp(2.2rem, 7vw, 5.5rem) 0; } +.eyebrow { color: var(--accent); font-size: .78rem; font-weight: 850; letter-spacing: .12em; } +h1 { max-width: 850px; font-size: clamp(2.5rem, 7vw, 5.3rem); letter-spacing: -.055em; line-height: .98; margin: .5rem 0 1.2rem; } +.lead { max-width: 780px; color: var(--muted); font-size: 1.12rem; } +.search-label { display: block; margin-top: 2rem; font-weight: 800; } +input[type="search"] { width: min(100%, 680px); margin-top: .5rem; padding: .9rem 1rem; border: 1px solid var(--border); border-radius: .8rem; background: var(--surface); color: var(--text); font: inherit; } +input:focus-visible, a:focus-visible, button:focus-visible { outline: 3px solid #d6ceff; outline-offset: 3px; } +.result-count { min-height: 1.5rem; color: var(--muted); } +.node-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; margin-top: 1rem; } +.node-card { display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: 1rem; padding: 1.25rem; background: rgba(20, 27, 49, .94); } +.node-card .tags { color: var(--accent); font-size: .78rem; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; } +.node-card h2 { margin: .45rem 0; font-size: 1.35rem; } +.node-card p { color: var(--muted); } +.node-card dl { display: grid; grid-template-columns: auto 1fr; gap: .25rem .6rem; font-size: .86rem; margin-top: auto; } +.node-card dt { color: var(--muted); } +.node-card dd { margin: 0; } +.node-card a { display: inline-block; margin-top: 1rem; font-weight: 800; } +.back-link { display: inline-block; margin-bottom: 1.5rem; font-weight: 800; } +.node-meta { border: 1px solid var(--border); border-radius: 1rem; padding: 1.1rem; background: var(--surface); } +.node-meta h1 { font-size: clamp(2rem, 5vw, 4rem); } +.meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: .8rem; } +.meta-item { padding: .75rem; border-radius: .7rem; background: var(--surface-soft); } +.meta-item span { display: block; color: var(--muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .07em; } +.node-content, .evidence-section { max-width: 820px; margin: 2rem auto; } +.node-content h1 { font-size: clamp(2rem, 5vw, 3.8rem); } +.node-content h2 { margin-top: 2.4rem; font-size: 1.65rem; } +.node-content h3 { margin-top: 1.8rem; } +.node-content code { padding: .12rem .35rem; border-radius: .3rem; background: var(--surface-soft); } +.node-content pre { overflow-x: auto; padding: 1rem; border: 1px solid var(--border); border-radius: .8rem; background: #080d1a; } +.node-content pre code { padding: 0; background: transparent; } +.evidence-banner { border-left: .3rem solid var(--accent); border-radius: .4rem; padding: .8rem 1rem; background: var(--surface); color: var(--muted); } +.evidence-banner p { margin: .35rem 0; } +.evidence-section { border-top: 1px solid var(--border); padding-top: 1.5rem; } +.evidence-section li { margin-bottom: .55rem; } +.status-pending { color: #ffd27b; } +.status-complete { color: var(--success); } +footer { border-top: 1px solid var(--border); color: var(--muted); padding: 1.2rem max(1rem, calc((100% - 1120px) / 2)); } +@media (max-width: 600px) { .site-header span { width: 100%; order: 3; } } diff --git a/config/compendium-preview-allowlist.json b/config/compendium-preview-allowlist.json new file mode 100644 index 0000000..d7785b8 --- /dev/null +++ b/config/compendium-preview-allowlist.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": "1.0.0", + "description": "Exact public nodes shown in the clean-room Compendium preview.", + "nodes": [ + { + "id": "CANON-LARAVEL-REQUEST-LIFECYCLE-001", + "source": "content/nodes/gold/CANON-LARAVEL-REQUEST-LIFECYCLE-001.md", + "evidence": "content/nodes/evidence/CANON-LARAVEL-REQUEST-LIFECYCLE-001.evidence.json", + "tags": ["Laravel", "request flow", "verification"] + }, + { + "id": "CANON-ROUTE-001", + "source": "content/nodes/gold/CANON-ROUTE-001.md", + "evidence": "content/nodes/evidence/CANON-ROUTE-001.evidence.json", + "tags": ["Laravel", "route", "HTTP"] + }, + { + "id": "CANON-CONTROLLER-001", + "source": "content/nodes/gold/CANON-CONTROLLER-001.md", + "evidence": "content/nodes/evidence/CANON-CONTROLLER-001.evidence.json", + "tags": ["Laravel", "controller", "action"] + }, + { + "id": "CANON-BLADE-VIEW-001", + "source": "content/nodes/gold/CANON-BLADE-VIEW-001.md", + "evidence": "content/nodes/evidence/CANON-BLADE-VIEW-001.evidence.json", + "tags": ["Laravel", "Blade", "view"] + }, + { + "id": "CANON-LOCALSTORAGE-001", + "source": "content/nodes/gold/CANON-LOCALSTORAGE-001.md", + "evidence": "content/nodes/evidence/CANON-LOCALSTORAGE-001.evidence.json", + "tags": ["JavaScript", "browser", "storage"] + }, + { + "id": "CANON-DOM-001", + "source": "content/nodes/gold/CANON-DOM-001.md", + "evidence": "content/nodes/evidence/CANON-DOM-001.evidence.json", + "tags": ["JavaScript", "DOM", "browser"] + } + ] +} diff --git a/config/navigator-preview-allowlist.json b/config/navigator-preview-allowlist.json new file mode 100644 index 0000000..59f23ee --- /dev/null +++ b/config/navigator-preview-allowlist.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": "1.0.0", + "description": "Exact public knowledge files permitted inside the limited Navigator preview.", + "nodes": [ + { + "id": "CANON-ROUTE-001", + "title": "Laravel Route", + "source": "content/nodes/gold/CANON-ROUTE-001.md", + "evidence": "content/nodes/evidence/CANON-ROUTE-001.evidence.json" + }, + { + "id": "CANON-CONTROLLER-001", + "title": "Laravel Controller", + "source": "content/nodes/gold/CANON-CONTROLLER-001.md", + "evidence": "content/nodes/evidence/CANON-CONTROLLER-001.evidence.json" + }, + { + "id": "CANON-BLADE-VIEW-001", + "title": "Blade View", + "source": "content/nodes/gold/CANON-BLADE-VIEW-001.md", + "evidence": "content/nodes/evidence/CANON-BLADE-VIEW-001.evidence.json" + }, + { + "id": "CANON-LARAVEL-REQUEST-LIFECYCLE-001", + "title": "Laravel Request Lifecycle", + "source": "content/nodes/gold/CANON-LARAVEL-REQUEST-LIFECYCLE-001.md", + "evidence": "content/nodes/evidence/CANON-LARAVEL-REQUEST-LIFECYCLE-001.evidence.json" + } + ] +} diff --git a/config/public-export/manifest.json b/config/public-export/manifest.json index 12f9446..6290bb9 100644 --- a/config/public-export/manifest.json +++ b/config/public-export/manifest.json @@ -1,8 +1,8 @@ { "schemaVersion": "1.0.0", - "source": "DeveloperAtlas private monorepo reviewed public/ tree", + "source": "DeveloperAtlas-Public reviewed public tree", "hashAlgorithm": "sha256-normalized-text-v1", - "generatedBy": "tooling/scripts/build-public-sync-manifest.mjs", + "generatedBy": "scripts/build-public-manifest.mjs", "selfExcluded": "config/public-export/manifest.json", "files": [ { @@ -57,25 +57,65 @@ "path": ".github/SECURITY.md", "sha256": "8298914b3e475f8eeb8dc15b4d3ef5ae35c847dd9fffad52111e41486484aac6" }, + { + "path": ".github/workflows/deploy-public-preview.yml", + "sha256": "174bba0fb573ae9104cf470e6c4886588c436bdf5a10ce75c8edec2f712f8599" + }, { "path": ".github/workflows/package-control-exercise.yml", "sha256": "6fad6a12aa37044da6542b643e538d6efaca0a6b389c416cc9aec5b12e32ec85" }, + { + "path": ".github/workflows/package-navigator-preview.yml", + "sha256": "f73d8ff6214ef424896f7dc73f7c5b4265cacea3a6443655da49824fcca364c4" + }, { "path": ".github/workflows/pr-control.yml", "sha256": "9409ddc5603ccbf522514481511974db583cd006b140f592a5ae382632bc8c27" }, { "path": ".github/workflows/public-boundary.yml", - "sha256": "2ec8f0d152131db18322981347a5c64aff211a2c335b9dc3049a511f1a044865" + "sha256": "90b7e807402311879b78c7bf00beda712f84dc4484eba3732e195eadd006f90a" }, { "path": ".gitignore", - "sha256": "84e0cb1503e93650c5e951944431bc0fccda4da530cd217309467896fc469e35" + "sha256": "7c85cdea06afb10aec9f2d8b0b5eaa2d0fe2d465f53e575e3e4ed6fb9591a6e5" }, { "path": "CHANGELOG.md", - "sha256": "a43e947033ed0474938ad409d6879ce10e87e50635f84499a6131f093080dfe8" + "sha256": "0e93c16f72fb4b74c13a462416945b6074cf0de8ca84b5ce2ace1b882b11b939" + }, + { + "path": "compendium-preview/app.js", + "sha256": "213d9f00be75a517d03b3d4203deff7acac4cf43fc75dc2bc4516c4d75bd4835" + }, + { + "path": "compendium-preview/data-manifest.json", + "sha256": "0ae667ff8c2f83c38f7c8f14d0ec5fbefe8b21d45971fbaac2aae6989d30b557" + }, + { + "path": "compendium-preview/data.js", + "sha256": "c4ade5ef8c3986694b123619210e0c7d3b23f27f5e8c8874820be144b1858311" + }, + { + "path": "compendium-preview/index.html", + "sha256": "9d69176275c263ef875df947d84fc4487951ee95c496f6499c42055bdd0bb979" + }, + { + "path": "compendium-preview/README.md", + "sha256": "44d4a7a3461e42b85422ca818db8876e6328c117a40e9d0e38b77cb3d1c759b9" + }, + { + "path": "compendium-preview/style.css", + "sha256": "b66487219d03f338086f5f363e68267330749b0a1afdb7c212f3372f03d5fe08" + }, + { + "path": "config/compendium-preview-allowlist.json", + "sha256": "378b6dd7d53e8b5efde862b96ba2a3f3c864514e664bfdabaec23b330238be9f" + }, + { + "path": "config/navigator-preview-allowlist.json", + "sha256": "61c94f8ac6f30ed0047363e19e2f08d38f967aef47e94cbe955a0d3c6bd55323" }, { "path": "config/public-export/php-laravel.json", @@ -347,19 +387,23 @@ }, { "path": "control-lite/app.js", - "sha256": "89ae6e6e0f5dbb090eafe449ec0a11a3f6891af9da7382b42789fa8136fb61a2" + "sha256": "4f0b5391a4c531daf6b46a53ce03c9922c1fba5a9830a0648692f54bf6a3ba50" + }, + { + "path": "control-lite/core.js", + "sha256": "fe3ac17f50a979b0e1af5b9115a8520e9c772e3aa60c2eaaa633cce0f32bdf7e" }, { "path": "control-lite/index.html", - "sha256": "decdafd30708d7a63e81f4662bed9011f97ee6d0bec64364c14912fb64214a3a" + "sha256": "a90471b123f8bf848e6c16fef589fe5116af7d7157c8a62a093e06067beaea51" }, { "path": "control-lite/README.md", - "sha256": "d5d3ba1c0dee950041fbb754323de9f1d9503ca34564a163952e2431cf6545a1" + "sha256": "c5f94c7e87ee27e4235c528a3f32e1e84ee9069c8491abbabbc1e6741ff3c98b" }, { "path": "control-lite/style.css", - "sha256": "6fefebbd9e80e8f01374c875e7903d708662225e85d517d2d7a7aabc3a2db82b" + "sha256": "5a538923d5163c2ec8de63ad868833961d82ed5951d2200c6d9f0406415234e8" }, { "path": "control/CHANGE_CONTRACT_TEMPLATE.md", @@ -405,10 +449,94 @@ "path": "control/examples/laravel-status-label/VERIFICATION_CHECKLIST.md", "sha256": "666268e372f24e9a290c005e79a086b20f2091756e6e9310c73ee6751c852172" }, + { + "path": "control/exercises/browser-list-count/ATLAS_INSTRUCTION.md", + "sha256": "f9686185c5fb1350061ad0a97ea76e16b1bb3db31f5a13ba1d11c81e7aed258f" + }, + { + "path": "control/exercises/browser-list-count/BASELINE_PROMPT.md", + "sha256": "0b5583b9570b0a39c7b3a066a97518f0829081ae3898e2c872d7e0ef1bc0eac7" + }, + { + "path": "control/exercises/browser-list-count/baseline/index.html", + "sha256": "ea7811da6452dd8ebad92896282aa75d267d08161ca9b0e550e80a6f603a6721" + }, + { + "path": "control/exercises/browser-list-count/baseline/script.js", + "sha256": "66d9174e3c2771c9b98af186fd6877c0b8a4f9c54f2e4832fcf0dc39db692d75" + }, + { + "path": "control/exercises/browser-list-count/baseline/style.css", + "sha256": "214a8f2de641cbb713e372a8769b781164c5c096aba26bf1fd42aace6dae66dd" + }, + { + "path": "control/exercises/browser-list-count/CHANGE_CONTRACT.md", + "sha256": "4203fe1ba6ba91934a655f56900364864a81d865cc0c73009b6c25547a57b393" + }, + { + "path": "control/exercises/browser-list-count/DECISION_RECORD.md", + "sha256": "1e86179928ad39498e43a54b0fe6308317d77ef08e7e4b0429bcdb3897109e81" + }, + { + "path": "control/exercises/browser-list-count/EVIDENCE_RECORD.md", + "sha256": "95090386aa921a299f64408192fa0714a22b2e015c7fadd55313cd0a0cfc2287" + }, + { + "path": "control/exercises/browser-list-count/project/index.html", + "sha256": "b8c07fb30c3ce41a70c879007b48490d841edd703d0d79c37a40231f5a03f00c" + }, + { + "path": "control/exercises/browser-list-count/project/script.js", + "sha256": "ed49ca5f94bf3e6bb75f5cd40bb68e5f2eaaf45c3289b9f4ece8322c7490ab5f" + }, + { + "path": "control/exercises/browser-list-count/project/style.css", + "sha256": "214a8f2de641cbb713e372a8769b781164c5c096aba26bf1fd42aace6dae66dd" + }, + { + "path": "control/exercises/browser-list-count/proposed/IN_SCOPE_PATCH.diff", + "sha256": "1dced0dceb383614cff91181f04ea965e96d8d3006abad0f9809b2b316d2a909" + }, + { + "path": "control/exercises/browser-list-count/proposed/OUT_OF_SCOPE_PATCH.diff", + "sha256": "546971028ef7929f50a79a80ab14a1b4ccff9546a36b130e0d7b40243cbf4ae9" + }, + { + "path": "control/exercises/browser-list-count/proposed/README.md", + "sha256": "fb00c55f931d107c1dcdf334f546724fc93793b6b6f8cc75b52930eca4ac6a0a" + }, + { + "path": "control/exercises/browser-list-count/README.md", + "sha256": "056bf970095507c31c1ed4f4221cf407b1cbc7ad2a61566b05910b796b990c95" + }, + { + "path": "control/exercises/browser-list-count/records/IN_SCOPE_CHANGED_FILES.txt", + "sha256": "db78022a7fd544a2b4d52f77ca8f35e58cd6f12febaa45becad30877dfd55d0f" + }, + { + "path": "control/exercises/browser-list-count/records/OUT_OF_SCOPE_CHANGED_FILES.txt", + "sha256": "076bfae7a2c5083d79a2a52e200673ee8849ac762c660494c530a682e55df5e4" + }, + { + "path": "control/exercises/browser-list-count/records/README.md", + "sha256": "c9bfef903e84f4dee752e15f3b679d7f944d6b82bf9fefeadf634e697dbb2a6b" + }, + { + "path": "control/exercises/browser-list-count/TASK.md", + "sha256": "fe7953807935266fd30c15b2dabf869309c9e28f0c46bfb315d8378fdb6966cb" + }, + { + "path": "control/exercises/browser-list-count/VERIFY.md", + "sha256": "eb9a8a4b6d0304037fa74e0cc717db32521e0bf3679031d5946a40aaa8633783" + }, { "path": "docs/getting-started/DOWNLOAD.md", "sha256": "fd359a3f7c64609425333ce107cc9e41417f250e286c42fcd875162a17bf850d" }, + { + "path": "docs/getting-started/PUBLIC_PREVIEW_RELEASE_REVIEW.md", + "sha256": "83d35c8f6825a4ac36e60f312a3e26a29ddf52e4818babe13be0eeb034692e08" + }, { "path": "docs/getting-started/TRY_ATLAS.md", "sha256": "daf7467fc804d2c26149789c06950a84aed1d92b61a59f4ebef59275f89b62a7" @@ -439,7 +567,7 @@ }, { "path": "docs/product/ROADMAP.md", - "sha256": "1db5ea3677194a5cae82fe69fa1c54e78e739afe21fbf1a5253a1de9dd159103" + "sha256": "fe5ee5b74d523ea1df012c8db45666fd150d912cc0d0f7406a834a199f90b882" }, { "path": "docs/public/architecture-overview.md", @@ -467,7 +595,7 @@ }, { "path": "docs/README.md", - "sha256": "84133a05c69cfefb769e09bfe2e9acd6f3ee66568dd996b3d20bf7579d92a1d5" + "sha256": "bb18115e0d0a5f6f4afb236287cb260501ca82713918f3a173484da72a75299d" }, { "path": "docs/testing/EVALUATION_TERMS.md", @@ -487,7 +615,7 @@ }, { "path": "docs/testing/START_TESTING.md", - "sha256": "240718474876f6fed19ad0ea573e217ef671ec95b02d394e7e81179b47a273f5" + "sha256": "d9c556480e3f3943004d23a5c8918d525d37f96554cceface3cf0170fcd784da" }, { "path": "docs/testing/templates/tester-tracking-template.csv", @@ -541,13 +669,117 @@ "path": "LICENSE", "sha256": "40ea12562b84deacf86284f2d5879cc4cec9ecb849febec0014e82e744383bb8" }, + { + "path": "navigator-preview/demo-workspace/atlas-navigator-demo.code-workspace", + "sha256": "9f5da2c373353a8517a9d43e0d4b3260343ec14326ea27af242f49966a1c0e41" + }, + { + "path": "navigator-preview/demo-workspace/boundary-lab/app/Http/Controllers/StatusController.php", + "sha256": "001c64451a1b7650e6682999b2503832667eabc63289c974579b0d54b3993df3" + }, + { + "path": "navigator-preview/demo-workspace/boundary-lab/README.md", + "sha256": "bc89c084a83b20f8f479d51f1c1c767e9f4d4c102b6146ceac946342c44ea772" + }, + { + "path": "navigator-preview/demo-workspace/boundary-lab/resources/views/status.blade.php", + "sha256": "eebd3395fd2e3d43721883d09f3b7ab710004f12c930f28ad1bbc9df52d9cb07" + }, + { + "path": "navigator-preview/demo-workspace/boundary-lab/routes/web.php", + "sha256": "a807233a96dd969b3d45d8891ae3f94852b5ea77702b8024060f85ead7ce1c93" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/app/Http/Controllers/Controller.php", + "sha256": "12bfcbfc6361492817115e10383054179d39151f24486d511a0be3f5a751423c" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/app/Http/Controllers/HomeController.php", + "sha256": "5a00c698e84f3e27cdcd4dfac38dc46c8da63ceeba77a605796b8f5295c1aab3" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/app/Http/Controllers/UserController.php", + "sha256": "906f7fe9674b6d232a54c546be6635adb9335f69117066be6777e243f91e55fc" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/artisan", + "sha256": "e715147ef2e70e68cdb5d89a2f38cb3f224bc49451e5034533f79885014465e6" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/public/js/app.js", + "sha256": "307c372c202a941febf2ca5665ac169a88fd61948b08fe01b7ceefcc44205ed7" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/README.md", + "sha256": "13cc14ca9286b4c6e35ccf4084d6deea3a95e6ac82bb53f773131f3ed64dac97" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/resources/views/components/alert.blade.php", + "sha256": "990d91206716a852abe6299431d0bddff4817a0f40f6afd2299fc74b0391046a" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/resources/views/layouts/app.blade.php", + "sha256": "f9da0b748e99287e99fbc5336b0b5a659964a0135d61555ebe10c46813b10413" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/resources/views/pages/about.blade.php", + "sha256": "a5e6ad59aa217edad1fa3d8987cbef2197902e2bc0be1739a0e3d00278076091" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/resources/views/partials/nav.blade.php", + "sha256": "0e4a642a3e7c42de8ddf4196295e4e7380a9e54dd85a96bdf638c82a7811c940" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/resources/views/users/index.blade.php", + "sha256": "91943b0edc7efddb8d75b47ce0d60beae4fd3aeebb74826cea980bb44d279bfe" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/resources/views/users/show.blade.php", + "sha256": "f74311a251c4b1aba214625c8265b8e8a8dc283e1cb9094295671c10b6e01b35" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/resources/views/welcome.blade.php", + "sha256": "4bd97111719b423e78f452ab96680c735de2a56d5bee60f7a65bdccec22aa2a6" + }, + { + "path": "navigator-preview/demo-workspace/clean-laravel/routes/web.php", + "sha256": "abaa2eadca037b2f79f96225b550b8cdcc5832aef3de98efbe8160d6f807d9fb" + }, + { + "path": "navigator-preview/demo-workspace/README.md", + "sha256": "239f6fbc763bc4caa0a9a6fdb17631a8b20b57be981991811e32d3a7eebc0bcc" + }, + { + "path": "navigator-preview/extension/extension.js", + "sha256": "8395593b67df35b1496f9d70384678c8d7c6a5ffeebd150f519009632947f2ef" + }, + { + "path": "navigator-preview/extension/flow-core.cjs", + "sha256": "72b83648de13e6943e61a6562d889892dd8ab562c9f8cc57f7981f46adc36d3d" + }, + { + "path": "navigator-preview/extension/LICENSE", + "sha256": "a05fc5a167bc0546e41c2d8d9cd78e415acf9dcd5ef2fd47f8d39c08a85f9691" + }, + { + "path": "navigator-preview/extension/media/atlas-preview.svg", + "sha256": "a3f438edda432c71f95318ff8336bf5589bf2c4beb6b710b9bd7c1a5829a11bf" + }, + { + "path": "navigator-preview/extension/package.json", + "sha256": "2ee71e60cdd4e0f5ee2bb337fd770214a33b04a82b4a4c1c881a0956981d1765" + }, + { + "path": "navigator-preview/README.md", + "sha256": "b6469817f23257ee42802437ef6b808a58d012a52b3cf6be8d6b29599d20026d" + }, { "path": "package-lock.json", "sha256": "f930b702104ca37c9611915f1fd118d1472cdf58ce910e5930e45e253066e3ac" }, { "path": "package.json", - "sha256": "f6d612b66572a623727de476fd483f9239c54bfbd3c0fc95d7e3208fa755e2ad" + "sha256": "e8698c35a323112b91d4fe0b0fa7a228d537d330d5078a755b602517b02de562" }, { "path": "packs/ai-collaboration/ai-action-model.md", @@ -615,7 +847,7 @@ }, { "path": "README.md", - "sha256": "5a2d246b250131461ec677ede590747d640afd39aa864d893c16c3cb3487e459" + "sha256": "36d4eba9374f372214a1dfad911b9a01dca6b03705c0bd46e8d39a6c71098a17" }, { "path": "screenshots/browser-list-example.png", @@ -649,6 +881,30 @@ "path": "screenshots/README.md", "sha256": "970e559d401dfe0309434d5fa97d41cfd895f21ab731548f40aedaf5729acd03" }, + { + "path": "scripts/build-compendium-preview.mjs", + "sha256": "6d964007fabb189a3ef69b467f4c22b3ce8d4fcb289073ccb18280811dcac7db" + }, + { + "path": "scripts/build-navigator-preview.mjs", + "sha256": "f82e2cadb40c4510a785eb99c3c24e13f4f566747d612579c2d6bb923b98d4c7" + }, + { + "path": "scripts/build-pages-preview.mjs", + "sha256": "369dda31ac02b2596687336ffb7fd4746d7f827b1f3adb3a9d2141a3ebd943db" + }, + { + "path": "scripts/build-public-manifest.mjs", + "sha256": "7a1d19392c72703736f9fb84c5cd2f7425c83312e93782eed2fd567e5027938d" + }, + { + "path": "scripts/compendium-render.mjs", + "sha256": "67017b4e0e034b711852125d32ba22bbdc06c6d17a406e6b9b4c48b0a41fad9c" + }, + { + "path": "scripts/package-navigator-demo.mjs", + "sha256": "f9fb78cb91592daaeab003702671350108b2db2875ea1e8a52f12d859d032a39" + }, { "path": "scripts/pr-control-policy.mjs", "sha256": "d44b0395b0d26eb4c8fa74f850a60661f280b6b2c87ecbead709c8e6e74e9e36" @@ -667,15 +923,51 @@ }, { "path": "scripts/validate-public-repo.mjs", - "sha256": "017f8148f05ac2f3d9311f8abe4b8fd0d10a9cefb2de34f85b883b1220bb871c" + "sha256": "02fc172e546bf9045ae369a4cd2442af7c4a6f80308edb05743fa8a51c7687ff" + }, + { + "path": "scripts/verify-control-exercise-scope.mjs", + "sha256": "33d5a37a8e318714c4b6d36ed443d2c828f431e422af75c766f25e0c0bfb2e30" }, { "path": "tests/browser-list.spec.mjs", "sha256": "2d00a656039f7926f384376be0853529dc780bac5e197bfae3e3221166e1ffea" }, + { + "path": "tests/compendium-preview.spec.mjs", + "sha256": "2d75a9c9bfa38aa3cc9e5d3992cc70b0ea9c4bb33c78b641289e5aa263891e2a" + }, + { + "path": "tests/control-core.spec.cjs", + "sha256": "b4980ed90196c189f7a28ee5464d0bf3e62e8ef4f580116cc50b3155353fb114" + }, + { + "path": "tests/control-exercise-behavior.spec.cjs", + "sha256": "b254500911a741721de076ed2f306124491331773a24a85e92d3bcfb4c607cc8" + }, + { + "path": "tests/control-exercise-browser.spec.mjs", + "sha256": "3ed4704abcff3bc481388c5f593fca53093d18c68579e4015834953210da83c5" + }, + { + "path": "tests/control-exercise.spec.cjs", + "sha256": "8ded5fd0282c212196ab74fe4412e312f339ca37a782e60bc7d5b9b199fe758b" + }, { "path": "tests/control-lite.spec.mjs", - "sha256": "27ef36c474ff585e5e926f5daec3a2e0703ac3d3ac4b5de493a067f34eb31993" + "sha256": "0d3ce52fea99e89f77351b80e956a83ab24d6834aef44429a79f403d5c64fdab" + }, + { + "path": "tests/navigator-demo.spec.cjs", + "sha256": "5a4ed5039c09d37cdb7c5a6f560dadcc0ce4d2b7a370351b3c4b118830fa735e" + }, + { + "path": "tests/navigator-preview.spec.cjs", + "sha256": "528d7d29db61e6b66a3656e29dcc01341bb0e774c10bd67c20acf9bd51b6aec7" + }, + { + "path": "tests/pages-preview.spec.cjs", + "sha256": "bf2b16c5442e5017216d70b73836b5fd9611df7cdf4ecb1bc247aa9399991034" }, { "path": "tests/pr-control.spec.mjs", diff --git a/control-lite/README.md b/control-lite/README.md index fb19da7..121cd1f 100644 --- a/control-lite/README.md +++ b/control-lite/README.md @@ -1,13 +1,19 @@ # Atlas Control Lite -This is a local browser-based preview of the Atlas artifact workflow. +Control Lite is a local browser preview of the Developer Atlas control loop: + +> Prepare → Instruct → Inspect → Evidence → Decide → Export ## Run it 1. Download or clone this repository. -2. Open `control-lite/index.html` in a modern browser. -3. Define one bounded change. -4. Generate and review the Change Contract, AI instruction, Evidence Record, and Decision Record. +2. Open `control-lite/index.html` in a modern browser, or serve the repository with `node scripts/serve-static.mjs`. +3. Load an Accept, Revise, or Reject example—or define your own bounded change. +4. Review the generated instruction, supplied changed-file list, evidence states, and human decision. +5. Export one combined `ATLAS_CHANGE_RECORD.md` or the four separate artifacts. + +The session autosaves in browser `localStorage` and can be exported as JSON for later editing. + +## Trust boundary -The tool has no backend, telemetry, account, AI connection, or command execution. Generated files are -templates; a human remains responsible for scope, evidence, and the final decision. +Control Lite has no backend, telemetry, account, AI connection, Git access, or command execution. It compares the paths a user supplies and generates reviewable local files. It does not prove that a patch is correct or that a command ran. A human remains responsible for scope, evidence, and the final decision. diff --git a/control-lite/app.js b/control-lite/app.js index 076cff8..d8b2799 100644 --- a/control-lite/app.js +++ b/control-lite/app.js @@ -1,225 +1,332 @@ +'use strict' + +const core = window.AtlasControlCore +const storageKey = 'developer-atlas-control-lite-session-v1' +const stepNames = ['prepare', 'instruct', 'inspect', 'evidence', 'decide', 'export'] const form = document.querySelector('#control-form') -const output = document.querySelector('#output') -const artifacts = document.querySelector('#artifacts') - -form.addEventListener('submit', (event) => { - event.preventDefault() - const data = readForm() - const generated = [ - ['CHANGE_CONTRACT.md', changeContract(data)], - ['AI_INSTRUCTION.md', aiInstruction(data)], - ['EVIDENCE_RECORD.md', evidenceRecord(data)], - ['DECISION_RECORD.md', decisionRecord(data)], - ] - - artifacts.textContent = '' - for (const [filename, content] of generated) artifacts.appendChild(renderArtifact(filename, content)) - output.hidden = false - output.scrollIntoView({ behavior: 'smooth', block: 'start' }) -}) - -function readForm() { - return { - goal: value('goal'), - allowed: lines('allowed'), - protected: lines('protected'), - acceptance: lines('acceptance'), - verification: lines('verification'), - parked: lines('parked'), - } +const status = document.querySelector('#status') +const evidenceRows = document.querySelector('#evidence-rows') +const scopeSummary = document.querySelector('#scope-summary') +const decisionGuidance = document.querySelector('#decision-guidance') +const artifactsContainer = document.querySelector('#artifacts') +let activeStep = 'prepare' +let state = loadStoredState() || core.cloneScenario('revise') + +form.addEventListener('submit', (event) => event.preventDefault()) +document.querySelector('#load-scenario').addEventListener('click', loadSelectedScenario) +document.querySelector('#import-session').addEventListener('click', () => document.querySelector('#import-file').click()) +document.querySelector('#import-file').addEventListener('change', importSession) +document.querySelector('#reset-session').addEventListener('click', resetSession) +document.querySelector('#copy-record').addEventListener('click', copyRecord) +document.querySelector('#download-record').addEventListener('click', () => downloadText('ATLAS_CHANGE_RECORD.md', core.combinedRecord(readState()))) +document.querySelector('#download-session').addEventListener('click', () => downloadText('atlas-control-session.json', `${JSON.stringify(readState(), null, 2)}\n`, 'application/json')) + +document.querySelectorAll('[data-step-target]').forEach((button) => button.addEventListener('click', () => showStep(button.dataset.stepTarget))) +document.querySelectorAll('[data-next]').forEach((button) => button.addEventListener('click', () => showStep(button.dataset.next))) +document.querySelectorAll('[data-back]').forEach((button) => button.addEventListener('click', () => showStep(button.dataset.back))) + +for (const field of form.querySelectorAll('textarea:not([readonly]), input:not([type="radio"]), select')) { + field.addEventListener('input', handleInput) + field.addEventListener('change', handleInput) } +for (const radio of document.querySelectorAll('input[name="decision"]')) radio.addEventListener('change', handleInput) -function value(id) { - return document.querySelector(`#${id}`).value.trim() -} +renderState(state) +showStep('prepare', false) -function lines(id) { - return value(id).split(/\r?\n/).map((line) => line.trim()).filter(Boolean) +function handleInput() { + state = readState() + synchronizeEvidenceUi() + refreshDerivedViews() + storeState(state) } -function bullets(items, checkbox = false) { - if (!items.length) return '- None declared.' - return items.map((item) => `${checkbox ? '- [ ]' : '-'} ${item}`).join('\n') +function readState() { + const evidence = [...evidenceRows.querySelectorAll('.evidence-row')].map((row) => ({ + check: row.dataset.check, + status: row.querySelector('select').value, + note: row.querySelector('input').value.trim(), + })) + return core.normalizeState({ + scenario: document.querySelector('#scenario').value, + goal: fieldValue('goal'), + allowed: fieldValue('allowed'), + protected: fieldValue('protected'), + acceptance: fieldValue('acceptance'), + verification: fieldValue('verification'), + parked: fieldValue('parked'), + changed: fieldValue('changed'), + patch: fieldValue('patch'), + evidence, + decision: document.querySelector('input[name="decision"]:checked')?.value || '', + reviewer: fieldValue('reviewer'), + date: fieldValue('decision-date'), + rationale: fieldValue('rationale'), + nextAction: fieldValue('next-action'), + }) } -function changeContract(data) { - return `# Atlas Change Contract - -## Goal - -${data.goal} - -## Allowed scope - -${bullets(data.allowed.map((file) => `\`${file}\``))} - -## Protected boundaries - -${bullets(data.protected)} - -## Acceptance criteria - -${bullets(data.acceptance, true)} - -## Verification - -${bullets(data.verification, true)} - -Never mark a command as passed unless a person or trusted system actually observed it. - -## Confirmation boundaries - -- Deleting data or files. -- Changing dependencies, migrations, public APIs, authentication, authorization, or payment behavior. -- Expanding beyond the allowed scope. - -## Parked ideas - -${bullets(data.parked)} -` +function renderState(nextState) { + state = core.normalizeState(nextState) + document.querySelector('#scenario').value = ['accept', 'revise', 'reject', 'custom'].includes(state.scenario) ? state.scenario : 'custom' + setField('goal', state.goal) + setField('allowed', state.allowed.join('\n')) + setField('protected', state.protected.join('\n')) + setField('acceptance', state.acceptance.join('\n')) + setField('verification', state.verification.join('\n')) + setField('parked', state.parked.join('\n')) + setField('changed', state.changed.join('\n')) + setField('patch', state.patch) + setField('reviewer', state.reviewer) + setField('decision-date', state.date) + setField('rationale', state.rationale) + setField('next-action', state.nextAction) + document.querySelectorAll('input[name="decision"]').forEach((radio) => { radio.checked = radio.value === state.decision }) + renderEvidenceRows(state.evidence) + refreshDerivedViews() + storeState(state) } -function aiInstruction(data) { - return `# Atlas-controlled AI instruction - -## Goal - -${data.goal} - -## Allowed files - -${bullets(data.allowed.map((file) => `\`${file}\``))} - -## Protected boundaries - -${bullets(data.protected)} - -Do not edit any path outside the allowed list. Park useful out-of-scope ideas instead of implementing them. - -## Acceptance criteria - -${bullets(data.acceptance)} - -## Verification honesty - -${bullets(data.verification)} - -Report every check as Passed, Failed, or Not run. Never infer a result that was not observed. - -## Required response format - -1. Restate the goal and allowed files. -2. Explain the smallest proposed change. -3. Show the exact changed-file list. -4. Provide the patch. -5. Report verification honestly. -6. List parked ideas separately. -` +function loadSelectedScenario() { + const name = document.querySelector('#scenario').value + renderState(core.cloneScenario(name)) + showStep('prepare') + announce(`${capitalize(name)} example loaded.`) } -function evidenceRecord(data) { - return `# Atlas Evidence Record - -## Scope review - -${bullets(data.allowed.map((file) => `Expected allowed path: \`${file}\``), true)} - -## Acceptance review - -${bullets(data.acceptance, true)} - -## Verification results - -${data.verification.map((check) => `- ${check}: **Not run**`).join('\n') || '- No checks declared.'} - -## Known limitations - -- This template contains no automatically observed command results. -- A human reviewer must replace “Not run” only after observing trustworthy evidence. -` +function resetSession() { + localStorage.removeItem(storageKey) + document.querySelector('#scenario').value = 'custom' + renderState(core.cloneScenario('custom')) + showStep('prepare') + announce('Control session reset.') } -function decisionRecord(data) { - return `# Atlas Decision Record - -## Change - -${data.goal} - -## Decision - -Choose exactly one: - -- [ ] Accept -- [ ] Revise -- [ ] Reject - -## Decided by - -[Accountable human name or role] - -## Evidence reviewed - -- Scope conformance: -- Required checks: -- Known limitations: - -## Rationale +function showStep(name, moveFocus = true) { + if (!stepNames.includes(name)) return + state = readState() + refreshDerivedViews() + activeStep = name + document.querySelectorAll('[data-step]').forEach((panel) => { panel.hidden = panel.dataset.step !== name }) + document.querySelectorAll('[data-step-target]').forEach((button) => { + if (button.dataset.stepTarget === name) button.setAttribute('aria-current', 'step') + else button.removeAttribute('aria-current') + }) + if (moveFocus) { + const heading = document.querySelector(`[data-step="${name}"] h2`) + heading?.setAttribute('tabindex', '-1') + heading?.focus() + } + storeState(state) +} -[Why this decision follows from the declared goal, boundaries, and observed evidence.] +function synchronizeEvidenceUi() { + const current = [...evidenceRows.querySelectorAll('.evidence-row')].map((row) => ({ + check: row.dataset.check, + status: row.querySelector('select').value, + note: row.querySelector('input').value, + })) + const synchronized = core.synchronizeEvidence(core.lines(fieldValue('verification')), current) + const currentSignature = JSON.stringify(current.map(({ check, status, note }) => ({ check, status, note }))) + const nextSignature = JSON.stringify(synchronized) + if (currentSignature !== nextSignature) renderEvidenceRows(synchronized) +} -## Required next action +function renderEvidenceRows(evidence) { + evidenceRows.textContent = '' + if (!evidence.length) { + const empty = document.createElement('p') + empty.className = 'callout' + empty.textContent = 'No required checks are declared. Return to Prepare and add the evidence needed for a trustworthy decision.' + evidenceRows.appendChild(empty) + return + } + evidence.forEach((entry, index) => { + const row = document.createElement('div') + row.className = 'evidence-row' + row.dataset.check = entry.check + + const check = document.createElement('p') + check.className = 'evidence-check' + check.textContent = entry.check + + const statusLabel = document.createElement('label') + statusLabel.textContent = 'Result' + const select = document.createElement('select') + select.setAttribute('aria-label', `Result for ${entry.check}`) + core.STATUS_VALUES.forEach((statusValue) => { + const option = document.createElement('option') + option.value = statusValue + option.textContent = statusValue + option.selected = entry.status === statusValue + select.appendChild(option) + }) + statusLabel.appendChild(select) + + const noteLabel = document.createElement('label') + noteLabel.textContent = 'Observed evidence or limitation' + const note = document.createElement('input') + note.type = 'text' + note.value = entry.note || '' + note.id = `evidence-note-${index}` + noteLabel.appendChild(note) + + select.addEventListener('change', handleInput) + note.addEventListener('input', handleInput) + row.append(check, statusLabel, noteLabel) + evidenceRows.appendChild(row) + }) +} -[What happens next. Unrun required checks normally require revision rather than acceptance.] +function refreshDerivedViews() { + const current = readStateWithoutRecursion() + document.querySelector('#instruction-preview').value = core.instruction(current) + renderScopeAssessment(current) + renderDecisionAssessment(current) + document.querySelector('#combined-record').value = core.combinedRecord(current) + const relatedNode = core.relatedNodeForScenario(current.scenario) + const compendiumLink = document.querySelector('#compendium-link') + const nodeQuery = relatedNode.id ? `?node=${encodeURIComponent(relatedNode.id)}` : '' + compendiumLink.href = `../compendium-preview/index.html${nodeQuery}` + compendiumLink.textContent = relatedNode.id ? `Open ${relatedNode.title} in the Compendium` : 'Open the Compendium catalog' + renderArtifacts(core.artifacts(current)) +} -## Decision date +function readStateWithoutRecursion() { + const evidence = [...evidenceRows.querySelectorAll('.evidence-row')].map((row) => ({ + check: row.dataset.check, + status: row.querySelector('select').value, + note: row.querySelector('input').value.trim(), + })) + return core.normalizeState({ + scenario: document.querySelector('#scenario').value, + goal: fieldValue('goal'), allowed: fieldValue('allowed'), protected: fieldValue('protected'), + acceptance: fieldValue('acceptance'), verification: fieldValue('verification'), parked: fieldValue('parked'), + changed: fieldValue('changed'), patch: fieldValue('patch'), evidence, + decision: document.querySelector('input[name="decision"]:checked')?.value || '', + reviewer: fieldValue('reviewer'), date: fieldValue('decision-date'), rationale: fieldValue('rationale'), nextAction: fieldValue('next-action'), + }) +} -YYYY-MM-DD -` +function renderScopeAssessment(current) { + const assessment = core.assessScope(current) + scopeSummary.textContent = '' + const heading = document.createElement('h3') + const body = document.createElement('p') + if (!assessment.hasChangedFiles) { + scopeSummary.dataset.tone = 'warning' + heading.textContent = 'Scope not reviewed' + body.textContent = 'Supply the exact changed-file list before deciding.' + } else if (assessment.outOfScope.length) { + scopeSummary.dataset.tone = 'danger' + heading.textContent = `${assessment.outOfScope.length} out-of-scope path${assessment.outOfScope.length === 1 ? '' : 's'} recorded` + body.textContent = 'A functional result does not erase a scope violation.' + } else { + scopeSummary.dataset.tone = 'success' + heading.textContent = 'Supplied paths are inside the allowlist' + body.textContent = `${assessment.inScope.length} changed path${assessment.inScope.length === 1 ? '' : 's'} checked. This is a path comparison, not a Git inspection.` + } + scopeSummary.append(heading, body) + if (assessment.outOfScope.length) scopeSummary.appendChild(textList(assessment.outOfScope)) } -function renderArtifact(filename, content) { - const article = document.createElement('article') - article.className = 'artifact' +function renderDecisionAssessment(current) { + const assessment = core.assessDecision(current) + decisionGuidance.textContent = '' + decisionGuidance.dataset.tone = assessment.aligned ? 'success' : current.decision ? 'warning' : 'warning' + const heading = document.createElement('h3') + heading.textContent = current.decision ? assessment.alignment : `Recorded evidence suggests: ${assessment.recommendation}` + const body = document.createElement('p') + body.textContent = `Out-of-scope paths: ${assessment.scope.outOfScope.length}. Failed checks: ${assessment.failed.length}. Not-run checks: ${assessment.notRun.length}.` + decisionGuidance.append(heading, body) +} - const header = document.createElement('header') - const title = document.createElement('h3') - title.textContent = filename +function renderArtifacts(artifacts) { + artifactsContainer.textContent = '' + for (const artifact of artifacts) { + const article = document.createElement('article') + article.className = 'artifact' + const header = document.createElement('header') + const title = document.createElement('h3') + title.textContent = artifact.filename + const download = document.createElement('button') + download.type = 'button' + download.className = 'secondary' + download.textContent = 'Download' + download.addEventListener('click', () => downloadText(artifact.filename, artifact.content)) + const textarea = document.createElement('textarea') + textarea.readOnly = true + textarea.value = artifact.content + header.append(title, download) + article.append(header, textarea) + artifactsContainer.appendChild(article) + } +} - const actions = document.createElement('div') - actions.className = 'artifact-actions' - const copy = button('Copy', 'secondary') - const download = button('Download') +async function copyRecord() { + const content = core.combinedRecord(readState()) + try { + await navigator.clipboard.writeText(content) + announce('Combined Atlas record copied.') + } catch { + const textarea = document.querySelector('#combined-record') + textarea.focus() + textarea.select() + announce('Clipboard access was unavailable. The record is selected for manual copying.') + } +} - const textarea = document.createElement('textarea') - textarea.readOnly = true - textarea.value = content +function downloadText(filename, content, type = 'text/markdown;charset=utf-8') { + const blob = new Blob([content], { type }) + const link = document.createElement('a') + const url = URL.createObjectURL(blob) + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + link.remove() + setTimeout(() => URL.revokeObjectURL(url), 0) + announce(`${filename} downloaded.`) +} - copy.addEventListener('click', async () => { - await navigator.clipboard.writeText(content) - copy.textContent = 'Copied' - setTimeout(() => { copy.textContent = 'Copy' }, 1200) - }) +async function importSession(event) { + const file = event.target.files?.[0] + event.target.value = '' + if (!file) return + try { + const parsed = JSON.parse(await file.text()) + if (parsed.schemaVersion !== '1.0.0') throw new Error('Unsupported session schema.') + renderState(parsed) + showStep('prepare') + announce('Control session imported.') + } catch (error) { + announce(`Session import failed: ${error.message}`) + } +} - download.addEventListener('click', () => { - const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' }) - const link = document.createElement('a') - link.href = URL.createObjectURL(blob) - link.download = filename - link.click() - URL.revokeObjectURL(link.href) - }) +function loadStoredState() { + try { + const raw = localStorage.getItem(storageKey) + return raw ? core.normalizeState(JSON.parse(raw)) : null + } catch { + return null + } +} - actions.append(copy, download) - header.append(title, actions) - article.append(header, textarea) - return article +function storeState(nextState) { + try { localStorage.setItem(storageKey, JSON.stringify(core.normalizeState(nextState))) } catch { /* local storage can be unavailable */ } } -function button(label, className = '') { - const element = document.createElement('button') - element.type = 'button' - element.textContent = label - element.className = className - return element +function fieldValue(id) { return document.querySelector(`#${id}`).value.trim() } +function setField(id, value) { document.querySelector(`#${id}`).value = value || '' } +function announce(message) { status.textContent = message } +function capitalize(value) { return value ? value[0].toUpperCase() + value.slice(1) : value } +function textList(items) { + const list = document.createElement('ul') + for (const item of items) { + const li = document.createElement('li') + li.textContent = item + list.appendChild(li) + } + return list } diff --git a/control-lite/core.js b/control-lite/core.js new file mode 100644 index 0000000..fcc6f67 --- /dev/null +++ b/control-lite/core.js @@ -0,0 +1,271 @@ +(function attachAtlasControlCore(globalScope) { + 'use strict' + + const STATUS_VALUES = ['Not run', 'Passed', 'Failed'] + + const scenarios = { + revise: { + scenario: 'revise', + goal: 'Improve the status page so a reviewer knows it is ready for a human check.', + allowed: ['app/Http/Controllers/StatusController.php', 'resources/views/status.blade.php'], + protected: ['No route, authentication, dependency, database, or unrelated UI changes.'], + acceptance: ['The heading is clear.', 'A short explanation is visible.', 'Only allowed files change.'], + verification: ['Show the exact changed-file list.', 'Run php artisan test.', 'Inspect the result in the browser.'], + parked: ['Color changes, polling, dashboards, and deployment integrations.'], + changed: ['app/Http/Controllers/StatusController.php', 'resources/views/status.blade.php'], + patch: 'Adds a short review-ready explanation while preserving the existing route and layout.', + evidence: [ + { check: 'Show the exact changed-file list.', status: 'Passed', note: 'Two changed paths were supplied and both are allowed.' }, + { check: 'Run php artisan test.', status: 'Not run', note: 'The static public exercise has no Laravel runtime.' }, + { check: 'Inspect the result in the browser.', status: 'Passed', note: 'The supplied rendered example was reviewed.' }, + ], + decision: 'Revise', + reviewer: 'Public exercise reviewer', + date: currentDate(), + rationale: 'The patch is in scope, but a required runtime check was not observed.', + nextAction: 'Run the maintained Laravel test command in a real project and update the evidence record.', + }, + accept: { + scenario: 'accept', + goal: 'Show how many practice-list items remain incomplete without changing stored data behavior.', + allowed: ['control/exercises/browser-list-count/project/index.html', 'control/exercises/browser-list-count/project/script.js'], + protected: ['Preserve the localStorage key and data shape, add/complete/undo/remove/reload behavior, and literal rendering of user input.'], + acceptance: ['A visible status reports incomplete items.', 'Singular and plural wording are correct.', 'Only allowed files change.'], + verification: ['Confirm the exact changed-file list.', 'Run the maintained Node behavior tests.', 'Run browser lifecycle and HTML-like-input smoke checks.'], + parked: ['Animations, categories, accounts, and cloud sync.'], + changed: ['control/exercises/browser-list-count/project/index.html', 'control/exercises/browser-list-count/project/script.js'], + patch: 'Adds an aria-live remaining-count element and updates it from the existing render function.', + evidence: [ + { check: 'Confirm the exact changed-file list.', status: 'Passed', note: 'Both changed files are explicitly allowed.' }, + { check: 'Run the maintained Node behavior tests.', status: 'Passed', note: 'Remaining-count behavior and wording checks passed.' }, + { check: 'Run browser lifecycle and HTML-like-input smoke checks.', status: 'Passed', note: 'Add, complete, undo, reload, remove, and safety checks passed.' }, + ], + decision: 'Accept', + reviewer: 'Public exercise reviewer', + date: currentDate(), + rationale: 'The change is bounded and every required check was observed as passed.', + nextAction: 'Preserve this record with the reviewed change.', + }, + reject: { + scenario: 'reject', + goal: 'Clarify the status-page copy without changing architecture or dependencies.', + allowed: ['resources/views/status.blade.php'], + protected: ['Do not add dependencies, routes, controllers, persistence, or remote services.'], + acceptance: ['The status explanation is clearer.', 'Only the allowed Blade view changes.'], + verification: ['Confirm the exact changed-file list.', 'Review the rendered status page.'], + parked: ['Live deployment checks and dashboards.'], + changed: ['resources/views/status.blade.php', 'package.json'], + patch: 'Updates the copy but also adds a new status-checking dependency.', + evidence: [ + { check: 'Confirm the exact changed-file list.', status: 'Failed', note: 'package.json changed outside the declared scope.' }, + { check: 'Review the rendered status page.', status: 'Passed', note: 'The copy is understandable, but the proposal violates a protected boundary.' }, + ], + decision: 'Reject', + reviewer: 'Public exercise reviewer', + date: currentDate(), + rationale: 'The proposal adds a dependency that is outside scope and explicitly protected.', + nextAction: 'Discard this proposal and request a view-only patch.', + }, + custom: { + scenario: 'custom', + goal: '', + allowed: [], + protected: [], + acceptance: [], + verification: [], + parked: [], + changed: [], + patch: '', + evidence: [], + decision: '', + reviewer: '', + date: currentDate(), + rationale: '', + nextAction: '', + }, + } + + function currentDate() { + return new Date().toISOString().slice(0, 10) + } + + function cloneScenario(name) { + return JSON.parse(JSON.stringify(scenarios[name] || scenarios.custom)) + } + + function lines(value) { + if (Array.isArray(value)) return value.map(cleanLine).filter(Boolean) + return String(value || '').split(/\r?\n/).map(cleanLine).filter(Boolean) + } + + function cleanLine(value) { + return String(value || '').trim() + } + + function normalizePath(value) { + return cleanLine(value).replaceAll('\\', '/').replace(/^\.\//, '').replace(/\/+/, '/') + } + + function synchronizeEvidence(verification, evidence) { + const existing = new Map((evidence || []).map((entry) => [cleanLine(entry.check), entry])) + return lines(verification).map((check) => { + const previous = existing.get(check) + const status = STATUS_VALUES.includes(previous?.status) ? previous.status : 'Not run' + return { check, status, note: cleanLine(previous?.note) } + }) + } + + function normalizeState(input) { + const state = input || {} + const verification = lines(state.verification) + return { + schemaVersion: '1.0.0', + scenario: cleanLine(state.scenario) || 'custom', + goal: cleanLine(state.goal), + allowed: lines(state.allowed), + protected: lines(state.protected), + acceptance: lines(state.acceptance), + verification, + parked: lines(state.parked), + changed: lines(state.changed), + patch: cleanLine(state.patch), + evidence: synchronizeEvidence(verification, state.evidence), + decision: ['Accept', 'Revise', 'Reject'].includes(state.decision) ? state.decision : '', + reviewer: cleanLine(state.reviewer), + date: /^\d{4}-\d{2}-\d{2}$/.test(state.date || '') ? state.date : currentDate(), + rationale: cleanLine(state.rationale), + nextAction: cleanLine(state.nextAction), + } + } + + function assessScope(stateInput) { + const state = normalizeState(stateInput) + const allowed = new Set(state.allowed.map(normalizePath)) + const changed = state.changed.map((display) => ({ display, normalized: normalizePath(display) })) + const inScope = changed.filter((entry) => allowed.has(entry.normalized)).map((entry) => entry.display) + const outOfScope = changed.filter((entry) => !allowed.has(entry.normalized)).map((entry) => entry.display) + const unchangedAllowed = state.allowed.filter((file) => !changed.some((entry) => entry.normalized === normalizePath(file))) + return { inScope, outOfScope, unchangedAllowed, hasChangedFiles: changed.length > 0 } + } + + function assessDecision(stateInput) { + const state = normalizeState(stateInput) + const scope = assessScope(state) + const notPassed = state.evidence.filter((entry) => entry.status !== 'Passed') + const failed = state.evidence.filter((entry) => entry.status === 'Failed') + const notRun = state.evidence.filter((entry) => entry.status === 'Not run') + const acceptReady = scope.hasChangedFiles && scope.outOfScope.length === 0 && state.evidence.length > 0 && notPassed.length === 0 + let recommendation = 'Revise' + if (acceptReady) recommendation = 'Accept' + if (scope.outOfScope.length > 0 || failed.length > 0) recommendation = 'Reject or revise' + + let alignment = 'No decision selected.' + let aligned = false + if (state.decision === 'Accept') { + aligned = acceptReady + alignment = aligned + ? 'Accept aligns with the recorded scope and evidence.' + : 'Accept does not align: required scope or evidence conditions remain incomplete.' + } else if (state.decision === 'Revise') { + aligned = !acceptReady + alignment = aligned + ? 'Revise aligns with unresolved scope or evidence.' + : 'Revise is conservative even though the recorded checks support acceptance.' + } else if (state.decision === 'Reject') { + aligned = scope.outOfScope.length > 0 || failed.length > 0 + alignment = aligned + ? 'Reject aligns with a recorded scope violation or failed check.' + : 'Reject is a human choice, but the current record contains no explicit violation or failed check.' + } + + return { scope, notPassed, failed, notRun, acceptReady, recommendation, alignment, aligned } + } + + function bullets(items, checkbox) { + const values = lines(items) + if (!values.length) return '- None declared.' + return values.map((item) => `${checkbox ? '- [ ]' : '-'} ${item}`).join('\n') + } + + function pathBullets(items) { + const values = lines(items) + if (!values.length) return '- None declared.' + return values.map((item) => `- \`${item}\``).join('\n') + } + + function instruction(stateInput) { + const state = normalizeState(stateInput) + return `# Atlas-controlled AI instruction\n\n## Human goal\n\n${state.goal || '[State the user-visible outcome.]'}\n\n## Allowed files\n\n${pathBullets(state.allowed)}\n\n## Protected boundaries\n\n${bullets(state.protected)}\n\nDo not edit any path outside the allowed list. Park useful out-of-scope ideas instead of implementing them.\n\n## Acceptance criteria\n\n${bullets(state.acceptance)}\n\n## Verification honesty\n\n${bullets(state.verification)}\n\nReport every check as Passed, Failed, or Not run. Never infer a result that was not observed.\n\n## Required response\n\n1. Restate the goal and allowed files.\n2. Explain the smallest proposed change.\n3. Show the exact changed-file list.\n4. Provide the patch.\n5. Report verification honestly.\n6. List parked ideas separately.\n` + } + + function changeContract(stateInput) { + const state = normalizeState(stateInput) + return `# Atlas Change Contract\n\n## Human goal\n\n${state.goal || '[State the user-visible outcome.]'}\n\n## Allowed scope\n\n${pathBullets(state.allowed)}\n\n## Protected boundaries\n\n${bullets(state.protected)}\n\n## Acceptance criteria\n\n${bullets(state.acceptance, true)}\n\n## Required verification\n\n${bullets(state.verification, true)}\n\nA written command is not proof that it ran. Keep unobserved checks as **Not run**.\n\n## Confirmation boundaries\n\n- Deleting data or files.\n- Changing dependencies, migrations, public APIs, authentication, authorization, or payment behavior.\n- Expanding beyond the allowed scope.\n\n## Parked ideas\n\n${bullets(state.parked)}\n` + } + + function evidenceRecord(stateInput) { + const state = normalizeState(stateInput) + const assessment = assessDecision(state) + const changed = state.changed.length ? pathBullets(state.changed) : '- No changed-file list supplied.' + const scopeResult = assessment.scope.outOfScope.length + ? `**Scope violation recorded.**\n\n${pathBullets(assessment.scope.outOfScope)}` + : assessment.scope.hasChangedFiles + ? '**All supplied changed files are inside the declared allowlist.**' + : '**Not reviewed:** no changed-file list was supplied.' + const results = state.evidence.length + ? state.evidence.map((entry) => `- ${entry.check}: **${entry.status}**${entry.note ? ` — ${entry.note}` : ''}`).join('\n') + : '- No required checks declared.' + return `# Atlas Evidence Record\n\n## Actually changed files\n\n${changed}\n\n## Scope review\n\n${scopeResult}\n\n## Patch or change summary\n\n${state.patch || 'Not supplied.'}\n\n## Verification results\n\n${results}\n\n## Known limitations\n\n- Control Lite compares supplied paths; it does not inspect repository history.\n- Control Lite does not execute commands or validate claims automatically.\n- A human reviewer remains responsible for the evidence and final decision.\n` + } + + function decisionRecord(stateInput) { + const state = normalizeState(stateInput) + const assessment = assessDecision(state) + return `# Atlas Decision Record\n\n## Change\n\n${state.goal || '[No goal recorded.]'}\n\n## Decision\n\n**${state.decision || 'Not selected'}**\n\n## Accountable human\n\n${state.reviewer || 'Not recorded.'}\n\n## Decision date\n\n${state.date || 'Not recorded.'}\n\n## Decision alignment advisory\n\n${assessment.alignment}\n\nThis advisory does not replace the accountable human decision.\n\n## Rationale\n\n${state.rationale || 'Not recorded.'}\n\n## Required next action\n\n${state.nextAction || 'Not recorded.'}\n` + } + + function relatedNodeForScenario(scenario) { + if (scenario === 'accept') return { id: 'CANON-LOCALSTORAGE-001', title: 'localStorage' } + if (scenario === 'revise' || scenario === 'reject') return { id: 'CANON-LARAVEL-REQUEST-LIFECYCLE-001', title: 'Laravel Request Lifecycle' } + return { id: '', title: 'Compendium catalog' } + } + + function combinedRecord(stateInput) { + const state = normalizeState(stateInput) + const assessment = assessDecision(state) + const relatedNode = relatedNodeForScenario(state.scenario) + return `# Atlas Controlled Change Record\n\n> Local public-preview record. Control Lite did not run an AI, inspect Git history, or execute verification commands.\n\n## Human goal\n\n${state.goal || '[No goal recorded.]'}\n\n## Change Contract\n\n### Allowed files\n\n${pathBullets(state.allowed)}\n\n### Protected boundaries\n\n${bullets(state.protected)}\n\n### Acceptance criteria\n\n${bullets(state.acceptance)}\n\n### Parked ideas\n\n${bullets(state.parked)}\n\n## Controlled instruction\n\n${instruction(state)}\n## Change inspection\n\n### Actually changed files\n\n${state.changed.length ? pathBullets(state.changed) : '- Not supplied.'}\n\n### Out-of-scope paths\n\n${assessment.scope.outOfScope.length ? pathBullets(assessment.scope.outOfScope) : '- None found in the supplied changed-file list.'}\n\n### Patch or change summary\n\n${state.patch || 'Not supplied.'}\n\n## Evidence\n\n${state.evidence.length ? state.evidence.map((entry) => `- **${entry.status}** — ${entry.check}${entry.note ? ` — ${entry.note}` : ''}`).join('\n') : '- No required checks declared.'}\n\n## Human decision\n\n- **Decision:** ${state.decision || 'Not selected'}\n- **Accountable human:** ${state.reviewer || 'Not recorded'}\n- **Date:** ${state.date || 'Not recorded'}\n- **Alignment advisory:** ${assessment.alignment}\n\n### Rationale\n\n${state.rationale || 'Not recorded.'}\n\n### Required next action\n\n${state.nextAction || 'Not recorded.'}\n\n## Related public learning\n\n- ${relatedNode.title}: \`compendium-preview/index.html${relatedNode.id ? `?node=${relatedNode.id}` : ''}\`\n` + } + + function artifacts(stateInput) { + const state = normalizeState(stateInput) + return [ + { filename: 'CHANGE_CONTRACT.md', content: changeContract(state) }, + { filename: 'AI_INSTRUCTION.md', content: instruction(state) }, + { filename: 'EVIDENCE_RECORD.md', content: evidenceRecord(state) }, + { filename: 'DECISION_RECORD.md', content: decisionRecord(state) }, + ] + } + + const api = { + STATUS_VALUES, + assessDecision, + assessScope, + artifacts, + changeContract, + cloneScenario, + combinedRecord, + currentDate, + decisionRecord, + evidenceRecord, + instruction, + lines, + normalizePath, + normalizeState, + relatedNodeForScenario, + synchronizeEvidence, + } + + if (typeof module !== 'undefined' && module.exports) module.exports = api + globalScope.AtlasControlCore = api +})(typeof window !== 'undefined' ? window : globalThis) diff --git a/control-lite/index.html b/control-lite/index.html index 0012215..77354ad 100644 --- a/control-lite/index.html +++ b/control-lite/index.html @@ -3,40 +3,194 @@ + Developer Atlas Control Lite -
-
-

LOCAL-FIRST PUBLIC PREVIEW

-

Atlas Control Lite

-

Define one bounded AI-assisted change and export reviewable Markdown. Nothing is sent anywhere.

+ +
+
+
+

LOCAL-FIRST PUBLIC PREVIEW

+

Atlas Control Lite

+

Define what may change, inspect what actually changed, record observed evidence, and keep the final decision human.

+
+
+ Runs in this browser + No account, telemetry, AI connection, or command execution. +
-
- - - - - - - +
+ + + + + + +
+ + + + +
+
+

Step 1

+

Prepare a bounded change

+

State the outcome before discussing implementation. One line per boundary or criterion.

+
+
+ + + + + + +
+
+ +
+
+ + + + + + + + + +
- +

+ diff --git a/control-lite/style.css b/control-lite/style.css index 5fb8cea..e35d8be 100644 --- a/control-lite/style.css +++ b/control-lite/style.css @@ -1 +1,86 @@ -*{box-sizing:border-box}body{margin:0;font-family:system-ui,sans-serif;background:#f4f2ed;color:#1d2522}.shell{width:min(980px,calc(100% - 32px));margin:40px auto}.eyebrow{font-weight:800;letter-spacing:.12em;font-size:.75rem}h1{font-size:clamp(2rem,6vw,4rem);margin:.2rem 0}form,.artifact{background:white;border:1px solid #c8cec9;border-radius:16px;padding:20px;margin-top:24px;box-shadow:0 8px 30px rgba(0,0,0,.05)}label{display:block;font-weight:700;margin-bottom:18px}textarea{display:block;width:100%;min-height:88px;margin-top:8px;padding:12px;border:1px solid #9ca7a1;border-radius:8px;font:inherit;resize:vertical}button{border:0;border-radius:8px;padding:12px 16px;font-weight:800;cursor:pointer;background:#253f35;color:white}.artifact header{display:flex;align-items:center;justify-content:space-between;gap:12px}.artifact textarea{min-height:260px;font-family:ui-monospace,monospace;font-size:.9rem}.artifact-actions{display:flex;gap:8px;flex-wrap:wrap}.secondary{background:#e6ebe8;color:#1d2522}.output-header{margin-top:36px}@media(max-width:600px){.artifact header{align-items:flex-start;flex-direction:column}} \ No newline at end of file +:root { + color-scheme: dark; + --bg: #0c1020; + --surface: #141a2f; + --surface-raised: #1a2240; + --border: #303a61; + --text: #f2f4ff; + --muted: #aab2d1; + --accent: #9b87f5; + --accent-strong: #c4b5fd; + --success: #7ee2b8; + --warning: #ffd479; + --danger: #ff9b9b; + --focus: #d7ccff; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { box-sizing: border-box; } +body { margin: 0; background: radial-gradient(circle at top, #182044 0, var(--bg) 38rem); color: var(--text); line-height: 1.55; } +button, input, select, textarea { font: inherit; } +button, .button-link { border: 1px solid transparent; border-radius: .7rem; background: var(--accent); color: #100d26; font-weight: 750; padding: .72rem 1rem; cursor: pointer; text-decoration: none; } +button:hover, .button-link:hover { filter: brightness(1.08); } +button:focus-visible, .button-link:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } +button.secondary { background: transparent; border-color: var(--accent); color: var(--accent-strong); } +button.ghost { background: transparent; border-color: var(--border); color: var(--muted); } +a { color: var(--accent-strong); } +.shell { width: min(1120px, calc(100% - 2rem)); margin: 0 auto; padding: 2rem 0 5rem; } +.skip-link { position: absolute; left: 1rem; top: -4rem; z-index: 20; background: white; color: black; padding: .7rem 1rem; } +.skip-link:focus { top: 1rem; } +.hero { display: grid; grid-template-columns: minmax(0, 1fr) minmax(15rem, 22rem); gap: 2rem; align-items: end; margin-bottom: 1.5rem; } +.eyebrow, .step-label { margin: 0 0 .45rem; color: var(--accent-strong); font-weight: 800; letter-spacing: .11em; font-size: .78rem; } +h1 { font-size: clamp(2.4rem, 7vw, 5.4rem); line-height: .96; letter-spacing: -.05em; margin: 0; } +.hero-copy { color: var(--muted); font-size: 1.18rem; max-width: 48rem; } +.privacy-card { display: grid; gap: .3rem; background: rgba(20, 26, 47, .84); border: 1px solid var(--border); border-radius: 1rem; padding: 1rem; } +.privacy-card span { color: var(--muted); font-size: .92rem; } +.toolbar { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; background: var(--surface); border: 1px solid var(--border); border-radius: 1rem; padding: 1rem; } +.toolbar label { font-weight: 750; } +select, input, textarea { width: 100%; border: 1px solid var(--border); border-radius: .65rem; background: #0f1529; color: var(--text); padding: .72rem .78rem; } +.toolbar select { width: auto; min-width: 15rem; } +.stepper { margin: 1.2rem 0; overflow-x: auto; } +.stepper ol { display: grid; grid-template-columns: repeat(6, minmax(8.5rem, 1fr)); gap: .5rem; list-style: none; margin: 0; padding: 0; min-width: 56rem; } +.stepper button { width: 100%; display: flex; align-items: center; gap: .55rem; background: transparent; color: var(--muted); border-color: var(--border); } +.stepper button span { display: grid; place-items: center; width: 1.65rem; height: 1.65rem; border-radius: 50%; border: 1px solid currentColor; } +.stepper button[aria-current="step"] { background: var(--surface-raised); color: var(--text); border-color: var(--accent); } +.panel { background: rgba(20, 26, 47, .96); border: 1px solid var(--border); border-radius: 1.1rem; padding: clamp(1rem, 3vw, 2rem); box-shadow: 0 1.2rem 4rem rgba(0, 0, 0, .22); } +.panel-heading { margin-bottom: 1.3rem; } +.panel-heading h2 { margin: 0; font-size: clamp(1.55rem, 3vw, 2.35rem); } +.panel-heading p:last-child { color: var(--muted); max-width: 54rem; } +.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; } +.field-grid .wide { grid-column: 1 / -1; } +label { display: grid; gap: .45rem; font-weight: 700; } +textarea { resize: vertical; font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; font-size: .92rem; line-height: 1.5; } +.output-text { min-height: 20rem; } +.panel-actions { display: flex; justify-content: space-between; gap: .8rem; margin-top: 1.5rem; } +.panel-actions.end { justify-content: flex-end; } +.callout, .assessment { border-left: .28rem solid var(--accent); background: var(--surface-raised); border-radius: .4rem; padding: .9rem 1rem; } +.assessment { margin-top: 1rem; } +.assessment[data-tone="success"] { border-left-color: var(--success); } +.assessment[data-tone="warning"] { border-left-color: var(--warning); } +.assessment[data-tone="danger"] { border-left-color: var(--danger); } +.assessment h3 { margin: 0 0 .45rem; } +.assessment ul { margin-bottom: 0; } +.evidence-list { display: grid; gap: .8rem; } +.evidence-row { display: grid; grid-template-columns: minmax(0, 2fr) minmax(9rem, .7fr) minmax(0, 1.6fr); gap: .8rem; align-items: end; padding: 1rem; border: 1px solid var(--border); border-radius: .8rem; background: #10172c; } +.evidence-check { font-weight: 700; align-self: center; } +.decision-options { display: grid; gap: .7rem; border: 0; padding: 0; margin: 0; } +.decision-options legend { font-weight: 800; margin-bottom: .6rem; } +.decision-options label { grid-template-columns: auto 1fr; align-items: start; border: 1px solid var(--border); border-radius: .8rem; padding: .9rem; background: #10172c; } +.decision-options input { width: auto; margin-top: .25rem; } +.export-actions { display: flex; flex-wrap: wrap; gap: .7rem; margin: 1rem 0; } +details { border: 1px solid var(--border); border-radius: .8rem; padding: 1rem; } +summary { cursor: pointer; font-weight: 800; } +.artifact-list { display: grid; gap: 1rem; margin-top: 1rem; } +.artifact { border-top: 1px solid var(--border); padding-top: 1rem; } +.artifact header { display: flex; justify-content: space-between; gap: 1rem; align-items: center; } +.artifact h3 { margin: 0; } +.artifact textarea { min-height: 12rem; margin-top: .7rem; } +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } + +@media (max-width: 760px) { + .hero, .field-grid { grid-template-columns: 1fr; } + .field-grid .wide { grid-column: auto; } + .evidence-row { grid-template-columns: 1fr; } + .toolbar { align-items: stretch; } + .toolbar select { width: 100%; } +} diff --git a/control/exercises/browser-list-count/ATLAS_INSTRUCTION.md b/control/exercises/browser-list-count/ATLAS_INSTRUCTION.md new file mode 100644 index 0000000..7cc4c25 --- /dev/null +++ b/control/exercises/browser-list-count/ATLAS_INSTRUCTION.md @@ -0,0 +1,34 @@ +# Atlas-controlled instruction + +## Goal + +Show how many practice-list items remain incomplete so a reviewer can understand progress at a glance. + +## Allowed files + +- `control/exercises/browser-list-count/project/index.html` +- `control/exercises/browser-list-count/project/script.js` + +## Protected boundaries + +Preserve the existing localStorage key and data shape, add/complete/undo/remove/reload behavior, and literal text rendering. Do not add dependencies, network requests, accounts, analytics, styling work, or edits outside the allowlist. + +## Acceptance criteria + +- Add a visible status line for incomplete items. +- Use `0 items remaining`, `1 item remaining`, and plural wording for larger values. +- Update after every existing state transition and after reload. +- Preserve HTML-like input as literal text. + +## Verification honesty + +Report the exact changed-file list. Run the maintained Node and browser tests when a suitable environment is available. Report each check as Passed, Failed, or Not run; never infer a pass. + +## Required response + +1. Restate the goal and allowed files. +2. Explain the smallest proposed change. +3. Show the exact changed-file list. +4. Provide the patch. +5. Report verification as Passed, Failed, or Not run. +6. Park unrelated suggestions separately. diff --git a/control/exercises/browser-list-count/BASELINE_PROMPT.md b/control/exercises/browser-list-count/BASELINE_PROMPT.md new file mode 100644 index 0000000..7effb6f --- /dev/null +++ b/control/exercises/browser-list-count/BASELINE_PROMPT.md @@ -0,0 +1,3 @@ +# Baseline prompt + +Add a remaining item count to the practice list. Make it look good and update automatically. diff --git a/control/exercises/browser-list-count/CHANGE_CONTRACT.md b/control/exercises/browser-list-count/CHANGE_CONTRACT.md new file mode 100644 index 0000000..a081cd2 --- /dev/null +++ b/control/exercises/browser-list-count/CHANGE_CONTRACT.md @@ -0,0 +1,35 @@ +# Atlas Change Contract — remaining item count + +## Human goal + +Show how many practice-list items remain incomplete so a reviewer can understand progress at a glance. + +## Allowed files + +- `control/exercises/browser-list-count/project/index.html` +- `control/exercises/browser-list-count/project/script.js` + +## Protected boundaries + +- Preserve the `atlasPracticeItems` localStorage key and stored data shape. +- Preserve add, complete, undo, remove, reload, and literal text rendering behavior. +- Do not add dependencies, network requests, accounts, analytics, or unrelated styling. +- Do not change files outside the two-file allowlist. + +## Acceptance criteria + +- A visible status line reports the number of incomplete items. +- It reads `0 items remaining` when the list is empty or every item is complete. +- It uses `1 item remaining` for the singular case. +- It updates after adding, completing, undoing, removing, and reloading items. +- HTML-like user input continues to render as literal text. + +## Required verification + +- Confirm the exact changed-file list. +- Run the maintained Node behavior tests. +- Run browser lifecycle and HTML-like-input smoke checks. + +## Parked ideas + +- Progress bars, categories, due dates, animations, cloud sync, and user accounts. diff --git a/control/exercises/browser-list-count/DECISION_RECORD.md b/control/exercises/browser-list-count/DECISION_RECORD.md new file mode 100644 index 0000000..05edb87 --- /dev/null +++ b/control/exercises/browser-list-count/DECISION_RECORD.md @@ -0,0 +1,25 @@ +# Atlas Decision Record — maintained public example + +## Decision + +**Accept** the in-scope proposal as the verified reference outcome for this synthetic exercise. + +## Accountable role + +Developer Atlas public-preview maintainer. + +## Evidence reviewed + +- Exact changed-file and patch-path verification: Passed. +- Dependency-free Node behavior and safety tests: Passed. +- Chromium DOM lifecycle and HTML-like-input smoke checks: Passed with documented sandbox shims. +- Hosted-origin Playwright specification: Prepared for CI; not run in the local implementation sandbox. +- Independent human review: Pending. + +## Rationale + +The candidate changes only the two allowed files, preserves the protected list behavior and storage format, meets the remaining-count criteria, and has observed behavior and DOM-safety evidence. The remaining hosted-origin run is a release gate rather than an undisclosed pass, and its local absence is preserved explicitly. + +## Required next action + +Run the full repository test workflow in GitHub Actions before publishing a release artifact, preserve the CI result with the pull request, and seek independent review of the exercise wording and expected decision. diff --git a/control/exercises/browser-list-count/EVIDENCE_RECORD.md b/control/exercises/browser-list-count/EVIDENCE_RECORD.md new file mode 100644 index 0000000..1a75075 --- /dev/null +++ b/control/exercises/browser-list-count/EVIDENCE_RECORD.md @@ -0,0 +1,29 @@ +# Atlas Evidence Record — maintained public example + +> This record documents repository-maintainer verification performed for the public example on 2026-07-23. It is automated project evidence, not independent human review or proof about another environment. + +## Scope review + +- **Passed:** the baseline-to-project comparison reports changes only in `project/index.html` and `project/script.js`. +- **Passed:** `IN_SCOPE_CHANGED_FILES.txt` matches those two paths. +- **Passed:** `IN_SCOPE_PATCH.diff` declares those two paths and no others. + +## Dependency-free behavior verification + +- **Passed:** initial state reports `0 items remaining`. +- **Passed:** add, complete, undo, remove, singular, and plural behavior passed. +- **Passed:** stored state reload counts only incomplete items. +- **Passed:** HTML-like names remain literal text. + +## Chromium DOM smoke verification + +- **Passed:** the actual candidate HTML, CSS, and JavaScript were loaded in headless Chromium. +- **Passed:** add, complete, undo, simulated reload from preserved storage, and HTML-like-input checks passed. +- **Limitation:** the sandbox blocks navigation to local and synthetic origins, so the smoke run supplied in-memory `localStorage` and `crypto.randomUUID` shims before loading the unchanged candidate files. +- **Not run locally:** the maintained hosted-origin Playwright specification. It is included for repository CI and release verification. + +## Known limitations + +- The local Chromium smoke does not replace the hosted-origin Playwright run on GitHub Actions. +- No independent reviewer has verified this example yet. +- Control Lite did not execute these checks; results were recorded after separate maintained verification. diff --git a/control/exercises/browser-list-count/README.md b/control/exercises/browser-list-count/README.md new file mode 100644 index 0000000..d9a3107 --- /dev/null +++ b/control/exercises/browser-list-count/README.md @@ -0,0 +1,26 @@ +# Controlled change exercise: remaining item count + +This is the first public Control exercise that can progress from a bounded request to real runnable verification. + +## Start + +1. Read [`TASK.md`](TASK.md). +2. Review the [`CHANGE_CONTRACT.md`](CHANGE_CONTRACT.md). +3. Compare the [`BASELINE_PROMPT.md`](BASELINE_PROMPT.md) with the [`ATLAS_INSTRUCTION.md`](ATLAS_INSTRUCTION.md). +4. Inspect both proposals in [`proposed/`](proposed/). +5. Use the changed-file records in [`records/`](records/) to perform the scope review. + +The `baseline/` and verified `project/` folders are browser-only local lists. It has no network calls, dependencies, account, backend, or telemetry. User-entered text is assigned through `textContent`. + +## Expected scope decision + +- `IN_SCOPE_PATCH.diff` changes only the two allowed files and uses existing browser APIs. +- `OUT_OF_SCOPE_PATCH.diff` changes the root `package.json` and introduces a dependency, violating explicit boundaries. + +A scope-conforming patch is not automatically accepted. Continue with the maintained verification in the next section of this exercise before recording a final decision. + +## Verification and maintained decision + +- [Run the verification](VERIFY.md) +- [Review the maintained evidence record](EVIDENCE_RECORD.md) +- [Review the maintained Accept decision](DECISION_RECORD.md) diff --git a/control/exercises/browser-list-count/TASK.md b/control/exercises/browser-list-count/TASK.md new file mode 100644 index 0000000..13a9203 --- /dev/null +++ b/control/exercises/browser-list-count/TASK.md @@ -0,0 +1,11 @@ +# Review task + +A teammate wants a remaining-item count in the local practice list. + +1. Open the starting point in `baseline/`. +2. Compare the baseline prompt with the Atlas-controlled instruction. +3. Inspect `proposed/IN_SCOPE_PATCH.diff` and `proposed/OUT_OF_SCOPE_PATCH.diff`. +4. Decide which proposal, if either, respects the Change Contract. +5. Continue to the verification instructions before selecting Accept. + +The in-scope proposal is intentionally small. The out-of-scope proposal produces similar visible text but also introduces an external runtime dependency and changes `package.json`, which is not allowed. diff --git a/control/exercises/browser-list-count/VERIFY.md b/control/exercises/browser-list-count/VERIFY.md new file mode 100644 index 0000000..2d7a5ad --- /dev/null +++ b/control/exercises/browser-list-count/VERIFY.md @@ -0,0 +1,19 @@ +# Verification instructions + +From the repository root: + +```bash +npm run verify:control-exercise +``` + +This dependency-free command verifies that only `index.html` and `script.js` differ between `baseline/` and `project/`, confirms the supplied changed-file and patch path lists, and runs the Node behavior and safety tests. + +For browser verification after installing the repository test dependencies: + +```bash +npm run test:browser -- tests/control-exercise-browser.spec.mjs +``` + +The browser checks cover add, complete, undo, reload, remove, singular/plural wording, and literal rendering of HTML-like input. + +A command listed here remains **Not run** until its result has actually been observed in the environment where the decision is made. diff --git a/control/exercises/browser-list-count/baseline/index.html b/control/exercises/browser-list-count/baseline/index.html new file mode 100644 index 0000000..25cc297 --- /dev/null +++ b/control/exercises/browser-list-count/baseline/index.html @@ -0,0 +1,37 @@ + + + + + + Atlas Browser List + + + +
+

SAFE DOM PRACTICE

+

Atlas Practice List

+ +

+ Add, complete, remove, and reload a local list. Names are always rendered as text. +

+ +
+

Practice items

+ +
+ + +
+ + +
+
+ +

No items yet. Add one small learning goal.

+
    +
    +
    + + + + diff --git a/control/exercises/browser-list-count/baseline/script.js b/control/exercises/browser-list-count/baseline/script.js new file mode 100644 index 0000000..be08714 --- /dev/null +++ b/control/exercises/browser-list-count/baseline/script.js @@ -0,0 +1,105 @@ +const storageKey = 'atlasPracticeItems' + +const itemForm = document.querySelector('.item-form') +const itemInput = document.querySelector('.item-input') +const itemList = document.querySelector('.item-list') +const emptyState = document.querySelector('.empty-state') + +let items = [] + +function addItem(name) { + items.push({ + id: crypto.randomUUID(), + name, + isDone: false, + }) +} + +function saveItems() { + localStorage.setItem(storageKey, JSON.stringify(items)) +} + +function loadItems() { + const savedItems = localStorage.getItem(storageKey) + if (!savedItems) return + + try { + const parsedItems = JSON.parse(savedItems) + if (Array.isArray(parsedItems)) { + items = parsedItems + .filter((item) => item && typeof item === 'object' && 'id' in item && 'name' in item) + .map((item) => ({ + id: String(item.id), + name: String(item.name), + isDone: Boolean(item.isDone), + })) + } + } catch { + localStorage.removeItem(storageKey) + } +} + +function createActionButton(action, label, itemId) { + const button = document.createElement('button') + button.type = 'button' + button.dataset.action = action + button.dataset.id = itemId + button.textContent = label + return button +} + +function renderItems() { + itemList.textContent = '' + emptyState.hidden = items.length > 0 + + items.forEach((item) => { + const itemRow = document.createElement('li') + itemRow.classList.add('item-row') + if (item.isDone) itemRow.classList.add('is-done') + + const itemName = document.createElement('span') + itemName.classList.add('item-name') + itemName.textContent = String(item.name) + + const itemActions = document.createElement('div') + itemActions.classList.add('item-actions') + itemActions.append( + createActionButton('toggle', item.isDone ? 'Undo' : 'Done', String(item.id)), + createActionButton('remove', 'Remove', String(item.id)) + ) + + itemRow.append(itemName, itemActions) + itemList.appendChild(itemRow) + }) +} + +itemForm.addEventListener('submit', (event) => { + event.preventDefault() + const itemName = itemInput.value.trim() + if (!itemName) return + + addItem(itemName) + saveItems() + renderItems() + itemInput.value = '' + itemInput.focus() +}) + +itemList.addEventListener('click', (event) => { + const clickedButton = event.target.closest('button') + if (!clickedButton || !itemList.contains(clickedButton)) return + + const itemId = clickedButton.dataset.id + if (clickedButton.dataset.action === 'toggle') { + items = items.map((item) => item.id === itemId ? { ...item, isDone: !item.isDone } : item) + } + if (clickedButton.dataset.action === 'remove') { + items = items.filter((item) => item.id !== itemId) + } + + saveItems() + renderItems() +}) + +loadItems() +renderItems() diff --git a/control/exercises/browser-list-count/baseline/style.css b/control/exercises/browser-list-count/baseline/style.css new file mode 100644 index 0000000..1ed5f86 --- /dev/null +++ b/control/exercises/browser-list-count/baseline/style.css @@ -0,0 +1,106 @@ +:root { + color-scheme: light dark; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +body { + max-width: 760px; + margin: 40px auto; + padding: 0 20px; + line-height: 1.6; +} + +.app-card { + border: 1px solid color-mix(in srgb, CanvasText 22%, transparent); + border-radius: 16px; + padding: clamp(20px, 5vw, 36px); + background: Canvas; + box-shadow: 0 16px 44px color-mix(in srgb, CanvasText 8%, transparent); +} + +.eyebrow { + margin: 0; + color: #6f5bd3; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.12em; +} + +h1 { + margin-block: 4px 12px; +} + +.list-app { + margin-top: 28px; +} + +.form-row { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.item-input { + min-width: 0; + flex: 1; + padding: 10px 12px; + font: inherit; +} + +button { + padding: 10px 14px; + border: 1px solid color-mix(in srgb, CanvasText 25%, transparent); + border-radius: 8px; + font: inherit; + cursor: pointer; +} + +button:focus-visible, +input:focus-visible { + outline: 3px solid #8a79e8; + outline-offset: 2px; +} + +.item-list { + padding-left: 0; + list-style: none; +} + +.item-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-top: 10px; + padding: 10px; + border: 1px solid color-mix(in srgb, CanvasText 18%, transparent); + border-radius: 8px; +} + +.item-name { + min-width: 0; + overflow-wrap: anywhere; +} + +.item-row.is-done .item-name { + opacity: 0.72; + text-decoration: line-through; +} + +.item-actions { + display: flex; + flex: none; + gap: 8px; +} + +.empty-state[hidden] { + display: none; +} + +@media (max-width: 520px) { + .form-row, + .item-row { + align-items: stretch; + flex-direction: column; + } +} diff --git a/control/exercises/browser-list-count/project/index.html b/control/exercises/browser-list-count/project/index.html new file mode 100644 index 0000000..9c1ac32 --- /dev/null +++ b/control/exercises/browser-list-count/project/index.html @@ -0,0 +1,38 @@ + + + + + + Atlas Browser List + + + +
    +

    SAFE DOM PRACTICE

    +

    Atlas Practice List

    + +

    + Add, complete, remove, and reload a local list. Names are always rendered as text. +

    + +
    +

    Practice items

    + +
    + + +
    + + +
    +
    + +

    0 items remaining

    +

    No items yet. Add one small learning goal.

    +
      +
      +
      + + + + diff --git a/control/exercises/browser-list-count/project/script.js b/control/exercises/browser-list-count/project/script.js new file mode 100644 index 0000000..313e8b6 --- /dev/null +++ b/control/exercises/browser-list-count/project/script.js @@ -0,0 +1,111 @@ +const storageKey = 'atlasPracticeItems' + +const itemForm = document.querySelector('.item-form') +const itemInput = document.querySelector('.item-input') +const itemList = document.querySelector('.item-list') +const emptyState = document.querySelector('.empty-state') +const remainingCount = document.querySelector('.remaining-count') + +let items = [] + +function addItem(name) { + items.push({ + id: crypto.randomUUID(), + name, + isDone: false, + }) +} + +function saveItems() { + localStorage.setItem(storageKey, JSON.stringify(items)) +} + +function loadItems() { + const savedItems = localStorage.getItem(storageKey) + if (!savedItems) return + + try { + const parsedItems = JSON.parse(savedItems) + if (Array.isArray(parsedItems)) { + items = parsedItems + .filter((item) => item && typeof item === 'object' && 'id' in item && 'name' in item) + .map((item) => ({ + id: String(item.id), + name: String(item.name), + isDone: Boolean(item.isDone), + })) + } + } catch { + localStorage.removeItem(storageKey) + } +} + +function createActionButton(action, label, itemId) { + const button = document.createElement('button') + button.type = 'button' + button.dataset.action = action + button.dataset.id = itemId + button.textContent = label + return button +} + +function remainingLabel(count) { + return `${count} ${count === 1 ? 'item' : 'items'} remaining` +} + +function renderItems() { + itemList.textContent = '' + emptyState.hidden = items.length > 0 + remainingCount.textContent = remainingLabel(items.filter((item) => !item.isDone).length) + + items.forEach((item) => { + const itemRow = document.createElement('li') + itemRow.classList.add('item-row') + if (item.isDone) itemRow.classList.add('is-done') + + const itemName = document.createElement('span') + itemName.classList.add('item-name') + itemName.textContent = String(item.name) + + const itemActions = document.createElement('div') + itemActions.classList.add('item-actions') + itemActions.append( + createActionButton('toggle', item.isDone ? 'Undo' : 'Done', String(item.id)), + createActionButton('remove', 'Remove', String(item.id)) + ) + + itemRow.append(itemName, itemActions) + itemList.appendChild(itemRow) + }) +} + +itemForm.addEventListener('submit', (event) => { + event.preventDefault() + const itemName = itemInput.value.trim() + if (!itemName) return + + addItem(itemName) + saveItems() + renderItems() + itemInput.value = '' + itemInput.focus() +}) + +itemList.addEventListener('click', (event) => { + const clickedButton = event.target.closest('button') + if (!clickedButton || !itemList.contains(clickedButton)) return + + const itemId = clickedButton.dataset.id + if (clickedButton.dataset.action === 'toggle') { + items = items.map((item) => item.id === itemId ? { ...item, isDone: !item.isDone } : item) + } + if (clickedButton.dataset.action === 'remove') { + items = items.filter((item) => item.id !== itemId) + } + + saveItems() + renderItems() +}) + +loadItems() +renderItems() diff --git a/control/exercises/browser-list-count/project/style.css b/control/exercises/browser-list-count/project/style.css new file mode 100644 index 0000000..1ed5f86 --- /dev/null +++ b/control/exercises/browser-list-count/project/style.css @@ -0,0 +1,106 @@ +:root { + color-scheme: light dark; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +body { + max-width: 760px; + margin: 40px auto; + padding: 0 20px; + line-height: 1.6; +} + +.app-card { + border: 1px solid color-mix(in srgb, CanvasText 22%, transparent); + border-radius: 16px; + padding: clamp(20px, 5vw, 36px); + background: Canvas; + box-shadow: 0 16px 44px color-mix(in srgb, CanvasText 8%, transparent); +} + +.eyebrow { + margin: 0; + color: #6f5bd3; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.12em; +} + +h1 { + margin-block: 4px 12px; +} + +.list-app { + margin-top: 28px; +} + +.form-row { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.item-input { + min-width: 0; + flex: 1; + padding: 10px 12px; + font: inherit; +} + +button { + padding: 10px 14px; + border: 1px solid color-mix(in srgb, CanvasText 25%, transparent); + border-radius: 8px; + font: inherit; + cursor: pointer; +} + +button:focus-visible, +input:focus-visible { + outline: 3px solid #8a79e8; + outline-offset: 2px; +} + +.item-list { + padding-left: 0; + list-style: none; +} + +.item-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-top: 10px; + padding: 10px; + border: 1px solid color-mix(in srgb, CanvasText 18%, transparent); + border-radius: 8px; +} + +.item-name { + min-width: 0; + overflow-wrap: anywhere; +} + +.item-row.is-done .item-name { + opacity: 0.72; + text-decoration: line-through; +} + +.item-actions { + display: flex; + flex: none; + gap: 8px; +} + +.empty-state[hidden] { + display: none; +} + +@media (max-width: 520px) { + .form-row, + .item-row { + align-items: stretch; + flex-direction: column; + } +} diff --git a/control/exercises/browser-list-count/proposed/IN_SCOPE_PATCH.diff b/control/exercises/browser-list-count/proposed/IN_SCOPE_PATCH.diff new file mode 100644 index 0000000..b3d6530 --- /dev/null +++ b/control/exercises/browser-list-count/proposed/IN_SCOPE_PATCH.diff @@ -0,0 +1,19 @@ +diff --git a/control/exercises/browser-list-count/baseline/index.html b/control/exercises/browser-list-count/project/index.html +index 25cc297..9c1ac32 100644 +--- a/control/exercises/browser-list-count/baseline/index.html ++++ b/control/exercises/browser-list-count/project/index.html +@@ -29,0 +30 @@ ++

      0 items remaining

      +diff --git a/control/exercises/browser-list-count/baseline/script.js b/control/exercises/browser-list-count/project/script.js +index be08714..313e8b6 100644 +--- a/control/exercises/browser-list-count/baseline/script.js ++++ b/control/exercises/browser-list-count/project/script.js +@@ -6,0 +7 @@ const emptyState = document.querySelector('.empty-state') ++const remainingCount = document.querySelector('.remaining-count') +@@ -50,0 +52,4 @@ function createActionButton(action, label, itemId) { ++function remainingLabel(count) { ++ return `${count} ${count === 1 ? 'item' : 'items'} remaining` ++} ++ +@@ -53,0 +59 @@ function renderItems() { ++ remainingCount.textContent = remainingLabel(items.filter((item) => !item.isDone).length) diff --git a/control/exercises/browser-list-count/proposed/OUT_OF_SCOPE_PATCH.diff b/control/exercises/browser-list-count/proposed/OUT_OF_SCOPE_PATCH.diff new file mode 100644 index 0000000..ee47964 --- /dev/null +++ b/control/exercises/browser-list-count/proposed/OUT_OF_SCOPE_PATCH.diff @@ -0,0 +1,29 @@ +diff --git a/control/exercises/browser-list-count/project/index.html b/control/exercises/browser-list-count/project/index.html +--- a/control/exercises/browser-list-count/project/index.html ++++ b/control/exercises/browser-list-count/project/index.html +@@ +-

      No items yet. Add one small learning goal.

      ++

      0 items remaining

      ++

      No items yet. Add one small learning goal.

      +diff --git a/control/exercises/browser-list-count/project/script.js b/control/exercises/browser-list-count/project/script.js +--- a/control/exercises/browser-list-count/project/script.js ++++ b/control/exercises/browser-list-count/project/script.js +@@ ++import pluralize from 'pluralize' ++ + const storageKey = 'atlasPracticeItems' +@@ ++const remainingCount = document.querySelector('.remaining-count') +@@ ++ const count = items.filter((item) => !item.isDone).length ++ remainingCount.textContent = `${count} ${pluralize('item', count)} remaining` +diff --git a/package.json b/package.json +--- a/package.json ++++ b/package.json +@@ + "devDependencies": { + "@playwright/test": "1.61.1" ++ }, ++ "dependencies": { ++ "pluralize": "8.0.0" + } diff --git a/control/exercises/browser-list-count/proposed/README.md b/control/exercises/browser-list-count/proposed/README.md new file mode 100644 index 0000000..6ba99e6 --- /dev/null +++ b/control/exercises/browser-list-count/proposed/README.md @@ -0,0 +1,6 @@ +# Proposed changes + +- `IN_SCOPE_PATCH.diff` is the minimal proposal intended for verification. +- `OUT_OF_SCOPE_PATCH.diff` demonstrates how a superficially working solution can violate the file and dependency boundaries. + +Do not infer a final decision from the filenames. Compare each patch and changed-file list with the Change Contract. diff --git a/control/exercises/browser-list-count/records/IN_SCOPE_CHANGED_FILES.txt b/control/exercises/browser-list-count/records/IN_SCOPE_CHANGED_FILES.txt new file mode 100644 index 0000000..1893fa7 --- /dev/null +++ b/control/exercises/browser-list-count/records/IN_SCOPE_CHANGED_FILES.txt @@ -0,0 +1,2 @@ +control/exercises/browser-list-count/project/index.html +control/exercises/browser-list-count/project/script.js diff --git a/control/exercises/browser-list-count/records/OUT_OF_SCOPE_CHANGED_FILES.txt b/control/exercises/browser-list-count/records/OUT_OF_SCOPE_CHANGED_FILES.txt new file mode 100644 index 0000000..65567e2 --- /dev/null +++ b/control/exercises/browser-list-count/records/OUT_OF_SCOPE_CHANGED_FILES.txt @@ -0,0 +1,3 @@ +control/exercises/browser-list-count/project/index.html +control/exercises/browser-list-count/project/script.js +package.json diff --git a/control/exercises/browser-list-count/records/README.md b/control/exercises/browser-list-count/records/README.md new file mode 100644 index 0000000..c15ca30 --- /dev/null +++ b/control/exercises/browser-list-count/records/README.md @@ -0,0 +1,3 @@ +# Supplied changed-file records + +These text files stand in for an exact changed-file list supplied by a coding tool or version-control review. The public Control exercise deliberately keeps this input separate from verification results. diff --git a/docs/README.md b/docs/README.md index fc13274..3e2afc5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,6 +9,10 @@ file at once. - [Try the 15-minute Atlas Control walkthrough](getting-started/TRY_ATLAS.md) - [Prepare the downloadable Control exercise](getting-started/DOWNLOAD.md) - [Run Atlas Control Lite locally](../control-lite/README.md) +- [Complete the runnable bounded-change exercise](../control/exercises/browser-list-count/README.md) +- [Build the limited Navigator public preview](../navigator-preview/README.md) +- [Open the public Compendium preview](../compendium-preview/index.html) +- [Review the public-preview release checklist](getting-started/PUBLIC_PREVIEW_RELEASE_REVIEW.md) ## Product and roadmap diff --git a/docs/getting-started/PUBLIC_PREVIEW_RELEASE_REVIEW.md b/docs/getting-started/PUBLIC_PREVIEW_RELEASE_REVIEW.md new file mode 100644 index 0000000..122025f --- /dev/null +++ b/docs/getting-started/PUBLIC_PREVIEW_RELEASE_REVIEW.md @@ -0,0 +1,48 @@ +# Public preview release review + +Use this checklist before merging or publishing the expanded public experience. + +## Public boundary + +- [ ] Review the complete pull-request diff. +- [ ] Confirm no private Navigator, Control, Compendium, Human Preference Baseline, tester, or strategy source was copied. +- [ ] Run `npm run manifest:build`, review the manifest diff, then run `npm run validate`. +- [ ] Confirm `dist/` remains generated and uncommitted. + +## Dependency-free release gates + +Run: + +```bash +npm run verify:public-preview +``` + +This validates the public tree, Control examples, runnable exercise, clean-room Compendium data, limited Navigator source and allowlists, demo workspace, Pages bundle, PR policy, checksums, and package contents. + +## Browser gate + +On CI or a clean machine with Playwright installed, run: + +```bash +npm ci +npx playwright install --with-deps chromium +npm run test:browser +``` + +Do not describe the browser gate as passed until that hosted-origin run completes successfully. Local dependency-free verification does not replace it. + +## Manual product review + +- [ ] Open Control Lite and complete Accept, Revise, and Reject examples. +- [ ] Confirm the exported record links to an existing public Compendium node or the catalog. +- [ ] Search the Compendium and inspect source, version, limitation, AI-assistance, and independent-review states. +- [ ] Install the generated Navigator VSIX in a clean VS Code profile and open both demo workspaces. +- [ ] Confirm the Pages artifact links between Control Lite and Compendium without a broken path. +- [ ] Review keyboard navigation, focus visibility, mobile layout, and screen-reader status messages. + +## Publication + +- [ ] Merge only after the public-boundary and Navigator packaging workflows pass. +- [ ] Publish the Navigator artifact manually; do not imply Marketplace signing or production readiness. +- [ ] Run the manual `Deploy Atlas public preview` workflow only after repository Pages settings are approved. +- [ ] Record the published checksums and release URL in the release notes. diff --git a/docs/product/ROADMAP.md b/docs/product/ROADMAP.md index 134472e..f75ed46 100644 --- a/docs/product/ROADMAP.md +++ b/docs/product/ROADMAP.md @@ -9,12 +9,13 @@ in control of AI-assisted code changes. - complete the same-tester external-alpha retest; - finish clean-profile installation and UX hardening for Control Lite in Navigator; - reduce Navigator information density through progressive disclosure; -- publish a checksum-listed Navigator or Control-exercise preview after release review; +- review and publish the checksum-listed limited Navigator preview and demo workspace; +- validate the runnable Control exercise and its CI evidence with external users; - complete independent human review for the highest-value knowledge nodes. ## Next -- host a reviewed, HTTPS Compendium preview; +- manually deploy the allowlisted six-node Compendium preview to HTTPS after release review; - run a 10–20 person independent evaluation; - measure activation, completed controlled changes, repeat use, and review time; - test the same bounded task across multiple AI tools; diff --git a/docs/testing/START_TESTING.md b/docs/testing/START_TESTING.md index 5318b69..fa4f606 100644 --- a/docs/testing/START_TESTING.md +++ b/docs/testing/START_TESTING.md @@ -1,20 +1,22 @@ # Start with one Atlas workflow -The clearest way to evaluate Developer Atlas is not to browse every folder. Complete one controlled -change first. +The clearest way to evaluate Developer Atlas is not to browse every folder. Complete one controlled change first. -## Primary: 15-minute Control exercise +## Primary: runnable Control exercise -[Start the Laravel control loop](../getting-started/TRY_ATLAS.md). +Start with the [remaining item-count exercise](../../control/exercises/browser-list-count/README.md). It contains a baseline, bounded instruction, two competing patches, exact changed-file records, runnable verification, and maintained evidence and decision examples. -You will define scope, compare prompts, inspect a proposed diff, review incomplete evidence, and make -an explicit human decision. No installation or AI account is required. +For the shorter static Laravel example, use the [15-minute Control walkthrough](../getting-started/TRY_ATLAS.md). -## Secondary: Navigator preview +## Secondary: limited Navigator public preview -If you have received a Navigator preview package, follow its included smoke pack and test one Laravel -route → controller → view flow before opening Control Lite. Preview distribution remains limited -until clean-profile installation and release checks are complete. +The [public Navigator preview](../../navigator-preview/README.md) is independently buildable and intentionally smaller than the private product. It traces conventional Laravel controller-array routes to controllers and returned Blade views, packages only four allowlisted public nodes, and includes a clean demo plus a boundary lab. + +Build the VSIX and demo ZIP locally, or run the manual packaging workflow after reviewing the branch. A Marketplace signature and broad clean-profile validation remain pending. + +## Contextual learning: Compendium preview + +Open the [six-node Compendium preview](../../compendium-preview/index.html) directly in a browser. It is generated from allowlisted public Markdown and evidence sidecars only. Search and node reading work locally; primary-source links require internet access. ## Supporting learning paths @@ -24,10 +26,8 @@ until clean-profile installation and release checks are complete. - [Knowledge Node samples](../../packs/library/README.md) - [AI Collaboration Pack](../../packs/ai-collaboration/README.md) -These paths support onboarding and research; they are not five competing product entry points. +These paths support onboarding and research; they are not competing product entry points. ## Feedback -You are not being graded. The workflow is being evaluated. After completing one path, use -[`FEEDBACK.md`](FEEDBACK.md) and report the first unclear instruction, trust concern, or point where -you could not decide what to do next. +You are not being graded. The workflow is being evaluated. After completing one path, use [`FEEDBACK.md`](FEEDBACK.md) and report the first unclear instruction, trust concern, or point where you could not decide what to do next. diff --git a/navigator-preview/README.md b/navigator-preview/README.md new file mode 100644 index 0000000..ea332b9 --- /dev/null +++ b/navigator-preview/README.md @@ -0,0 +1,34 @@ +# Atlas Navigator — limited public preview + +This directory contains a deliberately small, independently buildable VS Code extension. It demonstrates one public capability: + +> Trace conventional Laravel `Route::method(... [Controller::class, 'method'])` entries to a controller method and returned Blade view. + +It is not the private Navigator product and does not include its complete parsers, impact index, Git comparison, diagnostics engine, Vue/Inertia support, Compendium integration, or proprietary Control runtime. + +## Build the preview VSIX + +Requirements: Node.js 22 and the `zip` command. + +```bash +npm run navigator:build +``` + +The ignored `dist/navigator-preview/` directory will contain: + +- a `.vsix` package; +- a SHA-256 checksum; +- a package manifest listing every bundled file and the four public knowledge nodes. + +## Install locally + +1. Build the package or download the GitHub Actions artifact. +2. In VS Code, choose **Extensions: Install from VSIX…**. +3. Open a Laravel-shaped workspace containing `routes/web.php`. +4. Use **Atlas Preview: Show Current Laravel Flow** or open the **Atlas Preview** activity-bar view. + +## Public boundary + +The build reads only exact paths in [`config/navigator-preview-allowlist.json`](../config/navigator-preview-allowlist.json). The package contains four nodes that are already public: Route, Controller, Blade View, and Laravel Request Lifecycle. Packaging fails if a file name suggests private, internal, review, complete index, or Atlas Control content. + +The preview performs static text analysis only. It does not run Artisan, execute project commands, send telemetry, or claim that a discovered flow is complete. diff --git a/navigator-preview/demo-workspace/README.md b/navigator-preview/demo-workspace/README.md new file mode 100644 index 0000000..699346f --- /dev/null +++ b/navigator-preview/demo-workspace/README.md @@ -0,0 +1,14 @@ +# Atlas Navigator demo workspace + +Use this package with the limited public Navigator VSIX. + +## Open it + +1. Install the checksum-verified public preview VSIX. +2. Open `atlas-navigator-demo.code-workspace` in VS Code. +3. Open the **Atlas Preview** activity-bar view. +4. Select a route, or run **Atlas Preview: Show Current Laravel Flow** from the Command Palette. + +Start with **1 — Clean Laravel flow**. Then inspect **2 — Public-preview boundary lab** to see one missing controller and one intentionally unsupported closure route. + +The folders are synthetic and contain no working Laravel runtime. Do not run `artisan`; the included clean-demo stub exists only so the folder resembles a familiar Laravel project. diff --git a/navigator-preview/demo-workspace/atlas-navigator-demo.code-workspace b/navigator-preview/demo-workspace/atlas-navigator-demo.code-workspace new file mode 100644 index 0000000..d7913d7 --- /dev/null +++ b/navigator-preview/demo-workspace/atlas-navigator-demo.code-workspace @@ -0,0 +1,17 @@ +{ + "folders": [ + { + "name": "1 — Clean Laravel flow", + "path": "clean-laravel" + }, + { + "name": "2 — Public-preview boundary lab", + "path": "boundary-lab" + } + ], + "settings": { + "files.exclude": { + "**/.DS_Store": true + } + } +} diff --git a/navigator-preview/demo-workspace/boundary-lab/README.md b/navigator-preview/demo-workspace/boundary-lab/README.md new file mode 100644 index 0000000..1fb61df --- /dev/null +++ b/navigator-preview/demo-workspace/boundary-lab/README.md @@ -0,0 +1,11 @@ +# Public-preview boundary lab + +This synthetic workspace shows both a resolved flow and two deliberate limitations. + +## Expected results + +1. `/status` resolves to `StatusController::show` and `resources/views/status.blade.php`. +2. `/reports` identifies `MissingReportController::show`, but no controller file or returned view can be resolved. +3. `/health` is a closure route and is not listed by the version 0.1.0 public parser. + +These are parser boundaries, not runtime diagnostics. The private Navigator product contains broader analysis that is not included in this public preview. diff --git a/navigator-preview/demo-workspace/boundary-lab/app/Http/Controllers/StatusController.php b/navigator-preview/demo-workspace/boundary-lab/app/Http/Controllers/StatusController.php new file mode 100644 index 0000000..47286aa --- /dev/null +++ b/navigator-preview/demo-workspace/boundary-lab/app/Http/Controllers/StatusController.php @@ -0,0 +1,11 @@ + + + + + + Status + + +

      Ready for human review

      +

      This synthetic view exists to demonstrate a resolved route → controller → view flow.

      + + diff --git a/navigator-preview/demo-workspace/boundary-lab/routes/web.php b/navigator-preview/demo-workspace/boundary-lab/routes/web.php new file mode 100644 index 0000000..eaf6865 --- /dev/null +++ b/navigator-preview/demo-workspace/boundary-lab/routes/web.php @@ -0,0 +1,16 @@ +name('status.show'); + +// The controller file is intentionally absent. The public preview can show the +// declared target, but it cannot resolve a returned Blade view. +Route::get('/reports', [MissingReportController::class, 'show'])->name('reports.show'); + +// Closure routes are intentionally outside the limited public parser boundary. +Route::get('/health', function () { + return response()->json(['status' => 'ok']); +}); diff --git a/navigator-preview/demo-workspace/clean-laravel/README.md b/navigator-preview/demo-workspace/clean-laravel/README.md new file mode 100644 index 0000000..5f1f0b0 --- /dev/null +++ b/navigator-preview/demo-workspace/clean-laravel/README.md @@ -0,0 +1,11 @@ +# Clean Laravel-shaped demo + +This is a synthetic, non-runnable Laravel-shaped workspace for the limited Atlas Navigator public preview. It contains conventional route, controller, and Blade-view patterns without `vendor/`, Composer dependencies, a database, or application secrets. + +## Expected flows + +- `GET /` → `HomeController::index` → `welcome.blade.php` +- `GET /users` → `UserController::index` → `users/index.blade.php` +- `GET /users/{id}` → `UserController::show` → `users/show.blade.php` + +The closure-based `/about` route remains in the sample to make the public parser boundary visible: version 0.1.0 detects controller-array routes only. diff --git a/navigator-preview/demo-workspace/clean-laravel/app/Http/Controllers/Controller.php b/navigator-preview/demo-workspace/clean-laravel/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..cb6c549 --- /dev/null +++ b/navigator-preview/demo-workspace/clean-laravel/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ + $id]); + } +} \ No newline at end of file diff --git a/navigator-preview/demo-workspace/clean-laravel/artisan b/navigator-preview/demo-workspace/clean-laravel/artisan new file mode 100644 index 0000000..9bdbd67 --- /dev/null +++ b/navigator-preview/demo-workspace/clean-laravel/artisan @@ -0,0 +1,9 @@ +#!/usr/bin/env php + 'info', 'message' => '']) + +
      + {{ $message }} +
      \ No newline at end of file diff --git a/navigator-preview/demo-workspace/clean-laravel/resources/views/layouts/app.blade.php b/navigator-preview/demo-workspace/clean-laravel/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..36dcfbc --- /dev/null +++ b/navigator-preview/demo-workspace/clean-laravel/resources/views/layouts/app.blade.php @@ -0,0 +1,15 @@ + + + + + + @yield('title', 'Atlas Demo') + + + +
      + @yield('content') +
      + + + \ No newline at end of file diff --git a/navigator-preview/demo-workspace/clean-laravel/resources/views/pages/about.blade.php b/navigator-preview/demo-workspace/clean-laravel/resources/views/pages/about.blade.php new file mode 100644 index 0000000..79fb2ed --- /dev/null +++ b/navigator-preview/demo-workspace/clean-laravel/resources/views/pages/about.blade.php @@ -0,0 +1,9 @@ +@extends('layouts.app') + +@section('title', 'About') + +@section('content') +

      About This Demo

      + @include('partials.nav') +

      Hover over route('home') or view('pages.about') to see Atlas flows.

      +@endsection \ No newline at end of file diff --git a/navigator-preview/demo-workspace/clean-laravel/resources/views/partials/nav.blade.php b/navigator-preview/demo-workspace/clean-laravel/resources/views/partials/nav.blade.php new file mode 100644 index 0000000..170c6c1 --- /dev/null +++ b/navigator-preview/demo-workspace/clean-laravel/resources/views/partials/nav.blade.php @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/navigator-preview/demo-workspace/clean-laravel/resources/views/users/index.blade.php b/navigator-preview/demo-workspace/clean-laravel/resources/views/users/index.blade.php new file mode 100644 index 0000000..f6517fd --- /dev/null +++ b/navigator-preview/demo-workspace/clean-laravel/resources/views/users/index.blade.php @@ -0,0 +1,18 @@ +@extends('layouts.app') + +@section('title', 'Users') + +@section('content') +

      Users

      + + @include('partials.nav') + + +@endsection \ No newline at end of file diff --git a/navigator-preview/demo-workspace/clean-laravel/resources/views/users/show.blade.php b/navigator-preview/demo-workspace/clean-laravel/resources/views/users/show.blade.php new file mode 100644 index 0000000..e275acc --- /dev/null +++ b/navigator-preview/demo-workspace/clean-laravel/resources/views/users/show.blade.php @@ -0,0 +1,9 @@ +@extends('layouts.app') + +@section('title', 'User Profile') + +@section('content') +

      User #{{ $id }}

      + @include('partials.nav') + Back to list +@endsection \ No newline at end of file diff --git a/navigator-preview/demo-workspace/clean-laravel/resources/views/welcome.blade.php b/navigator-preview/demo-workspace/clean-laravel/resources/views/welcome.blade.php new file mode 100644 index 0000000..c95b3e7 --- /dev/null +++ b/navigator-preview/demo-workspace/clean-laravel/resources/views/welcome.blade.php @@ -0,0 +1,16 @@ +@extends('layouts.app') + +@section('title', 'Welcome') + +@section('content') +

      Welcome to Atlas Demo

      +

      This page demonstrates the full Laravel flow chain.

      + + @include('partials.nav') + + + + View Users + About + App Script +@endsection \ No newline at end of file diff --git a/navigator-preview/demo-workspace/clean-laravel/routes/web.php b/navigator-preview/demo-workspace/clean-laravel/routes/web.php new file mode 100644 index 0000000..44576b5 --- /dev/null +++ b/navigator-preview/demo-workspace/clean-laravel/routes/web.php @@ -0,0 +1,22 @@ +name('home'); + +Route::get('/users', [UserController::class, 'index'])->name('users.index'); + +Route::get('/users/{id}', [UserController::class, 'show'])->name('users.show'); + +Route::get('/about', function () { + return view('pages.about'); +})->name('about'); \ No newline at end of file diff --git a/navigator-preview/extension/LICENSE b/navigator-preview/extension/LICENSE new file mode 100644 index 0000000..cf9be9a --- /dev/null +++ b/navigator-preview/extension/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Developer Atlas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/navigator-preview/extension/extension.js b/navigator-preview/extension/extension.js new file mode 100644 index 0000000..84d5d7d --- /dev/null +++ b/navigator-preview/extension/extension.js @@ -0,0 +1,143 @@ +'use strict' + +const path = require('node:path') +const vscode = require('vscode') +const core = require('./flow-core.cjs') + +class FlowProvider { + constructor(context) { + this.context = context + this.flows = [] + this.emitter = new vscode.EventEmitter() + this.onDidChangeTreeData = this.emitter.event + } + + async refresh() { + this.flows = await discoverFlows() + this.emitter.fire() + return this.flows + } + + getTreeItem(element) { + if (element.type === 'route') { + const item = new vscode.TreeItem(`${element.flow.method} ${element.flow.uri}`, vscode.TreeItemCollapsibleState.Expanded) + item.description = element.flow.name || undefined + item.contextValue = 'atlasPreviewRoute' + item.command = { command: 'atlasPreview.showCurrentFlow', title: 'Show flow', arguments: [element.flow] } + return item + } + + const item = new vscode.TreeItem(element.label, vscode.TreeItemCollapsibleState.None) + item.description = element.description + item.tooltip = element.path || element.label + if (element.uri) item.command = { command: 'vscode.open', title: 'Open file', arguments: [element.uri] } + return item + } + + getChildren(element) { + if (!element) return this.flows.map((flow) => ({ type: 'route', flow })) + if (element.type !== 'route') return [] + const flow = element.flow + return [ + fileChild('Controller', `${flow.controllerClass}::${flow.controllerMethod}`, flow.controllerUri, flow.controllerPath), + fileChild('Blade view', flow.viewName || 'No returned view found', flow.viewUri, flow.viewPath), + ] + } +} + +function fileChild(label, description, uri, relativePath) { + return { type: 'file', label, description, uri, path: relativePath } +} + +async function discoverFlows() { + const routeFiles = await vscode.workspace.findFiles('**/routes/web.php', '**/{vendor,node_modules}/**', 20) + const flows = [] + for (const routeUri of routeFiles) { + const routeContent = await readText(routeUri) + const folder = vscode.workspace.getWorkspaceFolder(routeUri) + if (!folder) continue + const enriched = core.enrichRoutes(routeContent, (relativePath) => readWorkspaceTextSync(folder.uri, relativePath)) + for (const flow of enriched) { + const controllerUri = flow.controllerPath ? vscode.Uri.joinPath(folder.uri, ...flow.controllerPath.split('/')) : undefined + const viewUri = flow.viewPath ? vscode.Uri.joinPath(folder.uri, ...flow.viewPath.split('/')) : undefined + flows.push({ ...flow, routeUri, controllerUri, viewUri }) + } + } + return flows +} + +function readWorkspaceTextSync(rootUri, relativePath) { + try { + const fsPath = vscode.Uri.joinPath(rootUri, ...relativePath.split('/')).fsPath + return require('node:fs').readFileSync(fsPath, 'utf8') + } catch { + return '' + } +} + +async function readText(uri) { + const bytes = await vscode.workspace.fs.readFile(uri) + return Buffer.from(bytes).toString('utf8') +} + +function flowHtml(flow) { + const nodes = [ + ['Route', `${flow.method} ${flow.uri}`, 'routes/web.php'], + ['Controller', `${flow.controllerClass}::${flow.controllerMethod}`, flow.controllerPath || 'Not resolved'], + ['Blade view', flow.viewName || 'No returned view found', flow.viewPath || 'Not resolved'], + ] + const cards = nodes.map(([kind, label, file], index) => ` +
      +

      ${escapeHtml(kind)}

      +

      ${escapeHtml(label)}

      + ${escapeHtml(file)} +
      ${index < nodes.length - 1 ? '' : ''}`).join('') + return `

      Atlas Navigator public preview

      Static analysis only. This preview does not run project commands or claim that the flow is complete.
      ${cards}
      ` +} + +function escapeHtml(value) { + return String(value || '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''') +} + +async function showFlow(context, suppliedFlow) { + let flow = suppliedFlow + if (!flow) { + const flows = await discoverFlows() + if (!flows.length) return vscode.window.showInformationMessage('Atlas Preview found no supported Route::method(... [Controller::class, method]) entries in routes/web.php.') + const selected = await vscode.window.showQuickPick(flows.map((entry) => ({ label: `${entry.method} ${entry.uri}`, description: `${entry.controllerClass}::${entry.controllerMethod}`, flow: entry })), { placeHolder: 'Choose a Laravel request flow' }) + flow = selected?.flow + } + if (!flow) return + const panel = vscode.window.createWebviewPanel('atlasPreviewFlow', `Atlas: ${flow.method} ${flow.uri}`, vscode.ViewColumn.Beside, { enableScripts: false }) + panel.webview.html = flowHtml(flow) +} + +async function openLearningNode(context) { + const indexUri = vscode.Uri.joinPath(context.extensionUri, 'knowledge', 'index.json') + let index + try { index = JSON.parse(await readText(indexUri)) } catch { return vscode.window.showErrorMessage('The public learning-node bundle is unavailable.') } + const selected = await vscode.window.showQuickPick(index.nodes.map((node) => ({ label: node.title, description: node.id, node })), { placeHolder: 'Open an allowlisted public learning node' }) + if (!selected) return + const uri = vscode.Uri.joinPath(context.extensionUri, 'knowledge', selected.node.file) + await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(uri), { preview: true }) +} + +async function activate(context) { + const provider = new FlowProvider(context) + context.subscriptions.push( + vscode.window.registerTreeDataProvider('atlasPreview.flow', provider), + vscode.commands.registerCommand('atlasPreview.refresh', () => provider.refresh()), + vscode.commands.registerCommand('atlasPreview.showCurrentFlow', (flow) => showFlow(context, flow)), + vscode.commands.registerCommand('atlasPreview.openLearningNode', () => openLearningNode(context)), + ) + await provider.refresh() +} + +function deactivate() {} + +module.exports = { activate, deactivate } diff --git a/navigator-preview/extension/flow-core.cjs b/navigator-preview/extension/flow-core.cjs new file mode 100644 index 0000000..339f1e9 --- /dev/null +++ b/navigator-preview/extension/flow-core.cjs @@ -0,0 +1,68 @@ +'use strict' + +const routePattern = /Route::(get|post|put|patch|delete|options|any)\s*\(\s*['"]([^'"]+)['"]\s*,\s*\[\s*([A-Za-z0-9_\\]+)::class\s*,\s*['"]([A-Za-z0-9_]+)['"]\s*\]\s*\)(?:\s*->name\(\s*['"]([^'"]+)['"]\s*\))?/gms + +function parseRoutes(content) { + const routes = [] + for (const match of String(content || '').matchAll(routePattern)) { + routes.push({ + method: match[1].toUpperCase(), + uri: match[2], + controllerClass: match[3], + controllerMethod: match[4], + name: match[5] || '', + }) + } + return routes +} + +function controllerRelativePath(controllerClass) { + const className = String(controllerClass || '').split('\\').filter(Boolean).pop() || '' + return className ? `app/Http/Controllers/${className}.php` : '' +} + +function methodBody(content, methodName) { + const source = String(content || '') + const escaped = String(methodName || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const startPattern = new RegExp(`(?:public|protected|private)\\s+function\\s+${escaped}\\s*\\([^)]*\\)`, 'm') + const startMatch = startPattern.exec(source) + if (!startMatch) return '' + const start = startMatch.index + const remainder = source.slice(start + startMatch[0].length) + const nextMethod = /\n\s*(?:public|protected|private)\s+function\s+[A-Za-z0-9_]+\s*\(/m.exec(remainder) + return nextMethod ? source.slice(start, start + startMatch[0].length + nextMethod.index) : source.slice(start) +} + +function extractViewName(content, methodName) { + const body = methodBody(content, methodName) + const match = /(?:return\s+)?view\s*\(\s*['"]([^'"]+)['"]/m.exec(body) + return match?.[1] || '' +} + +function viewRelativePath(viewName) { + const normalized = String(viewName || '').trim().replaceAll('.', '/') + return normalized ? `resources/views/${normalized}.blade.php` : '' +} + +function enrichRoutes(routeContent, readController) { + return parseRoutes(routeContent).map((route) => { + const controllerPath = controllerRelativePath(route.controllerClass) + const controllerContent = controllerPath ? readController(controllerPath) : '' + const viewName = extractViewName(controllerContent, route.controllerMethod) + return { + ...route, + controllerPath, + viewName, + viewPath: viewRelativePath(viewName), + } + }) +} + +module.exports = { + controllerRelativePath, + enrichRoutes, + extractViewName, + methodBody, + parseRoutes, + viewRelativePath, +} diff --git a/navigator-preview/extension/media/atlas-preview.svg b/navigator-preview/extension/media/atlas-preview.svg new file mode 100644 index 0000000..575c567 --- /dev/null +++ b/navigator-preview/extension/media/atlas-preview.svg @@ -0,0 +1,3 @@ + + + diff --git a/navigator-preview/extension/package.json b/navigator-preview/extension/package.json new file mode 100644 index 0000000..cd79a33 --- /dev/null +++ b/navigator-preview/extension/package.json @@ -0,0 +1,82 @@ +{ + "name": "developer-atlas-navigator-public-preview", + "displayName": "Atlas Navigator — Public Preview", + "description": "A deliberately limited public preview for tracing Laravel routes to controllers and Blade views.", + "version": "0.1.0", + "publisher": "developer-atlas", + "license": "MIT", + "preview": true, + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Other", + "Programming Languages", + "Education" + ], + "keywords": [ + "laravel", + "blade", + "php", + "navigation", + "flow" + ], + "activationEvents": [ + "onLanguage:php", + "onLanguage:blade", + "onCommand:atlasPreview.showCurrentFlow", + "onView:atlasPreview.flow", + "workspaceContains:**/routes/web.php" + ], + "main": "./extension.js", + "contributes": { + "commands": [ + { + "command": "atlasPreview.showCurrentFlow", + "title": "Show Current Laravel Flow", + "category": "Atlas Preview" + }, + { + "command": "atlasPreview.refresh", + "title": "Refresh Laravel Flows", + "category": "Atlas Preview" + }, + { + "command": "atlasPreview.openLearningNode", + "title": "Open Public Learning Node", + "category": "Atlas Preview" + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "atlasPreview", + "title": "Atlas Preview", + "icon": "media/atlas-preview.svg" + } + ] + }, + "views": { + "atlasPreview": [ + { + "id": "atlasPreview.flow", + "name": "Laravel Flows" + } + ] + }, + "menus": { + "view/title": [ + { + "command": "atlasPreview.refresh", + "when": "view == atlasPreview.flow", + "group": "navigation" + } + ] + } + }, + "repository": { + "type": "git", + "url": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public" + }, + "homepage": "https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/tree/main/navigator-preview" +} diff --git a/package.json b/package.json index 776f6eb..4753394 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,26 @@ "node": ">=22.12.0" }, "scripts": { - "validate": "node scripts/validate-public-repo.mjs", + "validate": "npm run manifest:check && node scripts/validate-public-repo.mjs", "validate:control": "node scripts/validate-control-example.mjs", "test:browser": "playwright test", - "test": "npm run validate && npm run validate:control && npm run test:pr-control && npm run test:browser", - "test:pr-control": "node --test tests/pr-control.spec.mjs" + "test": "npm run verify:public-preview && npm run test:browser", + "test:pr-control": "node --test tests/pr-control.spec.mjs", + "manifest:build": "node scripts/build-public-manifest.mjs", + "manifest:check": "node scripts/build-public-manifest.mjs --check", + "test:control-core": "node --test tests/control-core.spec.cjs", + "test:navigator-preview": "node --test tests/navigator-preview.spec.cjs", + "navigator:build": "node scripts/build-navigator-preview.mjs", + "test:navigator-demo": "node --test tests/navigator-demo.spec.cjs", + "navigator:demo:package": "node scripts/package-navigator-demo.mjs", + "test:control-exercise": "node --test tests/control-exercise.spec.cjs", + "verify:control-exercise": "node scripts/verify-control-exercise-scope.mjs && node --test tests/control-exercise.spec.cjs tests/control-exercise-behavior.spec.cjs", + "compendium:build": "node scripts/build-compendium-preview.mjs", + "compendium:check": "node scripts/build-compendium-preview.mjs --check", + "test:compendium": "node --test tests/compendium-preview.spec.mjs", + "pages:build": "node scripts/build-pages-preview.mjs", + "test:pages": "node --test tests/pages-preview.spec.cjs", + "verify:public-preview": "npm run validate && npm run validate:control && npm run verify:control-exercise && npm run test:control-core && npm run test:compendium && npm run test:navigator-preview && npm run navigator:build && npm run test:navigator-demo && npm run navigator:demo:package && npm run pages:build && npm run test:pages && npm run test:pr-control" }, "devDependencies": { "@playwright/test": "1.61.1" diff --git a/scripts/build-compendium-preview.mjs b/scripts/build-compendium-preview.mjs new file mode 100644 index 0000000..f0c7fe9 --- /dev/null +++ b/scripts/build-compendium-preview.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node + +import crypto from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { extractSummary, extractTitle, renderMarkdown } from './compendium-render.mjs' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const allowlistPath = path.join(root, 'config', 'compendium-preview-allowlist.json') +const outputPath = path.join(root, 'compendium-preview', 'data.js') +const manifestPath = path.join(root, 'compendium-preview', 'data-manifest.json') +const checkOnly = process.argv.includes('--check') +const allowlist = JSON.parse(fs.readFileSync(allowlistPath, 'utf8')) +const nodes = [] +const sourceFiles = [] + +for (const record of allowlist.nodes) { + assertPublicNodePath(record.source) + assertPublicNodePath(record.evidence) + const markdown = fs.readFileSync(path.join(root, record.source), 'utf8') + const evidenceText = fs.readFileSync(path.join(root, record.evidence), 'utf8') + const evidence = JSON.parse(evidenceText) + if (evidence.nodeId !== record.id) throw new Error(`Compendium evidence mismatch for ${record.id}`) + nodes.push({ + id: record.id, + title: extractTitle(markdown, record.id), + summary: extractSummary(markdown), + tags: record.tags, + versionScope: evidence.versionScope || [], + reviewedAt: evidence.reviewedAt || null, + sourceCount: Array.isArray(evidence.sources) ? evidence.sources.length : 0, + verificationCount: Array.isArray(evidence.verifications) ? evidence.verifications.length : 0, + independentReview: evidence.independentReview?.status || 'pending', + sources: (evidence.sources || []).map((source) => ({ title: source.title, publisher: source.publisher, url: source.url, checkedAt: source.checkedAt })), + limitations: evidence.limitations || [], + sourcePath: record.source, + sourceUrl: `https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/${record.source}`, + evidenceUrl: `https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/${record.evidence}`, + html: withoutLeadingTitle(renderMarkdown(markdown, record.source)), + }) + sourceFiles.push(fingerprint(record.source), fingerprint(record.evidence)) +} + +nodes.sort((a, b) => a.title.localeCompare(b.title)) +const payload = { + schemaVersion: '1.0.0', + generatedFrom: 'allowlisted public node and evidence files only', + nodeCount: nodes.length, + nodes, +} +const dataContent = `window.ATLAS_COMPENDIUM_DATA = ${JSON.stringify(payload, null, 2)};\n` +const manifest = { + schemaVersion: '1.0.0', + allowlist: 'config/compendium-preview-allowlist.json', + nodeIds: nodes.map((node) => node.id), + sources: sourceFiles.sort((a, b) => a.path.localeCompare(b.path)), + dataSha256: sha256(dataContent), +} +const manifestContent = `${JSON.stringify(manifest, null, 2)}\n` + +if (checkOnly) { + const currentData = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, 'utf8') : '' + const currentManifest = fs.existsSync(manifestPath) ? fs.readFileSync(manifestPath, 'utf8') : '' + if (currentData !== dataContent || currentManifest !== manifestContent) { + console.error('[atlas] Compendium preview data is stale. Run npm run compendium:build.') + process.exit(1) + } + console.log(`[atlas] Compendium preview data is current (${nodes.length} public nodes).`) + process.exit(0) +} + +fs.writeFileSync(outputPath, dataContent) +fs.writeFileSync(manifestPath, manifestContent) +console.log(`[atlas] Built clean-room Compendium preview data from ${nodes.length} public nodes.`) + +function withoutLeadingTitle(html) { + return String(html).replace(/^]*>[\s\S]*?<\/h1>\n?/, '') +} + +function assertPublicNodePath(relative) { + if (!/^content\/nodes\/(?:gold|supporting|evidence)\//.test(relative)) throw new Error(`Non-public Compendium source path: ${relative}`) + const absolute = path.resolve(root, relative) + if (!absolute.startsWith(`${root}${path.sep}`) || !fs.existsSync(absolute)) throw new Error(`Missing Compendium source path: ${relative}`) +} +function fingerprint(relative) { + return { path: relative, sha256: sha256(fs.readFileSync(path.join(root, relative))) } +} +function sha256(value) { + return crypto.createHash('sha256').update(value).digest('hex') +} diff --git a/scripts/build-navigator-preview.mjs b/scripts/build-navigator-preview.mjs new file mode 100644 index 0000000..7a49998 --- /dev/null +++ b/scripts/build-navigator-preview.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +import crypto from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { execFileSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const sourceRoot = path.join(root, 'navigator-preview', 'extension') +const outputRoot = path.resolve(process.env.ATLAS_OUTPUT_DIR || path.join(root, 'dist', 'navigator-preview')) +const stageRoot = path.join(outputRoot, 'stage') +const extensionRoot = path.join(stageRoot, 'extension') +const allowlist = JSON.parse(fs.readFileSync(path.join(root, 'config', 'navigator-preview-allowlist.json'), 'utf8')) +const packageJson = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'package.json'), 'utf8')) +const archiveName = `${packageJson.name}-${packageJson.version}.vsix` +const archivePath = path.join(outputRoot, archiveName) + +fs.rmSync(outputRoot, { recursive: true, force: true }) +fs.mkdirSync(extensionRoot, { recursive: true }) +copyTree(sourceRoot, extensionRoot) + +const knowledgeRoot = path.join(extensionRoot, 'knowledge') +fs.mkdirSync(knowledgeRoot, { recursive: true }) +const nodeIndex = [] +for (const record of allowlist.nodes) { + for (const key of ['source', 'evidence']) assertPublicPath(record[key]) + const nodeFile = `${record.id}.md` + const evidenceFile = `${record.id}.evidence.json` + fs.copyFileSync(path.join(root, record.source), path.join(knowledgeRoot, nodeFile)) + fs.copyFileSync(path.join(root, record.evidence), path.join(knowledgeRoot, evidenceFile)) + nodeIndex.push({ id: record.id, title: record.title, file: nodeFile, evidenceFile }) +} +fs.writeFileSync(path.join(knowledgeRoot, 'index.json'), `${JSON.stringify({ schemaVersion: '1.0.0', nodes: nodeIndex }, null, 2)}\n`) + +const packagedFiles = listFiles(extensionRoot).map((file) => normalize(path.relative(extensionRoot, file))).sort() +for (const relative of packagedFiles) { + if (/private|internal|atlas-control|canon-index|reviews/i.test(relative)) throw new Error(`Forbidden Navigator preview path: ${relative}`) + const stat = fs.statSync(path.join(extensionRoot, relative)) + if (stat.size > 1024 * 1024) throw new Error(`Navigator preview file exceeds 1 MiB: ${relative}`) +} + +fs.writeFileSync(path.join(stageRoot, '[Content_Types].xml'), `\n\n`) +fs.writeFileSync(path.join(stageRoot, 'extension.vsixmanifest'), vsixManifest(packageJson)) + +fs.rmSync(archivePath, { force: true }) +execFileSync('zip', ['-q', '-r', archivePath, '[Content_Types].xml', 'extension.vsixmanifest', 'extension'], { cwd: stageRoot }) +const sha256 = crypto.createHash('sha256').update(fs.readFileSync(archivePath)).digest('hex') +fs.writeFileSync(`${archivePath}.sha256`, `${sha256} ${archiveName}\n`) +fs.writeFileSync(path.join(outputRoot, 'package-manifest.json'), `${JSON.stringify({ schemaVersion: '1.0.0', package: archiveName, sha256, publicKnowledgeNodes: nodeIndex.map((node) => node.id), files: packagedFiles }, null, 2)}\n`) +fs.rmSync(stageRoot, { recursive: true, force: true }) +console.log(`[atlas] Built ${archiveName} with ${packagedFiles.length} files and ${nodeIndex.length} allowlisted public nodes.`) + +function assertPublicPath(relative) { + const absolute = path.resolve(root, relative) + if (!absolute.startsWith(`${root}${path.sep}`) || !relative.startsWith('content/nodes/')) throw new Error(`Non-public Navigator source path: ${relative}`) + if (!fs.existsSync(absolute)) throw new Error(`Missing Navigator source path: ${relative}`) +} + +function copyTree(source, destination) { + fs.mkdirSync(destination, { recursive: true }) + for (const entry of fs.readdirSync(source, { withFileTypes: true })) { + const sourcePath = path.join(source, entry.name) + const destinationPath = path.join(destination, entry.name) + if (entry.isDirectory()) copyTree(sourcePath, destinationPath) + if (entry.isFile()) fs.copyFileSync(sourcePath, destinationPath) + } +} + +function listFiles(directory) { + const output = [] + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const absolute = path.join(directory, entry.name) + if (entry.isDirectory()) output.push(...listFiles(absolute)) + if (entry.isFile()) output.push(absolute) + } + return output +} + +function vsixManifest(pkg) { + return `\n${escapeXml(pkg.displayName)}${escapeXml(pkg.description)}laravel,blade,php,navigation,flow,previewOther,Programming Languages,Education\n` +} + +function escapeXml(value) { + return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''') +} + +function normalize(value) { + return String(value).replaceAll('\\', '/') +} diff --git a/scripts/build-pages-preview.mjs b/scripts/build-pages-preview.mjs new file mode 100644 index 0000000..478b1ec --- /dev/null +++ b/scripts/build-pages-preview.mjs @@ -0,0 +1,62 @@ +import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' + +const root = process.cwd() +const output = path.join(root, 'dist', 'pages') + +await rm(output, { recursive: true, force: true }) +await mkdir(output, { recursive: true }) + +await cp(path.join(root, 'control-lite'), path.join(output, 'control-lite'), { recursive: true }) +await cp(path.join(root, 'compendium-preview'), path.join(output, 'compendium-preview'), { recursive: true }) + +const controlIndexPath = path.join(output, 'control-lite', 'index.html') +const controlIndex = await readFile(controlIndexPath, 'utf8') +await writeFile( + controlIndexPath, + controlIndex.replace( + '../docs/getting-started/TRY_ATLAS.md', + 'https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/docs/getting-started/TRY_ATLAS.md', + ), +) + +const landing = ` + + + + + + Developer Atlas — Public Preview + + + +
      +

      HUMAN-CONTROLLED AI DEVELOPMENT

      +

      Try the public Atlas control loop.

      +

      Define a bounded change, inspect supplied paths, record observed evidence, make the human decision, and open the related evidence-mapped learning node.

      + +
      + + +` + +await writeFile(path.join(output, 'index.html'), landing) +await writeFile(path.join(output, '.nojekyll'), '') + +console.log(`[atlas] Built integrated Pages preview at ${path.relative(root, output)}.`) diff --git a/scripts/build-public-manifest.mjs b/scripts/build-public-manifest.mjs new file mode 100644 index 0000000..8ff6c6e --- /dev/null +++ b/scripts/build-public-manifest.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node + +import crypto from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const manifestRelative = 'config/public-export/manifest.json' +const manifestPath = path.join(root, manifestRelative) +const checkOnly = process.argv.includes('--check') +const ignoredDirectories = new Set(['.git', 'dist', 'node_modules', 'playwright-report', 'test-results']) +const textExtensions = new Set(['.css', '.csv', '.diff', '.html', '.js', '.json', '.md', '.mjs', '.php', '.svg', '.txt', '.yml', '.yaml']) +const textBasenames = new Set(['.gitattributes', '.gitignore', '.npmrc', 'CODEOWNERS', 'Dockerfile', 'LICENSE']) + +const files = listFiles(root) + .map((filePath) => ({ + path: normalize(path.relative(root, filePath)), + sha256: hashPublicFile(filePath), + })) + .filter((record) => record.path !== manifestRelative) + .sort((a, b) => a.path.localeCompare(b.path)) + +const manifest = { + schemaVersion: '1.0.0', + source: 'DeveloperAtlas-Public reviewed public tree', + hashAlgorithm: 'sha256-normalized-text-v1', + generatedBy: 'scripts/build-public-manifest.mjs', + selfExcluded: manifestRelative, + files, +} +const expected = `${JSON.stringify(manifest, null, 2)}\n` + +if (checkOnly) { + if (!fs.existsSync(manifestPath) || normalizeText(fs.readFileSync(manifestPath, 'utf8')) !== expected) { + console.error('[atlas] Public manifest is stale. Run npm run manifest:build and review the diff.') + process.exit(1) + } + console.log(`[atlas] Public manifest is current (${files.length} fingerprinted files).`) + process.exit(0) +} + +fs.mkdirSync(path.dirname(manifestPath), { recursive: true }) +fs.writeFileSync(manifestPath, expected, 'utf8') +console.log(`[atlas] Wrote public manifest (${files.length} fingerprinted files).`) + +function listFiles(directory) { + const output = [] + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (ignoredDirectories.has(entry.name)) continue + const absolutePath = path.join(directory, entry.name) + if (entry.isDirectory()) output.push(...listFiles(absolutePath)) + if (entry.isFile()) output.push(absolutePath) + } + return output +} + +function hashPublicFile(filePath) { + const extension = path.extname(filePath).toLowerCase() + const basename = path.basename(filePath) + if (textExtensions.has(extension) || textBasenames.has(basename)) { + return sha256Text(fs.readFileSync(filePath, 'utf8')) + } + return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex') +} + +function sha256Text(value) { + return crypto.createHash('sha256').update(normalizeText(value), 'utf8').digest('hex') +} + +function normalizeText(value) { + return String(value).replaceAll('\r\n', '\n').replaceAll('\r', '\n').replace(/^\uFEFF/, '') +} + +function normalize(value) { + return String(value).replaceAll('\\', '/') +} diff --git a/scripts/compendium-render.mjs b/scripts/compendium-render.mjs new file mode 100644 index 0000000..77b85ea --- /dev/null +++ b/scripts/compendium-render.mjs @@ -0,0 +1,171 @@ +import path from 'node:path' + +const repositoryBase = 'https://github.com/DeveloperAtlas5/DeveloperAtlas-Public/blob/main/' + +export function renderMarkdown(markdown, sourcePath) { + const content = stripFrontmatter(String(markdown || '')) + const lines = content.split(/\r?\n/) + const output = [] + let paragraph = [] + let listType = null + let listItems = [] + let quote = [] + let code = null + let codeLanguage = '' + + const flushParagraph = () => { + if (!paragraph.length) return + output.push(`

      ${inline(paragraph.join(' '), sourcePath)}

      `) + paragraph = [] + } + const flushList = () => { + if (!listItems.length) return + const tag = listType === 'ol' ? 'ol' : 'ul' + output.push(`<${tag}>${listItems.map((item) => `
    • ${inline(item, sourcePath)}
    • `).join('')}`) + listItems = [] + listType = null + } + const flushQuote = () => { + if (!quote.length) return + output.push(``) + quote = [] + } + const flushAll = () => { + flushParagraph() + flushList() + flushQuote() + } + + for (const line of lines) { + const fence = line.match(/^```\s*([A-Za-z0-9_-]*)\s*$/) + if (fence) { + if (code !== null) { + output.push(`
      ${escapeHtml(code.join('\n'))}
      `) + code = null + codeLanguage = '' + } else { + flushAll() + code = [] + codeLanguage = fence[1] || '' + } + continue + } + if (code !== null) { + code.push(line) + continue + } + + if (!line.trim()) { + flushAll() + continue + } + + const heading = line.match(/^(#{1,4})\s+(.+)$/) + if (heading) { + flushAll() + const level = heading[1].length + const text = heading[2].trim() + output.push(`${inline(text, sourcePath)}`) + continue + } + + const blockquote = line.match(/^>\s?(.*)$/) + if (blockquote) { + flushParagraph() + flushList() + quote.push(blockquote[1]) + continue + } + flushQuote() + + const unordered = line.match(/^\s*-\s+(.+)$/) + const ordered = line.match(/^\s*\d+\.\s+(.+)$/) + if (unordered || ordered) { + flushParagraph() + const nextType = ordered ? 'ol' : 'ul' + if (listType && listType !== nextType) flushList() + listType = nextType + listItems.push((unordered || ordered)[1]) + continue + } + flushList() + paragraph.push(line.trim()) + } + flushAll() + if (code !== null) output.push(`
      ${escapeHtml(code.join('\n'))}
      `) + return output.join('\n') +} + +export function stripFrontmatter(markdown) { + const value = String(markdown || '').replaceAll('\r\n', '\n') + if (!value.startsWith('---\n')) return value + const end = value.indexOf('\n---\n', 4) + return end === -1 ? value : value.slice(end + 5) +} + +export function extractTitle(markdown, fallback) { + const frontmatter = String(markdown || '').match(/^---\n([\s\S]*?)\n---\n/) + const titleLine = frontmatter?.[1].match(/^title:\s*(.+)$/m) + if (titleLine) return titleLine[1].trim().replace(/^['"]|['"]$/g, '') + const heading = stripFrontmatter(markdown).match(/^#\s+(.+)$/m) + return heading?.[1]?.replace(/^CANON-[^—]+—\s*/, '').trim() || fallback +} + +export function extractSummary(markdown) { + const content = stripFrontmatter(markdown) + for (const heading of ['Quick answer', 'One-sentence truth', 'Hover summary', 'Beginner explanation', 'Mental model']) { + const pattern = new RegExp(`^## ${escapeRegExp(heading)}\\s*$\\n+([^#\\n][^\\n]*)`, 'm') + const match = pattern.exec(content) + if (match) return plainText(match[1]).slice(0, 220) + } + const paragraphs = content.split(/\n\s*\n/).map((part) => part.trim()).filter((part) => part && !part.startsWith('#') && !part.startsWith('>') && !part.startsWith('```')) + return plainText(paragraphs[0] || '').slice(0, 220) +} + +export function escapeHtml(value) { + return String(value || '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''') +} + +function inline(value, sourcePath) { + let escaped = escapeHtml(value) + const tokens = [] + escaped = escaped.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => { + const token = `@@ATLAS_LINK_${tokens.length}@@` + tokens.push(`${label}`) + return token + }) + escaped = escaped.replace(/`([^`]+)`/g, '$1') + escaped = escaped.replace(/\*\*([^*]+)\*\*/g, '$1') + escaped = escaped.replace(/\*([^*]+)\*/g, '$1') + tokens.forEach((tokenValue, index) => { escaped = escaped.replace(`@@ATLAS_LINK_${index}@@`, tokenValue) }) + return escaped +} + +function resolveHref(href, sourcePath) { + const trimmed = String(href || '').trim() + if (/^https?:\/\//i.test(trimmed)) return trimmed + if (trimmed.startsWith('#')) return trimmed + const clean = trimmed.split('#')[0].split('?')[0] + const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(sourcePath), clean)) + return `${repositoryBase}${resolved}` +} + +function decodeEntities(value) { + return String(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll(''', "'") +} + +function escapeAttribute(value) { + return escapeHtml(value).replaceAll('`', '`') +} + +function slug(value) { + return plainText(value).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'section' +} + +function plainText(value) { + return String(value || '').replace(/\[([^\]]+)\]\([^)]+\)/g, '$1').replace(/[`*_>#]/g, '').replace(/\s+/g, ' ').trim() +} + +function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} diff --git a/scripts/package-navigator-demo.mjs b/scripts/package-navigator-demo.mjs new file mode 100644 index 0000000..fce1560 --- /dev/null +++ b/scripts/package-navigator-demo.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +import crypto from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { execFileSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const source = path.join(root, 'navigator-preview', 'demo-workspace') +const output = path.resolve(process.env.ATLAS_OUTPUT_DIR || path.join(root, 'dist', 'navigator-demo')) +const packageName = 'atlas-navigator-demo-workspace' +const stage = path.join(output, packageName) +const archive = path.join(output, `${packageName}.zip`) + +fs.rmSync(output, { recursive: true, force: true }) +copyTree(source, stage) +const files = listFiles(stage).map((file) => normalize(path.relative(stage, file))).sort() +for (const relative of files) { + if (/private|internal|feedback|secret|token|canon-flow-map/i.test(relative)) throw new Error(`Forbidden demo path: ${relative}`) + const content = fs.readFileSync(path.join(stage, relative), 'utf8') + if (/[A-Za-z]:\\Users\\|\/Users\/[^/\s]+\//.test(content)) throw new Error(`Personal path found in demo file: ${relative}`) +} +execFileSync('zip', ['-q', '-r', archive, packageName], { cwd: output }) +const sha256 = crypto.createHash('sha256').update(fs.readFileSync(archive)).digest('hex') +fs.writeFileSync(`${archive}.sha256`, `${sha256} ${path.basename(archive)}\n`) +fs.writeFileSync(path.join(output, 'package-manifest.json'), `${JSON.stringify({ schemaVersion: '1.0.0', package: path.basename(archive), sha256, files }, null, 2)}\n`) +fs.rmSync(stage, { recursive: true, force: true }) +console.log(`[atlas] Packaged Navigator demo workspace with ${files.length} files.`) + +function copyTree(sourcePath, destinationPath) { + fs.mkdirSync(destinationPath, { recursive: true }) + for (const entry of fs.readdirSync(sourcePath, { withFileTypes: true })) { + const from = path.join(sourcePath, entry.name) + const to = path.join(destinationPath, entry.name) + if (entry.isDirectory()) copyTree(from, to) + if (entry.isFile()) fs.copyFileSync(from, to) + } +} +function listFiles(directory) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const absolute = path.join(directory, entry.name) + return entry.isDirectory() ? listFiles(absolute) : [absolute] + }) +} +function normalize(value) { return String(value).replaceAll('\\', '/') } diff --git a/scripts/validate-public-repo.mjs b/scripts/validate-public-repo.mjs index b21cc5c..bb8a748 100644 --- a/scripts/validate-public-repo.mjs +++ b/scripts/validate-public-repo.mjs @@ -25,8 +25,14 @@ const requiredFiles = [ 'control/examples/laravel-status-label/COMPLETED_EXAMPLE.md', 'control/examples/laravel-status-label/DECISION_RECORD.md', 'control/examples/laravel-status-label/PROPOSED_PATCH.diff', + 'control/exercises/browser-list-count/EVIDENCE_RECORD.md', + 'control/exercises/browser-list-count/DECISION_RECORD.md', + 'control/exercises/browser-list-count/VERIFY.md', 'content/missions/README.md', 'content/nodes/README.md', + 'compendium-preview/index.html', + 'compendium-preview/data.js', + 'navigator-preview/README.md', 'examples/README.md', 'scripts/pr-control-policy.mjs', 'scripts/validate-pr-body.mjs', @@ -271,7 +277,7 @@ function normalizeText(value) { function listFiles(directory) { const output = [] for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { - if (['.git', 'node_modules', 'playwright-report', 'test-results'].includes(entry.name)) continue + if (['.git', 'dist', 'node_modules', 'playwright-report', 'test-results'].includes(entry.name)) continue const absolutePath = path.join(directory, entry.name) if (entry.isDirectory()) output.push(...listFiles(absolutePath)) if (entry.isFile()) output.push(absolutePath) diff --git a/scripts/verify-control-exercise-scope.mjs b/scripts/verify-control-exercise-scope.mjs new file mode 100644 index 0000000..ad63162 --- /dev/null +++ b/scripts/verify-control-exercise-scope.mjs @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +import crypto from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const exercise = path.join(root, 'control', 'exercises', 'browser-list-count') +const baseline = path.join(exercise, 'baseline') +const project = path.join(exercise, 'project') +const expectedChanged = ['index.html', 'script.js'] +const changed = [...new Set([...listRelativeFiles(baseline), ...listRelativeFiles(project)])] + .filter((relative) => hash(path.join(baseline, relative)) !== hash(path.join(project, relative))) + .sort() + +if (JSON.stringify(changed) !== JSON.stringify(expectedChanged)) { + throw new Error(`Exercise scope mismatch. Expected ${expectedChanged.join(', ')}, observed ${changed.join(', ') || 'no changes'}.`) +} + +const record = fs.readFileSync(path.join(exercise, 'records', 'IN_SCOPE_CHANGED_FILES.txt'), 'utf8').trim().split(/\r?\n/).filter(Boolean) +const expectedRecord = expectedChanged.map((relative) => `control/exercises/browser-list-count/project/${relative}`) +if (JSON.stringify(record) !== JSON.stringify(expectedRecord)) throw new Error('IN_SCOPE_CHANGED_FILES.txt is stale.') + +const patch = fs.readFileSync(path.join(exercise, 'proposed', 'IN_SCOPE_PATCH.diff'), 'utf8') +const patchPaths = [...patch.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gm)].map((match) => match[2]).sort() +if (JSON.stringify(patchPaths) !== JSON.stringify(expectedRecord)) throw new Error('IN_SCOPE_PATCH.diff path list is stale.') + +console.log(`[atlas] Control exercise scope verified: ${changed.join(', ')} only.`) + +function listRelativeFiles(directory) { + if (!fs.existsSync(directory)) return [] + return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const absolute = path.join(directory, entry.name) + if (entry.isDirectory()) return listRelativeFiles(absolute).map((relative) => path.join(entry.name, relative)) + return [entry.name] + }).map((relative) => relative.replaceAll('\\', '/')).sort() +} + +function hash(file) { + if (!fs.existsSync(file)) return null + return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') +} diff --git a/tests/compendium-preview.spec.mjs b/tests/compendium-preview.spec.mjs new file mode 100644 index 0000000..027fc3c --- /dev/null +++ b/tests/compendium-preview.spec.mjs @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import path from 'node:path' +import test from 'node:test' +import vm from 'node:vm' +import { fileURLToPath } from 'node:url' +import { escapeHtml, extractSummary, renderMarkdown } from '../scripts/compendium-render.mjs' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') + +test('renderer escapes raw HTML while preserving generated code blocks', () => { + const rendered = renderMarkdown('# Test\n\n\n\n```js\nconst x = ""\n```', 'content/nodes/gold/TEST.md') + assert.match(rendered, /<img src=x onerror="alert\(1\)">/) + assert.doesNotMatch(rendered, /const x = "<tag>"/) + assert.equal(escapeHtml('