feat(license-requests): profile+role tool mapping, approve-creates-assignment, copy-paste snippets (spec 032-v2) - #124
Conversation
Delete the .specify directory (templates, PowerShell scripts, constitution) and the nine .claude/commands/speckit.*.md slash commands. Reword the forward-looking /speckit.* instructions in the 028/031 spec READMEs and the auto-generated CLAUDE.md header; historical speckit mentions in completed spec docs are kept as-is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delete the post-sync Teams alert feature end to end: the evaluator, card renderers, webhook poster, alert-state ledger and shared types in src/lib/teams, the alert-only forecast-workspace engine and loadCostHistory query, the evaluateAndPostTeamsAlerts hook in the anthropic workspace cost sync, the TEAMS_WEBHOOK_URL / TEAMS_DASHBOARD_BASE_URL env vars, and the spec 030 docs and unit tests. Migration 0028 drops the anthropic_alert_state table. The license-request Teams integration (spec 032 — graph.ts, markdown.ts, teams_* columns) is intentionally untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…signment, copy-paste snippets (spec 032-v2)
Reworks the Microsoft Forms -> Hub license-request workflow per the revised
direction (specs/032-automation-workflow/{mockups,implementation-plan}-v2.html):
* Ingest v2: payload carries role + profile (+ required justification for
maxed/indie, case-insensitive matching); the Hub derives the tool via the
new tool_mappings table (seeded from the AI Tooling Guide, editable under
Settings -> Tool Mapping). Legacy toolName contract still accepted during
the Power Automate switchover.
* Approval is the terminal happy path (provision-first): one transaction
auto-creates unknown requesters as viewer users (no invite), creates the
encrypted license assignment, and stores the message. Complete dialog and
status retired; legacy approved rows get a "Record assignment" action.
* Teams/Graph posting postponed: every message is a copy-paste snippet.
Stored messages keep {{licenseCode}} unresolved - the audit log masks the
key; Reveal/Copy decrypt on demand from the assignment. graph.ts dormant.
* Queue: role/profile column, needs-decision badge for indie, pending-only
default facet, sidebar pending-count badge.
* Fixes: raw-SQL timestamps now converted for real (root cause of the
"search filters nothing" bug), form-payload rendering (label overlap,
empty answers collapsed), sm:max-w overrides on wide dialogs, Neon pool
idle timeout (dead-socket errors after idle).
* Migration 0029 + idempotent seeds (tool mappings, default approval
templates incl. API-key variant); unit + integration test suites for the
mapping, ingest contract, and approval transaction (deferred since v1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR implements spec 032-v2 changes to the license-request workflow: ingest now derives tools from (role, profile) mappings owned by the Hub, approval creates assignments (provision-first), and Teams posting is replaced by copy/paste snippets. It also removes the now-dormant Teams spend alert subsystem and introduces supporting schema, seeds, UI, and tests.
Changes:
- Add
(role, profile) → toolresolution via a newtool_mappingstable + admin Settings UI, and update ingest validation/route logic to support v2 (while keeping legacy v1toolNameingestion). - Retire Teams outbound posting in favor of copy-to-clipboard snippets, and remove the Claude spend Teams alerts modules/env/config/tests.
- Add migration/seeds and update queue UX (pending-only default facet, role/profile badges, pending-count sidebar badge) plus Neon pool idle timeout fix.
Reviewed changes
Copilot reviewed 82 out of 85 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/teams/webhook.test.ts | Removed obsolete Teams webhook unit tests. |
| tests/unit/teams/format.test.ts | Removed obsolete Teams formatting unit tests. |
| tests/unit/teams/forecast-workspace.test.ts | Removed obsolete forecast unit tests tied to Teams alerts. |
| tests/unit/teams/evaluator-diff.test.ts | Removed obsolete Teams alert evaluator unit tests. |
| tests/unit/teams/cards.test.ts | Removed obsolete Adaptive Card rendering unit tests. |
| tests/unit/license-requests/render-template.test.ts | Added test for {{licenseCode}} masking/unbound token behavior. |
| tests/unit/license-requests/mapping.test.ts | Added unit tests for role/profile normalization + mapping resolution. |
| tests/unit/license-requests/ingest-schema.test.ts | Added unit tests for case-insensitive role/profile validation and justification rules. |
| tests/integration/api/license-requests-ingest.test.ts | Added integration tests for v2 ingest, auth, idempotency, and legacy v1 behavior. |
| src/types/ingestion.ts | Extended ingestion details type to include v2 role/profile fields. |
| src/lib/validators.ts | Updated ingest schema for v2 inputs; added tool-mapping/approval/record-assignment schemas; adjusted reject schema placement. |
| src/lib/teams/webhook.ts | Removed Teams Workflows webhook poster implementation. |
| src/lib/teams/types.ts | Removed Teams alerts shared types. |
| src/lib/teams/state.ts | Removed Teams alerts state ledger helpers. |
| src/lib/teams/graph.ts | Marked Teams Graph module as dormant (retained for future migration). |
| src/lib/teams/format.ts | Removed Teams alerts formatting helpers. |
| src/lib/teams/evaluator.ts | Removed Teams spend alert evaluator/orchestration. |
| src/lib/teams/cards.ts | Removed Teams alerts adaptive card renderers. |
| src/lib/sync/sources/anthropic-workspace.ts | Removed post-sync Teams alert evaluation call. |
| src/lib/license-requests/templates.ts | Added listApprovalTemplates() to bulk-load approval templates for tool-switching UI. |
| src/lib/license-requests/mapping.ts | Added pure mapping resolution utilities used by ingest and tests. |
| src/lib/env.ts | Removed Teams alert env vars. |
| src/lib/db/schema.ts | Added license_request_profile enum, requires_api_key, v2 request fields, and tool_mappings table + relations; removed alert-state table definition. |
| src/lib/db/migrations/meta/_journal.json | Recorded migrations 0028/0029 in journal. |
| src/lib/db/migrations/0029_curly_strong_guy.sql | Introduced schema changes for v2 mappings/profile + nullable requested_tool_id + requires_api_key. |
| src/lib/db/migrations/0028_dear_khan.sql | Dropped Teams alert state table. |
| src/lib/db/index.ts | Added Neon pool idle/connection timeouts to avoid dead idle sockets. |
| src/lib/anthropic/queries.ts | Removed loadCostHistory type and function used by the deleted Teams alerts subsystem. |
| src/lib/anthropic/forecast-workspace.ts | Removed forecast implementation used by the deleted Teams alerts subsystem. |
| src/components/data-table.tsx | Added defaultSelected support for faceted filters (pending-only queue default). |
| src/components/copy-snippet.tsx | Added reusable copy-to-clipboard button for snippet-based workflow. |
| src/components/app-sidebar.tsx | Added pending-requests badge on Requests nav item. |
| src/app/settings/tool-mapping/tool-mapping-client.tsx | Added Settings UI for editing tool mappings. |
| src/app/settings/tool-mapping/page.tsx | Added Tool Mapping settings page (admin-only). |
| src/app/settings/settings-nav.tsx | Added Tool Mapping tab to settings navigation. |
| src/app/settings/license-templates/template-editor-dialog.tsx | Adjusted dialog width class to avoid shadcn base-class override issues. |
| src/app/requests/requests-table.tsx | Added role/profile column + needs-decision tool badge + pending-only default filter. |
| src/app/requests/[id]/rejection-dialog.tsx | Updated reject flow to end with copy/paste snippet rather than Teams send. |
| src/app/requests/[id]/record-assignment-dialog.tsx | Added legacy “record assignment” dialog for v1-approved rows. |
| src/app/requests/[id]/page.tsx | Switched to bulk-loading approval templates and updated props for new detail client flow. |
| src/app/requests/[id]/completion-dialog.tsx | Removed retired completion dialog. |
| src/app/layout.tsx | Added pending license request count query to drive sidebar badge for admins. |
| src/app/api/license-requests/ingest/route.ts | Implemented v2 ingest derivation via tool mappings; removed fire-and-forget Teams Graph post. |
| src/actions/tool-mappings.ts | Added server actions for listing/upserting/deleting tool mappings. |
| src/actions/license-templates.ts | Removed unused recent form-keys helper after template handling changes. |
| specs/032-automation-workflow/implementation-notes.html | Documented v2 decisions, deviations, and verification results. |
| specs/031-budget-workflow-and-redesign/README.md | Removed speckit command reference from next steps. |
| specs/028-reports-v2/README.md | Removed speckit command references; clarified next steps language. |
| scripts/seed-tool-mappings.ts | Added idempotent seed for tool mappings + requires_api_key marking. |
| scripts/seed-message-templates.ts | Added idempotent seed for approval message templates (seat vs API-key variants). |
| CLAUDE.md | Tweaked header wording to reflect compilation source. |
| .specify/templates/tasks-template.md | Removed specify template file. |
| .specify/templates/spec-template.md | Removed specify template file. |
| .specify/templates/plan-template.md | Removed specify template file. |
| .specify/templates/constitution-template.md | Removed specify template file. |
| .specify/templates/checklist-template.md | Removed specify template file. |
| .specify/templates/agent-file-template.md | Removed specify template file. |
| .specify/scripts/powershell/setup-plan.ps1 | Removed specify PowerShell script. |
| .specify/scripts/powershell/create-new-feature.ps1 | Removed specify PowerShell script. |
| .specify/scripts/powershell/common.ps1 | Removed specify PowerShell script. |
| .specify/scripts/powershell/check-prerequisites.ps1 | Removed specify PowerShell script. |
| .specify/memory/constitution.md | Removed specify constitution memory file. |
| .claude/commands/speckit.taskstoissues.md | Removed speckit Claude command file. |
| .claude/commands/speckit.tasks.md | Removed speckit Claude command file. |
| .claude/commands/speckit.specify.md | Removed speckit Claude command file. |
| .claude/commands/speckit.plan.md | Removed speckit Claude command file. |
| .claude/commands/speckit.implement.md | Removed speckit Claude command file. |
| .claude/commands/speckit.constitution.md | Removed speckit Claude command file. |
| .claude/commands/speckit.clarify.md | Removed speckit Claude command file. |
| .claude/commands/speckit.analyze.md | Removed speckit Claude command file. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (d.profile === "maxed" || d.profile === "indie") { | ||
| if (!d.justification || d.justification.trim().length === 0) { | ||
| ctx.addIssue({ | ||
| code: "custom", | ||
| message: `justification is required when profile is "${d.profile}"`, | ||
| path: ["justification"], | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Fixed in 23fdc03 — the justification requirement is now scoped to the v2 branch (role !== undefined), so a partially updated legacy caller sending a stray profile passes. Added a unit test covering exactly that case.
| async function handleSave() { | ||
| if (!editor) return; | ||
| setSaving(true); | ||
| const result = await upsertToolMapping({ | ||
| role: editor.role === ANY_ROLE ? null : editor.role, | ||
| profile: editor.profile, | ||
| toolId: editor.toolId === NEEDS_DECISION ? null : Number(editor.toolId), | ||
| defaultTierId: | ||
| editor.toolId === NEEDS_DECISION || editor.defaultTierId === NO_TIER | ||
| ? null | ||
| : Number(editor.defaultTierId), | ||
| }); | ||
| setSaving(false); | ||
| if (result.success) { | ||
| setEditor(null); | ||
| router.refresh(); | ||
| } else { | ||
| status.error(result.error); | ||
| } | ||
| } |
There was a problem hiding this comment.
Fixed in 23fdc03 — the server-action call is wrapped in try/catch/finally: saving always resets, and a thrown error surfaces as an inline status message instead of wedging the dialog.
| facetedFilters, | ||
| }: DataTableProps<TData, TValue>) { | ||
| const [sorting, setSorting] = useState<SortingState>([]); | ||
| const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); | ||
| const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(() => | ||
| (facetedFilters ?? []) | ||
| .filter((f) => f.defaultSelected && f.defaultSelected.length > 0) | ||
| .map((f) => ({ id: f.columnId, value: f.defaultSelected! })), | ||
| ); |
There was a problem hiding this comment.
Documented in 23fdc03 (option a): defaultSelected applies via lazy state init at first mount only, and the JSDoc now states that facetedFilters must be present and stable at mount — later prop changes are deliberately ignored so user edits are never clobbered. All current consumers pass a static config, so effect-syncing would add risk without a consumer that needs it.
| accessorFn: (row) => | ||
| row.requesterRole | ||
| ? `${ROLE_LABELS[row.requesterRole]} ${row.requesterProfile ? PROFILE_LABELS[row.requesterProfile] : ""}` | ||
| : "", |
There was a problem hiding this comment.
Fixed in 23fdc03 — the accessor now builds the string with filter(Boolean).join(" "), so no trailing space when the profile is null.
…, recoverable mapping dialog, accessor/docs cleanups * justification requirement only applies on the v2 branch (role present) — a partially updated legacy caller sending a stray profile no longer 400s * tool-mapping editor resets its saving state in finally so a thrown server action can't wedge the dialog * roleProfile queue accessor no longer emits a trailing space for rows without a profile * document that DataTable defaultSelected applies at first mount only Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Implements the revised spec 032 direction (see
specs/032-automation-workflow/mockups-v2.htmlandimplementation-plan-v2.html, review artifacts from 2026-07-09):role(development|conception|business) +profile("" = baseline | maxed | indie) +justification(required for maxed/indie), matched case-insensitively. The tool is derived via the newtool_mappingstable — seeded from the AI Tooling Guide, editable under Settings → Tool Mapping. Indie resolves to needs decision (approver picks). The legacytoolNamecontract remains accepted so an un-updated PA flow keeps working during switchover.license_assignmentsrow, and stores the message. The Complete dialog/status is retired; legacy approved-without-assignment rows (REQ-001) get a one-shot Record assignment action. First-write-wins semantics preserved (race loss rolls back assignment and auto-created user).{{licenseCode}}unresolved — the key exists only encrypted on the assignment, masked in the log, decrypted on Reveal/Copy (admin-only).src/lib/teams/graph.tsis dormant (zero callers) for the future stack migration.requires_api_keytools (Claude Console).sm:max-w-*dialog width overrides silently losing to the shadcn base class, Neon pool dead-idle-socket errors (idleTimeoutMillis).license_request_profileenum, 3 columns onlicense_requests,requested_tool_idnullable,ai_tools.requires_api_key,tool_mappings(partial unique indexes, same NULL handling asmessage_templates). Idempotent seeds:scripts/seed-tool-mappings.ts,scripts/seed-message-templates.ts(the template seed deferred since v1).Test plan
{{licenseCode}}masking contractpnpm lint && pnpm typecheckcleanDeploy notes
LICENSE_REQUEST_INGEST_SECRETin Vercel (the PA flow's bearer token).role/profile/justificationfrom the updated Form (Title-Case values are fine — matching is case-insensitive). Until then the legacy tool-name path keeps working.🤖 Generated with Claude Code